Add Codex plugin manifest, stdlib MCP server, SHA-pinned Actions

- .codex-plugin/plugin.json and assets/icon.svg package the existing skill
  tree for Codex plugin catalogs; skill version 1.0.12 across all manifests
- `remove-ai-watermarks mcp` serves identify, remove and batch over stdio,
  one CLI command per tool call, standard library only; server.json and
  glama.json for the MCP registry and Glama
- Pin every GitHub Action to a commit SHA
- Call module.train(False) instead of module.eval(), which plugin scanners
  read as dynamic code execution

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Victor KuznetsovandClaude Opus 5.5 committed 2026-10-17 14:13:04 -07:00
1 parent db006f034f
commit 06c0cb1bbe
41 files changed
+1288 -76

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@
},
"metadata": {
"description": "Agent skill and Claude Code plugin for the remove-ai-watermarks CLI.",
"version": "1.0.11"
"version": "1.0.12"
},
"plugins": [
{
+39
View File
@@ -0,0 +1,39 @@
{
"name": "remove-ai-watermarks",
"version": "1.0.12",
"description": "Identify and remove AI provenance marks from images and video the user generated or edited, using the remove-ai-watermarks CLI.",
"author": {
"name": "wiltodelta",
"url": "https://github.com/wiltodelta"
},
"homepage": "https://raiw.cc",
"repository": "https://github.com/wiltodelta/remove-ai-watermarks",
"license": "Apache-2.0",
"keywords": [
"ai-watermark",
"watermark-removal",
"c2pa",
"synthid",
"provenance",
"gemini",
"sora"
],
"skills": "./skills/",
"interface": {
"displayName": "Remove AI Watermarks",
"shortDescription": "Identify and remove AI watermarks and provenance metadata",
"longDescription": "Runs the open-source remove-ai-watermarks CLI locally to identify C2PA, IPTC, EXIF and TC260 provenance, visible AI-generator labels and supported invisible watermarks, then removes them from images and video the user generated or edited. It does not remove stock, agency or other third-party marks.",
"developerName": "wiltodelta",
"category": "Productivity",
"capabilities": ["Read", "Write"],
"websiteURL": "https://github.com/wiltodelta/remove-ai-watermarks",
"defaultPrompt": [
"Check this image for AI watermarks and provenance metadata",
"Remove the AI metadata from my generated video without re-encoding it",
"Remove the visible AI label from the corner of my image"
],
"brandColor": "#A78BFA",
"composerIcon": "./assets/icon.svg",
"logo": "./assets/icon.svg"
}
}
+15
View File
@@ -0,0 +1,15 @@
# Local state and secrets never belong in a plugin bundle.
.env
.env.*
!.env.example
.oracle-slots.json
.local-eval/
.venv/
__pycache__/
*.pyc
# Generated media and model artifacts.
data/
*.mp4
*.onnx
*.safetensors
*.pt
+3 -3
View File
@@ -62,7 +62,7 @@ jobs:
PYTHON
- name: Upload distributed release identity
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: distributed-release-${{ github.run_attempt }}
path: release-identity.json
@@ -104,7 +104,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the tap
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: wiltodelta/homebrew-tap
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
@@ -227,7 +227,7 @@ jobs:
needs: resolve
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Publish the agent skill when its version moved
env:
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
@@ -21,8 +21,8 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
python-version: "3.12"
- name: Download derivative weights
@@ -36,9 +36,9 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@v7
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
python-version: "3.12"
@@ -22,9 +22,9 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@v7
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
python-version: "3.12"
+2 -2
View File
@@ -15,9 +15,9 @@ jobs:
runs-on: ubuntu-latest
environment: pypi
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@v7
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Verify release tag matches package version
run: |
+2 -2
View File
@@ -32,8 +32,8 @@ jobs:
skill:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Sync dev environment
run: uv sync --frozen --extra dev
- name: Agent skill packaging tests
+10 -10
View File
@@ -16,8 +16,8 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Sync dev environment
run: uv sync --frozen --extra dev
- name: Check C2PA soft-binding registry snapshot
@@ -34,8 +34,8 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
enable-cache: false
# Policy and triaged blocks: docs/development.md "Known security-gate blocks".
@@ -69,8 +69,8 @@ jobs:
- os: windows-latest
python-version: "3.14"
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
python-version: ${{ matrix.python-version }}
- name: Sync dev environment
@@ -82,8 +82,8 @@ jobs:
name: versioned transport contracts py3.14
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
python-version: "3.14"
- name: Create clean runtime contract environment
@@ -104,8 +104,8 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
python-version: "3.12"
- name: Install ffmpeg
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Download distributed release identity
if: ${{ github.event_name == 'workflow_run' }}
uses: actions/download-artifact@v8
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: distributed-release-*
path: distribution-identities
+2
View File
@@ -40,6 +40,8 @@ Before a release, read [`docs/release-and-distribution.md`](docs/release-and-dis
The published Agent Skill is [`skills/remove-ai-watermarks/`](skills/remove-ai-watermarks/). Update it in the same change as any CLI edit that alters commands, extras, exit codes, mark keys, or the intended-use boundary. Install and publish steps: [`docs/agent-skill.md`](docs/agent-skill.md).
The same skill tree is the Codex plugin (`.codex-plugin/plugin.json`). `remove-ai-watermarks mcp` is a standard-library MCP server that runs one CLI command per tool call; keep it dependency-free, because the MCP registry launches it as a bare `uvx remove-ai-watermarks mcp` that cannot add extras ([`docs/mcp-server.md`](docs/mcp-server.md)).
## Module architecture
[`docs/module-internals.md`](docs/module-internals.md) is the canonical per-module map, including design decisions, thresholds, calibration history, incident records, and regression guards. Read the relevant section before changing a subsystem.
+14
View File
@@ -482,9 +482,23 @@ Claude Code can add this repository as a plugin marketplace:
/plugin install remove-ai-watermarks@remove-ai-watermarks
```
Codex reads the same skill through [`.codex-plugin/plugin.json`](.codex-plugin/plugin.json).
The skill refuses stock-agency and other third-party paid-asset marks. See
[the agent skill guide](docs/agent-skill.md).
## MCP server
`remove-ai-watermarks mcp` serves `identify`, `remove` and `batch` as Model
Context Protocol tools over stdio, each a thin wrapper around one CLI command:
```json
{"mcpServers": {"remove-ai-watermarks": {"command": "uvx", "args": ["remove-ai-watermarks", "mcp"]}}}
```
See [the MCP server guide](docs/mcp-server.md).
<!-- mcp-name: io.github.wiltodelta/remove-ai-watermarks -->
## ComfyUI
The separate
+20
View File
@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="512" height="512" fill="none" stroke="url(#lg)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!--
The raiw.cc brand mark: the Lucide "sparkle" and "x" glyphs.
Lucide, https://lucide.dev, ISC License, Copyright (c) Lucide Contributors.
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
-->
<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z" fill="url(#lg)" fill-opacity="0.15"/>
<g transform="translate(1 17) scale(0.3)" stroke-width="5">
<path d="M18 6 6 18"/>
<path d="m6 6 12 12"/>
</g>
<defs>
<linearGradient id="lg" x1="0" y1="0" x2="24" y2="24">
<stop stop-color="#a78bfa"/>
<stop offset="1" stop-color="#06b6d4"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+13 -2
View File
@@ -70,6 +70,17 @@ Do these in order after the skill is on the default branch.
are indexed via clawskills.sh, and the ClawHub prerequisite in step 6 is
now met. The list still requires proven real-world usage. Re-attempt once
the ClawHub listing shows organic installs.
- `hashgraph-online/awesome-codex-plugins`: READY, not submitted. It lists
only Codex plugins, so it needs `.codex-plugin/plugin.json` and the icon in
`assets/icon.svg`. Its contribution CI runs `scripts/validate-plugin-pr.py`
against the repository's `HEAD.zip`, then the HOL plugin scanner with a
minimum score of 80 and no high finding; that scan job is
`continue-on-error`, so it reports rather than blocks. The scanner's `\beval\s*\(`
rule also matches PyTorch's `.eval()`, which is why the code calls
`.train(False)`. On 2026-10-17 `plugin-scanner` 3.49.0 scored the tree 86
with no high finding. The remaining loss is a tracked video over the
scanner's 16 MiB file budget. Recheck with
`uvx --from plugin-scanner==3.49.0 plugin-scanner scan . --min-score 80 --fail-on-severity high`.
5. **Claude community marketplace.** Form at
[platform.claude.com/plugins/submit](https://platform.claude.com/plugins/submit)
(individuals) or the claude.ai directory form (Team/Enterprise). Validate
@@ -191,8 +202,8 @@ that names a command, flag or value the previous release lacked. Left behind, it
CLI is current right before it types an option that build rejects.
Bump `metadata.version` in `SKILL.md` plus `version` in
`skills/.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json` when
the skill instructions change. That version is independent of the PyPI package
`skills/.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json` and
`.codex-plugin/plugin.json` when the skill instructions change. That version is independent of the PyPI package
version.
## Behavior evals
+11
View File
@@ -28,6 +28,7 @@ defaults. This page focuses on choosing the right command.
| `video invisible` and `video all --invisible` | `remove-ai-watermarks[video,diffusion]` plus ffmpeg on PATH |
| HEIC/HEIF/AVIF pixel input | Add `remove-ai-watermarks[heif]` |
| Every production command and Python-compatible backend | `remove-ai-watermarks[all]` |
| `mcp` | Default package; each tool needs the extra of the command it runs |
`batch` requires the same extra as its selected mode. Extras can be combined in
one installation, for example `remove-ai-watermarks[visible,detect,heif]`.
@@ -690,6 +691,16 @@ For `visible` and `all` modes, the final summary includes counts for
validation results are printed explicitly, but the once-filled outputs are kept
and the batch does not retry them automatically.
## Serve the CLI to agent clients
```bash
remove-ai-watermarks mcp
```
Runs a Model Context Protocol server on stdin and stdout whose `identify`,
`remove` and `batch` tools each run one command of this CLI. Client setup and
the tool contract are in the [MCP server guide](mcp-server.md).
## Exit behavior
The CLI uses nonzero exit codes for meaningful incomplete outcomes, including
+2 -1
View File
@@ -27,7 +27,8 @@ to run the tool. Use the maintainer references only when changing the code.
| [Code provenance](code-provenance.md) | Required notices for licensed derivative work. |
| [Verification plan](verification-plan.md) | Verification methods, completed measurements, and remaining validation gaps. |
| [Release and distribution](release-and-distribution.md) | PyPI, Homebrew, Hugging Face Space, photo-classify Hub model, ComfyUI Registry, and the release workflow. |
| [Agent skill](agent-skill.md) | Install and maintain the published Agent Skill and Claude plugin. |
| [Agent skill](agent-skill.md) | Install and maintain the published Agent Skill, the Claude plugin and the Codex plugin. |
| [MCP server](mcp-server.md) | Connect an MCP client to `remove-ai-watermarks mcp`, or publish it to the MCP registry. |
| [Watermarking landscape](watermarking-landscape.md) | Vendor signals and detection approaches. |
| [SynthID technical reference](synthid.md) | Mechanism, provenance, robustness, regeneration. |
+91
View File
@@ -0,0 +1,91 @@
# MCP server
`remove-ai-watermarks mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io/)
server on stdin and stdout, so an agent client can identify and clean the
user's own images and video without a shell. It is a thin wrapper: each tool
runs one command of this CLI in a child process and returns that command's exit
code, output path and text.
The subcommand ships in the first release after 0.45.1. The server uses only
the Python standard library, so the default package runs it. The commands behind
a tool need the same extras they need on the command line
([CLI guide](cli.md#command-dependency-map)). A missing extra comes back as a
tool error carrying the CLI's install hint.
## Client configuration
Most clients take a command and arguments. With [uv](https://docs.astral.sh/uv/):
```json
{
"mcpServers": {
"remove-ai-watermarks": {
"command": "uvx",
"args": ["remove-ai-watermarks", "mcp"]
}
}
}
```
To make visible-mark removal and pixel detectors available, name the extras:
```json
{
"mcpServers": {
"remove-ai-watermarks": {
"command": "uvx",
"args": ["--from", "remove-ai-watermarks[visible,video]", "remove-ai-watermarks", "mcp"]
}
}
}
```
An existing installation can be started directly as `remove-ai-watermarks mcp`.
Paths in tool calls are read and written on the machine that runs the server.
## Tools
| Tool | Runs | Writes |
| --- | --- | --- |
| `identify` | `identify --json`, or `video identify --json` for a video suffix; `check_visible: false` adds `--no-visible` | Nothing |
| `remove` | `metadata --remove`, `visible` or `all` for an image; `video metadata --remove`, `video visible` or `video all` for a video | One output file, `<name>_clean` beside the source unless `output` is given |
| `batch` | `batch --mode` for images or `video batch --mode` for video | A separate output directory, `<dir>_clean` unless `output_dir` is given |
`mode` is `metadata` (the default), `visible` or `all`. For an image, `all`
adds the invisible-watermark scrub where the installed extras and a CUDA GPU
allow it; `video all` removes visible marks and metadata only. The server never
writes over an input: an `output` equal to the source, or an `output_dir` equal
to the input directory, is refused before anything runs.
Exit codes keep their CLI meaning. `0` and `2` are results, not tool errors, and
`2` means no supported signal was found, which is unknown rather than clean.
Click also exits `2` on a usage error such as a missing file; its `Usage:` line on
stderr marks that case as a tool error. Every other nonzero exit code is returned
with `isError: true` and the command's stderr.
## Scope
The server carries the project's intended-use boundary in its `instructions`:
it is for content the user generated or edited themselves, and not for stock,
agency, marketplace or other marks that protect a third party's asset. See
[legal and safety](legal-and-safety.md).
## Protocol
The server answers the initialize handshake for protocol versions 2025-11-25,
2025-06-18, 2025-03-26 and 2024-11-05, and lists its tools with input schemas
and annotations: `identify` is read-only, and `remove` and `batch` write new
files without destroying inputs. It does not implement `server/discover`. A
client that probes it first, such as the official Python SDK's `Client` in its
default mode, receives method-not-found and falls back to the handshake.
`tests/test_mcp_server.py` runs that client against the real subcommand.
## Registry listing
[`server.json`](../server.json) describes the PyPI package for the
[official MCP registry](https://registry.modelcontextprotocol.io/) under
`io.github.wiltodelta/remove-ai-watermarks`. The registry verifies ownership by
the `mcp-name:` marker in `README.md`, which becomes the PyPI description, so it
can only verify a release whose README carries that marker.
[`glama.json`](../glama.json) names the maintainer for Glama. Publishing steps
are in [release and distribution](release-and-distribution.md#mcp-registry).
+26 -2
View File
@@ -24,9 +24,10 @@ The package version appears in:
- `pyproject.toml`;
- `src/remove_ai_watermarks/__init__.py`;
- both `version` fields in `server.json`;
- the root package entry generated in `uv.lock`.
Update the first two, then refresh the lock file with uv. Do not edit a
Update the first three, then refresh the lock file with uv. Do not edit a
line-number-specific location in `uv.lock`; its package order changes.
Before choosing a version, verify it is absent from git tags, GitHub Releases,
@@ -159,7 +160,10 @@ The Agent Skill and Claude plugin live in this repository:
is the Claude plugin manifest, with source `./skills` so an install copies
only the skill tree;
- [`.claude-plugin/marketplace.json`](../.claude-plugin/marketplace.json)
makes the repository a Claude Code marketplace.
makes the repository a Claude Code marketplace;
- [`.codex-plugin/plugin.json`](../.codex-plugin/plugin.json) is the Codex
plugin manifest, pointing at the same `./skills/` tree, with its icon in
[`assets/icon.svg`](../assets/icon.svg).
They are not a fifth required release surface. A CLI change that affects
command routing, extras, exit codes, mark keys, or the intended-use boundary
@@ -215,6 +219,26 @@ the catalog has not accepted the initial submission. The catalog-specific
submission state and links live in
[`docs/agent-skill.md`](agent-skill.md#where-the-skill-can-actually-be-listed).
## MCP registry
[`server.json`](../server.json) lists the PyPI package in the
[official MCP registry](https://registry.modelcontextprotocol.io/) as
`io.github.wiltodelta/remove-ai-watermarks`, started as
`uvx remove-ai-watermarks mcp`. The registry accepts it only after PyPI serves a
release whose description, the README, carries the `mcp-name:` marker, and
`tests/test_mcp_server.py` pins `server.json` to the package version. The listing
is not a release surface that blocks a release; after PyPI publishes a version:
1. `mcp-publisher login github` with an account that owns `wiltodelta`;
2. `mcp-publisher validate` and then `mcp-publisher publish` from the
repository root;
3. read the entry back from
`https://registry.modelcontextprotocol.io/v0/servers?search=io.github.wiltodelta/remove-ai-watermarks`
and check its version.
[`glama.json`](../glama.json) names the maintainer for Glama's claim flow.
Client setup and the tool contract are in [the MCP server guide](mcp-server.md).
## Release verification
Forensic transports are versioned independently from the package. Before publishing
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "https://glama.ai/mcp/schemas/server.json",
"maintainers": ["wiltodelta"]
}
+3
View File
@@ -205,6 +205,9 @@ dev = [
"httpx>=0.28,<1",
# Development-only isolated browser automation for public provider oracles.
"playwright>=1.62,<2",
# The official MCP client, used only to test `remove-ai-watermarks mcp`
# against the reference implementation. The server itself is stdlib-only.
"mcp>=2.3,<3",
"pytest>=8.0.0",
"pytest-cov>=4.1.0",
"pytest-xdist>=3.5.0",
+1 -1
View File
@@ -121,7 +121,7 @@ def load_pinned_models() -> tuple[object, object]:
raw = convert_state_dict_for_scriptable_model(raw)
model = create(config, device="cpu") # type: ignore[call-arg]
load_state_dict(model, state_dict=raw)
model.eval()
model.train(False)
return model
generator = load(GENERATOR_CARD, "generator_base.pth", AudioSealWMConfig, create_generator)
+1 -1
View File
@@ -51,7 +51,7 @@ def export_onnx(checkpoint: Path, output: Path) -> None:
if output.exists():
raise SystemExit(f"refusing to overwrite existing output: {output}")
output.parent.mkdir(parents=True, exist_ok=True)
model = VisionEmbedding(load_headed_clip(checkpoint, torch.device("cpu"))).eval()
model = VisionEmbedding(load_headed_clip(checkpoint, torch.device("cpu"))).train(False)
export_options: dict[str, Any] = {}
if "dynamo" in inspect.signature(torch.onnx.export).parameters:
export_options["dynamo"] = False
+1 -1
View File
@@ -252,7 +252,7 @@ def _lpips_model() -> tuple[Any, Any]:
import torch
model = lpips.LPIPS(net="alex", verbose=False)
model.eval()
model.train(False)
return model, torch
+1 -1
View File
@@ -173,7 +173,7 @@ def load_model() -> object:
raise RuntimeError(
f"PixelSeal checkpoint/source mismatch: missing={loaded.missing_keys}, unexpected={loaded.unexpected_keys}"
)
model.eval()
model.train(False)
return model
+2 -2
View File
@@ -98,7 +98,7 @@ def _train_detector(emb: dict[str, np.ndarray], catalog: dict) -> tuple[nn.Modul
optimizer.zero_grad()
loss_fn(model(features).squeeze(-1), labels).backward()
optimizer.step()
model.eval()
model.train(False)
dev = np.stack(
[
emb[row["sha256"]]
@@ -161,7 +161,7 @@ def _train_provider(feat: dict[str, np.ndarray], catalog: dict) -> dict[str, nn.
optimizer.zero_grad()
_focal_loss(model(features).squeeze(-1), labels).backward()
optimizer.step()
heads[name] = model.eval()
heads[name] = model.train(False)
log.info("provider %s pos=%d", name, len(positive))
return heads
+1 -1
View File
@@ -127,7 +127,7 @@ def load_model() -> object:
import torch
model = torch.jit.load(str(checkpoint_path()), map_location="cpu")
model.eval()
model.train(False)
return model
+30
View File
@@ -0,0 +1,30 @@
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.wiltodelta/remove-ai-watermarks",
"title": "Remove AI Watermarks",
"description": "Identify and remove AI watermarks and provenance metadata from your own images and video.",
"version": "0.45.1",
"repository": {
"url": "https://github.com/wiltodelta/remove-ai-watermarks",
"source": "github"
},
"websiteUrl": "https://github.com/wiltodelta/remove-ai-watermarks/blob/main/docs/mcp-server.md",
"packages": [
{
"registryType": "pypi",
"registryBaseUrl": "https://pypi.org",
"identifier": "remove-ai-watermarks",
"version": "0.45.1",
"runtimeHint": "uvx",
"transport": {
"type": "stdio"
},
"packageArguments": [
{
"type": "positional",
"value": "mcp"
}
]
}
]
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "remove-ai-watermarks",
"displayName": "Remove AI Watermarks",
"version": "1.0.11",
"version": "1.0.12",
"description": "Identify and remove AI provenance marks from images and video the user generated or edited, using the remove-ai-watermarks CLI.",
"author": {
"name": "wiltodelta",
+1 -1
View File
@@ -5,7 +5,7 @@ license: Apache-2.0
compatibility: Python 3.11-3.14. Invisible image removal needs NVIDIA CUDA. Some video writes need ffmpeg. Installer may be uv, pipx, or pip.
metadata:
author: wiltodelta
version: "1.0.11"
version: "1.0.12"
homepage: https://raiw.cc
repository: https://github.com/wiltodelta/remove-ai-watermarks
---
@@ -138,6 +138,13 @@ user names a device, and never to claim a device the probe did not report.
Without `-o`, video commands write `<source>_clean` and keep the original.
Output extension must match the input container.
## MCP server
`remove-ai-watermarks mcp` (new after CLI 0.45.1) serves `identify`, `remove`
and `batch` as MCP tools over stdin and stdout for MCP clients. It blocks
waiting for protocol messages, so do not run it from this skill; call the
commands above directly.
## Exit codes
| Code | Meaning |
@@ -67,7 +67,7 @@ def load_headed_clip(checkpoint: Path, device: torch.device) -> HeadedCLIP:
model = HeadedCLIP(clip)
state = torch.load(checkpoint, map_location="cpu", weights_only=True)
model.load_state_dict(state, strict=True)
return model.to(device).eval()
return model.to(device).train(False)
def load_processor() -> Any:
@@ -823,7 +823,7 @@ class TwoStageZImagePipeline:
torch_dtype=self._face_stage_dtype(),
**kwargs,
).to(self.device)
model.eval()
model.train(False)
self._sam_model = model
self._sam_processor = processor
return model, processor
+2 -2
View File
@@ -467,7 +467,7 @@ def _load_runtime(device: Any, backend: ClassifierBackend = "torch") -> _Runtime
probe = np.load(_weight_path(folder, PROBE_FILE))
detector = _build_detector()
detector.load_state_dict(torch.load(_weight_path(folder, DETECTOR_FILE), map_location="cpu", weights_only=True))
detector.to(device).eval()
detector.to(device).train(False)
packed = torch.load(_weight_path(folder, PROVIDER_FILE), map_location="cpu", weights_only=True)
heads = {}
for name in PROVIDER_LABELS:
@@ -476,7 +476,7 @@ def _load_runtime(device: Any, backend: ClassifierBackend = "torch") -> _Runtime
continue
head = _build_provider_head()
head.load_state_dict(packed[name])
heads[name] = head.to(device).eval()
heads[name] = head.to(device).train(False)
ridge_threshold = float(probe["thr_oi_1pct"])
mlp_threshold, provider_margin = _operating_point(folder)
log.info("loaded photo-classify freeze from %s", folder)
+14
View File
@@ -1911,5 +1911,19 @@ def cmd_batch(
raise SystemExit(1)
# ── MCP server ──
@main.command("mcp")
def cmd_mcp() -> None:
"""Run a Model Context Protocol server on stdin/stdout.
Exposes identify, remove and batch as MCP tools for agent clients. Each tool
runs one command of this CLI, so results and exit codes match it. Start it as
`uvx remove-ai-watermarks mcp` from the client's server configuration.
"""
from remove_ai_watermarks.mcp_server import serve
serve()
if __name__ == "__main__":
main()
+297
View File
@@ -0,0 +1,297 @@
"""Model Context Protocol server over stdio, a thin wrapper around this package's CLI.
``remove-ai-watermarks mcp`` speaks newline-delimited JSON-RPC 2.0 on stdin and
stdout with the initialize handshake. Each tool call runs one CLI command in a
child process of the same interpreter, so a tool returns exactly what that command
would: its exit code, its output file and its text. Nothing but protocol messages
reaches stdout, and a crash inside a command cannot corrupt the stream.
The server needs no dependency beyond the standard library, so the bare package
runs it through ``uvx remove-ai-watermarks mcp``. Commands that need an optional
extra fail with the CLI's own install hint, returned as a tool error.
"""
from __future__ import annotations
import contextlib
import json
import logging
import os
import subprocess
import sys
from collections.abc import Callable
from pathlib import Path
from typing import IO, Any, cast
from remove_ai_watermarks import __version__
from remove_ai_watermarks.video import VIDEO_EXTENSIONS
log = logging.getLogger(__name__)
# Newest first. A client asking for an unlisted version gets the newest, and
# decides for itself whether it can continue.
PROTOCOL_VERSIONS = ("2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05")
INSTRUCTIONS = (
"Identify and remove AI provenance marks from images and video the user generated "
"or edited themselves: C2PA, IPTC, EXIF and TC260 metadata, visible AI-generator "
"labels, and supported invisible watermarks. Do not use it on stock, agency, "
"marketplace or other third-party marks that protect someone else's asset. A "
"result of 'unknown' or exit code 2 means no supported signal was found, not "
"that the file is clean. Paths are read on the machine running the server."
)
_MODES = ("metadata", "visible", "all")
TOOLS: list[dict[str, Any]] = [
{
"name": "identify",
"title": "Identify AI provenance",
"description": (
"Report which AI provenance signals an image or video carries: metadata "
"(C2PA, IPTC, EXIF, TC260), visible AI-generator labels, and supported local "
"invisible-watermark decoders. Read-only. Video is detected by file suffix."
),
"inputSchema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Image or video file to inspect."},
"check_visible": {
"type": "boolean",
"default": True,
"description": "Run the pixel and visible-mark detectors. False reads metadata only.",
},
},
"required": ["path"],
"additionalProperties": False,
},
"annotations": {"readOnlyHint": True, "openWorldHint": False},
},
{
"name": "remove",
"title": "Remove AI watermarks from one file",
"description": (
"Write a cleaned copy of an image or video the user generated or edited. "
"mode=metadata strips AI metadata without touching pixels or re-encoding video; "
"mode=visible also removes a known visible AI label; for an image, mode=all adds "
"the invisible-watermark scrub where the installed extras and hardware allow it. "
"The source is never overwritten; the default output is <name>_clean beside it."
),
"inputSchema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Image or video file to clean."},
"mode": {"type": "string", "enum": list(_MODES), "default": "metadata"},
"output": {"type": "string", "description": "Output file. Must differ from the source."},
},
"required": ["path"],
"additionalProperties": False,
},
"annotations": {"readOnlyHint": False, "destructiveHint": False, "openWorldHint": False},
},
{
"name": "batch",
"title": "Remove AI watermarks from a directory",
"description": (
"Clean every supported image (media=image) or video (media=video) in a directory "
"the user owns, writing results to a separate directory (default <dir>_clean). "
"Modes as for the remove tool."
),
"inputSchema": {
"type": "object",
"properties": {
"directory": {"type": "string", "description": "Directory of input files."},
"mode": {"type": "string", "enum": list(_MODES), "default": "metadata"},
"media": {"type": "string", "enum": ["image", "video"], "default": "image"},
"output_dir": {"type": "string", "description": "Output directory. Must differ from the input."},
},
"required": ["directory"],
"additionalProperties": False,
},
"annotations": {"readOnlyHint": False, "destructiveHint": False, "openWorldHint": False},
},
]
_TOOL_SCHEMAS = {tool["name"]: tool["inputSchema"] for tool in TOOLS}
Runner = Callable[[list[str]], subprocess.CompletedProcess[str]]
class ToolInputError(ValueError):
"""A tool call whose arguments the server refuses before running anything."""
def run_cli(args: list[str]) -> subprocess.CompletedProcess[str]:
"""Run one CLI command in a child process of this interpreter."""
env = {**os.environ, "COLUMNS": "120", "NO_COLOR": "1", "TERM": "dumb"}
command = [sys.executable, "-m", "remove_ai_watermarks.cli", *args]
log.info("Running CLI command: %s", command)
completed = subprocess.run( # noqa: S603 -- fixed interpreter, argument list, no shell
command,
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
env=env,
check=False,
)
log.info("CLI command exited %d: %s", completed.returncode, command)
return completed
def _validated(tool: str, arguments: dict[str, Any]) -> dict[str, Any]:
"""Check arguments against the tool's own input schema and fill its defaults."""
schema = _TOOL_SCHEMAS[tool]
properties: dict[str, dict[str, Any]] = schema["properties"]
unknown = sorted(set(arguments) - set(properties))
if unknown:
raise ToolInputError(f"unknown argument(s): {', '.join(unknown)}")
values: dict[str, Any] = {}
for key, spec in properties.items():
if key not in arguments:
if key in schema["required"]:
raise ToolInputError(f"'{key}' is required")
if "default" in spec:
values[key] = spec["default"]
continue
value = arguments[key]
if spec["type"] == "boolean" and not isinstance(value, bool):
raise ToolInputError(f"'{key}' must be a boolean")
if spec["type"] == "string" and (not isinstance(value, str) or not value.strip()):
raise ToolInputError(f"'{key}' must be a non-empty string")
if "enum" in spec and value not in spec["enum"]:
raise ToolInputError(f"'{key}' must be one of {', '.join(spec['enum'])}")
values[key] = value
return values
def _is_video(path: str) -> bool:
return Path(path).suffix.lower() in VIDEO_EXTENSIONS
def _identify_command(values: dict[str, Any]) -> tuple[list[str], str | None]:
path = values["path"]
prefix = ["video", "identify"] if _is_video(path) else ["identify"]
return [*prefix, "--json", *([] if values["check_visible"] else ["--no-visible"]), "--", path], None
def _remove_command(values: dict[str, Any]) -> tuple[list[str], str | None]:
path, mode = values["path"], values["mode"]
source = Path(path)
output = values.get("output") or str(source.with_stem(source.stem + "_clean"))
if Path(output).resolve() == source.resolve():
raise ToolInputError("'output' must differ from the source; the server never overwrites an input")
command = ["metadata", "--remove"] if mode == "metadata" else [mode]
return [*(["video"] if _is_video(path) else []), *command, "-o", output, "--", path], output
def _batch_command(values: dict[str, Any]) -> tuple[list[str], str | None]:
directory = values["directory"]
resolved = Path(directory).resolve()
output_dir = values.get("output_dir") or str(resolved.with_name(resolved.name + "_clean"))
if Path(output_dir).resolve() == resolved:
raise ToolInputError("'output_dir' must differ from the input directory")
command = ["video", "batch"] if values["media"] == "video" else ["batch"]
return [*command, "--mode", values["mode"], "-o", output_dir, "--", directory], output_dir
_COMMANDS: dict[str, Callable[[dict[str, Any]], tuple[list[str], str | None]]] = {
"identify": _identify_command,
"remove": _remove_command,
"batch": _batch_command,
}
def call_tool(name: str, arguments: dict[str, Any], runner: Runner = run_cli) -> dict[str, Any]:
"""Run one tool and return an MCP ``tools/call`` result."""
try:
args, output = _COMMANDS[name](_validated(name, arguments))
except ToolInputError as exc:
return {"content": [{"type": "text", "text": f"Invalid arguments: {exc}"}], "isError": True}
completed = runner(args)
code = completed.returncode
text = completed.stdout.strip()
stderr = completed.stderr.strip()[-4000:]
structured: dict[str, Any] = {"command": ["remove-ai-watermarks", *args], "exit_code": code, "output": output}
if "--json" in args and code == 0:
with contextlib.suppress(json.JSONDecodeError):
structured["report"] = json.loads(text)
if "report" not in structured:
structured["stdout"] = text
if stderr:
structured["stderr"] = stderr
# The CLI's own exit 2 means no supported signal, a result. Click also exits 2 on
# a usage error, such as a missing file, and prints its usage line on stderr.
no_signal = code == 2 and "Usage:" not in stderr
is_error = code != 0 and not no_signal
summary = f"exit code {code}"
if no_signal:
summary += " (no supported signal found; this is unknown, not proof the file is clean)"
body = "\n\n".join(part for part in (summary, text, stderr if is_error else "") if part)
return {"content": [{"type": "text", "text": body}], "structuredContent": structured, "isError": is_error}
def _error(request_id: object, code: int, message: str) -> dict[str, Any]:
return {"jsonrpc": "2.0", "id": request_id, "error": {"code": code, "message": message}}
def _object(value: object) -> dict[str, Any] | None:
"""A decoded JSON object, or ``None`` for any other JSON value."""
return cast("dict[str, Any]", value) if isinstance(value, dict) else None
def handle(raw: object, runner: Runner = run_cli) -> dict[str, Any] | None:
"""Answer one JSON-RPC message; ``None`` for notifications and client responses."""
message = _object(raw)
if message is None or message.get("jsonrpc") != "2.0":
return _error(None, -32600, "Invalid Request")
method = message.get("method")
if "id" not in message:
return None # a notification or a response to nothing this server sent
request_id: object = message["id"]
if not isinstance(method, str):
return _error(request_id, -32600, "Invalid Request")
params = _object(message.get("params") or {})
if params is None:
return _error(request_id, -32602, "params must be an object")
if method == "initialize":
requested = params.get("protocolVersion")
result: dict[str, Any] = {
"protocolVersion": requested if requested in PROTOCOL_VERSIONS else PROTOCOL_VERSIONS[0],
"capabilities": {"tools": {"listChanged": False}},
"serverInfo": {"name": "remove-ai-watermarks", "title": "Remove AI Watermarks", "version": __version__},
"instructions": INSTRUCTIONS,
}
elif method == "ping":
result = {}
elif method == "tools/list":
result = {"tools": TOOLS}
elif method == "tools/call":
name = params.get("name")
arguments = _object(params.get("arguments") or {})
if not isinstance(name, str) or name not in _COMMANDS:
return _error(request_id, -32602, f"Unknown tool: {name}")
if arguments is None:
return _error(request_id, -32602, "arguments must be an object")
result = call_tool(name, arguments, runner)
else:
return _error(request_id, -32601, f"Method not found: {method}")
return {"jsonrpc": "2.0", "id": request_id, "result": result}
def serve(stdin: IO[str] | None = None, stdout: IO[str] | None = None, runner: Runner = run_cli) -> None:
"""Serve newline-delimited JSON-RPC until stdin closes."""
source: IO[str] = sys.stdin if stdin is None else stdin
sink: IO[str] = sys.stdout if stdout is None else stdout
for line in source:
if not line.strip():
continue
try:
message = json.loads(line)
except json.JSONDecodeError:
response: dict[str, Any] | None = _error(None, -32700, "Parse error")
else:
response = handle(message, runner)
if response is not None:
sink.write(json.dumps(response, ensure_ascii=False) + "\n")
sink.flush()
+1 -1
View File
@@ -135,7 +135,7 @@ def load_video_vae_runtime(
dtype = torch.float16 if resolved_device == "cuda" else torch.float32
log.info("Loading %s on %s", model, resolved_device)
vae = AutoencoderKL.from_pretrained(model, torch_dtype=dtype).to(resolved_device)
vae.eval()
vae.train(False)
vae.enable_slicing()
scaling_factor = float(vae.config.scaling_factor)
log.info("Latent scaling factor %.5f", scaling_factor)
+5 -3
View File
@@ -14,6 +14,7 @@ ROOT = Path(__file__).resolve().parents[1]
SKILL = ROOT / "skills" / "remove-ai-watermarks" / "SKILL.md"
PLUGIN = ROOT / "skills" / ".claude-plugin" / "plugin.json"
MARKETPLACE = ROOT / ".claude-plugin" / "marketplace.json"
CODEX_PLUGIN = ROOT / ".codex-plugin" / "plugin.json"
AGENT_DOC = ROOT / "docs" / "agent-skill.md"
INSTALL_DOC = ROOT / "docs" / "installation.md"
INSTALL_REFERENCE = ROOT / "skills" / "remove-ai-watermarks" / "references" / "install.md"
@@ -541,15 +542,16 @@ def test_probe_minimum_version_is_a_release_that_exists() -> None:
assert current >= probe.MIN_CLI_VERSION, "the probe demands a version the project has not released"
def test_skill_version_agrees_across_all_three_manifests() -> None:
"""Three files carry this version, and a catalog reads a different one from each."""
def test_skill_version_agrees_across_every_manifest() -> None:
"""Four files carry this version, and a catalog reads a different one from each."""
frontmatter = _frontmatter(SKILL.read_text(encoding="utf-8"))
match = re.search(r'(?m)^\s+version:\s*"?([0-9][^"\n]*?)"?\s*$', frontmatter)
assert match is not None, "SKILL.md frontmatter no longer carries metadata.version"
plugin = json.loads(PLUGIN.read_text(encoding="utf-8"))
marketplace = json.loads(MARKETPLACE.read_text(encoding="utf-8"))
codex = json.loads(CODEX_PLUGIN.read_text(encoding="utf-8"))
assert match.group(1) == plugin["version"] == marketplace["metadata"]["version"]
assert match.group(1) == plugin["version"] == marketplace["metadata"]["version"] == codex["version"]
def test_a_bad_invocation_shares_the_no_signal_exit_code(tmp_path: Path) -> None:
+75
View File
@@ -0,0 +1,75 @@
"""The Codex plugin manifest, held to the checks a Codex plugin catalog runs on it."""
from __future__ import annotations
import json
import re
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / ".codex-plugin" / "plugin.json"
CLAUDE_PLUGIN = ROOT / "skills" / ".claude-plugin" / "plugin.json"
def _manifest() -> dict:
return json.loads(MANIFEST.read_text(encoding="utf-8"))
def test_manifest_carries_every_field_the_catalog_validator_requires() -> None:
manifest = _manifest()
for field in ("name", "version", "description", "repository", "license"):
assert str(manifest.get(field, "")).strip(), field
interface = manifest["interface"]
for field in ("displayName", "shortDescription", "composerIcon"):
assert str(interface.get(field, "")).strip(), field
assert re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", manifest["name"])
assert re.fullmatch(r"\d+\.\d+\.\d+", manifest["version"])
assert re.fullmatch(r"#[0-9A-Fa-f]{6}", interface["brandColor"])
def test_manifest_describes_the_same_plugin_as_the_claude_manifest() -> None:
manifest = _manifest()
claude = json.loads(CLAUDE_PLUGIN.read_text(encoding="utf-8"))
for field in ("name", "description", "repository", "license", "homepage"):
assert manifest[field] == claude[field], field
def test_every_declared_path_stays_inside_the_repository() -> None:
manifest = _manifest()
interface = manifest["interface"]
paths = [manifest["skills"], interface["composerIcon"], interface["logo"]]
for declared in paths:
assert declared.startswith("./"), declared
resolved = (ROOT / declared).resolve()
assert resolved.is_relative_to(ROOT), declared
assert resolved.exists(), declared
assert (ROOT / manifest["skills"] / "remove-ai-watermarks" / "SKILL.md").is_file()
def test_icon_is_a_small_self_contained_svg() -> None:
icon = ROOT / _manifest()["interface"]["composerIcon"]
content = icon.read_text(encoding="utf-8")
assert icon.suffix == ".svg"
assert icon.stat().st_size <= 50 * 1024
for forbidden in ("data:image/", "base64", "TODO", "PLACEHOLDER"):
assert forbidden not in content, forbidden
def test_no_tracked_code_matches_the_scanner_dynamic_execution_rule() -> None:
"""The catalog's scanner reports a high finding for any call spelled like
Python's built-in evaluator in its code files, and that rule cannot tell it from
the PyTorch module method of the same name. Its scan job is advisory, but the
finding still lands on the listing PR, so the code switches modules to inference
with the equivalent ``module.train(False)``."""
tracked = subprocess.run(
["git", "ls-files", "*.py", "*.js", "*.mjs", "*.cjs", "*.ts", "*.tsx", "*.jsx"], # noqa: S607
cwd=ROOT,
check=True,
capture_output=True,
text=True,
).stdout.split()
assert tracked
rule = re.compile(r"\beval\s*\(")
offenders = [name for name in tracked if rule.search((ROOT / name).read_text(encoding="utf-8", errors="ignore"))]
assert offenders == []
+272
View File
@@ -0,0 +1,272 @@
"""`remove-ai-watermarks mcp`: the protocol layer, the CLI mapping, and real clients."""
from __future__ import annotations
import io
import json
import re
import subprocess
import sys
from pathlib import Path
import pytest
from remove_ai_watermarks import mcp_server
from remove_ai_watermarks.metadata import KLING_SEI_TAG
ROOT = Path(__file__).resolve().parents[1]
KLING_MP4 = ROOT / "data/fixtures/provenance/higgsfield-kling-3-0-turbo.mp4"
SERVER = [sys.executable, "-m", "remove_ai_watermarks.cli", "mcp"]
class _Recorder:
"""Stands in for the CLI child process and records each argument list."""
def __init__(self, returncode: int = 0, stdout: str = "", stderr: str = "") -> None:
self.calls: list[list[str]] = []
self._result = (returncode, stdout, stderr)
def __call__(self, args: list[str]) -> subprocess.CompletedProcess[str]:
self.calls.append(args)
returncode, stdout, stderr = self._result
return subprocess.CompletedProcess(args, returncode, stdout, stderr)
def _request(method: str, params: dict | None = None, request_id: int = 1) -> dict:
message: dict = {"jsonrpc": "2.0", "id": request_id, "method": method}
if params is not None:
message["params"] = params
return message
def _assert_the_cli_accepts(args: list[str]) -> None:
"""Every subcommand and flag a tool builds must exist in the real Click tree."""
import click
from remove_ai_watermarks.cli import main
command: click.Command = main
position = 0
while isinstance(command, click.Group):
command = command.commands[args[position]]
position += 1
options = {option for param in command.params for option in (*param.opts, *param.secondary_opts)}
flags = [arg for arg in args[position : args.index("--")] if arg.startswith("-")]
assert set(flags) <= options, (args, sorted(set(flags) - options))
@pytest.mark.parametrize(
("requested", "negotiated"),
[("2025-06-18", "2025-06-18"), ("2024-11-05", "2024-11-05"), ("2099-01-01", mcp_server.PROTOCOL_VERSIONS[0])],
)
def test_initialize_negotiates_a_supported_protocol_version(requested: str, negotiated: str) -> None:
response = mcp_server.handle(_request("initialize", {"protocolVersion": requested, "capabilities": {}}))
assert response is not None
assert response["result"]["protocolVersion"] == negotiated
assert response["result"]["capabilities"] == {"tools": {"listChanged": False}}
assert "third-party" in response["result"]["instructions"]
def test_notifications_get_no_reply_and_unknown_methods_get_method_not_found() -> None:
assert mcp_server.handle({"jsonrpc": "2.0", "method": "notifications/initialized"}) is None
# A modern client probes server/discover first and falls back to initialize on this error.
response = mcp_server.handle(_request("server/discover"))
assert response is not None
assert response["error"]["code"] == -32601
def test_tools_list_exposes_exactly_the_three_tools() -> None:
response = mcp_server.handle(_request("tools/list"))
assert response is not None
tools = response["result"]["tools"]
assert [tool["name"] for tool in tools] == ["identify", "remove", "batch"]
assert tools[0]["annotations"]["readOnlyHint"] is True
for tool in tools:
assert tool["inputSchema"]["type"] == "object"
assert tool["inputSchema"]["additionalProperties"] is False
@pytest.mark.parametrize(
("arguments", "expected"),
[
({"path": "a.png"}, ["identify", "--json", "--", "a.png"]),
({"path": "a.png", "check_visible": False}, ["identify", "--json", "--no-visible", "--", "a.png"]),
({"path": "clip.MP4"}, ["video", "identify", "--json", "--", "clip.MP4"]),
],
)
def test_identify_maps_to_the_cli(arguments: dict, expected: list[str]) -> None:
runner = _Recorder(stdout='{"is_ai_generated": true}')
result = mcp_server.call_tool("identify", arguments, runner)
assert runner.calls == [expected]
_assert_the_cli_accepts(expected)
assert result["isError"] is False
assert result["structuredContent"]["report"] == {"is_ai_generated": True}
@pytest.mark.parametrize(
("arguments", "expected"),
[
({"path": "d/a.png"}, ["metadata", "--remove", "-o", "d/a_clean.png", "--", "d/a.png"]),
({"path": "a.jpg", "mode": "visible"}, ["visible", "-o", "a_clean.jpg", "--", "a.jpg"]),
({"path": "a.png", "mode": "all", "output": "o.png"}, ["all", "-o", "o.png", "--", "a.png"]),
({"path": "c.mov"}, ["video", "metadata", "--remove", "-o", "c_clean.mov", "--", "c.mov"]),
({"path": "c.mp4", "mode": "visible"}, ["video", "visible", "-o", "c_clean.mp4", "--", "c.mp4"]),
],
)
def test_remove_maps_to_the_cli_and_always_names_an_output(arguments: dict, expected: list[str]) -> None:
runner = _Recorder()
result = mcp_server.call_tool("remove", arguments, runner)
assert runner.calls == [expected]
_assert_the_cli_accepts(expected)
assert result["structuredContent"]["output"] == expected[expected.index("-o") + 1]
@pytest.mark.parametrize(
("arguments", "expected"),
[
({"directory": "in"}, ["batch", "--mode", "metadata", "-o", str(Path("in_clean").resolve()), "--", "in"]),
(
{"directory": "in", "media": "video", "mode": "visible", "output_dir": "out"},
["video", "batch", "--mode", "visible", "-o", "out", "--", "in"],
),
],
)
def test_batch_maps_to_the_cli(arguments: dict, expected: list[str]) -> None:
runner = _Recorder()
result = mcp_server.call_tool("batch", arguments, runner)
assert runner.calls == [expected]
assert result["structuredContent"]["output"] == expected[expected.index("-o") + 1]
_assert_the_cli_accepts(expected)
@pytest.mark.parametrize(
("tool", "arguments"),
[
("remove", {"path": "a.png", "output": "./a.png"}),
("remove", {"path": "a.png", "mode": "invisible"}),
("remove", {"path": ""}),
("remove", {}),
("identify", {"path": "a.png", "check_visible": "no"}),
("identify", {"path": "a.png", "shell": "rm -rf /"}),
("batch", {"directory": "in", "output_dir": "in"}),
],
)
def test_invalid_arguments_are_refused_before_anything_runs(tool: str, arguments: dict) -> None:
runner = _Recorder()
result = mcp_server.call_tool(tool, arguments, runner)
assert runner.calls == []
assert result["isError"] is True
@pytest.mark.parametrize(
("returncode", "stderr", "is_error"),
[
(0, "", False),
(2, "", False),
(1, "Error: boom", True),
# Click's own usage errors also exit 2; they are failures, not a no-signal verdict.
(2, "Usage: cli metadata [OPTIONS] SOURCE\nError: Invalid value for 'SOURCE'", True),
],
)
def test_only_failures_are_tool_errors(returncode: int, stderr: str, is_error: bool) -> None:
runner = _Recorder(returncode=returncode, stdout="table", stderr=stderr)
result = mcp_server.call_tool("remove", {"path": "a.png"}, runner)
assert result["isError"] is is_error
assert result["structuredContent"]["exit_code"] == returncode
assert ("not proof the file is clean" in result["content"][0]["text"]) is (returncode == 2 and not is_error)
if is_error:
assert stderr.splitlines()[-1] in result["content"][0]["text"]
def test_serve_answers_line_by_line_and_reports_parse_errors() -> None:
stdin = io.StringIO(
json.dumps(_request("ping", request_id=7))
+ "\n\nnot json\n"
+ json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"})
+ "\n"
)
stdout = io.StringIO()
mcp_server.serve(stdin, stdout, _Recorder())
replies = [json.loads(line) for line in stdout.getvalue().splitlines()]
assert replies == [
{"jsonrpc": "2.0", "id": 7, "result": {}},
{"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "Parse error"}},
]
def test_the_cli_subcommand_serves_real_commands_over_stdio(tmp_path: Path) -> None:
"""End to end through the installed CLI: every stdout line must be a protocol message."""
output = tmp_path / "clean.mp4"
messages = [
_request("initialize", {"protocolVersion": "2025-06-18", "capabilities": {}}, 1),
{"jsonrpc": "2.0", "method": "notifications/initialized"},
_request("tools/call", {"name": "identify", "arguments": {"path": str(KLING_MP4), "check_visible": False}}, 2),
_request("tools/call", {"name": "remove", "arguments": {"path": str(KLING_MP4), "output": str(output)}}, 3),
]
completed = subprocess.run( # noqa: S603
SERVER,
input="".join(json.dumps(message) + "\n" for message in messages),
capture_output=True,
text=True,
check=True,
timeout=300,
)
replies = {reply["id"]: reply for reply in map(json.loads, completed.stdout.splitlines())}
assert sorted(replies) == [1, 2, 3]
report = replies[2]["result"]["structuredContent"]["report"]
assert report["platform"] == "Kuaishou Kling AI"
assert "kling_sei" in report["metadata_markers"]
assert replies[3]["result"]["isError"] is False
assert KLING_SEI_TAG not in output.read_bytes()
def test_the_official_sdk_client_connects_and_calls_a_tool() -> None:
"""The reference client probes server/discover first, then falls back to initialize."""
anyio = pytest.importorskip("anyio")
mcp = pytest.importorskip("mcp")
async def session() -> tuple[list[str], dict]:
params = mcp.StdioServerParameters(command=SERVER[0], args=SERVER[1:], cwd=str(ROOT))
async with mcp.Client(params) as client:
tools = await client.list_tools()
result = await client.call_tool("identify", {"path": str(KLING_MP4), "check_visible": False})
return [tool.name for tool in tools.tools], result.structured_content
names, structured = anyio.run(session)
assert names == ["identify", "remove", "batch"]
assert structured["report"]["platform"] == "Kuaishou Kling AI"
def test_registry_manifest_matches_the_package_and_the_readme_marker() -> None:
"""The registry launches `uvx <identifier>==<version> <packageArguments>` and verifies
ownership by the README's `mcp-name:` marker, so each of those must agree."""
from click.testing import CliRunner
from remove_ai_watermarks import __version__
from remove_ai_watermarks.cli import main
manifest = json.loads((ROOT / "server.json").read_text(encoding="utf-8"))
(package,) = manifest["packages"]
readme = (ROOT / "README.md").read_text(encoding="utf-8")
assert manifest["version"] == package["version"] == __version__
assert package["identifier"] == "remove-ai-watermarks"
assert re.search(rf"mcp-name: {re.escape(manifest['name'])}(?=\s|-->|<)", readme)
args = [argument["value"] for argument in package["packageArguments"]]
assert CliRunner().invoke(main, [*args, "--help"]).exit_code == 0
Generated
+309 -29
View File
@@ -824,7 +824,7 @@ name = "cuda-bindings"
version = "13.4.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cuda-pathfinder" },
{ name = "cuda-pathfinder", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/3a/06569edfd5db020c1ff7c0361268a3eaa60ead0c0ccca460fef69d68022f/cuda_bindings-13.4.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df8b3767facca8acde216460df684dfc3826d519096c13adfd3030a55870dc79", size = 6494098, upload-time = "2026-09-23T02:21:50.472Z" },
@@ -857,43 +857,43 @@ wheels = [
[package.optional-dependencies]
cublas = [
{ name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
cudart = [
{ name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
cufft = [
{ name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
cufile = [
{ name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
cupti = [
{ name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
curand = [
{ name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
cusolver = [
{ name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
cusparse = [
{ name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
nvjitlink = [
{ name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
nvrtc = [
{ name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
nvtx = [
{ name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
{ name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
[[package]]
@@ -1220,8 +1220,8 @@ name = "httpcore2"
version = "2.13.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "h11" },
{ name = "truststore" },
{ name = "h11", marker = "sys_platform != 'emscripten'" },
{ name = "truststore", marker = "sys_platform != 'emscripten'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/f3/1db7aa2bc2524062192bb0e0323969492d1883152a232fe36eea65f4e35c/httpcore2-2.13.1.tar.gz", hash = "sha256:e0aa977abe17e69a3b820a24542a6fa88702676d83880b8d194dcd18408e5103", size = 68071, upload-time = "2026-09-23T07:47:22.372Z" }
wheels = [
@@ -1475,6 +1475,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/8b/d76219ebdbcf3d4209d9d21a0810db4c8d0a6f88e3ee87d30bdea4e90d30/jiter-0.17.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c0bf24c72fd0491405dce5d40194f2070e9021ce648c1a1d46234b93d848ff", size = 307147, upload-time = "2026-09-12T15:14:12.897Z" },
]
[[package]]
name = "jsonschema"
version = "4.26.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "jsonschema-specifications" },
{ name = "referencing" },
{ name = "rpds-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
]
[[package]]
name = "jsonschema-specifications"
version = "2025.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "referencing" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
]
[[package]]
name = "lightning"
version = "2.6.6"
@@ -1594,6 +1621,44 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
[[package]]
name = "mcp"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "httpx2" },
{ name = "jsonschema" },
{ name = "mcp-types" },
{ name = "opentelemetry-api" },
{ name = "pydantic" },
{ name = "pyjwt", extra = ["crypto"] },
{ name = "python-multipart" },
{ name = "pywin32", marker = "sys_platform == 'win32'" },
{ name = "sse-starlette" },
{ name = "starlette" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9d/8d/e0d339616f4810e9051d4aba6887afab289ab1f81875fe908b606cdfd0e3/mcp-2.3.0.tar.gz", hash = "sha256:8b147a50441cf059dc88c684e0aeed3687f0aa0f39c6cde7b90330effd2b34d8", size = 4171955, upload-time = "2026-10-02T22:06:56.091Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/d7/4a2878f128df729db3ca04e71bfce26405cc2e0c9ebe19c592feeb4c61b9/mcp-2.3.0-py3-none-any.whl", hash = "sha256:dd0c44c089d16453e8ae31a3877a0054d7a2314caaa81f5e0541b9b1734b2377", size = 368664, upload-time = "2026-10-02T22:06:52.119Z" },
]
[[package]]
name = "mcp-types"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9e/2d/7c251e34207f6c51000312fc8839111ac45cfe02023f90b44e7f1051dd8e/mcp_types-2.3.0.tar.gz", hash = "sha256:d1e46549edb35ee19a94940fcee6d1addd7e589ab7ea92dda83f5d84781fc362", size = 65898, upload-time = "2026-10-02T22:06:57.863Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/48/32583066b155bb5d4507e7887dc88ee23118cc4de8e2a12870c49719e2a8/mcp_types-2.3.0-py3-none-any.whl", hash = "sha256:968efdbdaedfab06adae40d378a34395f1090c5921d4be3c9cde283aaf76d91d", size = 69108, upload-time = "2026-10-02T22:06:54.099Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
@@ -1898,7 +1963,7 @@ name = "nvidia-cublas"
version = "13.1.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cuda-nvrtc" },
{ name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" },
@@ -1937,7 +2002,7 @@ name = "nvidia-cudnn-cu13"
version = "9.24.0.43"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cublas" },
{ name = "nvidia-cublas", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/ca/30/7c257e3d5cb4fecb147b93895c66e29c93f8e76d74b45bb418ff0587c4ec/nvidia_cudnn_cu13-9.24.0.43-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:a6812a554a1ff0413e9c52b84c26c050380649ab9615f9c16bded368ce9f421f", size = 650976863, upload-time = "2026-07-02T16:23:39.248Z" },
@@ -1949,7 +2014,7 @@ name = "nvidia-cufft"
version = "12.0.0.61"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-nvjitlink" },
{ name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" },
@@ -1979,9 +2044,9 @@ name = "nvidia-cusolver"
version = "12.0.4.66"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cublas" },
{ name = "nvidia-cusparse" },
{ name = "nvidia-nvjitlink" },
{ name = "nvidia-cublas", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "nvidia-cusparse", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" },
@@ -1993,7 +2058,7 @@ name = "nvidia-cusparse"
version = "12.6.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-nvjitlink" },
{ name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" },
@@ -2167,6 +2232,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/88/763b967f7efd7226b82c9fae16d560cba049b1f0c036647e65c610fd636e/opencv_python_headless-5.0.0.93-cp37-abi3-win_amd64.whl", hash = "sha256:829717b6a95554f273e49e357cee3b3a2a26b6f4842fbc1bed2b45bdd8f87e0e", size = 43825962, upload-time = "2026-07-02T05:50:09.627Z" },
]
[[package]]
name = "opentelemetry-api"
version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2e/02/6e0ae9cc61bd3169d401077b507b3ebc344745171e1051ab430be012dcd9/opentelemetry_api-1.45.1.tar.gz", hash = "sha256:aa38ed19bcc084ba42782a73255b3582283eced7ad6dddbd6695189e69adfb75", size = 72804, upload-time = "2026-10-06T17:32:58.133Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/41/f7dcf80b81ee8e71c1a2b59f14208bc723edbd89ed027a73b175abf6348e/opentelemetry_api-1.45.1-py3-none-any.whl", hash = "sha256:b31553efa588ae44bc306f863c785c5333a9ecc091248c6ee68b4b6c87fdedfb", size = 60256, upload-time = "2026-10-06T17:32:33.506Z" },
]
[[package]]
name = "opt-einsum"
version = "3.3.0"
@@ -2875,6 +2952,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
]
[[package]]
name = "pyjwt"
version = "2.15.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/ea/5194e52748b0da83d71e082d75496eaec6e58f419f5e184786ded517e6a9/pyjwt-2.15.1.tar.gz", hash = "sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8", size = 121252, upload-time = "2026-09-28T18:40:42.598Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/50/ca/44de4e75f8aadc457f0634be3b542815078ded46dca30efb960edeecad6e/pyjwt-2.15.1-py3-none-any.whl", hash = "sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193", size = 33860, upload-time = "2026-09-28T18:40:41.429Z" },
]
[package.optional-dependencies]
crypto = [
{ name = "cryptography" },
]
[[package]]
name = "pypdfium2"
version = "5.13.0"
@@ -3071,6 +3162,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" },
]
[[package]]
name = "python-multipart"
version = "0.0.32"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" },
]
[[package]]
name = "pytorch-lightning"
version = "2.6.6"
@@ -3102,7 +3202,7 @@ resolution-markers = [
"(python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
]
dependencies = [
{ name = "numpy" },
{ name = "numpy", marker = "python_full_version < '3.12'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5a/75/50581633d199812205ea8cdd0f6d52f12a624886b74bf1486335b67f01ff/pywavelets-1.9.0.tar.gz", hash = "sha256:148d12203377772bea452a59211d98649c8ee4a05eff019a9021853a36babdc8", size = 3938340, upload-time = "2025-08-04T16:20:04.978Z" }
wheels = [
@@ -3178,7 +3278,7 @@ resolution-markers = [
"(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",
]
dependencies = [
{ name = "numpy" },
{ name = "numpy", marker = "python_full_version >= '3.12'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/25/84/a9e80e33eb114e78bd2f1829a90d72d8d4d660109c721a8aa2f0d58d7592/pywavelets-1.10.0.tar.gz", hash = "sha256:f3cb8640225a8f3fdcbc3f6f3bd112c78927c9407cbfa65a5ff303990a6a3e32", size = 3948163, upload-time = "2026-09-07T17:15:15.637Z" }
wheels = [
@@ -3220,6 +3320,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/61/0375be8264b1b86b3182459bec94ac0d6ffba08c9b6a090840d4ba38d05b/pywavelets-1.10.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aeb5073bb1859f7a911b275ab0fef1e41acede52fb1661331d74cb7f45ecbb4c", size = 4217922, upload-time = "2026-09-07T17:14:40.596Z" },
]
[[package]]
name = "pywin32"
version = "312"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" },
{ url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" },
{ url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" },
{ url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" },
{ url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" },
{ url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" },
{ url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" },
{ url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" },
{ url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" },
{ url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" },
{ url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" },
{ url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.2"
@@ -3255,6 +3374,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
]
[[package]]
name = "referencing"
version = "0.37.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "rpds-py" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
]
[[package]]
name = "regex"
version = "2026.9.29"
@@ -3405,6 +3538,7 @@ dev = [
{ name = "httpx" },
{ name = "huggingface-hub" },
{ name = "invisible-watermark" },
{ name = "mcp" },
{ name = "numpy" },
{ name = "openai" },
{ name = "opencv-python-headless" },
@@ -3515,6 +3649,7 @@ requires-dist = [
{ name = "huggingface-hub", marker = "extra == 'source-classify'", specifier = ">=0.20.0,<2" },
{ name = "huggingface-hub", marker = "extra == 'text-draft'", specifier = ">=0.20.0,<2" },
{ name = "invisible-watermark", marker = "extra == 'dev'", specifier = ">=0.2.0" },
{ name = "mcp", marker = "extra == 'dev'", specifier = ">=2.3,<3" },
{ name = "numpy", marker = "python_full_version >= '3.13' and extra == 'pixels'", specifier = ">=2" },
{ name = "numpy", marker = "python_full_version < '3.13' and extra == 'pixels'", specifier = ">=1.24.0" },
{ name = "onnxruntime", marker = "extra == 'classify-onnx'", specifier = ">=1.24.0" },
@@ -3591,6 +3726,112 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
]
[[package]]
name = "rpds-py"
version = "2026.9.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/42/68/3bd46b8a5e01d3c2ebdf9c5e9497912e3fe0cde02bac21a7130ca866e403/rpds_py-2026.9.1.tar.gz", hash = "sha256:4793ef7f78268b124b73fa933440f01d258bbae01de9fa53e9080c9ab0425a12", size = 63948, upload-time = "2026-10-04T16:32:36.469Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f5/6d/ddb8dfcf41892ba9c672d27988592c038d30db61708230313831b6780a1b/rpds_py-2026.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2711d29b653b3bce48a63d18b9c6b53274669e6d6c4094dddeb4d9a0e45128b2", size = 344947, upload-time = "2026-10-04T16:28:55.207Z" },
{ url = "https://files.pythonhosted.org/packages/9e/b2/632c3d034961149eff606e3e357547620a9ab37b849ae5815f67838753fd/rpds_py-2026.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3231c4c0e521dafa5be0c9f114ee2c2ad46650836f2d72caa86801950c3e7044", size = 346770, upload-time = "2026-10-04T16:28:56.884Z" },
{ url = "https://files.pythonhosted.org/packages/6d/45/4df346410bb873d03ec60c3b891d1fceb8cabde1971d29844bbb625cc296/rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e01b3c878c8641913e688edd1b3f08658c6783d29cf6b826bd3c0d1ae7a1ffaa", size = 377795, upload-time = "2026-10-04T16:28:58.193Z" },
{ url = "https://files.pythonhosted.org/packages/ba/b8/a70f302207e4f29f873c01a986fcac5def49b9c8224328cef1ad7da6b7a9/rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3e524c7874ac72884d28e16dd5b8d839fd09e0fe76b020d3fbca23212a7b8c52", size = 383137, upload-time = "2026-10-04T16:28:59.411Z" },
{ url = "https://files.pythonhosted.org/packages/13/e3/3aa28a48a01169d0e20728bf0e5ebbea3ae2774285b40853ae49b3ef332a/rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761fdae6728ceb99ab182fad2f0cc1e262f610834dc891aea1d1a2a2e634776f", size = 496789, upload-time = "2026-10-04T16:29:01.011Z" },
{ url = "https://files.pythonhosted.org/packages/3e/a7/414e8d088602d66ad6f47c65bf07fe5090992a8574cdd4571c1a83b647ed/rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b723eb406dec5bc9ec516c73ab9c3239a3284e017f7eb89ee2b3258bd504fb7", size = 395929, upload-time = "2026-10-04T16:29:02.465Z" },
{ url = "https://files.pythonhosted.org/packages/2d/5c/aac3b960fd55d37b49f77c0eebbe043eaf36762ebd48e7b46951e59c1c05/rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:136a1c3fe4402b7008bc81cb62ee538481795b61a7e83df88dff3b3f02b726ff", size = 375426, upload-time = "2026-10-04T16:29:03.944Z" },
{ url = "https://files.pythonhosted.org/packages/d3/80/77987a41c40be7202a78d9aed4a650bfb05792237cfd3b9e22638d990fe5/rpds_py-2026.9.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:839dde845559254f34885267c6878f60d61d5205180226d976fe488d45fa128e", size = 381497, upload-time = "2026-10-04T16:29:05.308Z" },
{ url = "https://files.pythonhosted.org/packages/b8/d0/640b02eb1ecd71a913c1178b6235287b31006266bca9a26862e80e497b54/rpds_py-2026.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56cd8b3f77d7b6812f533b662186a1f28316931166ddc00fb893b1b0db7e9888", size = 405740, upload-time = "2026-10-04T16:29:06.578Z" },
{ url = "https://files.pythonhosted.org/packages/3e/8c/d034195d45d215625a23f2ae85dbbdb86a49a93a51272061c97c80b9d316/rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:074a4d198bc34d9a8ea425114fc3ded6d11ec01f6a314a8db67454a5152d8834", size = 555850, upload-time = "2026-10-04T16:29:08.115Z" },
{ url = "https://files.pythonhosted.org/packages/de/f8/532f1addfc86207bc3cf23749638bb0d457bce891595012ed8a08d0543b8/rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6beb738155fe8ab8091afdfa5a3226b21c2b1593f1e50ebb90eb25b44dbc0391", size = 618661, upload-time = "2026-10-04T16:29:09.554Z" },
{ url = "https://files.pythonhosted.org/packages/50/5b/b0f2c9833a36a5b1f6ac5cb0e635fabacf713266f46ceefba2102da0bf51/rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57492a550a1d88d29d003247e5f78dd8cf04a701fac0e4c8db8745a6d2504e0a", size = 583530, upload-time = "2026-10-04T16:29:11.072Z" },
{ url = "https://files.pythonhosted.org/packages/bf/20/464ed4b98d3c8c6a334f3b5e24d3886de04b05658a95fc40a37e0e6e4775/rpds_py-2026.9.1-cp311-cp311-win32.whl", hash = "sha256:d95a354e02393eada6d7351184671aced9d4cce109dabf927cb7aa99624352a1", size = 208125, upload-time = "2026-10-04T16:29:12.408Z" },
{ url = "https://files.pythonhosted.org/packages/df/c9/384a9b62f8cd89c8e3e609b8c9052c140e8c01d807261e0517c904b8bf29/rpds_py-2026.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:ce4d4f52e2a4324396caddbd45a97d8d7be5f42edd25d2355282a9c34f9b2f7f", size = 226045, upload-time = "2026-10-04T16:29:13.751Z" },
{ url = "https://files.pythonhosted.org/packages/4f/3b/0a691f0dcb2301fa9c736346da2d0d4991778373283301c5c12b8f952f3d/rpds_py-2026.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:fdcd198979b4ecffcc1beba366a7fbcf4eb41243691a82fe52ceb0b902f09c12", size = 222636, upload-time = "2026-10-04T16:29:14.989Z" },
{ url = "https://files.pythonhosted.org/packages/5d/34/a828586ea3329fbb50895b50e9cf98ca3d924a9f41a0b26d443bff1b3794/rpds_py-2026.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:50906f5aea24b5a865cbd0a589698288631d9f3a54c3a937c83aefa95a0d14af", size = 346822, upload-time = "2026-10-04T16:29:16.258Z" },
{ url = "https://files.pythonhosted.org/packages/90/81/ac6a0d064982251856ce009c9e1dd51a34110b3c055aa7d1aad18b4899a3/rpds_py-2026.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e21c1429e205828ea886a2293a4a2c8e01f4c25d9893ca330e97a6cf73f52e7b", size = 341380, upload-time = "2026-10-04T16:29:17.706Z" },
{ url = "https://files.pythonhosted.org/packages/42/ff/bf7d54f362748fd6a49277b9d6531c394074110fedf50ad791ec59133fbb/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693b2728bbcc48d09a981a356954b0c47c53ff25b545856f28a889ea619f69a", size = 373053, upload-time = "2026-10-04T16:29:19.063Z" },
{ url = "https://files.pythonhosted.org/packages/60/de/74b0ccdbbd28687b9b5fdb34c1cabed88352182facaced9d6f5486b8b9ed/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8601470267d938bcb7f3ab1a336100af51a4fd5b6ed030ef52461bb3ef5e7e07", size = 377742, upload-time = "2026-10-04T16:29:20.543Z" },
{ url = "https://files.pythonhosted.org/packages/c2/6d/b979775a3057b2a5c26d75ecbff60a20a24ef081db6dd76836fca3f20247/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3890a6aa36e6baa53d5258a2a25d3ef8b37ad165a6ab27a892d7c3e3a432cd69", size = 487289, upload-time = "2026-10-04T16:29:21.958Z" },
{ url = "https://files.pythonhosted.org/packages/1d/5d/7c34734ce3ece943d9d6ee0122a74a21bbc75b47acb5b7053c83f6efb1ed/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b5b393eda5ea42cca1c1a6665f2a4882b4fd5d1777e41ce0545a107fb008c9d", size = 390710, upload-time = "2026-10-04T16:29:23.378Z" },
{ url = "https://files.pythonhosted.org/packages/1d/6d/b26eb1e75395925b3a142ed351cbed2ec8212f5c9d937aee3df32c701baa/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:addeda51556dac7c1a2f14cda62db8b621cd12afba3091d03a96c72932387eab", size = 371261, upload-time = "2026-10-04T16:29:25.1Z" },
{ url = "https://files.pythonhosted.org/packages/7c/98/b2fdfe10301a9e27af21e634337fbba7baac0bc17fe44619a17c26bba2cb/rpds_py-2026.9.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d9edf30457d74eebfd76b045535e36f1cd89062566a128a0db2145ca042d787e", size = 376075, upload-time = "2026-10-04T16:29:26.748Z" },
{ url = "https://files.pythonhosted.org/packages/c5/b1/c4b8d954e49c3c69cf8063c0c9e0919c99f3d2cecc46311606a1cbe835dc/rpds_py-2026.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:815d26356930846a40c7bc1366e7b1b0320ab8a063e66c11298a208bed0fd237", size = 399358, upload-time = "2026-10-04T16:29:28.282Z" },
{ url = "https://files.pythonhosted.org/packages/b3/59/9559a7293c97dff0cbb293efffbd36644103883ecc27741af8ef90c84ca8/rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b5a6f40f0a1486b4b36c888123afc67acdbd9f33235927acf5ff295429a0ba3", size = 550267, upload-time = "2026-10-04T16:29:29.851Z" },
{ url = "https://files.pythonhosted.org/packages/6c/9d/6dc60e49511de4c8b00e74f9c1d43aed4d27f712cdb1ade92f2c199cbf64/rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b5b8b0753718d258fd454283fbd57e14545d3b40583fa672e27cb4f987626bcc", size = 614018, upload-time = "2026-10-04T16:29:31.54Z" },
{ url = "https://files.pythonhosted.org/packages/fb/bc/92a4ecf27301b886232a413360f6b348a81d55ffef654f2ebda5970ebc3d/rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:46d80bc76b51a6c24f9944368c28d38b8bcbcea1da4f2f8d3ebc31a67e8c6ec6", size = 578509, upload-time = "2026-10-04T16:29:33.01Z" },
{ url = "https://files.pythonhosted.org/packages/08/53/efb97f2e6589b7ab8394385a73870e0b2ab2a195281c117b21fd6d7d4855/rpds_py-2026.9.1-cp312-cp312-win32.whl", hash = "sha256:befc2d6a953e563f8a7bfd87a42c22ebf8a3e980dcb7b6a4d17b70b0e914e8a3", size = 205401, upload-time = "2026-10-04T16:29:34.451Z" },
{ url = "https://files.pythonhosted.org/packages/1c/84/1700cc748d0eaa747486e4e82882d2575224e6e9a3148df583058850c629/rpds_py-2026.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:5ce8943f79c2210f7abcc28e86367b03b28d95027fd01c46d2472373ae70c86f", size = 222923, upload-time = "2026-10-04T16:29:35.709Z" },
{ url = "https://files.pythonhosted.org/packages/c3/89/0302215373c2f8b4408b0fdfee955368cc378a98ff59508d47a7e0dc2dbf/rpds_py-2026.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:501909f2e4a1e2dee528ef766fe3c469060ebc17e54a8383d404ba07a81a6f02", size = 218415, upload-time = "2026-10-04T16:29:37.263Z" },
{ url = "https://files.pythonhosted.org/packages/83/ea/ee88fd9e756ff93fb6b1182a47ec09504a242620e33ce1d20679efefe841/rpds_py-2026.9.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a36b70596407634ca82d4b989a3729074a008537a0522e4c8046a67c729103e9", size = 346229, upload-time = "2026-10-04T16:29:38.82Z" },
{ url = "https://files.pythonhosted.org/packages/57/71/a097d6552f837500fc36e6b23d09cfb9890c3cc47531f9ca64e149799615/rpds_py-2026.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eba5d173f7d5708b22a93815017a4611873ed54db9f268077c0dd1ed99cfc858", size = 340514, upload-time = "2026-10-04T16:29:40.405Z" },
{ url = "https://files.pythonhosted.org/packages/bd/b7/497e85768bf4e0d8ddbaa096a4cac31d1509251dee2728a8490aa367e0b5/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:457866b85daf5034296666168b84a69e0b2e89dc4f1af102b46f6448a60b9063", size = 372611, upload-time = "2026-10-04T16:29:41.778Z" },
{ url = "https://files.pythonhosted.org/packages/52/4b/74ab4108916250b6e198e0d3af05bc6835eb046315f22f7a0ceb49667c5a/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a3a52a3ba86436ab3aef510fbe21512abc2ddd1993005dfe50514bd2284ef025", size = 377712, upload-time = "2026-10-04T16:29:43.242Z" },
{ url = "https://files.pythonhosted.org/packages/0c/8e/067e77d9d7b3cc793c9d909b7e97e7aadbbb1fb094093b6876902cc96d38/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7841166b7fa64c9c56404617ae4341448847482d45933b13135d26c130519e5", size = 488106, upload-time = "2026-10-04T16:29:44.692Z" },
{ url = "https://files.pythonhosted.org/packages/3c/b4/c5aae6c2dde269bf955f6b7d35065c655a57e47750d9668052ad67e74dda/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:926bdd3e3b5998ddf70cc64bc8cf57209571f9044542913afb673799fec77dd0", size = 390795, upload-time = "2026-10-04T16:29:46.129Z" },
{ url = "https://files.pythonhosted.org/packages/a0/36/76fab39973ee11e7f9f357c55138197bb01c86f6502cb76487e3b4f42db0/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7868b85224291c6cb6759f9b5adb9745f486d226f62b16a614dd5a2a5ab2b35b", size = 370794, upload-time = "2026-10-04T16:29:47.603Z" },
{ url = "https://files.pythonhosted.org/packages/3d/fe/cd2a80e6d7b871937a60e935c5d507aa390d143f4ff3636f640b9733d5df/rpds_py-2026.9.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3cd182d7291d29b92c521a0069d9c01ba6193628a9a105531d11b40a6d731a33", size = 375673, upload-time = "2026-10-04T16:29:49.223Z" },
{ url = "https://files.pythonhosted.org/packages/6c/18/7464a9953724e55a3b3206062fa0ffdeaa519584c6aabf65d3956d94f131/rpds_py-2026.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e6ea1cda8d8c688278430e4268a42f5e5da3bdd74578dfadc0820c3f1766ce83", size = 398758, upload-time = "2026-10-04T16:29:50.601Z" },
{ url = "https://files.pythonhosted.org/packages/c0/86/1534b436700fd49ff411063b7c4d7e938adfabf90895b6cf1622d5a7d1f6/rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5943980471829f6de242a20b109de3111ba6b77e3af0ffc587028ac854b05e6c", size = 550370, upload-time = "2026-10-04T16:29:52.002Z" },
{ url = "https://files.pythonhosted.org/packages/57/1c/e1fa82a8a01e3c5820f3ba98a8b2673f642128eb368fa88871b01dd2c909/rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:76d3af9732d2dab69f28179b40ba2d87e2f1d5824b4a694780aa787d685e8f36", size = 613106, upload-time = "2026-10-04T16:29:53.655Z" },
{ url = "https://files.pythonhosted.org/packages/29/55/b20b8c4c3dde8755bfcd5b08492a02d0cd2e26929aedf6199ca2a377d42a/rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:78326f4cb4427a56ba4996c0762b63be45f06b85f086526420d2b3a66e40f84d", size = 578235, upload-time = "2026-10-04T16:29:55.157Z" },
{ url = "https://files.pythonhosted.org/packages/55/42/df3f7bbc3f7ab37a8a9db8d6c2ff2c985422899f1f7926afbf7ec3c0b8b4/rpds_py-2026.9.1-cp313-cp313-win32.whl", hash = "sha256:172e47169583f46ce118cbec68e6795d0da0f4606b488b6434f8276bca0a058c", size = 205293, upload-time = "2026-10-04T16:29:56.669Z" },
{ url = "https://files.pythonhosted.org/packages/31/9c/ba5a9569d719bfdd6ce863df4133ac6a1658cf1b07cc3534c31db729fbbc/rpds_py-2026.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:3e93b2cd69a9830be33e03945cd7cda940a0a8bfcfbff41d6144f0cb0d3d8bd9", size = 222409, upload-time = "2026-10-04T16:29:58.049Z" },
{ url = "https://files.pythonhosted.org/packages/35/72/f28ca566f6c23c35bbf7445f65eb0364577b25a026305995e24f78b83d94/rpds_py-2026.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:d151e148117294133bf8af7eeace085e7e87432db15ab6adf640330298a47f6f", size = 217681, upload-time = "2026-10-04T16:29:59.449Z" },
{ url = "https://files.pythonhosted.org/packages/8a/f2/67b94be1532767803415c1c5a1fd88ea487643d74a673cec1ba140af77bb/rpds_py-2026.9.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c9d1aca01f49170fdcf5c92761b1fafe97f554b721ca4570c5949fff778f0d4b", size = 347178, upload-time = "2026-10-04T16:30:00.865Z" },
{ url = "https://files.pythonhosted.org/packages/04/37/b751de2b59b0197a1d92a5dd491de88e8a5e928c2e6562581974f1e85263/rpds_py-2026.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3f0e9ac28fc067d4d34b88ae43c48e9489455c97fee9633d851f7eeed5a05d35", size = 341878, upload-time = "2026-10-04T16:30:02.564Z" },
{ url = "https://files.pythonhosted.org/packages/72/e2/5873bc4643c250db9e05d48dc0c93763d4aa68bc3b81164cb1af3b45b284/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07deecbfce94c78473018bc7d10b337cc651d12df87a1eb2cb3e4024bc9c33d0", size = 373963, upload-time = "2026-10-04T16:30:04.026Z" },
{ url = "https://files.pythonhosted.org/packages/51/03/5acf7632158247f3f6386ff0af3a1ee48167d575037e8d0920594b76d92b/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:821b2755db9194409254012f429c56643416fb96ef9be090be82ec8826b7f477", size = 377975, upload-time = "2026-10-04T16:30:05.555Z" },
{ url = "https://files.pythonhosted.org/packages/e6/00/63fda451b8bffa5808fc8bb311ee7c073b340974b09f273c7b2a145d3d62/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c91c210ae7645626c608400e3519b4a642f837cce09ca830db3beb2e9f274d4", size = 490347, upload-time = "2026-10-04T16:30:07.156Z" },
{ url = "https://files.pythonhosted.org/packages/a1/ae/c093ffd070ba0fb02f76c565d06fecc65ad6e4afdbae78f7031076d3cdac/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54ac2158a6f96cfbabff0b2eedaf94b90c5ec7ca8317fcadc61e1c2b2e0ff6ef", size = 392962, upload-time = "2026-10-04T16:30:08.77Z" },
{ url = "https://files.pythonhosted.org/packages/22/9d/d08a1128ab199b2f0cf25bfeb0639bd05119fff4b7c47bec24ef9a8ec23f/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac2f5dbafd585dfe31f86a23ebf0d3ba480a9d49ebc87947267b5608d4ea0cd", size = 371623, upload-time = "2026-10-04T16:30:10.501Z" },
{ url = "https://files.pythonhosted.org/packages/53/c7/4758ddcbb75609414bbccfcb11d612436f9b3ee821bd2f33f0f1604ee648/rpds_py-2026.9.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:8aa5dda18d39b6143eb24809d158f9252c88f402749b6f1b62a506cc7d96cc35", size = 376997, upload-time = "2026-10-04T16:30:12.124Z" },
{ url = "https://files.pythonhosted.org/packages/87/e4/947bd7f608ff60faf46dc9d389c3dffd0e3d767d78a0be19978448ef0ce7/rpds_py-2026.9.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c90e7fa02e8f5de0d10c17595c568ada48c5302e749462c0ea1a4c362111a86", size = 400169, upload-time = "2026-10-04T16:30:13.804Z" },
{ url = "https://files.pythonhosted.org/packages/4b/35/fe93e020a0543b5670472c18d7e6af3197c08da571240ce1965c84f85c0f/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6d198bad4e49dd6732fbd636e2fc5c082f45c8cad0b4acb756b00c82c76072e", size = 551615, upload-time = "2026-10-04T16:30:15.332Z" },
{ url = "https://files.pythonhosted.org/packages/0d/4f/5d2a0136bb03b2a56a39dc6ff92d58a6e3e53a2e17079238b86228882f16/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:96beca19ec79de272e8668585380ff9092c47077c1d7a1e098e00bbd921f4785", size = 614655, upload-time = "2026-10-04T16:30:16.92Z" },
{ url = "https://files.pythonhosted.org/packages/09/1c/3f1025aaf70d9bf7272cc41f8b64ee76b48bf01726248138430e16f23b38/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5cf77eb04f20b720be95265a3e00eb2a14814074255cc27069c551b2db53118", size = 579602, upload-time = "2026-10-04T16:30:18.555Z" },
{ url = "https://files.pythonhosted.org/packages/53/0d/5c72e6204f76610608706da32b6b7e11ef7e10317558a7bb15a122e008dc/rpds_py-2026.9.1-cp314-cp314-win32.whl", hash = "sha256:a03d57b86d2a51d0a66c92177e2be154ad015f357791d306e714569999cdb4cc", size = 206315, upload-time = "2026-10-04T16:30:20.05Z" },
{ url = "https://files.pythonhosted.org/packages/a4/0b/489d48abbcc7d70cf3fbf662d9d22abf1f4650761c0a9ae05260800800d3/rpds_py-2026.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:837c6b305e26fe0f75b15c92cf3b2ba29e0ae19dc40b1c557b026cb426347d0c", size = 222852, upload-time = "2026-10-04T16:30:21.604Z" },
{ url = "https://files.pythonhosted.org/packages/91/16/bbb05a7e6a10cf79ba639be7f799d770ee15f64175cc61d081b218dd402a/rpds_py-2026.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:fce4b85234a0cbad67bf8e6e1201ee815d172c9aebad75f25645bc4d834f8e31", size = 218709, upload-time = "2026-10-04T16:30:23.036Z" },
{ url = "https://files.pythonhosted.org/packages/22/ac/ac507a0a4ec478ca470440a09583db4be5259ba7670aeba0620822f1e57a/rpds_py-2026.9.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a72c11530d71abfb66c8d7696a2f86c43e63fca8b948f1a784ac490f4ec688e", size = 349596, upload-time = "2026-10-04T16:30:24.558Z" },
{ url = "https://files.pythonhosted.org/packages/e9/f2/817a46b658d5070f477f722c298ee9a24525b0e4017347964146ef5fdd0e/rpds_py-2026.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:068c37bba854ec2fe42f7365c640af11dd9895890ccbf2df5070d0c059bd7f96", size = 343114, upload-time = "2026-10-04T16:30:26.048Z" },
{ url = "https://files.pythonhosted.org/packages/6c/42/6ade976b13ac1b4cb3bf2eb603f1be2fe74df19a29988d4c2b386be59d6f/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fca4eb6df565e2a928f1c7dad92d27db8f9df0f449e76423ed5d7e713ed445", size = 374197, upload-time = "2026-10-04T16:30:27.699Z" },
{ url = "https://files.pythonhosted.org/packages/d9/70/77cdf1d3f1a07faabe936016ae623aec7981f73108a8fe7a203ed2e21998/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c933c6678c6f116ff8af47a4c6db0868b8ace74af0343016c0ef00f00272ea69", size = 378025, upload-time = "2026-10-04T16:30:29.451Z" },
{ url = "https://files.pythonhosted.org/packages/3f/6b/18a44a3beaa9b7931acb04af7bd9539836477c630a794452d4826d6185d4/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:028ad274ea951dac64491b5d1e65712a4aeabfdbdb9fccf797b57bd899b0c495", size = 491969, upload-time = "2026-10-04T16:30:30.995Z" },
{ url = "https://files.pythonhosted.org/packages/73/27/fb39cfd6bddaf741b024f813890374578ff8ac1f1adc473659c048b03b05/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d0a99cf9de0b17a3943388e9294a59becf75e7c43421f387bd3c7a9901f7c", size = 393905, upload-time = "2026-10-04T16:30:32.628Z" },
{ url = "https://files.pythonhosted.org/packages/ed/71/0fa7bb77b57af0d710273964180d11b503f62b8a5c358eb2d8c3f62feff6/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0da298fb372dc192610a4b9ecbc68a0cd8b675bbbd1fc519d01b41cfd658333e", size = 374569, upload-time = "2026-10-04T16:30:34.257Z" },
{ url = "https://files.pythonhosted.org/packages/54/22/f41cfac269af3b449513ef1bc3d7f32fde52abbbd2d01c7e76b47743acd9/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:eb61be926bb81567c1f48bdc8aa22b9855048dc2efd53871f9f7e6e9a5632346", size = 377482, upload-time = "2026-10-04T16:30:35.997Z" },
{ url = "https://files.pythonhosted.org/packages/b4/fc/312b49006e7f8f9ca5f96647577b8aa6f3df30519c46bd448f5c425af0b2/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42e75466f83cd43f6026c81eab74246efb2bdadafb307b85700632d06c68f299", size = 400040, upload-time = "2026-10-04T16:30:37.76Z" },
{ url = "https://files.pythonhosted.org/packages/cf/a6/18cca7a878dc7fa95165a83343fd4d7b65643fd22e54e47340a451121d5c/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:617f59cde379b4f648a09797b7f683d04b90a46344cddab85639da5aff0f5531", size = 551849, upload-time = "2026-10-04T16:30:39.443Z" },
{ url = "https://files.pythonhosted.org/packages/e4/6d/1f5685e20f39604691bdc3c05aaa6b8bd2f954e9e996477adf2376768e33/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3edae8c5ddfdb6985d49ae9d150516e5076888879022f91a26c2de9276ce0bdb", size = 614842, upload-time = "2026-10-04T16:30:41.231Z" },
{ url = "https://files.pythonhosted.org/packages/c6/25/98652109fd9f7e10268dd4571aa52b81987001b806f37ef1a18260de714a/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0f045bb053c9057720d72c56dffe30dffdc05997b2897a827b9325f0ab6623fa", size = 582606, upload-time = "2026-10-04T16:30:43.345Z" },
{ url = "https://files.pythonhosted.org/packages/19/03/11ca09099bab5f53373a80a334c424ec917c13d050760a59499d2af5171e/rpds_py-2026.9.1-cp314-cp314t-win32.whl", hash = "sha256:bf35d0568abda97233239ce32896d3ad53fccc537832c104e30c94aa5fb93569", size = 203341, upload-time = "2026-10-04T16:30:44.954Z" },
{ url = "https://files.pythonhosted.org/packages/6f/8a/88909e3ffd9f47f5b58211473875d8c3c09079f0058c46fb72c55a702a26/rpds_py-2026.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:1e8d4d79d828299bf44a55db22a9388ab967b49d17132c88eab0f4360b48da8e", size = 222843, upload-time = "2026-10-04T16:30:46.486Z" },
{ url = "https://files.pythonhosted.org/packages/f3/ad/f0eba548e297987964ef8088403db875b333963bf8189f3f563b6936b20e/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c0d2cb595a420b34d5086db0add011e26e2c09d6a024afbac4228bf8f863a30", size = 384112, upload-time = "2026-10-04T16:31:42.643Z" },
{ url = "https://files.pythonhosted.org/packages/33/89/891fa4d81f1eae7fe262ecd0aba2c51de94a116c7f3ccfb034fa75f4c537/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3d6ed6a98cfd19155996605474982cc470d7601746a6439078f1a5a3fa8b050", size = 389662, upload-time = "2026-10-04T16:31:44.642Z" },
{ url = "https://files.pythonhosted.org/packages/4f/46/912cf623b2263f9c9c2d04e8508fc38dbe89983faeba5fb36ca4558ae550/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8171b44a054e5c67fd748ada04187f1250bf35b95f85e52ab64bcf3331a923bb", size = 504637, upload-time = "2026-10-04T16:31:46.667Z" },
{ url = "https://files.pythonhosted.org/packages/9b/5c/7f3cfa7c5f01765c9f47c8398210ed0c1eb686c0e99c223cb1c6776d3fb9/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fda1d96e542c37b6c804547dbf489c129fe7c97183a76a5ec275909ba1a063df", size = 402607, upload-time = "2026-10-04T16:31:49.068Z" },
{ url = "https://files.pythonhosted.org/packages/f0/76/2e915d4115ee453c33e6af17bc1df7e22ddc52974e178eeeb92a78c09983/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:270bdcdaac5d5b6f73c5e22e7e135c7f2a50e789f71d9e241d5be8d90026e19a", size = 382762, upload-time = "2026-10-04T16:31:51.24Z" },
{ url = "https://files.pythonhosted.org/packages/0d/e5/844382925d7b84a4074ad79fc2b9d05eb6231ea243b34f247e68e519b606/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:84a6ecc0c940169190d2c23bd969debd48c94dbc855acd60188a68d71d421608", size = 389563, upload-time = "2026-10-04T16:31:53.549Z" },
{ url = "https://files.pythonhosted.org/packages/81/82/d6e3951602dc2e24790ef9d299aa24bf5e2493df5043f2c6da00480cc447/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab4b2fda7c2b542f7f9d886cc6a838c5079d2b76f72e6081411faba11adde2c9", size = 416842, upload-time = "2026-10-04T16:31:55.76Z" },
{ url = "https://files.pythonhosted.org/packages/ea/1c/8d248e4ea671d525ab7daa0703ad81cc2175076fa0ccd7ef088facbd2b01/rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:44b32a7c4f0da3d28af31c259e38ddcff096f855e205ed0671d02fcf44f1ea1c", size = 562434, upload-time = "2026-10-04T16:31:57.874Z" },
{ url = "https://files.pythonhosted.org/packages/ad/38/e7ac0db898120d15dea25e3661b1242a40cb3b23d1f4db86c15a01fe0ba1/rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:a3dbc5ed9514908d5046107d7b1346bde71eea61de6e0e4919c19354f97e769f", size = 627909, upload-time = "2026-10-04T16:31:59.977Z" },
{ url = "https://files.pythonhosted.org/packages/03/c8/37e555514932cb1ee36c75349187aa882a98dee5d5654c761699ed8a0d27/rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6eae33003518fd4cb4f83a218d5371469dd3001aa3b87128c005b07762f7fe5e", size = 593658, upload-time = "2026-10-04T16:32:02.14Z" },
{ url = "https://files.pythonhosted.org/packages/12/5c/5f69a91add94a8a33acea68a2b034a464946a654ff7296af9d957ef0d6a5/rpds_py-2026.9.1-pp311-pypy311_pp80-macosx_10_12_x86_64.whl", hash = "sha256:0483515261947e4e8b8e1375bf7463e7eb6ccfb3d86e7b554d90cd5285f20f32", size = 351769, upload-time = "2026-10-04T16:32:04.675Z" },
{ url = "https://files.pythonhosted.org/packages/41/22/35ea8bf3b2682b90ab687c74e4828418ff12298a5b0b0a0c033f61c86a30/rpds_py-2026.9.1-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:4cfaf02209061880210819934de2f4f6aa83dc04dafe6770276acc240a56da31", size = 351719, upload-time = "2026-10-04T16:32:06.834Z" },
{ url = "https://files.pythonhosted.org/packages/a3/90/a9ba81408369d34c1aca73319c15adc91ed45c1d21e9e8ee4832ffb7fe0b/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cdc537c8633d7fd92a82e2e0d2ab74320a3f63d5e59fb9cf08711e08fe151c4", size = 381109, upload-time = "2026-10-04T16:32:08.994Z" },
{ url = "https://files.pythonhosted.org/packages/b3/44/5192a0bed94cec86bd2a5e1cc5a1bb8f7eec3aec907eacd2deb13e87e326/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10e208f2425d973938afcd56e28a7c4be32e27b6a60b5d381f49fb9d8acf9759", size = 388222, upload-time = "2026-10-04T16:32:11.321Z" },
{ url = "https://files.pythonhosted.org/packages/92/cd/5356549711448f18b52f7a11ffbe90f1228774996fbf3c6cd1b18d22abaf/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c0dbbcc19735fe5f8b0a54c07659d154a9e69f47e15d0a6ab7299215daf62cb", size = 500384, upload-time = "2026-10-04T16:32:13.478Z" },
{ url = "https://files.pythonhosted.org/packages/7a/88/ddda9d28adfe33119c75b2e733d4ba7e326f41d7e1b1093951771768ca48/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684fd492fff4fead00587544e059be2bbcb6f93454f21fa2a91b66fc7508be82", size = 401661, upload-time = "2026-10-04T16:32:15.653Z" },
{ url = "https://files.pythonhosted.org/packages/32/c3/bb59ba16a6b4a57995d15b8c04c00f28df3b938c4dcdde48fbe0f73edb0c/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1028417bb44037eb3069c1009bd7b7277212876cda22fbe565b0bca9fab6d2c", size = 380748, upload-time = "2026-10-04T16:32:17.727Z" },
{ url = "https://files.pythonhosted.org/packages/f5/b8/0580faa1c5a32ddc160130dd7ae54d2d007272a89a40a6b5db3e50f9f585/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:492e5e428cbe126221611f47e068f01660352feec4ad18bc0f5ea9b2ae88fb14", size = 385731, upload-time = "2026-10-04T16:32:19.855Z" },
{ url = "https://files.pythonhosted.org/packages/bf/70/f73564642bbe3322c7eeef2c2f258cf040b71c41a430b532e59d04a1b838/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:88b5268892fde430d5531f95bc560b6efbbd67c929662c586afd729a96e7461c", size = 412131, upload-time = "2026-10-04T16:32:22.285Z" },
{ url = "https://files.pythonhosted.org/packages/8c/5d/17fff2e1f8f68721a2afb5cb48f7e442c751bb6b4883157c5abaf618cd08/rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:01445c8d194aa032a08e944f16567672da1c62dbdbefd8b6d0693032e290cf68", size = 558193, upload-time = "2026-10-04T16:32:24.4Z" },
{ url = "https://files.pythonhosted.org/packages/2a/d0/869ab6fb08531f97aae4a780b4d61b4190d8aef48fe054ee28bcf7114466/rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_i686.whl", hash = "sha256:eef6a03b0b6d08d0835ccfa8ec8d1bc70525e3801387567137b50c557695e6da", size = 622448, upload-time = "2026-10-04T16:32:26.844Z" },
{ url = "https://files.pythonhosted.org/packages/ba/a5/9672e532fe02cd3b92c78cfd8743abca939d96853006c63835009a1ecb6f/rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6b9bf3135b4ad5981df9a73d71a35272d650a2985ae9c2746357b24d59de2448", size = 591126, upload-time = "2026-10-04T16:32:29.274Z" },
{ url = "https://files.pythonhosted.org/packages/80/ef/3a9f8e4279c7920af561deed4cb7ebebed2794a8f608cf404d5eef14a105/rpds_py-2026.9.1-pp312-pypy312_pp80-macosx_10_12_x86_64.whl", hash = "sha256:56c6952a9b15047466d0c2347c446a761d4527f89976156341e68f0ce5cc08b0", size = 357108, upload-time = "2026-10-04T16:32:31.641Z" },
{ url = "https://files.pythonhosted.org/packages/b7/55/4b2fa381a583760aea5c92841e4e928a358e1c6511b129219e9c2826a226/rpds_py-2026.9.1-pp312-pypy312_pp80-macosx_11_0_arm64.whl", hash = "sha256:b242c27c8f836305a4a72df9cdd564386ac57b807bd252a063223331c9316b37", size = 346877, upload-time = "2026-10-04T16:32:34.061Z" },
]
[[package]]
name = "ruamel-yaml"
version = "0.19.1"
@@ -3794,6 +4035,32 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "sse-starlette"
version = "3.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "starlette" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/be/0123026f719d1a7936f214a88b553bb5701e04ff2511147c1dab0c5035eb/sse_starlette-3.5.0.tar.gz", hash = "sha256:75de713aa8a9441513cc283220826da079d982770965b951e9437720e8bafdb2", size = 36794, upload-time = "2026-09-28T17:48:14.7Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/e4/cdda14023c316d71493bc54fdffc3dd006631b88866145c9d3cc33e0f1df/sse_starlette-3.5.0-py3-none-any.whl", hash = "sha256:3e6e1070df3f0f5d9cea81496de92dbb72f6721871d99748ece67441dd8b7997", size = 17407, upload-time = "2026-09-28T17:48:13.228Z" },
]
[[package]]
name = "starlette"
version = "1.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e9/0c/6efb252d091ecccd7d62048ae11f0ea35cd75a4fbaeea5e30f9c3bf91d10/starlette-1.8.0.tar.gz", hash = "sha256:1565dc0b35d5737a271ed1e0e04e949f4e81198799f216d2667b0a0fb9cf9522", size = 2730457, upload-time = "2026-10-13T07:54:39.53Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/b0/5742e4ac7af5eb58ec3470a537a49d7aa507e5539413e504b3a65ef50ba8/starlette-1.8.0-py3-none-any.whl", hash = "sha256:dfdd6b29c26483288088d990eee59631dedadd66ce20d203402a7ca8e3c4656f", size = 79612, upload-time = "2026-10-13T07:54:38.019Z" },
]
[[package]]
name = "sympy"
version = "1.14.0"
@@ -4200,6 +4467,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" },
]
[[package]]
name = "uvicorn"
version = "0.54.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click", marker = "sys_platform != 'emscripten'" },
{ name = "h11", marker = "sys_platform != 'emscripten'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/da/34/30e9280707135d2cfc589dfff3cb796bd07a3aeb1a3e415ba09dd89d7bb4/uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620", size = 112283, upload-time = "2026-09-25T06:52:37.601Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf", size = 87427, upload-time = "2026-09-25T06:52:35.829Z" },
]
[[package]]
name = "wcwidth"
version = "0.9.1"