From b671de9c7e3d3d863fbeb937f9c11401c3bc22aa Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 04:34:19 -0500 Subject: [PATCH 01/49] py(deps): Exempt fastmcp from the release cooldown The global `exclude-newer = "3 days"` cooldown holds every release back, including FastMCP's, so the collector's real-object tests would run against a FastMCP a patch behind the one being targeted. `false` exempts a package from any `exclude-newer` constraint without committing a date that would age into the lockfile. Both halves need it. `fastmcp` is a code-free metapackage whose only job is depending on `fastmcp-slim[client,server]` at its exact version, so exempting one without the other leaves the pair unresolvable. Filed ahead of the gp-libs entry, which keeps its own note. https://gofastmcp.com/getting-started/installation https://docs.astral.sh/uv/reference/settings/#exclude-newer-package --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 1e859cd4..19418d1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,8 @@ gp-sphinx = { workspace = true } sphinx-vite-builder = { workspace = true } [tool.uv.exclude-newer-package] +fastmcp = false +fastmcp-slim = false # Adopt gp-libs releases immediately, bypassing the global cooldown. gp-libs = false From 194cdee2ed283d45dfa73ff1e813c49e8b1274b1 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 04:19:40 -0500 Subject: [PATCH 02/49] fastmcp(fix[collector]): Read MCP SDK v2 field names SDK v2 renamed the annotation model fields to snake_case, keeping camelCase as serialization aliases. Tool hints survived only through a FastMCP shim that warns and is going away; a resource lastModified resolved to nothing, so it vanished from pages silently. Match FastMCP prompt schema note by shape rather than by its exact sentence, which v4 reworded, and only in a final paragraph so a multi-paragraph description survives. Documented names are unchanged; only the reads move. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 84 +++++++++++++------ 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index 554f2d43..769b580c 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -6,6 +6,7 @@ import importlib import inspect import logging +import re import typing as t from sphinx.application import Sphinx @@ -86,13 +87,24 @@ def decorator(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: return decorator -_HINTS = ("readOnlyHint", "destructiveHint", "idempotentHint", "openWorldHint") +#: Each hint's documented name paired with the attribute holding it. MCP SDK v2 +#: renamed the model fields to snake_case, keeping the camelCase spellings as +#: serialization aliases only — an attribute read has to use the field name. +#: The documented name stays camelCase because that is what the MCP schema +#: publishes and what the rendered pages and ``term_from_annotations`` speak. +_HINTS = ( + ("readOnlyHint", "read_only_hint"), + ("destructiveHint", "destructive_hint"), + ("idempotentHint", "idempotent_hint"), + ("openWorldHint", "open_world_hint"), +) def _annotation_hints(annotations: t.Any) -> dict[str, bool]: """Return the hints a tool actually sets, dropping the unset ones. FastMCP accepts ``ToolAnnotations`` or a plain mapping, so read both. + A mapping is keyed by the documented name; a model by its field. Examples -------- @@ -104,17 +116,26 @@ def _annotation_hints(annotations: t.Any) -> dict[str, bool]: if annotations is None: return {} hints: dict[str, bool] = {} - for key in _HINTS: + for name, field in _HINTS: value = ( - annotations.get(key) + annotations.get(name) if isinstance(annotations, dict) - else getattr(annotations, key, None) + else getattr(annotations, field, None) ) if isinstance(value, bool): - hints[key] = value + hints[name] = value return hints +#: Resource and template annotations, documented name paired with the field +#: holding it. Only ``lastModified`` was renamed; the other two already match. +_RESOURCE_ANNOTATION_FIELDS = ( + ("audience", "audience"), + ("priority", "priority"), + ("lastModified", "last_modified"), +) + + def _tool_from_callable( func: t.Callable[..., t.Any], *, @@ -365,29 +386,46 @@ def _iter_components(server: t.Any) -> t.Iterable[t.Any]: return tuple(components.values()) -_SCHEMA_NOTE_MARKER = "Provide as a JSON string matching the following schema:" +#: FastMCP appends its schema hint as a trailing blank-line-separated +#: paragraph. The wording is not stable — FastMCP 3 wrote "Provide as a JSON +#: string matching the following schema:" and FastMCP 4 writes "Provide a value +#: matching the following JSON schema:" — so match the shape both share rather +#: than either sentence, and only ever consider the final paragraph. A +#: description that genuinely ends in a paragraph like this does not exist; +#: one that merely has several paragraphs keeps all of them. +_SCHEMA_NOTE_RE = re.compile( + r"^Provide\b.*\bJSON\b.*\bschema\b", re.IGNORECASE | re.DOTALL +) def _strip_schema_note(text: str) -> str: r"""Remove FastMCP's auto-appended JSON-schema hint from a description. - FastMCP's prompt argument builder tacks on - ``"\n\nProvide as a JSON string matching the following schema: {...}"`` - to help LLMs; it's noise in human-facing docs. + FastMCP's prompt argument builder appends a schema hint to help LLMs + pass non-string arguments; it is noise in human-facing docs. Examples -------- >>> _strip_schema_note("Summary.") 'Summary.' - >>> _strip_schema_note("Summary.\n\nProvide as a JSON string matching the following schema: {}") + >>> _strip_schema_note( + ... "Summary.\n\nProvide a value matching the following JSON schema:" + ... ' {"type":"number"}. Encode non-string values as JSON.' + ... ) 'Summary.' - >>> _strip_schema_note(" Summary. \n\nProvide as a JSON string matching the following schema: {}") + >>> _strip_schema_note("Summary.\n\nProvide as a JSON string matching the following schema: {}") 'Summary.' + >>> _strip_schema_note("First.\n\nSecond.") + 'First.\n\nSecond.' + >>> _strip_schema_note('Provide a value matching the following JSON schema: {}.') + '' """ - idx = text.find(_SCHEMA_NOTE_MARKER) - if idx == -1: - return text.strip() - return text[:idx].strip() + head, sep, tail = text.rpartition("\n\n") + # Without a separator the note is the whole description, and `head` is + # already the empty string this should return. + if _SCHEMA_NOTE_RE.match((tail if sep else text).strip()): + return head.strip() + return text.strip() def _prompt_from_component(prompt: t.Any) -> PromptInfo: @@ -441,14 +479,10 @@ def _resource_from_component(res: t.Any) -> ResourceInfo: annotations = getattr(res, "annotations", None) ann_dict: dict[str, t.Any] = {} if annotations is not None: - for field_name in ( - "audience", - "priority", - "lastModified", - ): - val = getattr(annotations, field_name, None) + for name, field in _RESOURCE_ANNOTATION_FIELDS: + val = getattr(annotations, field, None) if val is not None: - ann_dict[field_name] = val + ann_dict[name] = val module_name = getattr(func, "__module__", "") if func is not None else "" return ResourceInfo( name=str(res.name), @@ -517,10 +551,10 @@ def _resource_template_from_component(tpl: t.Any) -> ResourceTemplateInfo: annotations = getattr(tpl, "annotations", None) ann_dict: dict[str, t.Any] = {} if annotations is not None: - for field_name in ("audience", "priority", "lastModified"): - val = getattr(annotations, field_name, None) + for name, field in _RESOURCE_ANNOTATION_FIELDS: + val = getattr(annotations, field, None) if val is not None: - ann_dict[field_name] = val + ann_dict[name] = val parameters = _template_params_from_schema(getattr(tpl, "parameters", None)) module_name = getattr(func, "__module__", "") if func is not None else "" return ResourceTemplateInfo( From 0bc7f672c1065427b97b8831a255f79ae5a38e37 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 04:22:22 -0500 Subject: [PATCH 03/49] py(deps[dev]): Add fastmcp so the extension can be tested against it sphinx-autodoc-fastmcp introspects a FastMCP server but declared no FastMCP dependency, so its tests could only use hand-written stubs -- which answer whichever spelling they were written with, and so cannot catch an SDK rename. Dev-only; the extension still works without FastMCP installed. --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 19418d1c..0989e862 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,11 @@ dev = [ # Docs "sphinx-autobuild", # Testing + # sphinx-autodoc-fastmcp introspects a live FastMCP server's object model, + # which no shim reproduces faithfully — an SDK field rename is invisible + # to a hand-written stub. Dev-only: the extension keeps working without + # FastMCP installed, and its real-object tests skip when it is absent. + "fastmcp>=4.0.2", "pytest", "pytest-asyncio", "pytest-rerunfailures", From ff59d5c589cf8e83ba01687e80fb6298a5e5a60b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 04:22:22 -0500 Subject: [PATCH 04/49] fastmcp(test): Exercise the collector against a real FastMCP server Builds a real server with real ToolAnnotations, a last_modified resource and a generated prompt-argument schema, so a rename in FastMCP object model (https://gofastmcp.com/servers/tools) fails here rather than silently emptying a documentation page. Skips when FastMCP is absent. --- tests/ext/fastmcp/test_real_server.py | 110 ++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 tests/ext/fastmcp/test_real_server.py diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py new file mode 100644 index 00000000..572692b0 --- /dev/null +++ b/tests/ext/fastmcp/test_real_server.py @@ -0,0 +1,110 @@ +"""Collector tests against a real FastMCP server. + +The other tests build components from hand-written shims, which cannot +catch a rename in FastMCP's own object model: a stub keeps answering the +spelling it was written with. These build a real server instead, so an +SDK field rename fails here rather than silently emptying a docs page. + +FastMCP is a dev-only dependency; the extension itself works without it. +""" + +from __future__ import annotations + +import typing as t +import warnings + +import pytest + +from sphinx_autodoc_fastmcp._collector import ( + _annotation_hints, + _prompt_from_component, + _resource_from_component, +) + +pytest.importorskip("fastmcp") + +from fastmcp import FastMCP # noqa: E402 +from mcp.types import Annotations, ToolAnnotations # noqa: E402 + +_LAST_MODIFIED = "2026-01-01T00:00:00Z" + + +class Fixture(t.NamedTuple): + """A server plus the decorated tool, which carries its own spec.""" + + server: FastMCP + tool: t.Any + + +@pytest.fixture(scope="module") +def fixture() -> Fixture: + """A server exercising every annotation the collector reads.""" + app: FastMCP = FastMCP("collector-fixture") + + @app.tool( + annotations=ToolAnnotations(read_only_hint=True, destructive_hint=False), + ) + def inspect_thing(count: int) -> str: + """Look at a thing.""" + return "looked" + + @app.resource( + "fixture://thing", + annotations=Annotations(last_modified=_LAST_MODIFIED), + ) + def thing() -> str: + """A thing.""" + return "{}" + + @app.prompt + def describe(count: int) -> str: + """Describe some things.""" + return "described" + + return Fixture(server=app, tool=inspect_thing) + + +def test_tool_hints_survive_the_object_model(fixture: Fixture) -> None: + """Hints reach the vocabulary without leaning on the camelCase bridge. + + FastMCP answers the SDK v1 spellings through a warn-once shim it plans + to remove, so reading them still works and would keep a value-only + assertion green. Failing on the warning is what makes this test notice + the removal before a docs build does. + """ + with warnings.catch_warnings(): + warnings.simplefilter("error") + hints = _annotation_hints(fixture.tool.__fastmcp__.annotations) + + assert hints == {"readOnlyHint": True, "destructiveHint": False} + + +def test_a_mapping_of_hints_is_read_by_its_documented_name() -> None: + """Callers passing a plain mapping keep working.""" + assert _annotation_hints({"readOnlyHint": True, "openWorldHint": None}) == { + "readOnlyHint": True, + } + + +@pytest.mark.asyncio +async def test_resource_last_modified_survives_the_object_model( + fixture: Fixture, +) -> None: + """A resource's last-modified annotation reaches the page.""" + resource = await fixture.server.get_resource("fixture://thing") + + info = _resource_from_component(resource) + + assert info.annotations["lastModified"] == _LAST_MODIFIED + + +@pytest.mark.asyncio +async def test_prompt_arguments_drop_the_generated_schema_note( + fixture: Fixture, +) -> None: + """FastMCP's schema hint is stripped whatever its current wording.""" + prompt = await fixture.server.get_prompt("describe") + + info = _prompt_from_component(prompt) + + assert "JSON" not in info.arguments[0].description From fcd05e9b7bea4b58162427952672a21975c28e97 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 04:26:06 -0500 Subject: [PATCH 05/49] fastmcp(fix[collector]): Collect tools from the live server The mock collector took 4 kwargs where FastMCP takes many more, and the module loop swallowed the resulting TypeError with a warning -- so an unrecognised keyword dropped the rest of its module and the build still passed. Measured: a three-tool module documented one. Read Tool components off the server instead, taking module attribution from Tool.fn.__module__ rather than the config list position. Reads the provider registry directly like prompts and resources do, not list_tools(), so a runtime toolset gate cannot erase tools from the docs. Ledger: .git/spike/fastmcp-v4-tool-collection.md --- .../src/sphinx_autodoc_fastmcp/_collector.py | 87 +++++++++++++++++++ tests/ext/fastmcp/test_real_server.py | 32 +++++++ 2 files changed, 119 insertions(+) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index 769b580c..75f67e6b 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -173,6 +173,76 @@ def _tool_from_callable( ) +def _tool_from_component( + tool: t.Any, + *, + area_map: dict[str, str], + axes: tuple[Axis, ...] = DEFAULT_AXES, +) -> ToolInfo: + """Build ``ToolInfo`` from a live FastMCP ``Tool`` component. + + Sibling of :func:`_tool_from_callable`, which reads the ``__fastmcp__`` + spec a decorator leaves on a function. This reads the registered + component instead, so it sees what the server actually serves — + including metadata a hand-written collector cannot carry. + + Module attribution comes from the underlying function rather than the + position of the module in ``fastmcp_tool_modules``, which is why this + path does not need that list at all. + """ + func: t.Callable[..., t.Any] = tool.fn + if hasattr(func, "__wrapped__"): + func = func.__wrapped__ + module_name = str(getattr(func, "__module__", "") or "").rpartition(".")[2] + + tags = set(getattr(tool, "tags", None) or ()) + meta = dict(getattr(tool, "meta", None) or {}) + ann_dict = _annotation_hints(getattr(tool, "annotations", None)) + name = str(tool.name) + area = area_map.get(module_name, module_name.replace("_tools", "")) + + return ToolInfo( + name=name, + title=str(getattr(tool, "title", None) or name.replace("_", " ").title()), + module_name=module_name, + area=area, + axes=resolve_axes(axes, tags=tags, annotations=ann_dict, meta=meta), + annotations=ann_dict, + meta=meta, + func=func, + docstring=func.__doc__ or "", + params=extract_params(func), + return_annotation=normalize_annotation_text( + inspect.signature(func).return_annotation + ), + ) + + +def _tools_from_server( + server: t.Any, + *, + area_map: dict[str, str], + axes: tuple[Axis, ...], +) -> list[ToolInfo] | None: + """Collect every registered tool off a live server, or ``None``. + + Returns ``None`` when fastmcp is not importable, so the caller can fall + back to the module-scanning modes rather than reporting zero tools. + """ + try: + from fastmcp.tools import Tool as _Tool + except ImportError: # pragma: no cover - defensive + logger.warning( + "sphinx_autodoc_fastmcp: could not import fastmcp Tool", exc_info=True + ) + return None + return [ + _tool_from_component(component, area_map=area_map, axes=axes) + for component in _iter_components(server) + if isinstance(component, _Tool) and getattr(component, "fn", None) is not None + ] + + def collect_tools(app: Sphinx) -> None: """Populate ``app.env.fastmcp_tools`` from configured modules.""" modules: list[str] = list(app.config.fastmcp_tool_modules) @@ -186,6 +256,23 @@ def collect_tools(app: Sphinx) -> None: ) mode = "register" + # Prefer the live server when one is configured. The module-scanning modes + # below cannot see a tool the mock collector rejected, and a rejected kwarg + # aborts the rest of its module -- so a tool this path reports is a tool the + # server actually serves. Reads the same provider dict prompts and resources + # already use, which deliberately bypasses middleware: a toolset gate that + # hides tools at runtime must not erase them from the documentation. + server_dotted = str(getattr(app.config, "fastmcp_server_module", "") or "") + if server_dotted: + server = _resolve_server_instance(server_dotted) + if server is not None: + from_server = _tools_from_server(server, area_map=area_map, axes=axes) + if from_server: + app.env.fastmcp_tools = { # type: ignore[attr-defined] + info.name: info for info in from_server + } + return + if not modules: logger.warning( "sphinx_autodoc_fastmcp: fastmcp_tool_modules is empty; no tools collected", diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index 572692b0..384e072b 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -19,6 +19,7 @@ _annotation_hints, _prompt_from_component, _resource_from_component, + _tools_from_server, ) pytest.importorskip("fastmcp") @@ -108,3 +109,34 @@ async def test_prompt_arguments_drop_the_generated_schema_note( info = _prompt_from_component(prompt) assert "JSON" not in info.arguments[0].description + + +def test_a_configured_server_yields_tools_the_mock_would_drop() -> None: + """A kwarg the mock rejects must not erase its module's other tools. + + ``collect_tools``' register mode drives a hand-written collector whose + signature lags FastMCP's, and its module loop swallows the resulting + ``TypeError`` with a warning — so one unknown kwarg silently drops the + rest of the module. Reading the live server instead sees what is served. + """ + server = FastMCP("drop-probe") + + @server.tool(title="Alpha") + def alpha() -> str: + """First.""" + return "a" + + @server.tool(title="Beta", version="2") + async def beta() -> str: + """Second.""" + return "b" + + @server.tool(title="Gamma") + def gamma() -> str: + """Third.""" + return "c" + + collected = _tools_from_server(server, area_map={}, axes=()) + + assert collected is not None + assert sorted(info.name for info in collected) == ["alpha", "beta", "gamma"] From 2ce408cb12052fc3987477a9375ee18c9ad124c8 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 04:27:50 -0500 Subject: [PATCH 06/49] fastmcp(fix[collector]): Warn when two components share a name FastMCP permits two tools or prompts to register under one name when they differ another way, so both are served. The name-keyed docs index silently kept the last. Keep the first and warn, as the URI-keyed resource index already does. Ledger: .git/spike/collector-collision-pass2.md --- .../src/sphinx_autodoc_fastmcp/_collector.py | 35 +++++++++++++++--- tests/ext/fastmcp/test_real_server.py | 36 +++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index 75f67e6b..d0ef9150 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -173,6 +173,27 @@ def _tool_from_callable( ) +def _index_by_unique_name( + index: dict[str, t.Any], name: str, info: t.Any, kind: str +) -> None: + """Store ``name -> info``, warning when a name is already taken. + + FastMCP keys tools and prompts by name, so a duplicate is a real + registration the server serves and the docs would otherwise drop in + silence. First-wins, matching :func:`_index_by_name`'s behaviour for + URI-keyed components, with a warning naming the collision. + """ + if name in index: + logger.warning( + "sphinx_autodoc_fastmcp: duplicate %s name %r; keeping the first " + "and skipping the rest", + kind, + name, + ) + return + index[name] = info + + def _tool_from_component( tool: t.Any, *, @@ -268,9 +289,10 @@ def collect_tools(app: Sphinx) -> None: if server is not None: from_server = _tools_from_server(server, area_map=area_map, axes=axes) if from_server: - app.env.fastmcp_tools = { # type: ignore[attr-defined] - info.name: info for info in from_server - } + tools_by_name: dict[str, ToolInfo] = {} + for served in from_server: + _index_by_unique_name(tools_by_name, served.name, served, "tool") + app.env.fastmcp_tools = tools_by_name # type: ignore[attr-defined] return if not modules: @@ -322,7 +344,10 @@ def collect_tools(app: Sphinx) -> None: if info is not None: collector_tools.append(info) - app.env.fastmcp_tools = {tool.name: tool for tool in collector_tools} # type: ignore[attr-defined] + collected: dict[str, ToolInfo] = {} + for collected_tool in collector_tools: + _index_by_unique_name(collected, collected_tool.name, collected_tool, "tool") + app.env.fastmcp_tools = collected # type: ignore[attr-defined] def _resolve_server_instance(dotted: str) -> t.Any | None: @@ -737,7 +762,7 @@ def collect_prompts_and_resources(app: Sphinx) -> None: ) elif isinstance(component, _Prompt): info_p = _prompt_from_component(component) - prompts[info_p.name] = info_p + _index_by_unique_name(prompts, info_p.name, info_p, "prompt") app.env.fastmcp_prompts = prompts # type: ignore[attr-defined] app.env.fastmcp_resources = resources # type: ignore[attr-defined] diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index 384e072b..a621fb5f 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -10,6 +10,7 @@ from __future__ import annotations +import logging import typing as t import warnings @@ -17,6 +18,7 @@ from sphinx_autodoc_fastmcp._collector import ( _annotation_hints, + _index_by_unique_name, _prompt_from_component, _resource_from_component, _tools_from_server, @@ -140,3 +142,37 @@ def gamma() -> str: assert collected is not None assert sorted(info.name for info in collected) == ["alpha", "beta", "gamma"] + + +def test_duplicate_tool_names_warn_instead_of_vanishing( + caplog: pytest.LogCaptureFixture, +) -> None: + """Two tools sharing a name must not silently become one. + + FastMCP keys tools by name but permits duplicates that differ another + way (a version, say), so both are really served. Keying the docs index + by name alone dropped one with no signal. + """ + server = FastMCP("dup-probe") + + @server.tool(name="same", version="1") + def first() -> str: + """First.""" + return "1" + + @server.tool(name="same", version="2") + def second() -> str: + """Second.""" + return "2" + + collected = _tools_from_server(server, area_map={}, axes=()) + assert collected is not None + assert len(collected) == 2, "both registrations should reach the collector" + + index: dict[str, object] = {} + with caplog.at_level(logging.WARNING): + for info in collected: + _index_by_unique_name(index, info.name, info, "tool") + + assert list(index) == ["same"] + assert any("duplicate tool name" in r.getMessage() for r in caplog.records) From 21dd4467bc120562198449e443de6f846617efd7 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 04:29:53 -0500 Subject: [PATCH 07/49] fastmcp(feat[resources]): Render a resource's MCP annotations audience, priority and lastModified were collected onto ResourceInfo and then dropped: the card builder never took them, so they reached no page. Emit them as card facts beside the MIME type, only when set. Ledger: .git/spike/fastmcp-v4-metadata.md --- .../src/sphinx_autodoc_fastmcp/_directives.py | 39 +++++++++++++++++-- .../fastmcp/test_directives_integration.py | 24 ++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py index 9f68bf5a..95a14e12 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py @@ -619,6 +619,30 @@ def run(self) -> list[nodes.Node]: ) +#: MCP resource annotations, in the order a reader wants them, paired with the +#: label each is rendered under. Keys are the wire spellings the collector +#: stores, which is also what the MCP schema publishes. +_ANNOTATION_LABELS: tuple[tuple[str, str], ...] = ( + ("audience", "Audience"), + ("priority", "Priority"), + ("lastModified", "Last modified"), +) + + +def _annotation_fact_rows(annotations: dict[str, t.Any]) -> list[ApiFactRow]: + """Render the resource annotations that are set, skipping the rest.""" + rows: list[ApiFactRow] = [] + for key, label in _ANNOTATION_LABELS: + value = annotations.get(key) + if value is None: + continue + text = ( + ", ".join(str(v) for v in value) if isinstance(value, list) else str(value) + ) + rows.append(ApiFactRow(label, nodes.literal("", text))) + return rows + + def _build_resource_card( *, env: BuildEnvironment, @@ -629,6 +653,7 @@ def _build_resource_card( docstring: str, badge_group: nodes.inline, mime_type: str, + annotations: dict[str, t.Any] | None = None, shell_class: str, entry_class: str, signature_class: str, @@ -651,12 +676,16 @@ def _build_resource_card( ), ) + fact_rows: list[ApiFactRow] = [] if mime_type: + fact_rows.append(ApiFactRow("MIME type", nodes.literal("", mime_type))) + # MCP annotations a resource sets: who it is for, how strongly it is + # recommended, and when it last changed. Collected all along; rendering + # them is what makes them reachable by a reader. + fact_rows.extend(_annotation_fact_rows(annotations or {})) + if fact_rows: content_nodes.append( - build_api_facts_section( - [ApiFactRow("MIME type", nodes.literal("", mime_type))], - classes=(_CSS.BODY_SECTION,), - ), + build_api_facts_section(fact_rows, classes=(_CSS.BODY_SECTION,)), ) section = nodes.section() @@ -737,6 +766,7 @@ def run(self) -> list[nodes.Node]: kind="resource", ), mime_type=res.mime_type, + annotations=res.annotations, shell_class=_CSS.RESOURCE_SECTION, entry_class=_CSS.RESOURCE_ENTRY, signature_class=_CSS.RESOURCE_SIGNATURE, @@ -798,6 +828,7 @@ def run(self) -> list[nodes.Node]: kind="resource-template", ), mime_type=tpl.mime_type, + annotations=tpl.annotations, shell_class=_CSS.RESOURCE_SECTION, entry_class=_CSS.RESOURCE_ENTRY, signature_class=_CSS.RESOURCE_SIGNATURE, diff --git a/tests/ext/fastmcp/test_directives_integration.py b/tests/ext/fastmcp/test_directives_integration.py index aa513b83..2f83d325 100644 --- a/tests/ext/fastmcp/test_directives_integration.py +++ b/tests/ext/fastmcp/test_directives_integration.py @@ -80,6 +80,11 @@ def _populate(app: Sphinx) -> None: mime_type="text/markdown", docstring="Static hello blob.", tags=("readonly",), + annotations={ + "audience": ["user"], + "priority": 0.7, + "lastModified": "2026-01-01T00:00:00Z", + }, ) } templates = { @@ -243,3 +248,22 @@ def test_ref_xrefs_resolve_with_no_undefined_labels( assert 'href="#fastmcp-resource-hello"' in html assert 'href="#fastmcp-resource-template-user-record"' in html assert "undefined label" not in fastmcp_directives_html.warnings + + +@pytest.mark.integration +def test_resource_annotations_render( + fastmcp_directives_html: SharedSphinxResult, +) -> None: + """A resource's MCP annotations reach the page. + + They were collected onto ``ResourceInfo`` all along and never emitted, + so a reader could not see who a resource is for or when it changed. + """ + html = read_output(fastmcp_directives_html, "index.html") + + assert "Audience" in html + assert "user" in html + assert "Priority" in html + assert "0.7" in html + assert "Last modified" in html + assert "2026-01-01T00:00:00Z" in html From 7dea5631b554aaaecfd389da01b97d0550c3b91c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 04:52:26 -0500 Subject: [PATCH 08/49] fastmcp(fix[collector]): Keep collected tools picklable Reading tools off a live server put the server callable on ToolInfo, and a tool registered inside a register(mcp) factory is a closure -- unpicklable, so Sphinx died on the environment cache and no incremental build completed. Drop the field when pickling. Nothing reads it; params, return_annotation and docstring are all captured at collection time. Found by an end-to-end docs build, which the collector-level check could not have caught. --- .../src/sphinx_autodoc_fastmcp/_models.py | 21 ++++++++++++--- tests/ext/fastmcp/test_real_server.py | 27 +++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_models.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_models.py index b07544e8..197be3a2 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_models.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_models.py @@ -355,9 +355,13 @@ class ToolInfo: holding only the hints the tool actually sets. meta : dict[str, t.Any] The tool's ``meta`` mapping, which axes can read terms from. - func : t.Callable[..., t.Any] - The undecorated tool function, kept so the renderer can re-inspect - its signature. + func : t.Callable[..., t.Any] | None + The undecorated tool function. Dropped when Sphinx pickles its + environment between builds — a tool registered inside a factory is + a closure, and a closure cannot be pickled — so it is ``None`` on + any incremental rebuild. Everything rendered is captured at + collection time in ``params``, ``return_annotation`` and + ``docstring``; nothing reads this field. docstring : str Raw ``__doc__`` of the tool function. Empty when it has none. params : list[ParamInfo] @@ -373,11 +377,20 @@ class ToolInfo: axes: dict[str, str] annotations: dict[str, bool] meta: dict[str, t.Any] - func: t.Callable[..., t.Any] + func: t.Callable[..., t.Any] | None docstring: str params: list[ParamInfo] return_annotation: str + def __getstate__(self) -> dict[str, t.Any]: + """Drop ``func`` so Sphinx can pickle its environment. + + A tool registered inside a ``register(mcp)`` factory is a local + function, and pickling one raises. Nothing reads the field, so + dropping it costs nothing and keeps incremental builds working. + """ + return {**self.__dict__, "func": None} + @dataclass class PromptArgInfo: diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index a621fb5f..9ac31dbf 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -176,3 +176,30 @@ def second() -> str: assert list(index) == ["same"] assert any("duplicate tool name" in r.getMessage() for r in caplog.records) + + +def test_collected_tools_survive_environment_pickling() -> None: + """Sphinx pickles its environment; a collected tool must survive it. + + Tools registered inside a ``register(mcp)`` factory are closures, and + pickling one raises — which broke incremental builds for every project + that registers tools that way. + """ + import pickle + + server = FastMCP("pickle-probe") + + def register(app: FastMCP) -> None: + @app.tool(title="Local") + def made_in_a_closure() -> str: + """Registered inside a factory.""" + return "x" + + register(server) + collected = _tools_from_server(server, area_map={}, axes=()) + assert collected is not None + + restored = pickle.loads(pickle.dumps(collected)) + + assert [info.name for info in restored] == ["made_in_a_closure"] + assert restored[0].docstring == "Registered inside a factory." From 243aebb5026a08046195425f735628e6134d80cd Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 04:35:30 -0500 Subject: [PATCH 09/49] py(deps) Record the fastmcp cooldown exemption in the lock The lockfile manifest still carried the resolved `exclude-newer` timestamp for `fastmcp` and `fastmcp-slim`, so it disagreed with the `false` now in pyproject.toml and `uv lock --check` failed. No version moves: fastmcp stays at 4.0.2. `uv lock` rewrites only the two manifest entries, because it holds existing pins unless a constraint forces them to change. --- uv.lock | 1393 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1385 insertions(+), 8 deletions(-) diff --git a/uv.lock b/uv.lock index 520b5a96..fb85fdb9 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,10 @@ revision = 3 requires-python = ">=3.10, <4.0" resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version >= '3.11' and python_full_version < '3.15'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')", "python_full_version < '3.11'", ] @@ -13,6 +16,8 @@ exclude-newer-span = "P3D" [options.exclude-newer-package] gp-libs = false +fastmcp = false +fastmcp-slim = false [manifest] members = [ @@ -50,6 +55,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, ] +[[package]] +name = "aiofile" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "caio", version = "0.9.25", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, +] + +[[package]] +name = "aiofile" +version = "3.12.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')", +] +dependencies = [ + { name = "caio", version = "0.12.2", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/31/edb06aabd8f8f0b56d659f30800795f40b93cba96be946ce179f6931e3a5/aiofile-3.12.3.tar.gz", hash = "sha256:caa6aa746b5e47e2165f7abd741b6415e49cf4d44fddc0f61844612cc3924d41", size = 21600, upload-time = "2026-08-04T22:59:27.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/79/6e45e778c4c3cab39e0937b007b720c15f76c50c6453d153282d0fcc3588/aiofile-3.12.3-py3-none-any.whl", hash = "sha256:5c1bcc9e929c50834608e8cc1a4cc1d7503eb60c15a535b779fd39e2f372c017", size = 22122, upload-time = "2026-08-04T22:59:25.838Z" }, +] + [[package]] name = "alabaster" version = "1.0.0" @@ -59,6 +98,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + [[package]] name = "anyio" version = "4.14.2" @@ -137,6 +185,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/6c/b400476d3ceba681ab929787edc9554f6d88fcc69435eb681b00fc0457a5/ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0", size = 1083655, upload-time = "2026-08-07T11:29:00.349Z" }, ] +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "authlib" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "joserfc" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/51/bc1729d3cfdc214b4935f4e886e4dd443c3065fd8e1e66423fe84b490f81/authlib-1.8.0.tar.gz", hash = "sha256:f3ecd5f1da737262fb53bf1a4d95c4ea1ad9dd509316587a255c99ab1838a4f0", size = 177759, upload-time = "2026-08-30T12:12:34.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/c6/6f124bcfbbfb20fba22c939b4e43a06dccfc0e1ca20e5634ca573cb1e271/authlib-1.8.0-py2.py3-none-any.whl", hash = "sha256:88aebbd9af6757e14e912d5dc007ae1dc1f3e27e3b2152ce7c552ee2c3b3c121", size = 260804, upload-time = "2026-08-30T12:12:33.162Z" }, +] + [[package]] name = "babel" version = "2.18.0" @@ -155,6 +225,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, ] +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + [[package]] name = "beautifulsoup4" version = "4.15.0" @@ -168,6 +256,94 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, ] +[[package]] +name = "cachetools" +version = "7.1.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/39/9a4689914dd907915cee74733b95888fc1d8a21aad47a24a0a2deec73ac4/cachetools-7.1.8.tar.gz", hash = "sha256:1221d547a0b24b7f26fa891d40d488b5258beab9aebd8ed68c729be3af849c43", size = 40909, upload-time = "2026-08-31T19:02:53.985Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/3d/9487690d0e937854db587205c66bab3c3cf88d9f00ed380b74cb88cc94ee/cachetools-7.1.8-py3-none-any.whl", hash = "sha256:a81e3844acaa7355b6567f97bd67a94a14ec3a9bc2cbbdae45b9592cc036775b", size = 16842, upload-time = "2026-08-31T19:02:52.554Z" }, +] + +[[package]] +name = "caio" +version = "0.9.25" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/80/ea4ead0c5d52a9828692e7df20f0eafe8d26e671ce4883a0a146bb91049e/caio-0.9.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ca6c8ecda611478b6016cb94d23fd3eb7124852b985bdec7ecaad9f3116b9619", size = 36836, upload-time = "2025-12-26T15:22:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/36715c97c873649d1029001578f901b50250916295e3dddf20c865438865/caio-0.9.25-cp310-cp310-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db9b5681e4af8176159f0d6598e73b2279bb661e718c7ac23342c550bd78c241", size = 79695, upload-time = "2025-12-26T15:22:18.818Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ab/07080ecb1adb55a02cbd8ec0126aa8e43af343ffabb6a71125b42670e9a1/caio-0.9.25-cp310-cp310-manylinux_2_34_aarch64.whl", hash = "sha256:bf61d7d0c4fd10ffdd98ca47f7e8db4d7408e74649ffaf4bef40b029ada3c21b", size = 79457, upload-time = "2026-03-04T22:08:16.024Z" }, + { url = "https://files.pythonhosted.org/packages/88/95/dd55757bb671eb4c376e006c04e83beb413486821f517792ea603ef216e9/caio-0.9.25-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:ab52e5b643f8bbd64a0605d9412796cd3464cb8ca88593b13e95a0f0b10508ae", size = 77705, upload-time = "2026-03-04T22:08:17.202Z" }, + { url = "https://files.pythonhosted.org/packages/ec/90/543f556fcfcfa270713eef906b6352ab048e1e557afec12925c991dc93c2/caio-0.9.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6956d9e4a27021c8bd6c9677f3a59eb1d820cc32d0343cea7961a03b1371965", size = 36839, upload-time = "2025-12-26T15:21:40.267Z" }, + { url = "https://files.pythonhosted.org/packages/51/3b/36f3e8ec38dafe8de4831decd2e44c69303d2a3892d16ceda42afed44e1b/caio-0.9.25-cp311-cp311-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf84bfa039f25ad91f4f52944452a5f6f405e8afab4d445450978cd6241d1478", size = 80255, upload-time = "2025-12-26T15:22:20.271Z" }, + { url = "https://files.pythonhosted.org/packages/df/ce/65e64867d928e6aff1b4f0e12dba0ef6d5bf412c240dc1df9d421ac10573/caio-0.9.25-cp311-cp311-manylinux_2_34_aarch64.whl", hash = "sha256:ae3d62587332bce600f861a8de6256b1014d6485cfd25d68c15caf1611dd1f7c", size = 80052, upload-time = "2026-03-04T22:08:20.402Z" }, + { url = "https://files.pythonhosted.org/packages/46/90/e278863c47e14ec58309aa2e38a45882fbe67b4cc29ec9bc8f65852d3e45/caio-0.9.25-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:fc220b8533dcf0f238a6b1a4a937f92024c71e7b10b5a2dfc1c73604a25709bc", size = 78273, upload-time = "2026-03-04T22:08:21.368Z" }, + { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/8a1b580875303500a9c12b9e0af58cb82e47f5bcf888c2457742a138273c/caio-0.9.25-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb", size = 81502, upload-time = "2026-03-04T22:08:22.381Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/0fe770b8ffc8362c48134d1592d653a81a3d8748d764bec33864db36319d/caio-0.9.25-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69", size = 80200, upload-time = "2026-03-04T22:08:23.382Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" }, + { url = "https://files.pythonhosted.org/packages/87/a4/e534cf7d2d0e8d880e25dd61e8d921ffcfe15bd696734589826f5a2df727/caio-0.9.25-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:628a630eb7fb22381dd8e3c8ab7f59e854b9c806639811fc3f4310c6bd711d79", size = 81565, upload-time = "2026-03-04T22:08:27.483Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ed/bf81aeac1d290017e5e5ac3e880fd56ee15e50a6d0353986799d1bc5cfd5/caio-0.9.25-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0ba16aa605ccb174665357fc729cf500679c2d94d5f1458a6f0d5ca48f2060a7", size = 80071, upload-time = "2026-03-04T22:08:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, +] + +[[package]] +name = "caio" +version = "0.12.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')", +] +sdist = { url = "https://files.pythonhosted.org/packages/75/c8/82b3c760141a1076408164b03e8789b51809add6aecd48aa9d7651cf6b59/caio-0.12.2.tar.gz", hash = "sha256:87a67c0dccc60e432888bd532ec504b66e124a5d8b391aab894583b55abd39ea", size = 80927, upload-time = "2026-08-04T14:43:33.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/db/2b780a0c859a0bc873683beb60d94215af5c082c76e11e632b4f122905a3/caio-0.12.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a294091062831970b702f10a7fb119c1b55a49db7b843deeb8939550531189ea", size = 77836, upload-time = "2026-08-04T14:42:49.581Z" }, + { url = "https://files.pythonhosted.org/packages/72/cc/bc8c1f81dc00d4232a2f842eca38df6c86281f012026416873ebaea2f592/caio-0.12.2-cp310-cp310-manylinux_2_34_aarch64.whl", hash = "sha256:af0c75b43f0cde52c758c000b797188e6d62579c4914173ca7afca382a47993d", size = 193718, upload-time = "2026-08-04T14:42:51.158Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ce/a7429564d74c7ed1cd7ed8c490e8ae9211d38786ac113a8e8eecba97f7c6/caio-0.12.2-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:c3922302878d17d6860ebe6b5e4dcc6821f7a9d162474a774b8534fedea80236", size = 190539, upload-time = "2026-08-04T14:42:52.521Z" }, + { url = "https://files.pythonhosted.org/packages/e0/43/43dc1bd7c961679267c2ca2c73fd5b732528bf719ba16d39cc1be0346a3c/caio-0.12.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e97f960ab7c84aaa7520367bf597222db4229054bc7aae70344cb7da7b19b6f1", size = 191334, upload-time = "2026-08-04T14:42:54.067Z" }, + { url = "https://files.pythonhosted.org/packages/64/b2/0ea4998968112f1211b1c057f1bd606f40f3efdab640f5e49529d2438707/caio-0.12.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a682783aef6eff8bc47cd5dd262081d7f746b8d3db1957c70a04017598a048d9", size = 190232, upload-time = "2026-08-04T14:42:55.551Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e9/c346d656b2f3626ef726aaac1c5ea58a981757cbed62087a9e81eaa4b09f/caio-0.12.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a2a7f0fa3b49f219f09705717c73a618eecbb3d697701e20abb4771d17e2bbcf", size = 84555, upload-time = "2026-08-04T14:42:56.84Z" }, + { url = "https://files.pythonhosted.org/packages/84/60/0b99da0d1345c0c1f9bde58ae96527b54b4aae32c2b6e74dcef514f7fa9c/caio-0.12.2-cp311-cp311-manylinux_2_34_aarch64.whl", hash = "sha256:1b04358ef65bd03d9c34d7b028efb422593b07485da82d6c5439f8c5dea35668", size = 196508, upload-time = "2026-08-04T14:42:58.074Z" }, + { url = "https://files.pythonhosted.org/packages/92/7e/7eb5a9ab97cfa5500a2cff8444e16e183aab6473912ac37d8bf9de283337/caio-0.12.2-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:e1253743841b0864bfd6827e496fea4591bb3999ec1c003e57de65370e2d9031", size = 192985, upload-time = "2026-08-04T14:42:59.404Z" }, + { url = "https://files.pythonhosted.org/packages/92/13/308773ae27f33bc141720ded63216c9a115f5838c1e341d2d37c2b051281/caio-0.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd94522714af3fd806093bdd10f3175f1c42c5ee72dda975b8b35b55c3400147", size = 194087, upload-time = "2026-08-04T14:43:00.645Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8b/08814adc89f2c7612da9c2f17d962626bf59eb3b4dd715ea8198ccecb66c/caio-0.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a857d06308dc4428b760f9430350b3847f4c62ad0e79e635215f7cc97f7adcc9", size = 192586, upload-time = "2026-08-04T14:43:02.388Z" }, + { url = "https://files.pythonhosted.org/packages/60/bc/b62bf048a6e11870291a24319ed027bdf658df9ba77d1ad762aa138e066b/caio-0.12.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2097cc0d19fa95e8d55aad770597bb0f76e4f70ed48278c965aa7c5b0b8c3bf5", size = 84702, upload-time = "2026-08-04T14:43:03.946Z" }, + { url = "https://files.pythonhosted.org/packages/f7/be/b40d55d793afcfa5bcdb32ade9289d9588e14e3026c2c87522e303cc6e8c/caio-0.12.2-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:2122dccbd1959b922543fc9f8a9d2af47bd5b59190d1ece2445d3d1b4d1be45f", size = 198292, upload-time = "2026-08-04T14:43:05.238Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/9bd2bca72bfa478337618eae88942c43c891ae225e11baeae275e5e5c6ab/caio-0.12.2-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:107e56554c179749de9440e1b5e5a19813572eebf3166e9dc3e5228b16966beb", size = 196207, upload-time = "2026-08-04T14:43:06.494Z" }, + { url = "https://files.pythonhosted.org/packages/48/9b/65f95efdd68b50b7a9f2555c93d9edc7da7aa5ae5e153163c41cf6fd5cd9/caio-0.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adc7785e61ff7cf372318f67ec65617eaa06975e20da177522665dca8be6ea5d", size = 195748, upload-time = "2026-08-04T14:43:07.893Z" }, + { url = "https://files.pythonhosted.org/packages/3c/16/6a5c010ca435a5184d11ca350874694ac19db249560126dc8df0f25791ce/caio-0.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07942d3b5999127ecb96256c38d5dbf49ed2864c087ed2a80b783901d0aa3ba1", size = 195835, upload-time = "2026-08-04T14:43:09.19Z" }, + { url = "https://files.pythonhosted.org/packages/4f/9b/31f0b49a2542ffa2f9d6140267e2b568e722a1feeb05cfbffea97666c62b/caio-0.12.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:40ebea9ebe3a3a66ae85fa00d4112d163654a33c82dcf9b26a99f7d30de13317", size = 84656, upload-time = "2026-08-04T14:43:10.513Z" }, + { url = "https://files.pythonhosted.org/packages/99/bc/62568d688af9712a34fe3f958d7a98c53bb2017e263260cd5deae67a90e9/caio-0.12.2-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:6003ec389a68d5ec8f089df82b2dc8915293dd630a4d11322d7e3455045981fd", size = 198443, upload-time = "2026-08-04T14:43:11.767Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e4/5ed627860285612e5307f06c109913c5918c947fbc223b55599e484c64b0/caio-0.12.2-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:eee9376d0e2af25b6defc5bce39f6efa90521c803aaf12eba931bd898a397cfc", size = 196356, upload-time = "2026-08-04T14:43:13.206Z" }, + { url = "https://files.pythonhosted.org/packages/81/e2/2a8cfc6ba3ef3f19e7c778e9fb6f98600f0971cca78bbdfc23a413a66349/caio-0.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:78e3ccafc98e009fcb00a97ad441585551e52c0ae7ecc50427a3ccd9b11502fd", size = 195893, upload-time = "2026-08-04T14:43:14.649Z" }, + { url = "https://files.pythonhosted.org/packages/d1/87/77c40fb2301d0b5bb27c2e79ae42fce718ed75396d5fe3e1c09d8e1400b1/caio-0.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f2355db8917f5a0f3638bf332fe0d87549c80e978fca01db84a8a14b9df56a05", size = 195969, upload-time = "2026-08-04T14:43:15.946Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b5/0ceca97eb546fe6bbace3399c8b11dfc503efcc7509d708a7a3f09ab50e9/caio-0.12.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8054cba5e7ee623bea34946e2b59eb7c7c2be8872d0a5d12215d6ff564938d5f", size = 78621, upload-time = "2026-08-04T14:43:17.316Z" }, + { url = "https://files.pythonhosted.org/packages/9f/43/54d01cf9b643ffd6678aedee5ad5d6e7a1063bd169a203bfb6fb471e6887/caio-0.12.2-cp314-cp314-macosx_26_0_arm64.whl", hash = "sha256:5d658212d585b8814b9caf766d8090acac05f01abfa2875d57fdc4a7e2af032e", size = 77834, upload-time = "2026-08-04T14:43:18.492Z" }, + { url = "https://files.pythonhosted.org/packages/90/00/a0ca7394a0c3a234811b0c38b39aa0db9373663981c1cf446e26e7a7c198/caio-0.12.2-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:391a7cc1dbfc5885d7d54406c9a7a4ec19d514d526e67d240d32734eebde378e", size = 198993, upload-time = "2026-08-04T14:43:19.992Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b2/f8f1a5e57c16c86825f1f0648e76f7760f84452a41efee0d04fa53ef3e2d/caio-0.12.2-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:121f4de82e2a875aff468ef2af7491fbecfffe9e71b507f5073fe2a156bb78df", size = 196408, upload-time = "2026-08-04T14:43:21.3Z" }, + { url = "https://files.pythonhosted.org/packages/01/db/5d94d1d58ef6f0acb39ab1a802793413a8b1e17108c05cf98cb4dc9e4b22/caio-0.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e0ba5f8c0dc7035c05817ca1399dd7d6121ea55c363a079c334a151a75094322", size = 196480, upload-time = "2026-08-04T14:43:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/8c/2a/366adf468d7654b895e232eeaa80d147df2ba293f708bd9cef78b95f92d0/caio-0.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0ad8d8f9f5ea47aee81aead563fe3aca5bb54c3fc21b62bd830eaf369eb04060", size = 196071, upload-time = "2026-08-04T14:43:24.187Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b1/4c0c989d2a24b8f3ba2e13b9115a107d9413b36aab8b299674b66da21c75/caio-0.12.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0b85f94819058a8f21c3dca26c5f006a0f003b8700483a326ec86d569d2bd1a3", size = 78627, upload-time = "2026-08-04T14:43:25.522Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/107cfe84199b9fb5a7317e1230d808944205fd91c7869641ac4e2ef5d603/caio-0.12.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a60459e42c680068a2a5286f15f54ccd887af34ad9ed1be1f0a7747ad6bd8820", size = 220217, upload-time = "2026-08-04T14:43:26.823Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1c/d6f03d3226519cd8f362081370326846348c442078e005753c57522a4190/caio-0.12.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:51bb86c0abce55d0b3467ba6671e95cd96356044e5abd58651ad0645cf37084b", size = 216293, upload-time = "2026-08-04T14:43:28.177Z" }, + { url = "https://files.pythonhosted.org/packages/43/a4/d53ed7e639b778b6f41a5e7c664b37f75830e38afa62dad62ec913674548/caio-0.12.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e33295963f5a4787355b8bfd754b4f0e7ac5d535138860748c1eb833ca10d620", size = 218220, upload-time = "2026-08-04T14:43:29.656Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d6/c353fd1dda371262995bba1b2f9aa42cc6cf7fc82c0853238510aa655bb9/caio-0.12.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15af6eb10d7705a92ee8143d8a4d89c2886ecb6b65ce1161d3dad1adb9b3cbec", size = 216106, upload-time = "2026-08-04T14:43:31.011Z" }, + { url = "https://files.pythonhosted.org/packages/61/8a/71b0144f783468ba9f1bbf8a2f8e45c7d85ae31ec192f10650aa46f31702/caio-0.12.2-py3-none-any.whl", hash = "sha256:5233e797c9fe2b541914b1bc2e2df82677e2206b537e44e252188f3c2cbb0ea9", size = 62548, upload-time = "2026-08-04T14:43:32.394Z" }, +] + [[package]] name = "certifi" version = "2026.7.22" @@ -177,6 +353,116 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, ] +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/d2/2cde336b375f55c76ca670f0be3978cc048e31e24f3b4d7ce8473150a388/cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be", size = 183779, upload-time = "2026-08-03T21:19:15.602Z" }, + { url = "https://files.pythonhosted.org/packages/94/1a/4b2f7c92293ba05cbd4a9a1b28faaf0326272d9488e6354657571c48a7aa/cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b", size = 184178, upload-time = "2026-08-03T21:19:16.67Z" }, + { url = "https://files.pythonhosted.org/packages/17/0b/ba385d8ccedf926c3cd06e8e2f327027da5afe5f0eb30f1f7bc43ac55125/cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004", size = 211037, upload-time = "2026-08-03T21:19:17.705Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b9/0f2e58b2cefa33255bff36935d42b13180fe559bba82596540eb404bde7d/cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9", size = 218652, upload-time = "2026-08-03T21:19:18.735Z" }, + { url = "https://files.pythonhosted.org/packages/37/15/180e0dab27b9312c7479003d14c9e547634b7dcb934e2cc4650e1b131a7a/cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98", size = 205422, upload-time = "2026-08-03T21:19:19.96Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/03026f0c850cbbaa9030750490225b4a7f4d524ea4df72c3cc740a90f4ef/cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9", size = 205444, upload-time = "2026-08-03T21:19:21.246Z" }, + { url = "https://files.pythonhosted.org/packages/75/77/60bebf6f818bec84210ac5b6979ce4eeadce6fbbaabc9c7ab23e506d1ce5/cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6", size = 218742, upload-time = "2026-08-03T21:19:22.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ae/679bf47e73fd77b352171727f07de559a003f14de5d02b904a6ec1fa73ca/cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf", size = 221054, upload-time = "2026-08-03T21:19:23.694Z" }, + { url = "https://files.pythonhosted.org/packages/09/b8/eefc0e06913b70aa153bf74c946094a18f58fd4aff11b7f372bfdfdca050/cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659", size = 213489, upload-time = "2026-08-03T21:19:24.922Z" }, + { url = "https://files.pythonhosted.org/packages/6f/13/4e56852824a03cdf68523a35686f1c28eacd4bd30a7b0a78e682e6e6e1d3/cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9", size = 220241, upload-time = "2026-08-03T21:19:26.214Z" }, + { url = "https://files.pythonhosted.org/packages/99/7f/040f9e163e4acac3ee3d85b02d00b2576e7ca980d8785f0a3a5f1a9bf7f5/cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41", size = 174578, upload-time = "2026-08-03T21:19:27.338Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0b/644a2ec1a4eaba49c2939410bb1eb1d25b09d6d0582f5d2f95c537043725/cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1", size = 185082, upload-time = "2026-08-03T21:19:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, + { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + [[package]] name = "charset-normalizer" version = "3.5.1" @@ -504,6 +790,98 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "cryptography" +version = "50.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" }, + { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, + { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" }, + { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, + { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" }, + { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, + { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, + { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/c7/27/8d207af749c453ee17ea087340b3f2b4adef75aadd1d277b1b129bdda84e/cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94", size = 3974350, upload-time = "2026-08-25T19:45:26.551Z" }, + { url = "https://files.pythonhosted.org/packages/14/9a/6d3a4d7852e22d657438b7bf51f66102c7d71c0e1fafeec652281d0403e5/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f", size = 4698675, upload-time = "2026-08-25T19:45:28.658Z" }, + { url = "https://files.pythonhosted.org/packages/73/35/5c3717edf9e68a0550ce04e28eab493fe545eccd81742af03f6a75fe260b/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671", size = 4707410, upload-time = "2026-08-25T19:45:30.816Z" }, + { url = "https://files.pythonhosted.org/packages/1d/e0/e786934472e3ac4ecdecc7b129a0ca1a2a40dffdafcf2c3ea9d4397f8def/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e", size = 4698378, upload-time = "2026-08-25T19:45:33.043Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/5b3f53a0b74d122f023476ede40ba5d3e70d5cf475f73b899740d26a4fb2/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6", size = 4706889, upload-time = "2026-08-25T19:45:35.086Z" }, + { url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006, upload-time = "2026-08-25T19:45:37.281Z" }, +] + +[[package]] +name = "cyclopts" +version = "4.23.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser" }, + { name = "rich" }, + { name = "rich-rst" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/4e/4afd08d7dd836c436bbc5999f2743eda0ad4eac8946a8eb4b72241e3c555/cyclopts-4.23.3.tar.gz", hash = "sha256:4299ec47f5be853f9a114fcc534c84d42bbf19fefa303994597ecb7e5fd3082b", size = 197067, upload-time = "2026-08-26T18:16:35.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/d0/247938ef46dd81ab293efcac06ac2fee922db5bede9039d059912081db51/cyclopts-4.23.3-py3-none-any.whl", hash = "sha256:b3a65872942afb08f3ab5ca3d65b0b3ecfc872c9ccbea9d6a74ec11aa8a0215e", size = 237180, upload-time = "2026-08-26T18:16:34.128Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + [[package]] name = "docutils" version = "0.21.2" @@ -513,18 +891,96 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, ] +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[package]] +name = "fastmcp" +version = "4.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastmcp-slim", extra = ["client", "server"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/1a/bee10aff530338f3b8982a3dd1c377c3ba5d0bc724615bf358422c4e9ecf/fastmcp-4.0.3.tar.gz", hash = "sha256:0dd50baf070105ee4436c245f087ac3c047e655c32d9e783de16d0bf15783a98", size = 42311418, upload-time = "2026-09-05T00:31:36.932Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/cb/66600db497b3be21cf141f01296308169859d48ed9b1bc8ba7c9d83279b7/fastmcp-4.0.3-py3-none-any.whl", hash = "sha256:f743f43193e1daf606e6497a9dddba5bbaad7df7957a98fd093716ecf4458b99", size = 8076, upload-time = "2026-09-05T00:31:33.957Z" }, +] + +[[package]] +name = "fastmcp-slim" +version = "4.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mcp-types" }, + { name = "platformdirs" }, + { name = "pydantic", extra = ["email"] }, + { name = "pydantic-settings" }, + { name = "python-dotenv" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/a9/b7b796b0d4a5e095dadef233833c82ac1136f5c110dd9947b2e9a4988518/fastmcp_slim-4.0.3.tar.gz", hash = "sha256:3ce04a82b82cc41ccb12bb3bb366cfd65b52bc797b8fee332a75da9d480f9b74", size = 685704, upload-time = "2026-09-05T00:31:12.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/4b/6b31820d87f56d5773538860878c8634b618cd1e9044b3bfbd8a78380a0f/fastmcp_slim-4.0.3-py3-none-any.whl", hash = "sha256:3756ed4bd9f82f40adaf51974b7cdd1463ab6b9f479cf69b69b2692969312b87", size = 859717, upload-time = "2026-09-05T00:31:10.93Z" }, +] + +[package.optional-dependencies] +client = [ + { name = "authlib" }, + { name = "exceptiongroup" }, + { name = "httpx2" }, + { name = "mcp" }, + { name = "opentelemetry-api" }, + { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, + { name = "starlette" }, +] +server = [ + { name = "authlib" }, + { name = "cyclopts" }, + { name = "exceptiongroup" }, + { name = "griffelib" }, + { name = "httpx2" }, + { name = "joserfc" }, + { name = "jsonref" }, + { name = "jsonschema-path" }, + { name = "mcp" }, + { name = "openapi-pydantic" }, + { name = "opentelemetry-api" }, + { name = "packaging" }, + { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, + { name = "pyperclip" }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "starlette" }, + { name = "uncalled-for" }, + { name = "uvicorn" }, + { name = "watchfiles" }, + { name = "websockets", version = "16.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "websockets", version = "17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] + [[package]] name = "gp-furo-theme" version = "0.1.0a38" @@ -624,6 +1080,7 @@ dependencies = [ dev = [ { name = "codecov" }, { name = "coverage" }, + { name = "fastmcp" }, { name = "gp-furo-theme" }, { name = "gp-sphinx" }, { name = "hatchling" }, @@ -667,6 +1124,7 @@ requires-dist = [{ name = "gp-sphinx", editable = "packages/gp-sphinx" }] dev = [ { name = "codecov" }, { name = "coverage" }, + { name = "fastmcp", specifier = ">=4.0.2" }, { name = "gp-furo-theme", editable = "packages/gp-furo-theme" }, { name = "gp-sphinx", editable = "packages/gp-sphinx" }, { name = "hatchling", specifier = ">=1.0" }, @@ -788,6 +1246,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" }, ] +[[package]] +name = "griffelib" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/b4/a767e91c606deefc447a96eaf59edd77397960b1d677dffd833ee8449831/griffelib-2.2.0.tar.gz", hash = "sha256:e1bc36fe9cd21d4b6b659b456346755e4cfdc5676c0a5214083126ee12612b3c", size = 227048, upload-time = "2026-08-16T14:04:58.383Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/b6/f65ac785d4ac90dcf7c831ac6256f5dd4a19780f4e1575b2c0d6eeebe319/griffelib-2.2.0-py3-none-any.whl", hash = "sha256:d71c3bc2bbed9f958488634fe788b843a9f705d6d2838ca32cd6c25eeb64dfc4", size = 166779, upload-time = "2026-08-16T14:04:54.365Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -814,6 +1281,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/84/1798b6d85ecde0e31546004efd25c5de1b1f49250644a60cce460e12593a/hatchling-1.32.0-py3-none-any.whl", hash = "sha256:0e17c9c3b9aa7c625acc8d0f5b622f107d5049af9ecf5ada4de1aada5be7cdbc", size = 78435, upload-time = "2026-08-11T05:03:42.644Z" }, ] +[[package]] +name = "httpcore2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, +] + +[[package]] +name = "httpx2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" }, + { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, +] + [[package]] name = "idna" version = "3.19" @@ -832,6 +1338,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl", hash = "sha256:ea0c9a0384df69ed86a943a15cde37d0360b82491b3910dc2215e202e62b5b02", size = 14794, upload-time = "2026-08-24T12:35:12.548Z" }, ] +[[package]] +name = "importlib-metadata" +version = "9.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/7e/1e7e8dc30634b93ebb3d58a3dea569ad146e656218d3960ab04f62047b29/importlib_metadata-9.0.1.tar.gz", hash = "sha256:ab830580bc0ef3db61ce8fae716389e5462b67e033018bab6d8f80ef17172f99", size = 59124, upload-time = "2026-08-28T15:30:34.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/55/ecca97ae19075f1fac62def77731e7f535e6c1fb8f92ff08160c5e6dade8/importlib_metadata-9.0.1-py3-none-any.whl", hash = "sha256:bba5600596a7e21f3eef53281cf28d6a5195634d2f2b78ff9501a3272c6eaab0", size = 27920, upload-time = "2026-08-28T15:30:33.433Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -841,6 +1359,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280", size = 20837, upload-time = "2026-07-14T01:28:02.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", size = 11677, upload-time = "2026-07-14T01:28:01.59Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -853,6 +1416,88 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "joserfc" +version = "1.7.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/94/80fea1514b7c6d7d37804d3fe9ca81455f633347fc98731bd71ffe1faa17/joserfc-1.7.5.tar.gz", hash = "sha256:d5ff536e658e17664f8c1b1ab60dc4aa62aa973fcef1edd33cc44bda45d6f5ea", size = 234990, upload-time = "2026-08-29T13:05:42.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/c5/82addfd375e5ee6520644e0553e4aadde92d668c4fc99cc716d337fe7bb3/joserfc-1.7.5-py3-none-any.whl", hash = "sha256:add2c2c84e8373b084d526a8b53daba5d7a513a118cd2dcd9fc9f979d0922159", size = 71269, upload-time = "2026-08-29T13:05:40.718Z" }, +] + +[[package]] +name = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +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-path" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/79/cd02a4df6d9270efdc7d3feefe6edd730b0820c39eeaa107a2faee8322d5/jsonschema_path-0.5.0.tar.gz", hash = "sha256:493b156ba895c97602655b620a8456caa2ce08c1aa389f5a7addec065e6e855c", size = 19597, upload-time = "2026-05-19T20:45:00.971Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/2c/9e69d73c4297508be9e3b64a970ea3971b3eb8db64ffc5802d40bd25981f/jsonschema_path-0.5.0-py3-none-any.whl", hash = "sha256:2790a070bc7abb08ea3dbe4d340ece4efadf639223001f020c7503229ba068e2", size = 24077, upload-time = "2026-05-19T20:44:59.225Z" }, +] + +[[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 = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + [[package]] name = "librt" version = "0.15.0" @@ -1013,7 +1658,10 @@ version = "4.2.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version >= '3.11' and python_full_version < '3.15'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')", ] dependencies = [ { name = "mdurl" }, @@ -1108,6 +1756,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.1.1" +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/d4/6e/21fb8e5d579dbe21d96ea4d5034200d46d8bdf2261053b5bd041f3c2f612/mcp-2.1.1.tar.gz", hash = "sha256:50b7ba1ebbe117008ea7bdd288234043e69c20b403d6851d19661e6d431a75ef", size = 3984589, upload-time = "2026-08-25T16:14:02.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/af/8644cc5fa26a59afd2df2e98eeb19e72926887fa4b7441aba4ff661140db/mcp-2.1.1-py3-none-any.whl", hash = "sha256:1c6c31c5d6471c58db76af3af8af67f46d11d01f0a59077d0a308cbdb3d3e915", size = 357912, upload-time = "2026-08-25T16:13:59.024Z" }, +] + +[[package]] +name = "mcp-types" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/dd/1c4417dc0b722c23a1669032d5f044e41170fe5d4773b488a50fcce98c32/mcp_types-2.1.1.tar.gz", hash = "sha256:77dcbe48fba73cca71a673f2646a5f037a017b7a0a07ac89cec1113028890eda", size = 66674, upload-time = "2026-08-25T16:14:03.861Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/d0/242e63c510f4a17381f55b1549a3f94f5687a0595984febd2b6f87a687a0/mcp_types-2.1.1-py3-none-any.whl", hash = "sha256:26f9f7f03f2a5730717a5b98e2ab7eb640ac352d05a00cdc725c311864778295", size = 69656, upload-time = "2026-08-25T16:14:00.667Z" }, +] + [[package]] name = "mdit-py-plugins" version = "0.6.1" @@ -1130,6 +1816,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "more-itertools" +version = "11.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, +] + [[package]] name = "mypy" version = "2.3.1" @@ -1232,7 +1927,10 @@ version = "5.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version >= '3.11' and python_full_version < '3.15'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')", ] dependencies = [ { name = "docutils" }, @@ -1247,6 +1945,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl", hash = "sha256:9c91c52b3cdb4d94a6506e4fab4e2f296c7623a0da0dcbe6de1565c3dad67a8a", size = 85817, upload-time = "2026-05-13T09:38:17.904Z" }, ] +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, +] + [[package]] name = "packaging" version = "26.3" @@ -1256,6 +1978,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] +[[package]] +name = "pathable" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/f3/5a20387de9bcd0607871bfc2198ee0e15836da7baa4592ccd7f24c27c986/pathable-0.6.0.tar.gz", hash = "sha256:6404b8b82aef5ff0fd478934137128b99b12212ba35afdde5525ca4f8388ea58", size = 18970, upload-time = "2026-05-19T18:15:11.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/e8/6d75ffd9784bce2e93d1ae4415649427e39a53bb172d4672b2b59c6f0a7b/pathable-0.6.0-py3-none-any.whl", hash = "sha256:82c4ca6c98c502ad12e0d4e9779b6210afee93c38990988c8c5d1b49bdcdf566", size = 18983, upload-time = "2026-05-19T18:15:10.728Z" }, +] + [[package]] name = "pathspec" version = "1.1.1" @@ -1359,6 +2090,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, ] +[[package]] +name = "platformdirs" +version = "4.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/b7/802a56eca9f2fac455b8bab5375a2647b0f0e14a2cd63ef077de3c4a7658/platformdirs-4.11.7.tar.gz", hash = "sha256:4f41487eeeeeb07f3a6625e61d9bc0ae6809f92d3386dbd74392fbb76108104d", size = 35127, upload-time = "2026-09-01T13:35:10.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/6e/80993e10a0482f630cef528635789233224f36b1ffd11592aa15d13ff9ce/platformdirs-4.11.7-py3-none-any.whl", hash = "sha256:8a02cb259042c79d1cd0450facc2fe6dc9d303ae7901afbe33bf8ea0b188cef6", size = 23938, upload-time = "2026-09-01T13:35:09.02Z" }, +] + [[package]] name = "playwright" version = "1.62.0" @@ -1387,6 +2127,191 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "py-key-value-aio" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/e2/d689d922894a7ecde73b6daeaf9b13dab5aae06fe6aaaf7514722644d382/py_key_value_aio-0.4.5.tar.gz", hash = "sha256:c6563a2c6abe5da5e20f4f9e875c2a9b425a2244a54fadbf46cf140a9eea45d7", size = 107547, upload-time = "2026-05-27T16:37:08.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/95/b8ba862968712caa12a19666175334fa979e1f198b896a430adb3bacfe87/py_key_value_aio-0.4.5-py3-none-any.whl", hash = "sha256:ab862adbcb8c72547d1c57821f22cbbb71ab86509039c96f36e914e0336c8dd7", size = 170005, upload-time = "2026-05-27T16:37:06.629Z" }, +] + +[package.optional-dependencies] +filetree = [ + { name = "aiofile", version = "3.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "aiofile", version = "3.12.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "anyio" }, +] +keyring = [ + { name = "keyring" }, +] +memory = [ + { name = "cachetools" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/6b/8f79692844269427abb3e4dd9e68edfcbe65ae25527d99183214de716c59/pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6", size = 2076533, upload-time = "2026-08-28T09:57:35.421Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d0/c787604c71c2bdcda1a5656942fc822cd0f9cd879b9484bb84fc42172703/pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615", size = 1924650, upload-time = "2026-08-28T09:57:37.944Z" }, + { url = "https://files.pythonhosted.org/packages/4a/77/ca2f8e997d9bfdb32205297aff38f210f398822d895b1af1b59fd9df9c13/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb", size = 1951261, upload-time = "2026-08-28T09:57:39.339Z" }, + { url = "https://files.pythonhosted.org/packages/a0/53/bd12e1a9255df4edee00353778e2614b5346265d51e1567ab72153e803a2/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b", size = 2021808, upload-time = "2026-08-28T09:57:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/d7/41/f7f312751ebc6d6767da91964a9c7954c18e226a1720ab234e3dfb9d6c17/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6", size = 2196184, upload-time = "2026-08-28T09:57:42.275Z" }, + { url = "https://files.pythonhosted.org/packages/3d/93/ce93aa030ab6bac4683ba8861e7baad89dd24b02e66b8801a0e4f6a00311/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793", size = 2238212, upload-time = "2026-08-28T09:57:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/34/a1/c8e6b66f499f510752c07a092dfe27621f9c255635e59d38704b5681c35a/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b", size = 2064073, upload-time = "2026-08-28T09:57:45.613Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/605e2b127ee30dbf4b1da9da4843587cf2b2d16486c241cc7a5be2d2c1bd/pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461", size = 2093102, upload-time = "2026-08-28T09:57:46.953Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f7/1ab28093c09032ddce7c92c7a55d503b6ecd70f42c32492946c1cb5477b1/pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736", size = 2133452, upload-time = "2026-08-28T09:57:48.362Z" }, + { url = "https://files.pythonhosted.org/packages/30/c8/47c79b756f12f85e8b0fbdb2b495f6b6eb32e6c98a4beae7a570a0b7c63c/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3", size = 2146477, upload-time = "2026-08-28T09:57:49.74Z" }, + { url = "https://files.pythonhosted.org/packages/13/5c/79fc00cb8f651d6061991de8d7cedf1c78c73cbd4862c42ef418f03b8bfa/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f", size = 2300832, upload-time = "2026-08-28T09:57:51.639Z" }, + { url = "https://files.pythonhosted.org/packages/b4/72/dd1a29853cf6d22a1ebd9e3baf0239cbc57d2d16caff36a89e38eb9b1db3/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1", size = 2320505, upload-time = "2026-08-28T09:57:53.236Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/ba4a8e06a9ddad0b4caf69cfaeecc0fbfcec20473bd808f5127fd16491c4/pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069", size = 1956853, upload-time = "2026-08-28T09:57:54.592Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/205ed9d7ddaf44acd489889708ea124a3f41bdb42c141c8684d528ad0e7a/pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d", size = 2042551, upload-time = "2026-08-28T09:57:56.017Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" }, + { url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" }, + { url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" }, + { url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" }, + { url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" }, + { url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, + { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, + { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, + { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, + { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, + { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, + { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, + { url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" }, + { url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" }, + { url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" }, + { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, + { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, + { url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" }, + { url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" }, + { url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" }, + { url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" }, +] + [[package]] name = "pyee" version = "13.0.1" @@ -1408,6 +2333,32 @@ 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.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + [[package]] name = "pytest" version = "9.1.1" @@ -1520,6 +2471,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/3f/172d73600ad2771774cda108efb813fc724fc345e5240a81a1085f1ade5d/pytest_watcher-0.6.3-py3-none-any.whl", hash = "sha256:83e7748c933087e8276edb6078663e6afa9926434b4fd8b85cf6b32b1d5bec89", size = 12431, upload-time = "2026-01-10T23:28:17.64Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" }, +] + +[[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 = "python-slugify" version = "8.0.4" @@ -1532,6 +2501,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, ] +[[package]] +name = "pywin32" +version = "312" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/1b/9cfdeac80ee45bebbbcb31f1b7b99a0d81a1c72de48d837be984e0e88b1d/pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e", size = 6361387, upload-time = "2026-06-04T07:49:14.329Z" }, + { url = "https://files.pythonhosted.org/packages/33/b1/7afc96d041d982c27bc2df6f853d43f01fd273e3d39d04be3647ddeb533d/pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db", size = 6926780, upload-time = "2026-06-04T07:49:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/4140da9ad54108e517f4a16b2d83da3033e08662144623e1239587cb7db6/pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd", size = 4307203, upload-time = "2026-06-04T07:49:18.993Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -1596,6 +2599,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { 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 = "requests" version = "2.34.2" @@ -1611,6 +2629,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +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 = "rich-rst" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/d6/d0b9fafc73b65767200da027acab1db1bdb1048f4fea5ebf659df01c700e/rich_rst-2.1.0.tar.gz", hash = "sha256:f4d117b49697f338769759fa5cacf5197da4888b347b9fda2e50aef5cd8d93bd", size = 302732, upload-time = "2026-07-05T02:59:44.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/68/1fc93dd759605b5d00fc98b50200739e41ed32bd22d6ba35ca6c3932371b/rich_rst-2.1.0-py3-none-any.whl", hash = "sha256:7ecd1343ee12c879d0e7ae74c3eb6d263b023d2929c6d114212eb1fd91057255", size = 272987, upload-time = "2026-07-05T02:59:42.792Z" }, +] + [[package]] name = "roman-numerals" version = "4.1.0" @@ -1632,6 +2677,261 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/2c/daca29684cbe9fd4bc711f8246da3c10adca1ccc4d24436b17572eb2590e/roman_numerals_py-4.1.0-py3-none-any.whl", hash = "sha256:553114c1167141c1283a51743759723ecd05604a1b6b507225e91dc1a6df0780", size = 4547, upload-time = "2025-12-17T18:25:40.136Z" }, ] +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')", +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, + { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, + { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, + { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, + { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, +] + [[package]] name = "ruff" version = "0.16.5" @@ -1657,6 +2957,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/e3/7df5a396e445b9ba49ce9a9437439a4d80042c61c0ade199abf8d16de1ac/ruff-0.16.5-py3-none-win_arm64.whl", hash = "sha256:a64abe90968719b851bb7cedffaa8753fbdbdadab483089682db623f3edc587e", size = 10391564, upload-time = "2026-08-27T16:34:16.064Z" }, ] +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + [[package]] name = "snowballstemmer" version = "3.1.1" @@ -1712,7 +3025,10 @@ version = "8.2.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version >= '3.11' and python_full_version < '3.15'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')", ] dependencies = [ { name = "alabaster" }, @@ -1764,7 +3080,10 @@ version = "2025.8.25" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version >= '3.11' and python_full_version < '3.15'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')", ] dependencies = [ { name = "colorama" }, @@ -1956,7 +3275,10 @@ version = "0.7.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version >= '3.11' and python_full_version < '3.15'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')", ] dependencies = [ { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" } }, @@ -2176,6 +3498,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/55/ab40a0d1378ee5c859590a633052cf1d0a1f8435af87558a9f7cd576601a/sphinxext_rediraffe-0.3.0-py3-none-any.whl", hash = "sha256:f4220beafa99c99177488276b8e4fcf61fbeeec4253c1e4aae841a18c475330c", size = 7194, upload-time = "2025-09-28T15:31:52.388Z" }, ] +[[package]] +name = "sse-starlette" +version = "3.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/00/b42a44342a054d58cb1115d7c8aa9cb4290dd9442f9c1b91a4b8173dba22/sse_starlette-3.4.8.tar.gz", hash = "sha256:ed89ffbb75cbf78a5fe2f2109cd584792ee7f9dfac96f791db546df8f15f3f9c", size = 32548, upload-time = "2026-08-05T11:19:49.982Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/3a/764912c58293d95b6dcdf4cc255f9d10de310580ced547b082eb9d72018c/sse_starlette-3.4.8-py3-none-any.whl", hash = "sha256:6e82314c786709a3cd9520f2285cf9fff90e181e598e8a357b0cf80f66afba0d", size = 16516, upload-time = "2026-08-05T11:19:48.748Z" }, +] + [[package]] name = "starlette" version = "1.6.0" @@ -2282,6 +3617,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl", hash = "sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3", size = 14211, upload-time = "2026-06-01T19:41:33.434Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "types-docutils" version = "0.23.0.20260827" @@ -2309,6 +3653,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +] + +[[package]] +name = "uncalled-for" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5a/92ce0b3ea5481915f55da994c2c2c5f7a3c09949afde196ee89f8ab961aa/uncalled_for-0.4.0.tar.gz", hash = "sha256:335b95bd2422332ec210d518f314a16e4c640921c39fc8bf2ad095bd3538f4af", size = 56979, upload-time = "2026-08-10T14:51:46.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/40/97cec87c077eb3291fc7905e6633e08b7ca593c57d30238444bcb6bb3d53/uncalled_for-0.4.0-py3-none-any.whl", hash = "sha256:16c4bb3337532e4bd5569adc192285976f3ad5305402256d34c67a12b5c968bd", size = 15502, upload-time = "2026-08-10T14:51:45.068Z" }, +] + [[package]] name = "urllib3" version = "2.7.0" @@ -2606,7 +3971,10 @@ version = "17.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version >= '3.11' and python_full_version < '3.15'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')", ] sdist = { url = "https://files.pythonhosted.org/packages/18/72/fba934cb3dff7a85d811820efffcd141ddd52b5a2a01637f64551373ff4d/websockets-17.1.tar.gz", hash = "sha256:acfea4c20bf54384883ea33b1240fc1db4f52e190823a4e2b334bc3e8bfca96a", size = 187520, upload-time = "2026-08-26T17:25:33.063Z" } wheels = [ @@ -2758,3 +4126,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/be/7e/75a0a491b512412e08333b9f8412757af6186fe1c598186261002de1a793/websockets-17.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8196d217eeca52b9235ee1f8a684a09885a5f953d5a31e80ef915bf2c5c94f9d", size = 217870, upload-time = "2026-08-26T17:25:29.745Z" }, { url = "https://files.pythonhosted.org/packages/41/63/23572870e01836a98346075b9e17a8bc24a6ddd9800a3204ceee58677f3c/websockets-17.1-py3-none-any.whl", hash = "sha256:f221081107b8c48184d99f7019604486376e7ef826037e70aad6b02540732c23", size = 211134, upload-time = "2026-08-26T17:25:31.397Z" }, ] + +[[package]] +name = "zipp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, +] From 7d6d92dbfe3b28281a9c9ba1054c7c6557d6615d Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 11:23:01 -0500 Subject: [PATCH 10/49] fastmcp(docs): Document live-server tool collection why: fastmcp_server_module was documented as collecting prompts and resources only. This branch makes it collect tools too, and take precedence over fastmcp_tool_modules, so three passages described behaviour the code no longer has. what: correct the confval description, the conf.py snippet and the "Live server collection" section; state the precedence rule and the duplicate-name warning next to the on_duplicate sentence that would otherwise mislead. Both resource directives now say which facts their card renders, which is what autodirectives publishes. Add docs/_ext/fastmcp_demo_server.py so the site demonstrates what it documents: a resource carrying every annotation, one carrying none, a template and a prompt. It registers no tools, so collect_tools still falls through to fastmcp_tool_modules and the demo tool cards are unaffected. Also tighten _SCHEMA_NOTE_RE. Matching by shape alone ate a written final paragraph asking the reader for a JSON schema; requiring the schema object after the colon separates the generated note from prose. Found while checking a downstream report of curly quotes inside the note. --- docs/_ext/fastmcp_demo_server.py | 98 +++++++++++++++++++ docs/conf.py | 1 + .../sphinx-autodoc-fastmcp/examples.md | 20 ++++ .../packages/sphinx-autodoc-fastmcp/how-to.md | 16 ++- .../src/sphinx_autodoc_fastmcp/__init__.py | 6 +- .../src/sphinx_autodoc_fastmcp/_collector.py | 15 ++- .../src/sphinx_autodoc_fastmcp/_directives.py | 6 ++ 7 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 docs/_ext/fastmcp_demo_server.py diff --git a/docs/_ext/fastmcp_demo_server.py b/docs/_ext/fastmcp_demo_server.py new file mode 100644 index 00000000..a1b74d8a --- /dev/null +++ b/docs/_ext/fastmcp_demo_server.py @@ -0,0 +1,98 @@ +"""Synthetic FastMCP server for the documentation page live demos. + +Companion to :mod:`fastmcp_demo_tools`, which supplies the tool cards. This +module supplies what only a live server can: resources, resource templates and +prompts, read off the running instance rather than replayed through a +hand-written collector. + +It registers no tools, so tool collection still falls through to +``fastmcp_tool_modules`` and the demo tool cards keep rendering. + +Examples +-------- +>>> mcp.name +'gp-sphinx-demo' +>>> changelog_entry.__doc__.splitlines()[0] +'One changelog entry, as Markdown.' +""" + +from __future__ import annotations + +from fastmcp import FastMCP +from mcp.types import Annotations + +mcp: FastMCP = FastMCP("gp-sphinx-demo") + + +@mcp.resource( + "docs://changelog", + mime_type="text/markdown", + annotations=Annotations( + audience=["user"], + priority=0.8, + last_modified="2026-09-05T00:00:00Z", + ), +) +def changelog() -> str: + """Return the project changelog, as Markdown. + + Carries every annotation a resource can set, so the card renders a + complete facts row. + + Returns + ------- + str + Changelog body. + """ + return "# Changelog\n" + + +@mcp.resource("docs://readme", mime_type="text/markdown") +def readme() -> str: + """Return the project README, as Markdown. + + Sets no annotations, so the card shows its MIME type alone — annotation + facts appear only when set. + + Returns + ------- + str + README body. + """ + return "# gp-sphinx\n" + + +@mcp.resource("docs://changelog/{version}", mime_type="text/markdown") +def changelog_entry(version: str) -> str: + """One changelog entry, as Markdown. + + Parameters + ---------- + version : str + Release to fetch, such as ``0.1.0a38``. + + Returns + ------- + str + Entry body. + """ + return f"## {version}\n" + + +@mcp.prompt +def summarize_release(version: str, audience: str) -> str: + """Draft a release summary for one version. + + Parameters + ---------- + version : str + Release to summarize. + audience : str + Who the summary is written for. + + Returns + ------- + str + Prompt text. + """ + return f"Summarize {version} for {audience}." diff --git a/docs/conf.py b/docs/conf.py index f8e54765..988c10d0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -107,6 +107,7 @@ "mermaid_examples", ], fastmcp_tool_modules=["fastmcp_demo_tools"], + fastmcp_server_module="fastmcp_demo_server:mcp", fastmcp_area_map={ "fastmcp_demo_tools": "packages/sphinx-autodoc-fastmcp/examples", }, diff --git a/docs/packages/sphinx-autodoc-fastmcp/examples.md b/docs/packages/sphinx-autodoc-fastmcp/examples.md index 97b3fb7a..f9ccf8ba 100644 --- a/docs/packages/sphinx-autodoc-fastmcp/examples.md +++ b/docs/packages/sphinx-autodoc-fastmcp/examples.md @@ -29,6 +29,26 @@ for a plain inline reference. .. fastmcp-tool-summary:: ``` +### Resource cards + +Read from the live server at {confval}`fastmcp_server_module`. `docs://changelog` +sets every MCP annotation, `docs://readme` sets none — annotation facts appear +only when the resource carries them. + +```{eval-rst} +.. fastmcp-resource:: docs://changelog + +.. fastmcp-resource:: docs://readme + +.. fastmcp-resource-template:: docs://changelog/{version} +``` + +### Prompt card + +```{eval-rst} +.. fastmcp-prompt:: summarize_release +``` + ## Demo module reference The demo objects above, as plain Python API — the targets the diff --git a/docs/packages/sphinx-autodoc-fastmcp/how-to.md b/docs/packages/sphinx-autodoc-fastmcp/how-to.md index 75ccf1db..5da6ce00 100644 --- a/docs/packages/sphinx-autodoc-fastmcp/how-to.md +++ b/docs/packages/sphinx-autodoc-fastmcp/how-to.md @@ -19,8 +19,8 @@ fastmcp_area_map = { } fastmcp_collector_mode = "register" -# Optional: point at a live FastMCP server instance to autodoc its prompts, -# resources, and resource templates. Format is "module.path:attr_name". +# Optional: point at a live FastMCP server instance to autodoc its tools, +# prompts, resources, and resource templates. Format is "module.path:attr_name". # Both an instance and a zero-arg factory callable are accepted. fastmcp_server_module = "my_project.server:mcp" ``` @@ -152,15 +152,25 @@ them separately to your `extensions` list. ## Live server collection Pointing {confval}`fastmcp_server_module` at a live FastMCP instance enables autodoc of -**prompts**, **resources**, and **resource templates** — see the four new +**tools**, **prompts**, **resources**, and **resource templates** — see the four new directives below. The collector accepts either: * A live instance: `"my_project.server:mcp"` (where `mcp = FastMCP(...)`). * A zero-argument factory: `"my_project.server:make_server"` returning a `FastMCP` instance. +Tools come from the server in preference to {confval}`fastmcp_tool_modules`, so a +tool the server serves is documented whether or not a module hook exposes it, and +each tool takes its area from its own function rather than from its position in +that list. Leave {confval}`fastmcp_server_module` unset to keep the +module-scanning modes. + If the resolved object is not a `FastMCP` (no `local_provider` attribute), collection is skipped and a warning is logged. The collector also invokes the server's `register_all` / `_register_all` hook (if exported) to ensure components registered lazily appear in the docs; FastMCP's default `on_duplicate="error"` policy is suppressed for this call. + +FastMCP keys tools and prompts by name while permitting two registrations to +share one, so both are served. The docs index holds one entry per name: it keeps +the first and warns, naming the collision. diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/__init__.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/__init__.py index d0c47127..c6d8fd44 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/__init__.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/__init__.py @@ -178,9 +178,11 @@ def setup(app: Sphinx) -> dict[str, t.Any]: "env", description=( '``"pkg.module:attribute"`` path to a live ``FastMCP`` ' - "instance. When set, the prompt / resource collector reads " + "instance. When set, the collector reads " "``local_provider._components`` directly so docs enumerate " - "the same surface as the running server." + "the same surface as the running server: tools, prompts, " + "resources and resource templates. Tools read this way take " + "precedence over ``fastmcp_tool_modules``." ), ) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index d0ef9150..5581385e 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -502,11 +502,12 @@ def _iter_components(server: t.Any) -> t.Iterable[t.Any]: #: paragraph. The wording is not stable — FastMCP 3 wrote "Provide as a JSON #: string matching the following schema:" and FastMCP 4 writes "Provide a value #: matching the following JSON schema:" — so match the shape both share rather -#: than either sentence, and only ever consider the final paragraph. A -#: description that genuinely ends in a paragraph like this does not exist; -#: one that merely has several paragraphs keeps all of them. +#: than either sentence, and only ever consider the final paragraph. Both +#: spellings put the schema object immediately after a colon, and requiring it +#: is what separates the generated note from a written sentence that happens to +#: ask the reader for a JSON schema. _SCHEMA_NOTE_RE = re.compile( - r"^Provide\b.*\bJSON\b.*\bschema\b", re.IGNORECASE | re.DOTALL + r"^Provide\b.*\bJSON\b.*\bschema\b[^{]*:\s*\{", re.IGNORECASE | re.DOTALL ) @@ -531,6 +532,12 @@ def _strip_schema_note(text: str) -> str: 'First.\n\nSecond.' >>> _strip_schema_note('Provide a value matching the following JSON schema: {}.') '' + + A written paragraph that asks for a schema is not the generated note, and + survives — the note always carries the schema object after its colon. + + >>> _strip_schema_note("The filter.\n\nProvide a JSON schema for the rows.") + 'The filter.\n\nProvide a JSON schema for the rows.' """ head, sep, tail = text.rpartition("\n\n") # Without a separator the note is the whole description, and `head` is diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py index 95a14e12..0e9a3800 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py @@ -722,6 +722,9 @@ def _build_resource_card( class FastMCPResourceDirective(SphinxDirective): """Autodocument one MCP resource (fixed URI). + The card lists the resource's MIME type and whichever of its ``audience``, + ``priority`` and ``lastModified`` annotations are set, as facts. + Supports the standard Sphinx ``:no-index:`` flag (mirrors :class:`FastMCPToolDirective`): when set, the card renders but its canonical section ID is not registered as a cross-reference target, so a resource @@ -782,6 +785,9 @@ def run(self) -> list[nodes.Node]: class FastMCPResourceTemplateDirective(SphinxDirective): """Autodocument one MCP resource template (parameterised URI). + The card lists the template's MIME type and whichever of its ``audience``, + ``priority`` and ``lastModified`` annotations are set, as facts. + Supports the standard Sphinx ``:no-index:`` flag (mirrors :class:`FastMCPToolDirective`): when set, the card renders but its canonical section ID is not registered as a cross-reference target. From afba0d2f3ed298dab5da592d54a4aeba6fe71026 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 16:12:26 -0500 Subject: [PATCH 11/49] fastmcp(fix[collector]): Strip Annotated metadata from type display why: under PEP 563 an annotation reaches the collector as the author's source text, which is the best display available -- until it carries Annotated metadata, because that metadata is code. A parameter declared with a description built at import time rendered the call that produced it rather than its value. what: _strip_annotated reduces Annotated[X, ...] to X, handling both the source-string and resolved-object forms. Every site that enriches a type from a signature routes through it: extract_params for the module modes, and the prompt argument enrichment. Text-level stripping rather than typing.get_type_hints, which fails on TYPE_CHECKING-only names -- sphinx.util.typing falls back to raw __annotations__ on three exception types for exactly that reason, and sphinx-autodoc-typehints goes further and still falls back. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 10 ++- .../src/sphinx_autodoc_fastmcp/_parsing.py | 61 ++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index 5581385e..3e788007 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -22,7 +22,11 @@ coerce_axes, resolve_axes, ) -from sphinx_autodoc_fastmcp._parsing import extract_params, first_paragraph +from sphinx_autodoc_fastmcp._parsing import ( + _strip_annotated, + extract_params, + first_paragraph, +) from sphinx_autodoc_typehints_gp import normalize_annotation_text logger = logging.getLogger(__name__) @@ -574,7 +578,9 @@ def _prompt_from_component(prompt: t.Any) -> PromptInfo: for arg in arguments: param = sig.parameters.get(arg.name) if param is not None: - arg.type_str = normalize_annotation_text(param.annotation) + arg.type_str = normalize_annotation_text( + _strip_annotated(param.annotation) + ) tags = tuple(sorted(str(tag) for tag in getattr(prompt, "tags", None) or ())) module_name = getattr(func, "__module__", "") if func is not None else "" return PromptInfo( diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_parsing.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_parsing.py index ddfb494c..a862ebed 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_parsing.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_parsing.py @@ -2,6 +2,7 @@ from __future__ import annotations +import ast import inspect import re import typing as t @@ -91,6 +92,64 @@ def first_paragraph(docstring: str) -> str: return paragraphs[0].strip().replace("\n", " ") +def _strip_annotated(annotation: t.Any) -> t.Any: + """Reduce ``Annotated[X, ...]`` to ``X``, in object or source-string form. + + Under PEP 563 an annotation arrives as the author's source text, which is + the best display available -- except when it carries ``Annotated`` + metadata, because that metadata is code. + + Examples + -------- + An annotation without metadata is its own best display: + + >>> _strip_annotated("list[str] | None") + 'list[str] | None' + + Metadata is dropped, whether the annotation arrives as source text or as a + resolved object: + + >>> _strip_annotated("t.Annotated[list[str], Field(description='x')]") + 'list[str]' + >>> import typing + >>> _strip_annotated(typing.Annotated[int, "meta"]) + + + A description built at import time would otherwise reach the page as the + call that produced it: + + >>> _strip_annotated("Annotated[str, Field(description=f'{summary()}')]") + 'str' + + Text that does not parse is returned untouched rather than raising: + + >>> _strip_annotated("Annotated[str,") + 'Annotated[str,' + """ + if isinstance(annotation, str): + if "Annotated[" not in annotation: + return annotation + try: + node = ast.parse(annotation, mode="eval").body + except SyntaxError: + return annotation + if isinstance(node, ast.Subscript): + target = node.value + name = ( + target.attr + if isinstance(target, ast.Attribute) + else getattr(target, "id", "") + ) + if name == "Annotated": + sl = node.slice + if isinstance(sl, ast.Tuple) and sl.elts: + return ast.unparse(sl.elts[0]) + return annotation + if t.get_origin(annotation) is t.Annotated: + return t.get_args(annotation)[0] + return annotation + + def extract_params(func: t.Callable[..., t.Any]) -> list[ParamInfo]: """Extract parameter info from function signature and docstring.""" sig = inspect.signature(func) @@ -100,7 +159,7 @@ def extract_params(func: t.Callable[..., t.Any]) -> list[ParamInfo]: for name, param in sig.parameters.items(): is_optional = param.default != inspect.Parameter.empty display = classify_annotation_display( - param.annotation, + _strip_annotated(param.annotation), strip_none=is_optional, ) From 7dd83d18642e2aa93e03db018f315778a26b4db2 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 16:14:17 -0500 Subject: [PATCH 12/49] fastmcp(fix[collector]): Read tool parameters from the published schema why: parameter tables were built from the Python signature, which disagrees with what the server serves. An injected Context was documented though FastMCP excludes it from the schema and no caller can pass it, and its default rendered as the repr of a sentinel -- an object address, so two builds of one source emitted different HTML. what: _params_from_schema takes the parameter set, required flags, defaults and descriptions from the tool's published schema, and keeps the signature only for the type display the schema cannot express in Python terms. Defaults render from JSON values, so they are stable. The schema already carries both NumPy docstring text and evaluated Field descriptions, so reading it costs no authoring convenience. Module-scanning modes are unchanged; they have no schema to read. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 60 ++++++++++++++++++- tests/ext/fastmcp/test_real_server.py | 42 ++++++++++++- 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index 3e788007..db8ad6e6 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -5,6 +5,7 @@ import contextlib import importlib import inspect +import json import logging import re import typing as t @@ -14,6 +15,7 @@ from sphinx_autodoc_fastmcp._models import ( DEFAULT_AXES, Axis, + ParamInfo, PromptArgInfo, PromptInfo, ResourceInfo, @@ -27,7 +29,10 @@ extract_params, first_paragraph, ) -from sphinx_autodoc_typehints_gp import normalize_annotation_text +from sphinx_autodoc_typehints_gp import ( + classify_annotation_display, + normalize_annotation_text, +) logger = logging.getLogger(__name__) @@ -198,6 +203,57 @@ def _index_by_unique_name( index[name] = info +def _render_default(value: t.Any) -> str: + """Render a JSON-schema default deterministically.""" + if value is None: + return "None" + if isinstance(value, bool): + return str(value) + if isinstance(value, str): + return repr(value) + return json.dumps(value) + + +def _params_from_schema( + schema: dict[str, t.Any], func: t.Callable[..., t.Any] +) -> list[ParamInfo]: + """Build parameter rows from the tool's published schema. + + The schema decides which parameters exist and their required/default/ + description; the signature supplies the type display, which the schema + cannot express in Python terms. + """ + props: dict[str, t.Any] = schema.get("properties", {}) or {} + required = set(schema.get("required", []) or []) + try: + sig_params = inspect.signature(func).parameters + except (TypeError, ValueError): # pragma: no cover - defensive + sig_params = {} # type: ignore[assignment] + + rows: list[ParamInfo] = [] + for name, prop in props.items(): + sig_param = sig_params.get(name) + annotation = ( + _strip_annotated(sig_param.annotation) + if sig_param is not None + and sig_param.annotation is not inspect.Parameter.empty + else "" + ) + is_required = name in required + rows.append( + ParamInfo( + name=name, + type_str=classify_annotation_display( + annotation, strip_none=not is_required + ).text, + required=is_required, + default=("" if is_required else _render_default(prop.get("default"))), + description=str(prop.get("description", "") or ""), + ), + ) + return rows + + def _tool_from_component( tool: t.Any, *, @@ -236,7 +292,7 @@ def _tool_from_component( meta=meta, func=func, docstring=func.__doc__ or "", - params=extract_params(func), + params=_params_from_schema(getattr(tool, "parameters", {}) or {}, func), return_annotation=normalize_annotation_text( inspect.signature(func).return_annotation ), diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index 9ac31dbf..0d91c4d7 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -11,6 +11,7 @@ from __future__ import annotations import logging +import re import typing as t import warnings @@ -26,7 +27,7 @@ pytest.importorskip("fastmcp") -from fastmcp import FastMCP # noqa: E402 +from fastmcp import Context, FastMCP # noqa: E402 from mcp.types import Annotations, ToolAnnotations # noqa: E402 _LAST_MODIFIED = "2026-01-01T00:00:00Z" @@ -203,3 +204,42 @@ def made_in_a_closure() -> str: assert [info.name for info in restored] == ["made_in_a_closure"] assert restored[0].docstring == "Registered inside a factory." + + +def test_injected_context_is_not_documented() -> None: + """A tool's injected ``Context`` stays out of the parameter table. + + FastMCP drops it from the published schema because no caller can pass + it, so a table built from the signature would document an argument the + server does not accept. + """ + app: FastMCP = FastMCP("context-fixture") + + @app.tool + def search(terms: str, ctx: Context) -> str: + """Search.""" + return "ok" + + collected = _tools_from_server(app, area_map={}, axes=()) + assert collected is not None + names = [param.name for param in collected[0].params] + assert names == ["terms"] + + +def test_parameter_defaults_are_reproducible() -> None: + """No parameter default carries an object address. + + A default rendered through ``str()`` puts the repr of a sentinel into + the HTML, so two builds of one source produce different bytes. + """ + app: FastMCP = FastMCP("default-fixture") + + @app.tool + def search(terms: str, ctx: Context) -> str: + """Search.""" + return "ok" + + collected = _tools_from_server(app, area_map={}, axes=()) + assert collected is not None + rendered = repr([param.default for param in collected[0].params]) + assert not re.search(r"0x[0-9a-f]{6,}", rendered), rendered From eeff24b476da078699b0a3cfec2092887bb6267f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 17:28:16 -0500 Subject: [PATCH 13/49] fastmcp(fix[collector]): Close four gaps in live-server collection why: reading the server's registry replaced the module modes rather than adding to them, and read less than the registry holds. Four consequences, each measured: - A mounted child server's tools are absent from the parent's registry, and the early return discarded the module entries that were the only record of them. A parent with one mounted child documented one tool where the server serves two. - ProxyTool and FastMCPProviderTool carry no `fn`, so filtering on one dropped every proxied or provider-backed tool without a warning. - MCP SDK v1 publishes the documented camelCase names as attributes. Reading only the v2 fields emptied hints and lastModified for FastMCP 3 consumers, and the extension declares no version floor. - collect_tools and collect_prompts_and_resources each resolved fastmcp_server_module, so a factory ran twice and the two collectors read different servers. - A parameter declared with Field(alias=...) publishes the alias, which names no signature parameter, so its type rendered as an em dash. what: merge server-collected tools over module-collected ones instead of replacing them; document components with no callable from the schema alone; fall back to the v1 attribute only when the v2 field is absent, so a v2 model never touches its deprecated alias; resolve the server once per build; and describe an aliased parameter from its schema type. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 120 +++++++++++++----- tests/ext/fastmcp/test_real_server.py | 45 +++++++ 2 files changed, 135 insertions(+), 30 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index db8ad6e6..a727b09f 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -126,11 +126,19 @@ def _annotation_hints(annotations: t.Any) -> dict[str, bool]: return {} hints: dict[str, bool] = {} for name, field in _HINTS: - value = ( - annotations.get(name) - if isinstance(annotations, dict) - else getattr(annotations, field, None) - ) + if isinstance(annotations, dict): + value = annotations.get(name) + else: + # SDK v2 renamed the fields; SDK v1 still publishes the documented + # camelCase name as the attribute. Read the new field first so a v1 + # consumer keeps its hints instead of silently losing them. + # Only reach for the documented camelCase name when the v2 + # field is absent entirely. On a v2 model that name is a + # deprecated alias, and reading it warns. + if hasattr(annotations, field): + value = getattr(annotations, field, None) + else: + value = getattr(annotations, name, None) if isinstance(value, bool): hints[name] = value return hints @@ -214,8 +222,26 @@ def _render_default(value: t.Any) -> str: return json.dumps(value) +def _schema_type_text(prop: dict[str, t.Any]) -> str: + """Describe a schema property when no signature parameter names it. + + A ``Field(alias=...)`` publishes the alias, so the signature has no + parameter of that name and cannot supply a type. The schema's own type is + a weaker display than the Python annotation, but it beats an em dash. + """ + declared = prop.get("type") + if isinstance(declared, str): + return declared + options = [ + opt.get("type") + for opt in prop.get("anyOf", []) or [] + if isinstance(opt, dict) and opt.get("type") not in (None, "null") + ] + return str(options[0]) if options else "" + + def _params_from_schema( - schema: dict[str, t.Any], func: t.Callable[..., t.Any] + schema: dict[str, t.Any], func: t.Callable[..., t.Any] | None ) -> list[ParamInfo]: """Build parameter rows from the tool's published schema. @@ -226,9 +252,9 @@ def _params_from_schema( props: dict[str, t.Any] = schema.get("properties", {}) or {} required = set(schema.get("required", []) or []) try: - sig_params = inspect.signature(func).parameters + sig_params = inspect.signature(func).parameters if func is not None else {} except (TypeError, ValueError): # pragma: no cover - defensive - sig_params = {} # type: ignore[assignment] + sig_params = {} rows: list[ParamInfo] = [] for name, prop in props.items(): @@ -239,6 +265,8 @@ def _params_from_schema( and sig_param.annotation is not inspect.Parameter.empty else "" ) + if not annotation: + annotation = _schema_type_text(prop) is_required = name in required rows.append( ParamInfo( @@ -271,8 +299,11 @@ def _tool_from_component( position of the module in ``fastmcp_tool_modules``, which is why this path does not need that list at all. """ - func: t.Callable[..., t.Any] = tool.fn - if hasattr(func, "__wrapped__"): + # A proxied or provider-backed tool carries no Python callable. Everything + # rendered comes from the component itself, so it still documents; only the + # signature-derived extras are unavailable. + func: t.Callable[..., t.Any] | None = getattr(tool, "fn", None) + if func is not None and hasattr(func, "__wrapped__"): func = func.__wrapped__ module_name = str(getattr(func, "__module__", "") or "").rpartition(".")[2] @@ -291,10 +322,12 @@ def _tool_from_component( annotations=ann_dict, meta=meta, func=func, - docstring=func.__doc__ or "", + docstring=(func.__doc__ or "") if func is not None else "", params=_params_from_schema(getattr(tool, "parameters", {}) or {}, func), - return_annotation=normalize_annotation_text( - inspect.signature(func).return_annotation + return_annotation=( + normalize_annotation_text(inspect.signature(func).return_annotation) + if func is not None + else "" ), ) @@ -320,10 +353,28 @@ def _tools_from_server( return [ _tool_from_component(component, area_map=area_map, axes=axes) for component in _iter_components(server) - if isinstance(component, _Tool) and getattr(component, "fn", None) is not None + if isinstance(component, _Tool) ] +def _server_for(app: Sphinx) -> t.Any | None: + """Resolve ``fastmcp_server_module`` once per build. + + ``collect_tools`` and ``collect_prompts_and_resources`` both run on + ``builder-inited``. Resolving separately would call a factory twice and + collect tools and components from two different servers. + """ + dotted = str(getattr(app.config, "fastmcp_server_module", "") or "") + if not dotted: + return None + cached = getattr(app, "_fastmcp_server_cache", None) + if cached is not None and cached[0] == dotted: + return cached[1] + server = _resolve_server_instance(dotted) + app._fastmcp_server_cache = (dotted, server) # type: ignore[attr-defined] + return server + + def collect_tools(app: Sphinx) -> None: """Populate ``app.env.fastmcp_tools`` from configured modules.""" modules: list[str] = list(app.config.fastmcp_tool_modules) @@ -343,19 +394,17 @@ def collect_tools(app: Sphinx) -> None: # server actually serves. Reads the same provider dict prompts and resources # already use, which deliberately bypasses middleware: a toolset gate that # hides tools at runtime must not erase them from the documentation. - server_dotted = str(getattr(app.config, "fastmcp_server_module", "") or "") - if server_dotted: - server = _resolve_server_instance(server_dotted) - if server is not None: - from_server = _tools_from_server(server, area_map=area_map, axes=axes) - if from_server: - tools_by_name: dict[str, ToolInfo] = {} - for served in from_server: - _index_by_unique_name(tools_by_name, served.name, served, "tool") - app.env.fastmcp_tools = tools_by_name # type: ignore[attr-defined] - return - - if not modules: + served_by_name: dict[str, ToolInfo] = {} + server = _server_for(app) + if server is not None: + for served in _tools_from_server(server, area_map=area_map, axes=axes) or (): + _index_by_unique_name(served_by_name, served.name, served, "tool") + + if served_by_name and not modules: + app.env.fastmcp_tools = served_by_name # type: ignore[attr-defined] + return + + if not served_by_name and not modules: logger.warning( "sphinx_autodoc_fastmcp: fastmcp_tool_modules is empty; no tools collected", ) @@ -407,6 +456,11 @@ def collect_tools(app: Sphinx) -> None: collected: dict[str, ToolInfo] = {} for collected_tool in collector_tools: _index_by_unique_name(collected, collected_tool.name, collected_tool, "tool") + # The server's own registry does not reach a mounted child server, so a + # module entry naming one is the only record of it. Server-collected tools + # win on a shared name; module entries fill the gaps rather than being + # discarded. + collected.update(served_by_name) app.env.fastmcp_tools = collected # type: ignore[attr-defined] @@ -661,7 +715,10 @@ def _resource_from_component(res: t.Any) -> ResourceInfo: ann_dict: dict[str, t.Any] = {} if annotations is not None: for name, field in _RESOURCE_ANNOTATION_FIELDS: - val = getattr(annotations, field, None) + if hasattr(annotations, field): + val = getattr(annotations, field, None) + else: + val = getattr(annotations, name, None) if val is not None: ann_dict[name] = val module_name = getattr(func, "__module__", "") if func is not None else "" @@ -733,7 +790,10 @@ def _resource_template_from_component(tpl: t.Any) -> ResourceTemplateInfo: ann_dict: dict[str, t.Any] = {} if annotations is not None: for name, field in _RESOURCE_ANNOTATION_FIELDS: - val = getattr(annotations, field, None) + if hasattr(annotations, field): + val = getattr(annotations, field, None) + else: + val = getattr(annotations, name, None) if val is not None: ann_dict[name] = val parameters = _template_params_from_schema(getattr(tpl, "parameters", None)) @@ -792,7 +852,7 @@ def collect_prompts_and_resources(app: Sphinx) -> None: template_names: dict[str, str] = {} if server_dotted: - server = _resolve_server_instance(server_dotted) + server = _server_for(app) if server is None: logger.warning( "sphinx_autodoc_fastmcp: fastmcp_server_module %r did not resolve " diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index 0d91c4d7..ba9dc79e 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -12,6 +12,7 @@ import logging import re +import types import typing as t import warnings @@ -22,6 +23,7 @@ _index_by_unique_name, _prompt_from_component, _resource_from_component, + _tool_from_component, _tools_from_server, ) @@ -243,3 +245,46 @@ def search(terms: str, ctx: Context) -> str: assert collected is not None rendered = repr([param.default for param in collected[0].params]) assert not re.search(r"0x[0-9a-f]{6,}", rendered), rendered + + +def test_v1_annotation_attributes_are_still_read() -> None: + """A tool built against MCP SDK v1 keeps its hints. + + v1 published the documented camelCase names as the attributes. Reading + only the v2 field names would empty the badge vocabulary for those + consumers, and the extension declares no version floor. + """ + v1_style = types.SimpleNamespace(readOnlyHint=True, destructiveHint=False) + + assert _annotation_hints(v1_style) == { + "readOnlyHint": True, + "destructiveHint": False, + } + + +def test_a_tool_without_a_python_callable_is_still_documented() -> None: + """A proxied or provider-backed tool reaches the page. + + ``ProxyTool`` and ``FastMCPProviderTool`` carry no ``fn``; filtering on + one drops every tool a mounted or proxied server contributes, which is + the silent-loss bug the live-server path exists to remove. + """ + app: FastMCP = FastMCP("fnless-fixture") + + @app.tool + def real(a: int) -> str: + """Real.""" + return "ok" + + fnless = types.SimpleNamespace( + name="proxied", + title=None, + tags=None, + meta=None, + annotations=None, + parameters={"properties": {"a": {"type": "integer"}}, "required": ["a"]}, + fn=None, + ) + info = _tool_from_component(fnless, area_map={}) + assert info.name == "proxied" + assert [param.name for param in info.params] == ["a"] From 53415e9d553f3893556b3e0e1bdf0997234b333b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 17:41:07 -0500 Subject: [PATCH 14/49] fastmcp(fix[collector]): Collect what the server serves, including mounts why: components were read from the server's own registry, which a mounted child server does not appear in. A parent with one mounted child served two tools and documented one, and the module-scanning fallback was the only thing keeping such a tool on the page at all. what: walk `server.providers` recursively, following a provider's wrapped server, with a depth cap and a cycle guard. A namespaced mount renames what it carries, so reproduce the prefix from the transform rather than reading past it -- a name the server does not serve is worse than a missing page, and a transform whose renaming cannot be reproduced is refused with a warning instead. `list_tools()` is not the primitive here even with middleware disabled: it still filters by enabled state and auth, so a tool switched off at runtime would vanish from its own documentation. The test asserts the collected names equal `list_tools(run_middleware=False)` for a plain and a namespaced mount, so the two cannot drift apart silently. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 69 +++++++++++++++++-- tests/ext/fastmcp/test_real_server.py | 38 ++++++++++ 2 files changed, 100 insertions(+), 7 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index a727b09f..1970e603 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -603,13 +603,68 @@ def _iter_components(server: t.Any) -> t.Iterable[t.Any]: comprehensions, so reading ``_components.values()`` is equivalent and avoids needing an event loop at Sphinx build time. """ - provider = getattr(server, "local_provider", None) - if provider is None: - return () - components = getattr(provider, "_components", None) - if components is None: - return () - return tuple(components.values()) + seen: set[int] = set() + found: list[t.Any] = [] + + def _renamed(component: t.Any, prefix: str) -> t.Any: + if not prefix or not hasattr(component, "model_copy"): + return component + return component.model_copy(update={"name": prefix + str(component.name)}) + + def walk(node: t.Any, depth: int, prefix: str = "") -> None: + if node is None or depth > 8 or id(node) in seen: + return + seen.add(id(node)) + for provider in getattr(node, "providers", None) or (): + components = getattr(provider, "_components", None) + if components is not None: + found.extend( + _renamed(component, prefix) for component in components.values() + ) + continue + inner = getattr(provider, "server", None) + if inner is not None: + walk(inner, depth + 1, prefix) + continue + wrapped = getattr(provider, "_inner", None) + if wrapped is None: + continue + # A namespaced mount renames every component it carries. Reproduce + # the prefix rather than reading past it, so the documented name is + # the served one; refuse the branch when a transform is not one we + # can reproduce, because a wrong name is worse than a missing page. + added = "" + reproducible = True + for transform in getattr(provider, "transforms", None) or (): + name_prefix = getattr(transform, "_name_prefix", None) + if isinstance(name_prefix, str): + added += name_prefix + else: + reproducible = False + if not reproducible: + logger.warning( + "sphinx_autodoc_fastmcp: a mounted provider renames its " + "components in a way this collector cannot reproduce; its " + "components are not documented", + ) + continue + inner_server = getattr(wrapped, "server", None) + if inner_server is not None: + walk(inner_server, depth + 1, prefix + added) + continue + inner_components = getattr(wrapped, "_components", None) + if inner_components is not None: + found.extend( + _renamed(component, prefix + added) + for component in inner_components.values() + ) + + walk(server, 0, "") + if not found: + provider = getattr(server, "local_provider", None) + components = getattr(provider, "_components", None) if provider else None + return tuple(components.values()) if components else () + return tuple(found) #: FastMCP appends its schema hint as a trailing blank-line-separated diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index ba9dc79e..823cb505 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -10,6 +10,7 @@ from __future__ import annotations +import asyncio import logging import re import types @@ -288,3 +289,40 @@ def real(a: int) -> str: info = _tool_from_component(fnless, area_map={}) assert info.name == "proxied" assert [param.name for param in info.params] == ["a"] + + +@pytest.mark.parametrize("namespace", [None, "kid"]) +def test_mounted_tools_match_what_the_server_serves(namespace: str | None) -> None: + """A mounted child server's tools are documented under their served names. + + The parent's own registry does not hold them, so reading it alone + documented one tool where the server served two. A namespaced mount also + renames what it carries, and a name the server does not serve is worse + than a missing page. + """ + child: FastMCP = FastMCP("child") + + @child.tool + def child_tool(a: int) -> str: + """Child.""" + return "ok" + + parent: FastMCP = FastMCP("parent") + + @parent.tool + def parent_tool(b: int) -> str: + """Parent.""" + return "ok" + + if namespace is None: + parent.mount(child) + else: + parent.mount(child, namespace=namespace) + + collected = _tools_from_server(parent, area_map={}, axes=()) + assert collected is not None + served = asyncio.run(parent.list_tools(run_middleware=False)) + + assert sorted(tool.name for tool in collected) == sorted( + tool.name for tool in served + ) From 4c5d10c466fe079fe3dd6a6a6b6ccc8e66ea8765 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 17:53:45 -0500 Subject: [PATCH 15/49] fastmcp(fix[collector]): Apply a mount's namespace the way the server does why: the walk prefixed every component's name. A namespace moves into a resource's URI, not its name, so a resource mounted under one was documented as one_thing at data://thing where the server serves thing at data://one/thing -- both halves wrong, and a directive using the served URI could not resolve. Checking tools alone had proved nothing about resources. The cycle guard also suppressed any server already visited, so one child mounted under two namespaces documented one of its two served names. A repeated mount is not a cycle. what: apply each transform through its own `_transform_uri` and `_transform_name`, choosing by whether the component is URI-keyed, so the rule stays FastMCP's rather than a copy of it. Scope the guard to the active traversal path. The test now asserts collected identity equals what the server lists for tools, resources and prompts, and for a child mounted twice. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 96 ++++++++++++------- tests/ext/fastmcp/test_real_server.py | 76 +++++++++++++++ 2 files changed, 136 insertions(+), 36 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index 1970e603..82b64782 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -596,70 +596,94 @@ def _ignore_duplicate_policy(provider: t.Any) -> t.Iterator[None]: def _iter_components(server: t.Any) -> t.Iterable[t.Any]: - """Yield every FastMCPComponent registered on ``server.local_provider``. - - Bypasses the async ``_list_*`` helpers and iterates the underlying - ``_components`` dict directly — the helpers are trivial type-filter - comprehensions, so reading ``_components.values()`` is equivalent and - avoids needing an event loop at Sphinx build time. + """Yield every component the server serves, under its served identity. + + Walks ``server.providers`` rather than ``local_provider`` alone, so a + mounted child server's components are included. Reads the provider + registries directly instead of the async ``_list_*`` helpers, which need an + event loop and additionally filter by enabled state and auth — a tool + switched off at runtime must not vanish from its own documentation. + + A namespaced mount renames what it carries, so each transform is applied + through its own methods: a URI-keyed component takes the namespace in its + URI, a name-keyed one in its name. A transform this cannot reproduce is + refused with a warning, because a name the server does not serve is worse + than a missing page. """ - seen: set[int] = set() found: list[t.Any] = [] - def _renamed(component: t.Any, prefix: str) -> t.Any: - if not prefix or not hasattr(component, "model_copy"): + def _served(component: t.Any, transforms: tuple[t.Any, ...]) -> t.Any: + if not transforms or not hasattr(component, "model_copy"): return component - return component.model_copy(update={"name": prefix + str(component.name)}) - - def walk(node: t.Any, depth: int, prefix: str = "") -> None: - if node is None or depth > 8 or id(node) in seen: + update: dict[str, t.Any] = {} + for field in ("uri", "uri_template"): + value = getattr(component, field, None) + if value is None: + continue + text = str(value) + for transform in transforms: + text = transform._transform_uri(text) # noqa: SLF001 + update[field] = text + if not update: + name = str(getattr(component, "name", "")) + for transform in transforms: + name = transform._transform_name(name) # noqa: SLF001 + update["name"] = name + return component.model_copy(update=update) + + def walk( + node: t.Any, + depth: int, + transforms: tuple[t.Any, ...], + path: frozenset[int], + ) -> None: + # Guard the active path only. The same child mounted under two + # namespaces is served twice under two names, and is not a cycle. + if node is None or depth > 8 or id(node) in path: return - seen.add(id(node)) + path = path | {id(node)} for provider in getattr(node, "providers", None) or (): components = getattr(provider, "_components", None) if components is not None: found.extend( - _renamed(component, prefix) for component in components.values() + _served(component, transforms) for component in components.values() ) continue inner = getattr(provider, "server", None) if inner is not None: - walk(inner, depth + 1, prefix) + walk(inner, depth + 1, transforms, path) continue wrapped = getattr(provider, "_inner", None) if wrapped is None: continue - # A namespaced mount renames every component it carries. Reproduce - # the prefix rather than reading past it, so the documented name is - # the served one; refuse the branch when a transform is not one we - # can reproduce, because a wrong name is worse than a missing page. - added = "" - reproducible = True + added: list[t.Any] = [] for transform in getattr(provider, "transforms", None) or (): - name_prefix = getattr(transform, "_name_prefix", None) - if isinstance(name_prefix, str): - added += name_prefix + if hasattr(transform, "_transform_name") and hasattr( + transform, "_transform_uri" + ): + added.append(transform) else: - reproducible = False - if not reproducible: - logger.warning( - "sphinx_autodoc_fastmcp: a mounted provider renames its " - "components in a way this collector cannot reproduce; its " - "components are not documented", - ) + logger.warning( + "sphinx_autodoc_fastmcp: a mounted provider renames its " + "components in a way this collector cannot reproduce; " + "its components are not documented", + ) + added = [] + break + if not added and getattr(provider, "transforms", None): continue + chain = transforms + tuple(added) inner_server = getattr(wrapped, "server", None) if inner_server is not None: - walk(inner_server, depth + 1, prefix + added) + walk(inner_server, depth + 1, chain, path) continue inner_components = getattr(wrapped, "_components", None) if inner_components is not None: found.extend( - _renamed(component, prefix + added) - for component in inner_components.values() + _served(component, chain) for component in inner_components.values() ) - walk(server, 0, "") + walk(server, 0, (), frozenset()) if not found: provider = getattr(server, "local_provider", None) components = getattr(provider, "_components", None) if provider else None diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index 823cb505..fb287f09 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -22,6 +22,7 @@ from sphinx_autodoc_fastmcp._collector import ( _annotation_hints, _index_by_unique_name, + _iter_components, _prompt_from_component, _resource_from_component, _tool_from_component, @@ -31,6 +32,9 @@ pytest.importorskip("fastmcp") from fastmcp import Context, FastMCP # noqa: E402 +from fastmcp.prompts import Prompt as _Prompt # noqa: E402 +from fastmcp.resources import Resource as _Resource # noqa: E402 +from fastmcp.tools import Tool as _Tool # noqa: E402 from mcp.types import Annotations, ToolAnnotations # noqa: E402 _LAST_MODIFIED = "2026-01-01T00:00:00Z" @@ -326,3 +330,75 @@ def parent_tool(b: int) -> str: assert sorted(tool.name for tool in collected) == sorted( tool.name for tool in served ) + + +@pytest.mark.parametrize("namespace", [None, "one"]) +def test_mounted_components_match_what_the_server_serves( + namespace: str | None, +) -> None: + """Every component kind is documented under its served identity. + + A namespace moves into a resource's URI and into a tool's name, so + checking tools alone proves nothing about resources. + """ + child: FastMCP = FastMCP("child") + + @child.tool + def child_tool(a: int) -> str: + """Child tool.""" + return "ok" + + @child.resource("data://thing") + def child_resource() -> str: + """Child resource.""" + return "{}" + + @child.prompt + def child_prompt(a: int) -> str: + """Child prompt.""" + return "drafted" + + parent: FastMCP = FastMCP("parent") + if namespace is None: + parent.mount(child) + else: + parent.mount(child, namespace=namespace) + + walked = _iter_components(parent) + assert sorted(tool.name for tool in walked if isinstance(tool, _Tool)) == sorted( + tool.name for tool in asyncio.run(parent.list_tools(run_middleware=False)) + ) + assert sorted( + str(res.uri) for res in walked if isinstance(res, _Resource) + ) == sorted( + str(res.uri) for res in asyncio.run(parent.list_resources(run_middleware=False)) + ) + assert sorted( + prompt.name for prompt in walked if isinstance(prompt, _Prompt) + ) == sorted( + prompt.name for prompt in asyncio.run(parent.list_prompts(run_middleware=False)) + ) + + +def test_one_child_mounted_twice_is_served_twice() -> None: + """A child mounted under two namespaces is two served components. + + Suppressing an already-visited server treats the second mount as a + cycle and drops it, though the server publishes both names. + """ + child: FastMCP = FastMCP("child") + + @child.tool + def hello(a: int) -> str: + """Hello.""" + return "ok" + + parent: FastMCP = FastMCP("parent") + parent.mount(child, namespace="one") + parent.mount(child, namespace="two") + + assert sorted( + tool.name for tool in _iter_components(parent) if isinstance(tool, _Tool) + ) == sorted( + tool.name for tool in asyncio.run(parent.list_tools(run_middleware=False)) + ) From c0cc4e5b0b63d8e5e9609527efdb6d9a04c9cdb5 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 17:57:37 -0500 Subject: [PATCH 16/49] fastmcp(fix[collector]): Match served identity under nesting and transforms why: four ways the collected identity still diverged from the served one. Nested mounts applied the outer namespace first, documenting inner_outer_hello where the server serves outer_inner_hello. A transform added to the server itself was ignored, so a server serving public_hi documented hi. A component with no callable lost its description, leaving newly documented proxy tools with an empty card. And a union parameter reaching the schema fallback kept only its first alternative. what: build the transform chain inner-to-outer; collect a server's own transforms alongside its providers'; fall back to the component's description when there is no callable docstring; and give _schema_type_text the union handling _template_params_from_schema already had, so both callers share one renderer rather than two. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 42 +++++----- tests/ext/fastmcp/test_real_server.py | 79 +++++++++++++++++++ 2 files changed, 103 insertions(+), 18 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index 82b64782..7a827427 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -232,12 +232,13 @@ def _schema_type_text(prop: dict[str, t.Any]) -> str: declared = prop.get("type") if isinstance(declared, str): return declared - options = [ - opt.get("type") - for opt in prop.get("anyOf", []) or [] - if isinstance(opt, dict) and opt.get("type") not in (None, "null") + union = prop.get("anyOf") or prop.get("oneOf") or () + parts = [ + str(member.get("type", "")) + for member in union + if isinstance(member, dict) and member.get("type") ] - return str(options[0]) if options else "" + return " | ".join(parts) def _params_from_schema( @@ -322,7 +323,11 @@ def _tool_from_component( annotations=ann_dict, meta=meta, func=func, - docstring=(func.__doc__ or "") if func is not None else "", + docstring=( + (func.__doc__ or "") + if func is not None + else str(getattr(tool, "description", "") or "") + ), params=_params_from_schema(getattr(tool, "parameters", {}) or {}, func), return_annotation=( normalize_annotation_text(inspect.signature(func).return_annotation) @@ -642,6 +647,15 @@ def walk( if node is None or depth > 8 or id(node) in path: return path = path | {id(node)} + # A transform attached to the server itself renames everything it + # serves, and applies outside any mount's own namespace. + own = tuple( + transform + for transform in getattr(node, "transforms", None) or () + if hasattr(transform, "_transform_name") + and hasattr(transform, "_transform_uri") + ) + transforms = transforms + own for provider in getattr(node, "providers", None) or (): components = getattr(provider, "_components", None) if components is not None: @@ -672,7 +686,9 @@ def walk( break if not added and getattr(provider, "transforms", None): continue - chain = transforms + tuple(added) + # Inner namespaces apply first: the server serves + # outer_inner_hello, not inner_outer_hello. + chain = tuple(added) + transforms inner_server = getattr(wrapped, "server", None) if inner_server is not None: walk(inner_server, depth + 1, chain, path) @@ -836,17 +852,7 @@ def _template_params_from_schema( for name, subschema in props.items(): if not isinstance(subschema, dict): continue - type_str = str(subschema.get("type", "")) if subschema.get("type") else "" - # Anyof/oneof unions: join short type names. - if not type_str: - union = subschema.get("anyOf") or subschema.get("oneOf") or () - parts = [ - str(member.get("type", "")) - for member in union - if isinstance(member, dict) and member.get("type") - ] - if parts: - type_str = " | ".join(parts) + type_str = _schema_type_text(subschema) rows.append( PromptArgInfo( name=str(name), diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index fb287f09..3e40f4b8 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -25,6 +25,7 @@ _iter_components, _prompt_from_component, _resource_from_component, + _schema_type_text, _tool_from_component, _tools_from_server, ) @@ -402,3 +403,81 @@ def hello(a: int) -> str: ) == sorted( tool.name for tool in asyncio.run(parent.list_tools(run_middleware=False)) ) + + +def test_nested_mounts_nest_their_namespaces_in_order() -> None: + """An inner namespace applies before the one it is mounted under.""" + inner: FastMCP = FastMCP("inner") + + @inner.tool + def hello(a: int) -> str: + """Hello.""" + return "ok" + + @inner.resource("data://thing") + def thing() -> str: + """Thing.""" + return "{}" + + mid: FastMCP = FastMCP("mid") + mid.mount(inner, namespace="inner") + outer: FastMCP = FastMCP("outer") + outer.mount(mid, namespace="outer") + + walked = _iter_components(outer) + assert sorted(tool.name for tool in walked if isinstance(tool, _Tool)) == sorted( + tool.name for tool in asyncio.run(outer.list_tools(run_middleware=False)) + ) + assert sorted( + str(res.uri) for res in walked if isinstance(res, _Resource) + ) == sorted( + str(res.uri) for res in asyncio.run(outer.list_resources(run_middleware=False)) + ) + + +def test_a_transform_on_the_server_renames_what_it_serves() -> None: + """A namespace added to the server itself reaches its components.""" + from fastmcp.server.transforms import Namespace + + server: FastMCP = FastMCP("server") + + @server.tool + def hi(a: int) -> str: + """Hi.""" + return "ok" + + server.add_transform(Namespace("public")) + + assert sorted( + tool.name for tool in _iter_components(server) if isinstance(tool, _Tool) + ) == sorted( + tool.name for tool in asyncio.run(server.list_tools(run_middleware=False)) + ) + + +def test_a_tool_without_a_callable_keeps_its_description() -> None: + """A component with no ``fn`` still explains itself. + + Both the card and the summary read ``ToolInfo.docstring``. + """ + fnless = types.SimpleNamespace( + name="proxied", + title=None, + tags=None, + meta=None, + annotations=None, + description="What the proxied tool does.", + parameters={}, + fn=None, + ) + + assert _tool_from_component(fnless, area_map={}).docstring == ( + "What the proxied tool does." + ) + + +def test_a_schema_union_keeps_every_alternative() -> None: + """A union parameter documents all its accepted types.""" + assert _schema_type_text({"anyOf": [{"type": "integer"}, {"type": "string"}]}) == ( + "integer | string" + ) From 8fc1ac24c975a2fbd3c711ecfd935cc0bed17be1 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 18:21:48 -0500 Subject: [PATCH 17/49] fastmcp(fix[collector]): Reproduce every rename the server applies why: seven more ways the collected identity diverged from the served one, each measured against list_tools(run_middleware=False): - mount(..., tool_names=...) wraps the provider in a ToolTransform, which has no namespace methods, so the whole mount was refused with a warning that named nothing; with a namespace around it the provider was wrapped twice and peeling one layer found nothing and said nothing. - A transform added to a provider directly, or to a child server, was either ignored or applied after the namespace it sits inside, so api_hello documented as hello and outer_inner_hello as inner_outer_hello. - Annotated metadata nested inside a container survived stripping, so dict[str, Annotated[int, Field(description=f'...')]] still leaked code. - A schema property with no default key documented None, which a default_factory parameter does not accept. - Server-collected tools were merged over module ones with a dict update, discarding a same-named module entry without the collision report every other duplicate gets. - A mount tree past the depth cap was cut off silently. - An empty annotation list rendered a blank fact row as if set. what: treat a ToolTransform's rename map as reproducible alongside a Namespace; peel every wrapper layer and lead the chain with the innermost provider's own transforms; strip Annotated anywhere in the expression tree; render a default only when the schema publishes one; merge served tools first through _index_by_unique_name; warn at the depth cap; skip an empty list annotation. Every case is asserted equal to what the server lists, for tools, resources and prompts. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 105 +++++++--- .../src/sphinx_autodoc_fastmcp/_directives.py | 2 +- .../src/sphinx_autodoc_fastmcp/_parsing.py | 44 ++-- tests/ext/fastmcp/test_real_server.py | 192 ++++++++++++++++++ 4 files changed, 299 insertions(+), 44 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index 7a827427..be663c09 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -276,7 +276,14 @@ def _params_from_schema( annotation, strip_none=not is_required ).text, required=is_required, - default=("" if is_required else _render_default(prop.get("default"))), + # Absence is not null: a default_factory parameter publishes + # no default at all, and documenting None would name a value + # the parameter does not accept. + default=( + _render_default(prop["default"]) + if not is_required and "default" in prop + else "" + ), description=str(prop.get("description", "") or ""), ), ) @@ -458,14 +465,12 @@ def collect_tools(app: Sphinx) -> None: if info is not None: collector_tools.append(info) - collected: dict[str, ToolInfo] = {} + # Server-collected tools win on a shared name; a module entry naming the + # same tool is reported like any other collision rather than overwritten + # in silence. Module entries fill the gaps. + collected: dict[str, ToolInfo] = dict(served_by_name) for collected_tool in collector_tools: _index_by_unique_name(collected, collected_tool.name, collected_tool, "tool") - # The server's own registry does not reach a mounted child server, so a - # module entry naming one is the only record of it. Server-collected tools - # win on a shared name; module entries fill the gaps rather than being - # discarded. - collected.update(served_by_name) app.env.fastmcp_tools = collected # type: ignore[attr-defined] @@ -617,6 +622,12 @@ def _iter_components(server: t.Any) -> t.Iterable[t.Any]: """ found: list[t.Any] = [] + def _reproducible(transform: t.Any) -> bool: + return ( + hasattr(transform, "_transform_name") + and hasattr(transform, "_transform_uri") + ) or isinstance(getattr(transform, "_transforms", None), dict) + def _served(component: t.Any, transforms: tuple[t.Any, ...]) -> t.Any: if not transforms or not hasattr(component, "model_copy"): return component @@ -627,12 +638,22 @@ def _served(component: t.Any, transforms: tuple[t.Any, ...]) -> t.Any: continue text = str(value) for transform in transforms: - text = transform._transform_uri(text) # noqa: SLF001 + if hasattr(transform, "_transform_uri"): + text = transform._transform_uri(text) # noqa: SLF001 update[field] = text if not update: name = str(getattr(component, "name", "")) + is_tool = hasattr(component, "parameters") for transform in transforms: - name = transform._transform_name(name) # noqa: SLF001 + if hasattr(transform, "_transform_name"): + name = transform._transform_name(name) # noqa: SLF001 + elif is_tool: + # ToolTransform: a per-tool rename map keyed by the + # original name, applied only to tools. + config = getattr(transform, "_transforms", {}).get(name) + renamed = getattr(config, "name", None) + if renamed: + name = str(renamed) update["name"] = name return component.model_copy(update=update) @@ -644,47 +665,73 @@ def walk( ) -> None: # Guard the active path only. The same child mounted under two # namespaces is served twice under two names, and is not a cycle. - if node is None or depth > 8 or id(node) in path: + if node is None or id(node) in path: + return + if depth > 8: + logger.warning( + "sphinx_autodoc_fastmcp: mount tree deeper than 8 levels; " + "components below %r are not documented", + getattr(node, "name", node), + ) return path = path | {id(node)} - # A transform attached to the server itself renames everything it - # serves, and applies outside any mount's own namespace. own = tuple( transform for transform in getattr(node, "transforms", None) or () - if hasattr(transform, "_transform_name") - and hasattr(transform, "_transform_uri") + if _reproducible(transform) ) - transforms = transforms + own + # A server's own transforms apply before the namespace it was mounted + # under: the server serves outer_inner_hello, not inner_outer_hello. + transforms = own + transforms for provider in getattr(node, "providers", None) or (): + # A transform added to a provider directly renames what it holds. + local = tuple( + transform + for transform in getattr(provider, "transforms", None) or () + if _reproducible(transform) + ) components = getattr(provider, "_components", None) if components is not None: found.extend( - _served(component, transforms) for component in components.values() + _served(component, local + transforms) + for component in components.values() ) continue inner = getattr(provider, "server", None) if inner is not None: - walk(inner, depth + 1, transforms, path) + walk(inner, depth + 1, local + transforms, path) continue wrapped = getattr(provider, "_inner", None) if wrapped is None: continue + # A mount may wrap its provider more than once -- a rename map + # inside a namespace, say. Peel every layer, collecting each + # layer's transforms so the innermost applies first. + layers: list[t.Any] = [provider] + while getattr(wrapped, "_inner", None) is not None: + layers.append(wrapped) + wrapped = wrapped._inner # noqa: SLF001 + # The innermost provider's own transforms apply before any + # wrapper's, so they lead the chain. added: list[t.Any] = [] - for transform in getattr(provider, "transforms", None) or (): - if hasattr(transform, "_transform_name") and hasattr( - transform, "_transform_uri" - ): + reproducible = True + for transform in getattr(wrapped, "transforms", None) or (): + if _reproducible(transform): added.append(transform) else: - logger.warning( - "sphinx_autodoc_fastmcp: a mounted provider renames its " - "components in a way this collector cannot reproduce; " - "its components are not documented", - ) - added = [] - break - if not added and getattr(provider, "transforms", None): + reproducible = False + for layer in reversed(layers): + for transform in getattr(layer, "transforms", None) or (): + if _reproducible(transform): + added.append(transform) + else: + reproducible = False + if not reproducible: + logger.warning( + "sphinx_autodoc_fastmcp: a mounted provider renames its " + "components in a way this collector cannot reproduce; " + "its components are not documented", + ) continue # Inner namespaces apply first: the server serves # outer_inner_hello, not inner_outer_hello. diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py index 0e9a3800..badc4813 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py @@ -634,7 +634,7 @@ def _annotation_fact_rows(annotations: dict[str, t.Any]) -> list[ApiFactRow]: rows: list[ApiFactRow] = [] for key, label in _ANNOTATION_LABELS: value = annotations.get(key) - if value is None: + if value is None or (isinstance(value, list) and not value): continue text = ( ", ".join(str(v) for v in value) if isinstance(value, list) else str(value) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_parsing.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_parsing.py index a862ebed..a3975322 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_parsing.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_parsing.py @@ -92,6 +92,28 @@ def first_paragraph(docstring: str) -> str: return paragraphs[0].strip().replace("\n", " ") +class _AnnotatedStripper(ast.NodeTransformer): + """Replace every ``Annotated[X, ...]`` in an expression tree with ``X``. + + Nesting matters: ``dict[str, Annotated[int, ...]]`` carries its metadata + one level down, where a top-level check never looks. + """ + + def visit_Subscript(self, node: ast.Subscript) -> ast.AST: # noqa: N802 + self.generic_visit(node) + target = node.value + name = ( + target.attr + if isinstance(target, ast.Attribute) + else getattr(target, "id", "") + ) + if name == "Annotated": + sl = node.slice + if isinstance(sl, ast.Tuple) and sl.elts: + return sl.elts[0] + return node + + def _strip_annotated(annotation: t.Any) -> t.Any: """Reduce ``Annotated[X, ...]`` to ``X``, in object or source-string form. @@ -125,26 +147,20 @@ def _strip_annotated(annotation: t.Any) -> t.Any: >>> _strip_annotated("Annotated[str,") 'Annotated[str,' + + Metadata nested inside a container is stripped too: + + >>> _strip_annotated("dict[str, Annotated[int, Field(description=f'{x()}')]]") + 'dict[str, int]' """ if isinstance(annotation, str): if "Annotated[" not in annotation: return annotation try: - node = ast.parse(annotation, mode="eval").body - except SyntaxError: + tree = ast.parse(annotation, mode="eval") + except (SyntaxError, ValueError, RecursionError): return annotation - if isinstance(node, ast.Subscript): - target = node.value - name = ( - target.attr - if isinstance(target, ast.Attribute) - else getattr(target, "id", "") - ) - if name == "Annotated": - sl = node.slice - if isinstance(sl, ast.Tuple) and sl.elts: - return ast.unparse(sl.elts[0]) - return annotation + return ast.unparse(_AnnotatedStripper().visit(tree).body) if t.get_origin(annotation) is t.Annotated: return t.get_args(annotation)[0] return annotation diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index 3e40f4b8..3b4e6f83 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -28,6 +28,7 @@ _schema_type_text, _tool_from_component, _tools_from_server, + collect_tools, ) pytest.importorskip("fastmcp") @@ -481,3 +482,194 @@ def test_a_schema_union_keeps_every_alternative() -> None: assert _schema_type_text({"anyOf": [{"type": "integer"}, {"type": "string"}]}) == ( "integer | string" ) + + +@pytest.mark.parametrize( + "mount_kwargs", + [ + {"tool_names": {"hello": "greet"}}, + {"namespace": "ns", "tool_names": {"hello": "greet"}}, + ], + ids=["rename", "namespace+rename"], +) +def test_a_renamed_mount_is_documented_under_its_served_name( + mount_kwargs: dict[str, t.Any], +) -> None: + """``mount(..., tool_names=...)`` renames through a second wrapper. + + A namespace around a rename wraps the provider twice; peeling one + layer found nothing and said nothing. + """ + child: FastMCP = FastMCP("child") + + @child.tool + def hello(a: int) -> str: + """Hello.""" + return "ok" + + parent: FastMCP = FastMCP("parent") + parent.mount(child, **mount_kwargs) + + assert sorted( + tool.name for tool in _iter_components(parent) if isinstance(tool, _Tool) + ) == sorted( + tool.name for tool in asyncio.run(parent.list_tools(run_middleware=False)) + ) + + +def test_a_transform_on_a_provider_renames_what_it_holds() -> None: + """``provider.add_transform`` is honoured without a mount around it.""" + from fastmcp.server.transforms import Namespace + + child: FastMCP = FastMCP("child") + + @child.tool + def hello(a: int) -> str: + """Hello.""" + return "ok" + + child.local_provider.add_transform(Namespace("api")) + parent: FastMCP = FastMCP("parent") + parent.mount(child) + + assert sorted( + tool.name for tool in _iter_components(parent) if isinstance(tool, _Tool) + ) == sorted( + tool.name for tool in asyncio.run(parent.list_tools(run_middleware=False)) + ) + + +def test_a_module_tool_colliding_with_a_served_tool_is_reported( + caplog: pytest.LogCaptureFixture, +) -> None: + """The server wins a shared name, and the module entry is warned about. + + Merging with a bare dict update discarded the module entry in silence, + bypassing the collision report every other same-kind duplicate gets. + """ + import sys + + def list_sessions(server: str) -> list[str]: + """Module copy.""" + return [] + + t.cast(t.Any, list_sessions).__fastmcp__ = types.SimpleNamespace( + name="list_sessions", title="List", tags=set(), annotations=None + ) + module = types.ModuleType("collision_mod") + module.list_sessions = list_sessions # type: ignore[attr-defined] + sys.modules["collision_mod"] = module + + app: FastMCP = FastMCP("served") + + @app.tool + def list_sessions_served(server: str) -> list[str]: + """Served copy.""" + return [] + + app.local_provider.remove_tool("list_sessions_served") + + @app.tool(name="list_sessions") + def served(server: str) -> list[str]: + """Served copy.""" + return [] + + class _Env: + pass + + class _Config: + fastmcp_server_module = "x" + fastmcp_tool_modules = ["collision_mod"] + fastmcp_area_map: dict[str, str] = {} + fastmcp_axes: tuple[t.Any, ...] = () + fastmcp_collector_mode = "introspect" + + class _App: + config = _Config() + env = _Env() + + fake = _App() + fake._fastmcp_server_cache = ("x", app) # type: ignore[attr-defined] + try: + with caplog.at_level(logging.WARNING): + collect_tools(t.cast(t.Any, fake)) + finally: + del sys.modules["collision_mod"] + + documented = t.cast(t.Any, fake.env).fastmcp_tools + assert documented["list_sessions"].docstring == "Served copy." + assert any("duplicate tool name" in rec.message for rec in caplog.records) + + +def test_a_child_transform_applies_before_its_mount_namespace() -> None: + """A child's own namespace nests inside the one it is mounted under.""" + from fastmcp.server.transforms import Namespace + + child: FastMCP = FastMCP("child") + + @child.tool + def hello(a: int) -> str: + """Hello.""" + return "ok" + + @child.resource("data://thing") + def thing() -> str: + """Thing.""" + return "{}" + + child.add_transform(Namespace("inner")) + parent: FastMCP = FastMCP("parent") + parent.mount(child, namespace="outer") + + walked = _iter_components(parent) + assert sorted(tool.name for tool in walked if isinstance(tool, _Tool)) == sorted( + tool.name for tool in asyncio.run(parent.list_tools(run_middleware=False)) + ) + assert sorted( + str(res.uri) for res in walked if isinstance(res, _Resource) + ) == sorted( + str(res.uri) for res in asyncio.run(parent.list_resources(run_middleware=False)) + ) + + +def test_a_transformed_provider_added_under_a_namespace_keeps_both() -> None: + """``add_provider`` around a provider with its own transform nests both.""" + from fastmcp.server.transforms import Namespace + + provider = type(FastMCP("x").local_provider)() + + @provider.tool + def ping() -> str: + """Ping.""" + return "ok" + + provider.add_transform(Namespace("inner")) + parent: FastMCP = FastMCP("parent") + parent.add_provider(provider, namespace="outer") + + assert sorted( + tool.name for tool in _iter_components(parent) if isinstance(tool, _Tool) + ) == sorted( + tool.name for tool in asyncio.run(parent.list_tools(run_middleware=False)) + ) + + +def test_an_absent_schema_default_is_not_documented_as_none() -> None: + """A ``default_factory`` parameter publishes no default, and ``None`` is + a value it does not accept.""" + from pydantic import Field + + app: FastMCP = FastMCP("defaults") + + @app.tool + def search( + filters: list[str] = Field(default_factory=list), + explicit: str | None = None, + ) -> str: + """Search.""" + return "ok" + + collected = _tools_from_server(app, area_map={}, axes=()) + assert collected is not None + defaults = {param.name: param.default for param in collected[0].params} + assert defaults == {"filters": "", "explicit": "None"} From 22af69ae105a12a0fa45502623b28d389e6b2901 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 19:08:14 -0500 Subject: [PATCH 18/49] fastmcp(fix[collector]): Apply a tool transform in full, name what cannot be read why: four more divergences from what the server serves, each measured. - A ToolTransformConfig renames arguments and changes title, description and tags; copying only its name documented lookup(raw_query) with the original title where the server publishes lookup(query) titled Look Up. - An OpenAPI or other dynamic provider lists its tools only asynchronously and holds no registry, so the walk skipped it without a word and documented an empty index for a server that serves tools. - A boolean property subschema -- {"properties": {"payload": true}} is valid JSON Schema -- raised AttributeError and aborted the build. - @tool(description=...) on a function with no docstring documented nothing, because the fallback only covered a missing callable. what: apply a matching ToolTransformConfig to the component through its own apply(), which is synchronous and yields the same TransformedTool the server lists; warn naming any provider with no registry to read; treat a boolean subschema as one with no fields; fall back to the component's description whenever the docstring is empty. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 66 ++++++++------ tests/ext/fastmcp/test_real_server.py | 89 +++++++++++++++++++ 2 files changed, 126 insertions(+), 29 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index be663c09..b80df3dc 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -259,6 +259,9 @@ def _params_from_schema( rows: list[ParamInfo] = [] for name, prop in props.items(): + if not isinstance(prop, dict): + # ``true`` / ``false`` are valid subschemas with no fields. + prop = {} sig_param = sig_params.get(name) annotation = ( _strip_annotated(sig_param.annotation) @@ -331,9 +334,8 @@ def _tool_from_component( meta=meta, func=func, docstring=( - (func.__doc__ or "") - if func is not None - else str(getattr(tool, "description", "") or "") + ((func.__doc__ or "") if func is not None else "") + or str(getattr(tool, "description", "") or "") ), params=_params_from_schema(getattr(tool, "parameters", {}) or {}, func), return_annotation=( @@ -631,31 +633,29 @@ def _reproducible(transform: t.Any) -> bool: def _served(component: t.Any, transforms: tuple[t.Any, ...]) -> t.Any: if not transforms or not hasattr(component, "model_copy"): return component - update: dict[str, t.Any] = {} - for field in ("uri", "uri_template"): - value = getattr(component, field, None) - if value is None: - continue - text = str(value) - for transform in transforms: - if hasattr(transform, "_transform_uri"): - text = transform._transform_uri(text) # noqa: SLF001 - update[field] = text - if not update: - name = str(getattr(component, "name", "")) - is_tool = hasattr(component, "parameters") - for transform in transforms: - if hasattr(transform, "_transform_name"): - name = transform._transform_name(name) # noqa: SLF001 - elif is_tool: - # ToolTransform: a per-tool rename map keyed by the - # original name, applied only to tools. - config = getattr(transform, "_transforms", {}).get(name) - renamed = getattr(config, "name", None) - if renamed: - name = str(renamed) - update["name"] = name - return component.model_copy(update=update) + is_tool = hasattr(component, "parameters") + for transform in transforms: + if hasattr(transform, "_transform_uri"): + update: dict[str, t.Any] = {} + for field in ("uri", "uri_template"): + value = getattr(component, field, None) + if value is not None: + update[field] = transform._transform_uri(str(value)) # noqa: SLF001 + if not update: + update["name"] = transform._transform_name( # noqa: SLF001 + str(getattr(component, "name", "")) + ) + component = component.model_copy(update=update) + elif is_tool: + # ToolTransform: apply the whole configuration -- name, + # title, description, tags and argument renames -- exactly + # as the server does, rather than copying the name alone. + config = getattr(transform, "_transforms", {}).get( + str(getattr(component, "name", "")) + ) + if config is not None and hasattr(config, "apply"): + component = config.apply(component) + return component def walk( node: t.Any, @@ -703,6 +703,14 @@ def walk( continue wrapped = getattr(provider, "_inner", None) if wrapped is None: + # An OpenAPI or other dynamic provider publishes its tools + # only through an async listing; there is no registry to + # read. Say so rather than document an empty index. + logger.warning( + "sphinx_autodoc_fastmcp: provider %s exposes no component " + "registry; its components are not documented", + type(provider).__name__, + ) continue # A mount may wrap its provider more than once -- a rename map # inside a namespace, say. Peel every layer, collecting each @@ -898,7 +906,7 @@ def _template_params_from_schema( rows: list[PromptArgInfo] = [] for name, subschema in props.items(): if not isinstance(subschema, dict): - continue + subschema = {} type_str = _schema_type_text(subschema) rows.append( PromptArgInfo( diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index 3b4e6f83..0e22ce17 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -23,6 +23,7 @@ _annotation_hints, _index_by_unique_name, _iter_components, + _params_from_schema, _prompt_from_component, _resource_from_component, _schema_type_text, @@ -673,3 +674,91 @@ def search( assert collected is not None defaults = {param.name: param.default for param in collected[0].params} assert defaults == {"filters": "", "explicit": "None"} + + +def test_a_boolean_subschema_does_not_abort_collection() -> None: + """``{"properties": {"payload": true}}`` is valid JSON Schema. + + A boolean carries no fields to read; documenting the name with no type + beats failing the build. + """ + rows = _params_from_schema({"properties": {"payload": True}}, lambda payload: None) + + assert [(row.name, row.type_str) for row in rows] == [("payload", "")] + + +def test_a_provider_without_a_registry_is_named_in_a_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """A dynamic provider that lists only asynchronously is reported. + + Skipping it in silence documents an empty index for a server that + serves tools. + """ + + class Dynamic: + transforms: tuple[t.Any, ...] = () + + async def _list_tools(self) -> list[t.Any]: + return [] + + server: FastMCP = FastMCP("server") + server.add_provider(t.cast(t.Any, Dynamic())) + + with caplog.at_level(logging.WARNING): + _iter_components(server) + + assert any("Dynamic" in rec.message for rec in caplog.records) + + +def test_a_tool_transform_is_applied_in_full() -> None: + """A rename map changes title and arguments, not only the tool name.""" + from fastmcp.server.transforms import ToolTransform + from fastmcp.tools.tool_transform import ArgTransformConfig, ToolTransformConfig + + child: FastMCP = FastMCP("child") + + @child.tool + def search(raw_query: str) -> str: + """Search.""" + return "ok" + + child.local_provider.add_transform( + ToolTransform( + { + "search": ToolTransformConfig( + name="lookup", + title="Look Up", + arguments={"raw_query": ArgTransformConfig(name="query")}, + ) + } + ) + ) + parent: FastMCP = FastMCP("parent") + parent.mount(child, namespace="ns") + + collected = _tools_from_server(parent, area_map={}, axes=()) + assert collected is not None + served = asyncio.run(parent.list_tools(run_middleware=False)) + + assert [ + (info.name, info.title, [param.name for param in info.params]) + for info in collected + ] == [ + (tool.name, tool.title, sorted(tool.parameters.get("properties", {}))) + for tool in served + ] + + +def test_a_description_kwarg_survives_an_undocumented_function() -> None: + """``@tool(description=...)`` is the tool's explanation when the + function has no docstring.""" + app: FastMCP = FastMCP("described") + + @app.tool(description="Fetch the selected user record.") + def fetch(user_id: int) -> str: + return "ok" + + collected = _tools_from_server(app, area_map={}, axes=()) + assert collected is not None + assert collected[0].docstring == "Fetch the selected user record." From 471d5ab6980d1ca235c21e0bfb89e6bbce4760d3 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 19:41:28 -0500 Subject: [PATCH 19/49] fastmcp(fix[collector]): Refuse every rename the collector cannot reproduce why: four more divergences from what the server serves, each measured. - A ToolTransform was applied to any component carrying `parameters`, which a resource template also does; a template sharing a targeted tool's name raised TypeError and aborted the build. - A transformed tool's callable is FastMCP's forwarding function, so module attribution landed on FastMCP's own module and every summary link for a renamed tool pointed at tool_transform/. - A custom Transform on a server or provider was filtered out and the pre-transform names published, where the mount path had refused and warned. The Transform contract is async-only, so a rule that is not data cannot be reproduced; publishing past it is the wrong name. - A registry-less provider inside a namespace fell through without the diagnostic its un-namespaced form already got. what: apply a ToolTransform only to a Tool; follow parent_tool to the original callable for attribution; fail closed with a warning at the server and provider levels, matching the mount path; warn on the namespaced fallthrough. Remove the local-registry fallback, which re-published exactly the identities a refusal had declined. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 67 ++++++++--- tests/ext/fastmcp/test_real_server.py | 111 ++++++++++++++++++ 2 files changed, 162 insertions(+), 16 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index b80df3dc..d7a6d795 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -316,7 +316,16 @@ def _tool_from_component( func: t.Callable[..., t.Any] | None = getattr(tool, "fn", None) if func is not None and hasattr(func, "__wrapped__"): func = func.__wrapped__ - module_name = str(getattr(func, "__module__", "") or "").rpartition(".")[2] + # A transformed tool's callable is FastMCP's forwarding function, which + # lives in FastMCP's own module. Attribution -- and so the area map -- + # follows the tool it was made from. + origin: t.Any = tool + while getattr(origin, "parent_tool", None) is not None: + origin = origin.parent_tool + source = getattr(origin, "fn", None) or func + if source is not None and hasattr(source, "__wrapped__"): + source = source.__wrapped__ + module_name = str(getattr(source, "__module__", "") or "").rpartition(".")[2] tags = set(getattr(tool, "tags", None) or ()) meta = dict(getattr(tool, "meta", None) or {}) @@ -624,6 +633,18 @@ def _iter_components(server: t.Any) -> t.Iterable[t.Any]: """ found: list[t.Any] = [] + try: + from fastmcp.tools import Tool as _ToolType + except ImportError: # pragma: no cover - defensive + _ToolType = None # type: ignore[assignment,misc] + + def _is_tool(component: t.Any) -> bool: + if _ToolType is not None: + return isinstance(component, _ToolType) + return hasattr(component, "parameters") and not hasattr( + component, "uri_template" + ) + def _reproducible(transform: t.Any) -> bool: return ( hasattr(transform, "_transform_name") @@ -633,7 +654,7 @@ def _reproducible(transform: t.Any) -> bool: def _served(component: t.Any, transforms: tuple[t.Any, ...]) -> t.Any: if not transforms or not hasattr(component, "model_copy"): return component - is_tool = hasattr(component, "parameters") + is_tool = _is_tool(component) for transform in transforms: if hasattr(transform, "_transform_uri"): update: dict[str, t.Any] = {} @@ -675,21 +696,30 @@ def walk( ) return path = path | {id(node)} - own = tuple( - transform - for transform in getattr(node, "transforms", None) or () - if _reproducible(transform) - ) + own_all = tuple(getattr(node, "transforms", None) or ()) + if not all(_reproducible(transform) for transform in own_all): + logger.warning( + "sphinx_autodoc_fastmcp: server %r renames its components in a " + "way this collector cannot reproduce; its components are not " + "documented", + getattr(node, "name", node), + ) + return + own = own_all # A server's own transforms apply before the namespace it was mounted # under: the server serves outer_inner_hello, not inner_outer_hello. transforms = own + transforms for provider in getattr(node, "providers", None) or (): # A transform added to a provider directly renames what it holds. - local = tuple( - transform - for transform in getattr(provider, "transforms", None) or () - if _reproducible(transform) - ) + local = tuple(getattr(provider, "transforms", None) or ()) + if not all(_reproducible(transform) for transform in local): + logger.warning( + "sphinx_autodoc_fastmcp: provider %s renames its components " + "in a way this collector cannot reproduce; its components " + "are not documented", + type(provider).__name__, + ) + continue components = getattr(provider, "_components", None) if components is not None: found.extend( @@ -753,12 +783,17 @@ def walk( found.extend( _served(component, chain) for component in inner_components.values() ) + continue + logger.warning( + "sphinx_autodoc_fastmcp: provider %s exposes no component " + "registry; its components are not documented", + type(wrapped).__name__, + ) walk(server, 0, (), frozenset()) - if not found: - provider = getattr(server, "local_provider", None) - components = getattr(provider, "_components", None) if provider else None - return tuple(components.values()) if components else () + # No fallback to the local registry: a walk that found nothing either saw + # an empty server or refused a rename it could not reproduce, and reading + # past that refusal would publish the identities it just declined. return tuple(found) diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index 0e22ce17..7f53dbb7 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -762,3 +762,114 @@ def fetch(user_id: int) -> str: collected = _tools_from_server(app, area_map={}, axes=()) assert collected is not None assert collected[0].docstring == "Fetch the selected user record." + + +def test_a_tool_transform_leaves_a_same_named_template_alone() -> None: + """A rename map targets tools only. + + A resource template also carries ``parameters``; applying a tool + transform to it raised and aborted the build. + """ + from fastmcp.server.transforms import ToolTransform + from fastmcp.tools.tool_transform import ToolTransformConfig + + server: FastMCP = FastMCP("server") + + @server.tool + def hello(a: int) -> str: + """Hello.""" + return "ok" + + @server.resource("data://hello/{x}", name="hello") + def hello_template(x: str) -> str: + """Template.""" + return "{}" + + server.local_provider.add_transform( + ToolTransform({"hello": ToolTransformConfig(name="hi")}) + ) + + assert sorted( + f"{type(component).__name__}:{component.name}" + for component in _iter_components(server) + ) == ["FunctionResourceTemplate:hello", "TransformedTool:hi"] + + +def test_a_renamed_tool_keeps_its_source_module() -> None: + """Area attribution follows the tool a transform was made from. + + The forwarding callable lives in FastMCP's own module; attributing to + it broke every summary link for a renamed tool. + """ + child: FastMCP = FastMCP("child") + + @child.tool + def hello(a: int) -> str: + """Hello.""" + return "ok" + + parent: FastMCP = FastMCP("parent") + parent.mount(child, tool_names={"hello": "greet"}) + + collected = _tools_from_server( + parent, area_map={__name__.rpartition(".")[2]: "my/area"}, axes=() + ) + assert collected is not None + assert collected[0].module_name == __name__.rpartition(".")[2] + assert collected[0].area == "my/area" + + +@pytest.mark.parametrize("level", ["server", "provider"]) +def test_an_unreadable_transform_fails_closed_with_a_warning( + level: str, caplog: pytest.LogCaptureFixture +) -> None: + """A transform whose rule is not data is refused, not read past. + + Publishing the pre-transform name documents an identity the server no + longer serves; the mount path already refused, these levels did not. + """ + from fastmcp.server.transforms import Transform + + class Prefix(Transform): + async def list_tools(self, tools: t.Any) -> t.Any: + return [ + tool.model_copy(update={"name": "public_" + tool.name}) + for tool in tools + ] + + server: FastMCP = FastMCP("server") + + @server.tool + def hello(a: int) -> str: + """Hello.""" + return "ok" + + if level == "server": + server.add_transform(Prefix()) + else: + server.local_provider.add_transform(Prefix()) + + with caplog.at_level(logging.WARNING): + walked = _iter_components(server) + + assert walked == () + assert any("cannot reproduce" in rec.message for rec in caplog.records) + + +def test_a_registry_less_provider_under_a_namespace_is_named( + caplog: pytest.LogCaptureFixture, +) -> None: + """Namespacing a dynamic provider does not silence the diagnostic.""" + from fastmcp.server.providers.base import Provider + + class Dynamic(Provider): + async def _list_tools(self) -> list[t.Any]: + return [] + + server: FastMCP = FastMCP("server") + server.add_provider(Dynamic(), namespace="api") + + with caplog.at_level(logging.WARNING): + assert _iter_components(server) == () + + assert any("Dynamic" in rec.message for rec in caplog.records) From f77bb5fff76c8002e95df437daaa2f1766027aaa Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 5 Sep 2026 19:52:38 -0500 Subject: [PATCH 20/49] fastmcp(fix[collector]): List components the way the server does why: reproducing each server-side rename as data could never be general. The Transform contract is entirely async, so a custom transform, a dynamic provider, and -- measured -- a tool disabled with server.disable() all forced the walk to refuse, and the last emptied the whole server: a silently empty index, the exact failure the refusal existed to prevent. what: list through FastMCP's own Provider.list_tools/resources/templates/ prompts on an event loop owned by a background thread, the shape sphinx_vite_builder._internal.bus already uses; asyncio.run raises inside sphinx-autobuild's loop, a thread-owned one does not. Those listings apply every mount, namespace, rename and custom transform exactly as the server does, and keep disabled components -- filtering by enabled state and auth happens one level up in FastMCP.list_*, which is deliberately not used. Through that listing a mounted tool is a proxy with no callable, so _origin_tool follows parent_tool and the proxy's server back to the tool that has one, matching a namespaced name by Namespace's documented namespace_name shape. Attribution, docstrings and Python type display are unchanged from the registry walk on every shape it handled. Measured equal to server.list_*(run_middleware=False) on ten shapes, plus a disabled tool kept where that listing drops it. The refuse-and-warn tests for custom transforms and dynamic providers become served-identity assertions, since those are now documented rather than refused. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 277 +++++++----------- tests/ext/fastmcp/test_real_server.py | 94 +++--- 2 files changed, 151 insertions(+), 220 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index d7a6d795..1b72799a 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -2,12 +2,14 @@ from __future__ import annotations +import asyncio import contextlib import importlib import inspect import json import logging import re +import threading import typing as t from sphinx.application import Sphinx @@ -293,6 +295,46 @@ def _params_from_schema( return rows +def _origin_tool(tool: t.Any, depth: int = 0) -> t.Any: + """Follow a served tool back to the one that owns a Python callable. + + A transform records the tool it was made from as ``parent_tool``. A + mount lists its child's tools as ``FastMCPProviderTool`` proxies, which + carry no callable but hold the child server; the original is that + server's tool of the same name, itself possibly another proxy. + """ + if depth > 8: + return tool + parent = getattr(tool, "parent_tool", None) + if parent is not None: + return _origin_tool(parent, depth + 1) + if getattr(tool, "fn", None) is not None: + return tool + inner = getattr(tool, "_server", None) + if inner is None: + return tool + try: + from fastmcp.server.providers.base import Provider + except ImportError: # pragma: no cover - defensive + return tool + # The proxy carries the name as served one level up, which may already + # include a namespace the child does not know about. Namespace prefixes + # as ``namespace_name``, so the child's tool is the longest inner name + # that ends the served one at an underscore boundary. + wanted = str(getattr(tool, "name", "")) + best: t.Any = None + for candidate in _Loop.call(Provider.list_tools(inner)): + name = str(getattr(candidate, "name", "")) + if name == wanted: + best = candidate + break + if wanted.endswith("_" + name) and ( + best is None or len(name) > len(str(best.name)) + ): + best = candidate + return _origin_tool(best, depth + 1) if best is not None else tool + + def _tool_from_component( tool: t.Any, *, @@ -319,10 +361,13 @@ def _tool_from_component( # A transformed tool's callable is FastMCP's forwarding function, which # lives in FastMCP's own module. Attribution -- and so the area map -- # follows the tool it was made from. - origin: t.Any = tool - while getattr(origin, "parent_tool", None) is not None: - origin = origin.parent_tool + origin = _origin_tool(tool) source = getattr(origin, "fn", None) or func + if source is not None: + # A mounted or transformed tool lists with a proxy or forwarding + # callable; everything read from a signature -- types, docstring -- + # comes from the tool it was made from. + func = source if source is not None and hasattr(source, "__wrapped__"): source = source.__wrapped__ module_name = str(getattr(source, "__module__", "") or "").rpartition(".")[2] @@ -616,185 +661,65 @@ def _ignore_duplicate_policy(provider: t.Any) -> t.Iterator[None]: provider._on_duplicate = original -def _iter_components(server: t.Any) -> t.Iterable[t.Any]: - """Yield every component the server serves, under its served identity. +class _Loop: + """An event loop owned by a background thread, started on first use. - Walks ``server.providers`` rather than ``local_provider`` alone, so a - mounted child server's components are included. Reads the provider - registries directly instead of the async ``_list_*`` helpers, which need an - event loop and additionally filter by enabled state and auth — a tool - switched off at runtime must not vanish from its own documentation. - - A namespaced mount renames what it carries, so each transform is applied - through its own methods: a URI-keyed component takes the namespace in its - URI, a name-keyed one in its name. A transform this cannot reproduce is - refused with a warning, because a name the server does not serve is worse - than a missing page. + Sphinx runs handlers synchronously and may already be inside a loop under + sphinx-autobuild, where ``asyncio.run`` raises. Mirrors + ``sphinx_vite_builder._internal.bus``. """ - found: list[t.Any] = [] - try: - from fastmcp.tools import Tool as _ToolType - except ImportError: # pragma: no cover - defensive - _ToolType = None # type: ignore[assignment,misc] + _loop: asyncio.AbstractEventLoop | None = None + _lock = threading.Lock() - def _is_tool(component: t.Any) -> bool: - if _ToolType is not None: - return isinstance(component, _ToolType) - return hasattr(component, "parameters") and not hasattr( - component, "uri_template" - ) + @classmethod + def call(cls, coro: t.Coroutine[t.Any, t.Any, t.Any], timeout: float = 30) -> t.Any: + with cls._lock: + if cls._loop is None: + ready = threading.Event() - def _reproducible(transform: t.Any) -> bool: - return ( - hasattr(transform, "_transform_name") - and hasattr(transform, "_transform_uri") - ) or isinstance(getattr(transform, "_transforms", None), dict) - - def _served(component: t.Any, transforms: tuple[t.Any, ...]) -> t.Any: - if not transforms or not hasattr(component, "model_copy"): - return component - is_tool = _is_tool(component) - for transform in transforms: - if hasattr(transform, "_transform_uri"): - update: dict[str, t.Any] = {} - for field in ("uri", "uri_template"): - value = getattr(component, field, None) - if value is not None: - update[field] = transform._transform_uri(str(value)) # noqa: SLF001 - if not update: - update["name"] = transform._transform_name( # noqa: SLF001 - str(getattr(component, "name", "")) - ) - component = component.model_copy(update=update) - elif is_tool: - # ToolTransform: apply the whole configuration -- name, - # title, description, tags and argument renames -- exactly - # as the server does, rather than copying the name alone. - config = getattr(transform, "_transforms", {}).get( - str(getattr(component, "name", "")) - ) - if config is not None and hasattr(config, "apply"): - component = config.apply(component) - return component - - def walk( - node: t.Any, - depth: int, - transforms: tuple[t.Any, ...], - path: frozenset[int], - ) -> None: - # Guard the active path only. The same child mounted under two - # namespaces is served twice under two names, and is not a cycle. - if node is None or id(node) in path: - return - if depth > 8: - logger.warning( - "sphinx_autodoc_fastmcp: mount tree deeper than 8 levels; " - "components below %r are not documented", - getattr(node, "name", node), - ) - return - path = path | {id(node)} - own_all = tuple(getattr(node, "transforms", None) or ()) - if not all(_reproducible(transform) for transform in own_all): - logger.warning( - "sphinx_autodoc_fastmcp: server %r renames its components in a " - "way this collector cannot reproduce; its components are not " - "documented", - getattr(node, "name", node), - ) - return - own = own_all - # A server's own transforms apply before the namespace it was mounted - # under: the server serves outer_inner_hello, not inner_outer_hello. - transforms = own + transforms - for provider in getattr(node, "providers", None) or (): - # A transform added to a provider directly renames what it holds. - local = tuple(getattr(provider, "transforms", None) or ()) - if not all(_reproducible(transform) for transform in local): - logger.warning( - "sphinx_autodoc_fastmcp: provider %s renames its components " - "in a way this collector cannot reproduce; its components " - "are not documented", - type(provider).__name__, - ) - continue - components = getattr(provider, "_components", None) - if components is not None: - found.extend( - _served(component, local + transforms) - for component in components.values() - ) - continue - inner = getattr(provider, "server", None) - if inner is not None: - walk(inner, depth + 1, local + transforms, path) - continue - wrapped = getattr(provider, "_inner", None) - if wrapped is None: - # An OpenAPI or other dynamic provider publishes its tools - # only through an async listing; there is no registry to - # read. Say so rather than document an empty index. - logger.warning( - "sphinx_autodoc_fastmcp: provider %s exposes no component " - "registry; its components are not documented", - type(provider).__name__, - ) - continue - # A mount may wrap its provider more than once -- a rename map - # inside a namespace, say. Peel every layer, collecting each - # layer's transforms so the innermost applies first. - layers: list[t.Any] = [provider] - while getattr(wrapped, "_inner", None) is not None: - layers.append(wrapped) - wrapped = wrapped._inner # noqa: SLF001 - # The innermost provider's own transforms apply before any - # wrapper's, so they lead the chain. - added: list[t.Any] = [] - reproducible = True - for transform in getattr(wrapped, "transforms", None) or (): - if _reproducible(transform): - added.append(transform) - else: - reproducible = False - for layer in reversed(layers): - for transform in getattr(layer, "transforms", None) or (): - if _reproducible(transform): - added.append(transform) - else: - reproducible = False - if not reproducible: - logger.warning( - "sphinx_autodoc_fastmcp: a mounted provider renames its " - "components in a way this collector cannot reproduce; " - "its components are not documented", - ) - continue - # Inner namespaces apply first: the server serves - # outer_inner_hello, not inner_outer_hello. - chain = tuple(added) + transforms - inner_server = getattr(wrapped, "server", None) - if inner_server is not None: - walk(inner_server, depth + 1, chain, path) - continue - inner_components = getattr(wrapped, "_components", None) - if inner_components is not None: - found.extend( - _served(component, chain) for component in inner_components.values() - ) - continue - logger.warning( - "sphinx_autodoc_fastmcp: provider %s exposes no component " - "registry; its components are not documented", - type(wrapped).__name__, - ) + def run() -> None: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + cls._loop = loop + ready.set() + loop.run_forever() + + threading.Thread( + target=run, name="sphinx-autodoc-fastmcp", daemon=True + ).start() + ready.wait() + assert cls._loop is not None + return asyncio.run_coroutine_threadsafe(coro, cls._loop).result(timeout=timeout) + + +def _iter_components(server: t.Any) -> t.Iterable[t.Any]: + """Yield every component the server serves, under its served identity. - walk(server, 0, (), frozenset()) - # No fallback to the local registry: a walk that found nothing either saw - # an empty server or refused a rename it could not reproduce, and reading - # past that refusal would publish the identities it just declined. - return tuple(found) + Lists through FastMCP's own ``Provider.list_*`` rather than reading + registries: those methods apply every mount, namespace, rename and custom + transform exactly as the server does, and keep disabled components -- + filtering by enabled state and auth happens one level up, in + ``FastMCP.list_*``, which is deliberately not used so a tool switched off + at runtime stays in its own documentation. Bypasses middleware. + """ + try: + from fastmcp.server.providers.base import Provider + except ImportError: # pragma: no cover - defensive + return () + + async def listing() -> list[t.Any]: + out: list[t.Any] = [] + for method in ( + Provider.list_tools, + Provider.list_resources, + Provider.list_resource_templates, + Provider.list_prompts, + ): + out.extend(await method(server)) + return out + + return tuple(_Loop.call(listing())) #: FastMCP appends its schema hint as a trailing blank-line-separated diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index 7f53dbb7..911775ac 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -687,30 +687,6 @@ def test_a_boolean_subschema_does_not_abort_collection() -> None: assert [(row.name, row.type_str) for row in rows] == [("payload", "")] -def test_a_provider_without_a_registry_is_named_in_a_warning( - caplog: pytest.LogCaptureFixture, -) -> None: - """A dynamic provider that lists only asynchronously is reported. - - Skipping it in silence documents an empty index for a server that - serves tools. - """ - - class Dynamic: - transforms: tuple[t.Any, ...] = () - - async def _list_tools(self) -> list[t.Any]: - return [] - - server: FastMCP = FastMCP("server") - server.add_provider(t.cast(t.Any, Dynamic())) - - with caplog.at_level(logging.WARNING): - _iter_components(server) - - assert any("Dynamic" in rec.message for rec in caplog.records) - - def test_a_tool_transform_is_applied_in_full() -> None: """A rename map changes title and arguments, not only the tool name.""" from fastmcp.server.transforms import ToolTransform @@ -820,13 +796,11 @@ def hello(a: int) -> str: @pytest.mark.parametrize("level", ["server", "provider"]) -def test_an_unreadable_transform_fails_closed_with_a_warning( - level: str, caplog: pytest.LogCaptureFixture -) -> None: - """A transform whose rule is not data is refused, not read past. +def test_a_custom_transform_is_documented_as_served(level: str) -> None: + """A transform whose rule is not data still documents the served name. - Publishing the pre-transform name documents an identity the server no - longer serves; the mount path already refused, these levels did not. + Listing through FastMCP's own ``Provider.list_*`` applies every + transform the way the server does, so nothing has to be reproduced. """ from fastmcp.server.transforms import Transform @@ -849,27 +823,59 @@ def hello(a: int) -> str: else: server.local_provider.add_transform(Prefix()) - with caplog.at_level(logging.WARNING): - walked = _iter_components(server) - - assert walked == () - assert any("cannot reproduce" in rec.message for rec in caplog.records) + assert sorted( + tool.name for tool in _iter_components(server) if isinstance(tool, _Tool) + ) == sorted( + tool.name for tool in asyncio.run(server.list_tools(run_middleware=False)) + ) -def test_a_registry_less_provider_under_a_namespace_is_named( - caplog: pytest.LogCaptureFixture, -) -> None: - """Namespacing a dynamic provider does not silence the diagnostic.""" +@pytest.mark.parametrize("namespace", [None, "api"]) +def test_a_dynamic_provider_is_documented(namespace: str | None) -> None: + """A provider that lists only asynchronously is still collected.""" from fastmcp.server.providers.base import Provider class Dynamic(Provider): async def _list_tools(self) -> list[t.Any]: - return [] + return [_Tool.from_function(lambda a: "ok", name="dyn_tool")] server: FastMCP = FastMCP("server") - server.add_provider(Dynamic(), namespace="api") + if namespace is None: + server.add_provider(Dynamic()) + else: + server.add_provider(Dynamic(), namespace=namespace) - with caplog.at_level(logging.WARNING): - assert _iter_components(server) == () + assert sorted( + tool.name for tool in _iter_components(server) if isinstance(tool, _Tool) + ) == sorted( + tool.name for tool in asyncio.run(server.list_tools(run_middleware=False)) + ) + + +def test_a_disabled_tool_stays_in_its_documentation() -> None: + """A tool switched off at runtime is documented anyway. + + ``FastMCP.list_tools`` drops it even with middleware off; the + provider-level listing does not, and the docs describe what the server + can serve. + """ + server: FastMCP = FastMCP("server") + + @server.tool + def visible(a: int) -> str: + """Visible.""" + return "ok" + + @server.tool + def hidden(a: int) -> str: + """Hidden.""" + return "ok" + + server.disable(keys={"tool:hidden@"}) - assert any("Dynamic" in rec.message for rec in caplog.records) + assert "hidden" not in { + tool.name for tool in asyncio.run(server.list_tools(run_middleware=False)) + } + assert sorted( + tool.name for tool in _iter_components(server) if isinstance(tool, _Tool) + ) == ["hidden", "visible"] From e554fb29561578630a3c238c066d9050d9456e47 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 03:48:45 -0500 Subject: [PATCH 21/49] fastmcp(fix[collector]): List a mounted server the way it lists itself why: the listing was only unfiltered at the root. FastMCPProvider lists a mounted child by calling the child's own list_tools(), which drops disabled components and runs the child's middleware -- so a tool switched off or gated inside a mounted server disappeared from its documentation, the failure listing through the provider level exists to prevent. Measured: a child's disabled tool survives direct collection and vanishes under a mount. Two more, both from the proxy that mounting inserts: - Recovering the wrapped tool by stripping the namespace off the served name picks a sibling literally called namespace_name. A child defining hello and ns_hello, mounted under ns, documented the decoy's description and types for both. - Replacing the callable with the origin's made a transformed tool contradict its own schema: Tool.from_tool retyping an argument to str still documented int. what: descend into a mounted child and compose each provider's and server's transforms the way Provider.list_* does, instead of asking the child for its own listing. Match a proxy on the _original_name it recorded. Use the origin callable only where it is the truth -- module attribution, and a proxy with no transform between; a transformed tool's types come from the schema it publishes. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 87 ++++++++++++------- tests/ext/fastmcp/test_real_server.py | 86 ++++++++++++++++++ 2 files changed, 144 insertions(+), 29 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index 1b72799a..ed05b911 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -317,22 +317,17 @@ def _origin_tool(tool: t.Any, depth: int = 0) -> t.Any: from fastmcp.server.providers.base import Provider except ImportError: # pragma: no cover - defensive return tool - # The proxy carries the name as served one level up, which may already - # include a namespace the child does not know about. Namespace prefixes - # as ``namespace_name``, so the child's tool is the longest inner name - # that ends the served one at an underscore boundary. - wanted = str(getattr(tool, "name", "")) - best: t.Any = None + # The proxy records the name it wrapped. Guessing it back out of the + # served name picks the wrong tool when a sibling is literally called + # ``namespace_name``. + wanted = str(getattr(tool, "_original_name", "") or getattr(tool, "name", "")) + version = getattr(tool, "version", None) for candidate in _Loop.call(Provider.list_tools(inner)): - name = str(getattr(candidate, "name", "")) - if name == wanted: - best = candidate - break - if wanted.endswith("_" + name) and ( - best is None or len(name) > len(str(best.name)) + if str(getattr(candidate, "name", "")) == wanted and ( + version is None or getattr(candidate, "version", None) == version ): - best = candidate - return _origin_tool(best, depth + 1) if best is not None else tool + return _origin_tool(candidate, depth + 1) + return tool def _tool_from_component( @@ -363,10 +358,9 @@ def _tool_from_component( # follows the tool it was made from. origin = _origin_tool(tool) source = getattr(origin, "fn", None) or func - if source is not None: - # A mounted or transformed tool lists with a proxy or forwarding - # callable; everything read from a signature -- types, docstring -- - # comes from the tool it was made from. + if func is None and getattr(tool, "parent_tool", None) is None: + # A proxy carries no callable of its own; an untransformed one + # documents exactly what it wraps. func = source if source is not None and hasattr(source, "__wrapped__"): source = source.__wrapped__ @@ -661,6 +655,50 @@ def _ignore_duplicate_policy(provider: t.Any) -> t.Iterator[None]: provider._on_duplicate = original +async def _apply(kind: str, holder: t.Any, components: list[t.Any]) -> list[t.Any]: + """Run ``holder``'s transforms over a listing, in registration order.""" + for transform in getattr(holder, "transforms", None) or (): + method = getattr(transform, f"list_{kind}", None) + if method is not None: + components = list(await method(components)) + return components + + +async def _provider_components( + kind: str, provider: t.Any, depth: int, path: frozenset[int] +) -> list[t.Any]: + """List one provider's components, unfiltered, with its transforms applied. + + A mounted server is descended into rather than asked for its own listing: + ``FastMCPProvider._list_tools`` calls the child's ``list_tools()``, which + drops disabled components and runs the child's middleware, so a tool + switched off or gated inside a mounted server would vanish from its + documentation. + """ + child = getattr(provider, "server", None) + if child is not None: + base = await _server_components(kind, child, depth + 1, path) + elif getattr(provider, "_inner", None) is not None: + base = await _provider_components(kind, provider._inner, depth, path) # noqa: SLF001 + else: + method = getattr(provider, f"_list_{kind}", None) + base = list(await method()) if method is not None else [] + return await _apply(kind, provider, base) + + +async def _server_components( + kind: str, server: t.Any, depth: int, path: frozenset[int] +) -> list[t.Any]: + """List every component a server serves, its own transforms applied last.""" + if depth > 8 or id(server) in path: + return [] + path = path | {id(server)} + out: list[t.Any] = [] + for provider in getattr(server, "providers", None) or (): + out.extend(await _provider_components(kind, provider, depth, path)) + return await _apply(kind, server, out) + + class _Loop: """An event loop owned by a background thread, started on first use. @@ -703,20 +741,11 @@ def _iter_components(server: t.Any) -> t.Iterable[t.Any]: ``FastMCP.list_*``, which is deliberately not used so a tool switched off at runtime stays in its own documentation. Bypasses middleware. """ - try: - from fastmcp.server.providers.base import Provider - except ImportError: # pragma: no cover - defensive - return () async def listing() -> list[t.Any]: out: list[t.Any] = [] - for method in ( - Provider.list_tools, - Provider.list_resources, - Provider.list_resource_templates, - Provider.list_prompts, - ): - out.extend(await method(server)) + for kind in ("tools", "resources", "resource_templates", "prompts"): + out.extend(await _server_components(kind, server, 0, frozenset())) return out return tuple(_Loop.call(listing())) diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index 911775ac..e371bd0b 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -879,3 +879,89 @@ def hidden(a: int) -> str: assert sorted( tool.name for tool in _iter_components(server) if isinstance(tool, _Tool) ) == ["hidden", "visible"] + + +def test_a_disabled_tool_inside_a_mount_stays_documented() -> None: + """Disabling reaches into a mounted child, and documentation does not. + + ``FastMCPProvider`` lists its child through the child's own + ``list_tools()``, which drops disabled components and runs the child's + middleware, so a tool switched off or gated inside a mounted server + vanished from the docs. + """ + child: FastMCP = FastMCP("child") + + @child.tool + def visible(a: int) -> str: + """Visible.""" + return "ok" + + @child.tool + def hidden(a: int) -> str: + """Hidden.""" + return "ok" + + child.disable(keys={"tool:hidden@"}) + parent: FastMCP = FastMCP("parent") + parent.mount(child, namespace="ns") + + assert "ns_hidden" not in { + tool.name for tool in asyncio.run(parent.list_tools(run_middleware=False)) + } + assert sorted( + tool.name for tool in _iter_components(parent) if isinstance(tool, _Tool) + ) == ["ns_hidden", "ns_visible"] + + +def test_a_namespaced_tool_resolves_past_a_decoy_sibling() -> None: + """A sibling literally named ``namespace_name`` is not the origin. + + Recovering the original by stripping the namespace off the served name + matches the decoy, and documents its description and types. + """ + child: FastMCP = FastMCP("child") + + @child.tool + def hello(x: int) -> str: + """Real hello.""" + return "ok" + + @child.tool + def ns_hello(x: str) -> str: + """Decoy.""" + return "ok" + + parent: FastMCP = FastMCP("parent") + parent.mount(child, namespace="ns") + + collected = _tools_from_server(parent, area_map={}, axes=()) + assert collected is not None + documented = {info.name: info for info in collected} + assert documented["ns_hello"].docstring == "Real hello." + assert [p.type_str for p in documented["ns_hello"].params] == ["int"] + assert documented["ns_ns_hello"].docstring == "Decoy." + + +def test_a_transformed_argument_documents_its_served_type() -> None: + """A transform that retypes an argument owns the displayed type. + + The tool it was made from still says ``int``; the server publishes + ``string``, and the docs describe what a caller may send. + """ + from fastmcp.tools.tool_transform import ArgTransform + + def square(x: int) -> int: + """Square.""" + return x * x + + transformed = _Tool.from_tool( + _Tool.from_function(square), + name="square_str", + transform_args={"x": ArgTransform(type=str)}, + ) + server: FastMCP = FastMCP("server") + server.add_tool(transformed) + + collected = _tools_from_server(server, area_map={}, axes=()) + assert collected is not None + assert [(p.name, p.type_str) for p in collected[0].params] == [("x", "string")] From 76dcb386ddf9c9d80f63c0a5ea3f0bb48340a32e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 04:02:58 -0500 Subject: [PATCH 22/49] fastmcp(fix[collector]): Survive a failing provider, traverse an aggregate why: descending past a provider to read it unfiltered also stepped past FastMCP's own handling of two things it does around that call. - An aggregate defaults to provider_error_strategy="warn": an unreachable remote is logged and its siblings still serve. Listing providers directly inherited none of that, so one failing provider raised out of builder-inited and the build produced no documentation at all, where the server itself still answered with its healthy tools. - An AggregateProvider holds providers rather than a server or an inner provider, so it fell through to its own listing, which re-enters each child's filtered one. A mount's disabled tool survived directly and vanished once wrapped in an aggregate -- the boundary fix from 8512ace1, one wrapper out. Separately, the docstring outranked the published description, so an explicit description= or one a transform rewrote lost to the function's own docstring. What the server publishes is what a caller reads. what: gather providers through the holder's configured failure strategy, recurse into an aggregate's providers, and prefer the component's description with the docstring as fallback. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 47 +++++++- tests/ext/fastmcp/test_real_server.py | 102 ++++++++++++++++++ 2 files changed, 144 insertions(+), 5 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index ed05b911..8c293e26 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -381,9 +381,12 @@ def _tool_from_component( annotations=ann_dict, meta=meta, func=func, + # What the server publishes is what a caller reads, so an explicit + # description -- or one a transform rewrote -- wins over the + # function's own docstring. docstring=( - ((func.__doc__ or "") if func is not None else "") - or str(getattr(tool, "description", "") or "") + str(getattr(tool, "description", "") or "") + or ((func.__doc__ or "") if func is not None else "") ), params=_params_from_schema(getattr(tool, "parameters", {}) or {}, func), return_annotation=( @@ -664,6 +667,37 @@ async def _apply(kind: str, holder: t.Any, components: list[t.Any]) -> list[t.An return components +async def _gather( + kind: str, + holder: t.Any, + providers: t.Iterable[t.Any], + depth: int, + path: frozenset[int], +) -> list[t.Any]: + """List several providers, honouring the holder's failure strategy. + + FastMCP's aggregate defaults to ``provider_error_strategy="warn"``: an + unreachable remote is logged and skipped, and its siblings still serve. + Letting one failure escape here would abort the whole build instead. + """ + raise_on_error = getattr(holder, "provider_error_strategy", "warn") == "raise" + out: list[t.Any] = [] + for provider in providers: + try: + out.extend(await _provider_components(kind, provider, depth, path)) + except Exception: + if raise_on_error: + raise + logger.warning( + "sphinx_autodoc_fastmcp: provider %s failed to list its %s; " + "its components are not documented", + type(provider).__name__, + kind.replace("_", " "), + exc_info=True, + ) + return out + + async def _provider_components( kind: str, provider: t.Any, depth: int, path: frozenset[int] ) -> list[t.Any]: @@ -680,6 +714,10 @@ async def _provider_components( base = await _server_components(kind, child, depth + 1, path) elif getattr(provider, "_inner", None) is not None: base = await _provider_components(kind, provider._inner, depth, path) # noqa: SLF001 + elif getattr(provider, "providers", None) is not None: + # An aggregate holds providers of its own; asking it to list would + # re-enter the filtered path for every one of them. + base = await _gather(kind, provider, provider.providers, depth, path) else: method = getattr(provider, f"_list_{kind}", None) base = list(await method()) if method is not None else [] @@ -693,9 +731,8 @@ async def _server_components( if depth > 8 or id(server) in path: return [] path = path | {id(server)} - out: list[t.Any] = [] - for provider in getattr(server, "providers", None) or (): - out.extend(await _provider_components(kind, provider, depth, path)) + providers = getattr(server, "providers", None) or () + out = await _gather(kind, server, providers, depth, path) return await _apply(kind, server, out) diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index e371bd0b..f5ccdaa5 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -965,3 +965,105 @@ def square(x: int) -> int: collected = _tools_from_server(server, area_map={}, axes=()) assert collected is not None assert [(p.name, p.type_str) for p in collected[0].params] == [("x", "string")] + + +def test_one_failing_provider_does_not_abort_the_build( + caplog: pytest.LogCaptureFixture, +) -> None: + """An unreachable provider is logged and skipped, not raised. + + FastMCP's aggregate defaults to ``provider_error_strategy="warn"`` and + still serves its healthy providers; letting the failure escape aborts + ``builder-inited`` and produces no documentation at all. + """ + from fastmcp.server.providers.base import Provider + + class Broken(Provider): + async def _list_tools(self) -> list[t.Any]: + msg = "remote unavailable" + raise OSError(msg) + + server: FastMCP = FastMCP("server") + + @server.tool + def local(a: int) -> str: + """Local.""" + return "ok" + + server.add_provider(Broken()) + + with caplog.at_level(logging.WARNING): + collected = [ + tool.name for tool in _iter_components(server) if isinstance(tool, _Tool) + ] + + assert collected == ["local"] + assert any("Broken" in rec.message for rec in caplog.records) + + +def test_a_configured_raise_strategy_is_honoured() -> None: + """``provider_error_strategy="raise"`` still fails the collection.""" + from fastmcp.server.providers.base import Provider + + class Broken(Provider): + async def _list_tools(self) -> list[t.Any]: + msg = "remote unavailable" + raise OSError(msg) + + server: FastMCP = FastMCP("server") + server.add_provider(Broken()) + server.provider_error_strategy = "raise" + + with pytest.raises(OSError, match="remote unavailable"): + _iter_components(server) + + +def test_an_aggregate_provider_is_traversed_not_asked() -> None: + """A mount wrapped in an aggregate keeps its disabled components. + + An aggregate holds providers of its own; asking it to list re-enters + each child's filtered listing. + """ + from fastmcp.server.providers.aggregate import AggregateProvider + from fastmcp.server.providers.fastmcp_provider import FastMCPProvider + + child: FastMCP = FastMCP("child") + + @child.tool + def visible(a: int) -> str: + """Visible.""" + return "ok" + + @child.tool + def hidden(a: int) -> str: + """Hidden.""" + return "ok" + + child.disable(keys={"tool:hidden@"}) + server: FastMCP = FastMCP("server") + server.add_provider(AggregateProvider([FastMCPProvider(child)])) + + assert sorted( + tool.name for tool in _iter_components(server) if isinstance(tool, _Tool) + ) == ["hidden", "visible"] + + +def test_a_published_description_outranks_the_docstring() -> None: + """``description=`` is what the server tells a caller.""" + server: FastMCP = FastMCP("server") + + @server.tool(description="Published description.") + def both(a: int) -> str: + """Docstring text.""" + return "ok" + + @server.tool + def only_doc(a: int) -> str: + """Only a docstring.""" + return "ok" + + collected = _tools_from_server(server, area_map={}, axes=()) + assert collected is not None + documented = {info.name: info.docstring for info in collected} + assert documented["both"] == "Published description." + assert documented["only_doc"] == "Only a docstring." From c61304de9693bc25681923591a36012454b2b198 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 04:21:46 -0500 Subject: [PATCH 23/49] fastmcp(fix[collector]): Bound each provider, and keep what authors wrote why: three ways collection still lost to something it should have survived. - The listing was bounded as a whole, on the future the Sphinx thread waits on, so the timeout fired outside the per-provider failure policy. A provider that hangs aborted the build with TimeoutError even under provider_error_strategy="warn", where a provider that raises was already logged and skipped. - The schema-note pattern matched any final paragraph asking for a JSON schema, so an authored description -- 'Provide a JSON schema: {"type": "object"}.' -- was erased entirely. Both generated spellings say "matching the following"; an author does not. - A rename-only transform yields an unannotated forwarding callable, so the Returns fact disappeared from a tool that returns exactly what it did before. Its published output schema still describes the result. what: bound each provider with asyncio.wait_for inside the failure policy, so a slow remote is logged and skipped like a failing one; require "matching the following" in the note pattern; and fall back to the output schema for the return display, unwrapping the single `result` property FastMCP uses for a non-object return. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 49 +++++++++-- tests/ext/fastmcp/test_real_server.py | 83 +++++++++++++++++++ 2 files changed, 125 insertions(+), 7 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index 8c293e26..83e63536 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -330,6 +330,22 @@ def _origin_tool(tool: t.Any, depth: int = 0) -> t.Any: return tool +def _return_from_schema(schema: t.Any) -> str: + """Describe a tool's result from the output schema it publishes. + + A transform's forwarding callable is ``(**kwargs)`` with no return + annotation, but the tool still publishes what it returns. FastMCP wraps a + non-object result in a single ``result`` property. + """ + if not isinstance(schema, dict): + return "" + props = schema.get("properties") + if isinstance(props, dict) and set(props) == {"result"}: + inner = props["result"] + return _schema_type_text(inner) if isinstance(inner, dict) else "" + return _schema_type_text(schema) + + def _tool_from_component( tool: t.Any, *, @@ -390,9 +406,12 @@ def _tool_from_component( ), params=_params_from_schema(getattr(tool, "parameters", {}) or {}, func), return_annotation=( - normalize_annotation_text(inspect.signature(func).return_annotation) - if func is not None - else "" + ( + normalize_annotation_text(inspect.signature(func).return_annotation) + if func is not None + else "" + ) + or _return_from_schema(getattr(tool, "output_schema", None)) ), ) @@ -667,6 +686,13 @@ async def _apply(kind: str, holder: t.Any, components: list[t.Any]) -> list[t.An return components +#: Seconds one provider may take to list its components. A slow remote is +#: treated like a failing one -- logged and skipped -- so it cannot hold up +#: the build. Bounding each provider rather than the whole listing keeps the +#: cancellation inside the failure policy. +_PROVIDER_TIMEOUT = 30.0 + + async def _gather( kind: str, holder: t.Any, @@ -684,7 +710,12 @@ async def _gather( out: list[t.Any] = [] for provider in providers: try: - out.extend(await _provider_components(kind, provider, depth, path)) + out.extend( + await asyncio.wait_for( + _provider_components(kind, provider, depth, path), + timeout=_PROVIDER_TIMEOUT, + ) + ) except Exception: if raise_on_error: raise @@ -748,7 +779,9 @@ class _Loop: _lock = threading.Lock() @classmethod - def call(cls, coro: t.Coroutine[t.Any, t.Any, t.Any], timeout: float = 30) -> t.Any: + def call( + cls, coro: t.Coroutine[t.Any, t.Any, t.Any], timeout: float | None = 30 + ) -> t.Any: with cls._lock: if cls._loop is None: ready = threading.Event() @@ -785,7 +818,8 @@ async def listing() -> list[t.Any]: out.extend(await _server_components(kind, server, 0, frozenset())) return out - return tuple(_Loop.call(listing())) + # Each provider is bounded individually; this only backstops the join. + return tuple(_Loop.call(listing(), timeout=None)) #: FastMCP appends its schema hint as a trailing blank-line-separated @@ -797,7 +831,8 @@ async def listing() -> list[t.Any]: #: is what separates the generated note from a written sentence that happens to #: ask the reader for a JSON schema. _SCHEMA_NOTE_RE = re.compile( - r"^Provide\b.*\bJSON\b.*\bschema\b[^{]*:\s*\{", re.IGNORECASE | re.DOTALL + r"^Provide\b.*\bmatching the following\b.*\bschema\b[^{]*:\s*\{", + re.IGNORECASE | re.DOTALL, ) diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index f5ccdaa5..5f9823c1 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -27,6 +27,7 @@ _prompt_from_component, _resource_from_component, _schema_type_text, + _strip_schema_note, _tool_from_component, _tools_from_server, collect_tools, @@ -1067,3 +1068,85 @@ def only_doc(a: int) -> str: documented = {info.name: info.docstring for info in collected} assert documented["both"] == "Published description." assert documented["only_doc"] == "Only a docstring." + + +def test_a_slow_provider_is_skipped_like_a_failing_one( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: + """A provider that hangs does not hold the build hostage. + + Bounding the whole listing raises in the Sphinx thread, outside the + per-provider failure policy, so a slow remote aborted collection even + under ``provider_error_strategy="warn"``. + """ + from fastmcp.server.providers.base import Provider + + from sphinx_autodoc_fastmcp import _collector + + class Slow(Provider): + async def _list_tools(self) -> list[t.Any]: + await asyncio.sleep(30) + return [] + + monkeypatch.setattr(_collector, "_PROVIDER_TIMEOUT", 0.5) + server: FastMCP = FastMCP("server") + + @server.tool + def local(a: int) -> str: + """Local.""" + return "ok" + + server.add_provider(Slow()) + + with caplog.at_level(logging.WARNING): + collected = [ + tool.name for tool in _iter_components(server) if isinstance(tool, _Tool) + ] + + assert collected == ["local"] + assert any("Slow" in rec.message for rec in caplog.records) + + +def test_an_authored_schema_request_is_not_stripped() -> None: + """Only FastMCP's generated instruction is removed. + + Both spellings say "matching the following"; an author asking for a + schema does not, and their guidance is the whole description. + """ + authored = 'Provide a JSON schema: {"type": "object"}.' + + assert _strip_schema_note(authored) == authored + assert _strip_schema_note(f"Summary.\n\n{authored}") == f"Summary.\n\n{authored}" + assert ( + _strip_schema_note( + "Summary.\n\nProvide a value matching the following JSON schema: " + '{"type":"number"}. Encode non-string values as JSON.' + ) + == "Summary." + ) + + +def test_a_renamed_tool_keeps_its_return_information() -> None: + """A rename leaves the return contract intact, so the docs should too. + + The forwarding callable carries no annotation; the published output + schema still describes the result. + """ + from fastmcp.server.transforms import ToolTransform + from fastmcp.tools.tool_transform import ToolTransformConfig + + server: FastMCP = FastMCP("server") + + @server.tool + def counts(a: int) -> int: + """Counts.""" + return 1 + + server.local_provider.add_transform( + ToolTransform({"counts": ToolTransformConfig(name="renamed")}) + ) + + collected = _tools_from_server(server, area_map={}, axes=()) + assert collected is not None + assert collected[0].name == "renamed" + assert collected[0].return_annotation == "integer" From cae0916c4d410b47175a4e5ec6e2ab62c8d5a9d6 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 04:25:45 -0500 Subject: [PATCH 24/49] fastmcp(test[collector]): Assert served identity across the attachment matrix why: each round of review has found a defect on a shape the previous tests did not vary, and a passing suite kept reading as coverage it was not. The 64-cell matrix that returned no mismatches was built entirely with mount(), so it missed both an aggregate provider re-entering a filtered listing and a failing provider aborting the build. what: parametrize over how a child is attached -- mount, add_provider, an aggregate wrapper, each with and without a namespace, plus a child carrying its own transform -- at one and two levels, asserting collected identity equals what the server lists for tools, resources, templates and prompts. A second case asserts a disabled tool survives every attachment, since each reaches its child by a different path. Removing either boundary fix fails the matrix on exactly the shapes it governs, which the point tests could not do. --- tests/ext/fastmcp/test_real_server.py | 136 +++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 1 deletion(-) diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index 5f9823c1..caa92834 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -37,7 +37,10 @@ from fastmcp import Context, FastMCP # noqa: E402 from fastmcp.prompts import Prompt as _Prompt # noqa: E402 -from fastmcp.resources import Resource as _Resource # noqa: E402 +from fastmcp.resources import ( + Resource as _Resource, # noqa: E402 + ResourceTemplate as _ResourceTemplate, # noqa: E402 +) from fastmcp.tools import Tool as _Tool # noqa: E402 from mcp.types import Annotations, ToolAnnotations # noqa: E402 @@ -1150,3 +1153,134 @@ def counts(a: int) -> int: assert collected is not None assert collected[0].name == "renamed" assert collected[0].return_annotation == "integer" + + +def _identity(component: t.Any) -> str: + """The name or URI a component is served under.""" + return str( + getattr(component, "uri", None) + or getattr(component, "uri_template", None) + or component.name + ) + + +def _matrix_leaf() -> FastMCP: + """A server carrying one component of every kind.""" + server: FastMCP = FastMCP("leaf") + + @server.tool + def alpha(a: int) -> str: + """Alpha.""" + return "ok" + + @server.resource("data://thing") + def thing() -> str: + """Thing.""" + return "{}" + + @server.resource("data://item/{x}") + def item(x: str) -> str: + """Item.""" + return "{}" + + @server.prompt + def draft(topic: str) -> str: + """Draft.""" + return "drafted" + + return server + + +def _attach(shape: str, parent: FastMCP, child: FastMCP, index: int) -> None: + """Attach ``child`` to ``parent`` the way ``shape`` names.""" + from fastmcp.server.providers.aggregate import AggregateProvider + from fastmcp.server.providers.fastmcp_provider import FastMCPProvider + from fastmcp.server.transforms import Namespace + + namespace = f"n{index}" + if shape == "mount": + parent.mount(child) + elif shape == "mount+ns": + parent.mount(child, namespace=namespace) + elif shape == "add_provider": + parent.add_provider(FastMCPProvider(child)) + elif shape == "add_provider+ns": + parent.add_provider(FastMCPProvider(child), namespace=namespace) + elif shape == "aggregate": + parent.add_provider(AggregateProvider([FastMCPProvider(child)])) + elif shape == "aggregate+ns": + parent.add_provider( + AggregateProvider([FastMCPProvider(child)]), namespace=namespace + ) + elif shape == "child-transform": + child.add_transform(Namespace(f"x{index}")) + parent.mount(child) + else: # pragma: no cover - guards a typo in the parametrization + msg = f"unknown attachment shape {shape!r}" + raise AssertionError(msg) + + +#: Every way a server can carry another server's components. Each round of +#: review has found a defect on a shape the previous matrix did not vary, so +#: the axis is the attachment itself, not one example of it. +_ATTACHMENTS = ( + "mount", + "mount+ns", + "add_provider", + "add_provider+ns", + "aggregate", + "aggregate+ns", + "child-transform", +) + +_KINDS = { + "tools": "list_tools", + "resources": "list_resources", + "resource_templates": "list_resource_templates", + "prompts": "list_prompts", +} + + +@pytest.mark.parametrize("shape", _ATTACHMENTS) +@pytest.mark.parametrize("depth", [1, 2]) +def test_collected_identity_equals_served_identity(shape: str, depth: int) -> None: + """Every component is documented under the identity the server serves. + + The matrix varies how a child is attached and how deeply, because a + rename applies differently at each boundary and the collector has to + arrive at the same answer the server does. + """ + server = _matrix_leaf() + for index in range(depth): + parent: FastMCP = FastMCP(f"level{index}") + _attach(shape, parent, server, index) + server = parent + + walked = _iter_components(server) + for kind, method in _KINDS.items(): + served = asyncio.run(getattr(server, method)(run_middleware=False)) + cls = { + "tools": _Tool, + "resources": _Resource, + "resource_templates": _ResourceTemplate, + "prompts": _Prompt, + }[kind] + assert sorted(_identity(c) for c in walked if isinstance(c, cls)) == sorted( + _identity(c) for c in served + ), kind + + +@pytest.mark.parametrize("shape", _ATTACHMENTS) +def test_a_disabled_tool_survives_every_attachment(shape: str) -> None: + """Documentation describes what a server can serve, at every boundary. + + Each attachment reaches its child differently, and one of them listed + through the child's own filtered listing. + """ + child = _matrix_leaf() + child.disable(keys={"tool:alpha@"}) + parent: FastMCP = FastMCP("parent") + _attach(shape, parent, child, 0) + + names = [tool.name for tool in _iter_components(parent) if isinstance(tool, _Tool)] + assert any(name.endswith("alpha") for name in names), names From 4e1cbfb809386693198af7c3048824e05ce16c4d Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 04:43:16 -0500 Subject: [PATCH 25/49] fastmcp(fix[collector]): Read what FastMCP publishes instead of inferring it why: three places inferred a fact the server states outright, and two of them were regressions from the previous round's own fixes. - The provider timeout wrapped a whole subtree, so a slow descendant discarded the healthy components already gathered beneath it. Measured: a child listing ['local'] directly listed [] through a mount. - A Field(alias=...) publishes under a name that may be another parameter's own. Looking the published name up in the signature borrowed that other parameter's annotation, so the table said str for a property the server accepts as integer. Alias ownership is in the declaration. - A TypedDict whose only field is result publishes the same shape as a generated wrapper, so a renamed tool returning an object documented the inner type. FastMCP marks the wrappers it makes with x-fastmcp-wrap-result. what: bound the leaf listing rather than a subtree; map published alias to declaring parameter before consulting the signature, resolving deferred annotations first since PEP 563 hides the metadata that carries the alias; and unwrap only a schema carrying the marker. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 51 ++++++-- tests/ext/fastmcp/test_real_server.py | 109 ++++++++++++++++++ 2 files changed, 148 insertions(+), 12 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index 83e63536..d30fda61 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -259,12 +259,35 @@ def _params_from_schema( except (TypeError, ValueError): # pragma: no cover - defensive sig_params = {} + # A Field(alias=...) publishes under the alias, which may be another + # parameter's own name. Map published name -> declaring parameter so the + # signature is consulted for the parameter that actually owns it. + # Under PEP 563 an annotation is source text, so the metadata carrying + # the alias only exists once resolved. Resolution can fail on a + # TYPE_CHECKING-only name; an unmapped alias then behaves as before. + resolved: dict[str, t.Any] = {} + if func is not None: + try: + resolved = t.get_type_hints(func, include_extras=True) + except (NameError, AttributeError, TypeError, RecursionError): + # The set sphinx.util.typing catches, plus the recursion case + # sphinx-autodoc-typehints adds. + resolved = {} + aliases: dict[str, str] = {} + for param_name, param in sig_params.items(): + annotation = resolved.get(param_name, param.annotation) + for meta in t.get_args(annotation)[1:]: + alias = getattr(meta, "alias", None) + if isinstance(alias, str) and alias: + aliases[alias] = param_name + break + rows: list[ParamInfo] = [] for name, prop in props.items(): if not isinstance(prop, dict): # ``true`` / ``false`` are valid subschemas with no fields. prop = {} - sig_param = sig_params.get(name) + sig_param = sig_params.get(aliases.get(name, name)) annotation = ( _strip_annotated(sig_param.annotation) if sig_param is not None @@ -335,12 +358,15 @@ def _return_from_schema(schema: t.Any) -> str: A transform's forwarding callable is ``(**kwargs)`` with no return annotation, but the tool still publishes what it returns. FastMCP wraps a - non-object result in a single ``result`` property. + non-object result in a single ``result`` property and marks the schema + with ``x-fastmcp-wrap-result``. """ if not isinstance(schema, dict): return "" props = schema.get("properties") - if isinstance(props, dict) and set(props) == {"result"}: + # A TypedDict whose only field is ``result`` publishes the same shape as + # a generated wrapper. FastMCP marks the ones it made. + if schema.get("x-fastmcp-wrap-result") and isinstance(props, dict): inner = props["result"] return _schema_type_text(inner) if isinstance(inner, dict) else "" return _schema_type_text(schema) @@ -688,8 +714,8 @@ async def _apply(kind: str, holder: t.Any, components: list[t.Any]) -> list[t.An #: Seconds one provider may take to list its components. A slow remote is #: treated like a failing one -- logged and skipped -- so it cannot hold up -#: the build. Bounding each provider rather than the whole listing keeps the -#: cancellation inside the failure policy. +#: the build. The bound sits on the leaf that lists, not on a subtree, so a +#: slow descendant cannot erase its healthy siblings. _PROVIDER_TIMEOUT = 30.0 @@ -710,12 +736,7 @@ async def _gather( out: list[t.Any] = [] for provider in providers: try: - out.extend( - await asyncio.wait_for( - _provider_components(kind, provider, depth, path), - timeout=_PROVIDER_TIMEOUT, - ) - ) + out.extend(await _provider_components(kind, provider, depth, path)) except Exception: if raise_on_error: raise @@ -751,7 +772,13 @@ async def _provider_components( base = await _gather(kind, provider, provider.providers, depth, path) else: method = getattr(provider, f"_list_{kind}", None) - base = list(await method()) if method is not None else [] + # Bound the leaf that actually does the work. Bounding a subtree + # discards the healthy components already gathered beneath it. + base = ( + list(await asyncio.wait_for(method(), timeout=_PROVIDER_TIMEOUT)) + if method is not None + else [] + ) return await _apply(kind, provider, base) diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index caa92834..7405c251 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -42,6 +42,15 @@ ResourceTemplate as _ResourceTemplate, # noqa: E402 ) from fastmcp.tools import Tool as _Tool # noqa: E402 +from pydantic import Field # noqa: E402 + + +class _Boxed(t.TypedDict): + """An object whose only field is named like a generated wrapper's.""" + + result: int + + from mcp.types import Annotations, ToolAnnotations # noqa: E402 _LAST_MODIFIED = "2026-01-01T00:00:00Z" @@ -1284,3 +1293,103 @@ def test_a_disabled_tool_survives_every_attachment(shape: str) -> None: names = [tool.name for tool in _iter_components(parent) if isinstance(tool, _Tool)] assert any(name.endswith("alpha") for name in names), names + + +def test_a_slow_provider_does_not_erase_its_healthy_siblings( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: + """The bound sits on the leaf that lists, not on a subtree. + + Bounding a mounted child as a whole discards the components already + gathered beneath it, so one slow descendant emptied the mount. + """ + from fastmcp.server.providers.base import Provider + + from sphinx_autodoc_fastmcp import _collector + + class Slow(Provider): + async def _list_tools(self) -> list[t.Any]: + await asyncio.sleep(30) + return [] + + monkeypatch.setattr(_collector, "_PROVIDER_TIMEOUT", 0.5) + child: FastMCP = FastMCP("child") + + @child.tool + def local(a: int) -> str: + """Local.""" + return "ok" + + child.add_provider(Slow()) + parent: FastMCP = FastMCP("parent") + parent.mount(child, namespace="ns") + + with caplog.at_level(logging.WARNING): + collected = [ + tool.name for tool in _iter_components(parent) if isinstance(tool, _Tool) + ] + + assert collected == ["ns_local"] + + +def test_an_alias_is_resolved_to_the_parameter_that_declares_it() -> None: + """A published alias may be another parameter's own name. + + Looking the published name up in the signature then borrows the wrong + annotation, and the table contradicts what the server accepts. + """ + server: FastMCP = FastMCP("server") + + @server.tool + def query( + foo: t.Annotated[int, Field(alias="bar")] = 1, + bar: t.Annotated[str, Field(alias="baz")] = "x", + ) -> str: + """Query.""" + return "ok" + + collected = _tools_from_server(server, area_map={}, axes=()) + assert collected is not None + assert [(p.name, p.type_str) for p in collected[0].params] == [ + ("bar", "int"), + ("baz", "str"), + ] + + +def test_only_a_marked_wrapper_is_unwrapped() -> None: + """A TypedDict whose only field is ``result`` is not a wrapper. + + It publishes the same shape as one FastMCP generated, so the marker is + what distinguishes them. + """ + from fastmcp.server.transforms import ToolTransform + from fastmcp.tools.tool_transform import ToolTransformConfig + + boxed_server: FastMCP = FastMCP("boxed") + + @boxed_server.tool + def boxed(a: int) -> _Boxed: + """Boxed.""" + return {"result": 1} + + boxed_server.local_provider.add_transform( + ToolTransform({"boxed": ToolTransformConfig(name="renamed")}) + ) + + plain_server: FastMCP = FastMCP("plain") + + @plain_server.tool + def counts(a: int) -> int: + """Counts.""" + return 1 + + plain_server.local_provider.add_transform( + ToolTransform({"counts": ToolTransformConfig(name="also_renamed")}) + ) + + boxed_collected = _tools_from_server(boxed_server, area_map={}, axes=()) + plain_collected = _tools_from_server(plain_server, area_map={}, axes=()) + assert boxed_collected is not None + assert plain_collected is not None + assert boxed_collected[0].return_annotation == "object" + assert plain_collected[0].return_annotation == "integer" From 2b1fe963b1ad04c7414ab708f8c6cece782ce734 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 05:22:24 -0500 Subject: [PATCH 26/49] fastmcp(fix[collector]): Use the TypedDict backport, and bound what can stall why: three defects, one of which the gates could not see. - The real-server fixture used typing.TypedDict, which pydantic rejects below Python 3.12. Verified against a real 3.10 with this branch's pins: PydanticUserError at tool registration. - A Field carrying an alias may be the parameter's default rather than Annotated metadata. Reading only the metadata form left the published name matched against the wrong parameter, so a property the server accepts as integer documented as str -- the same defect as the Annotated case, through the other spelling. - _PROVIDER_TIMEOUT bounded the underlying _list_* call but not the transforms wrapped around it, and the outer wait has no deadline, so a transform that stalls held the build open. Measured: 34s to return where the bound was 0.5s. what: take TypedDict from typing_extensions; read an alias from either carrier, including validation_alias; and bound each transform call the way the listing beneath it is bounded. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 14 +++- tests/ext/fastmcp/test_real_server.py | 69 ++++++++++++++++++- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index d30fda61..bf354bcd 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -276,8 +276,13 @@ def _params_from_schema( aliases: dict[str, str] = {} for param_name, param in sig_params.items(): annotation = resolved.get(param_name, param.annotation) - for meta in t.get_args(annotation)[1:]: - alias = getattr(meta, "alias", None) + # A Field can be attached either as Annotated metadata or as the + # parameter's default, and either spelling publishes the alias. + carriers = (*t.get_args(annotation)[1:], param.default) + for meta in carriers: + alias = getattr(meta, "alias", None) or getattr( + meta, "validation_alias", None + ) if isinstance(alias, str) and alias: aliases[alias] = param_name break @@ -708,7 +713,10 @@ async def _apply(kind: str, holder: t.Any, components: list[t.Any]) -> list[t.An for transform in getattr(holder, "transforms", None) or (): method = getattr(transform, f"list_{kind}", None) if method is not None: - components = list(await method(components)) + # A transform is as able to stall as the listing it wraps. + components = list( + await asyncio.wait_for(method(components), timeout=_PROVIDER_TIMEOUT) + ) return components diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index 7405c251..409112ff 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -43,10 +43,15 @@ ) from fastmcp.tools import Tool as _Tool # noqa: E402 from pydantic import Field # noqa: E402 +from typing_extensions import TypedDict # noqa: E402 -class _Boxed(t.TypedDict): - """An object whose only field is named like a generated wrapper's.""" +class _Boxed(TypedDict): + """An object whose only field is named like a generated wrapper's. + + ``typing_extensions`` rather than ``typing``: pydantic rejects + ``typing.TypedDict`` below Python 3.12. + """ result: int @@ -1393,3 +1398,63 @@ def counts(a: int) -> int: assert plain_collected is not None assert boxed_collected[0].return_annotation == "object" assert plain_collected[0].return_annotation == "integer" + + +def test_an_alias_declared_in_a_default_is_resolved() -> None: + """A ``Field`` may be the default rather than ``Annotated`` metadata. + + Both spellings publish the alias, and reading only one of them borrows + the other parameter's annotation. + """ + server: FastMCP = FastMCP("server") + + @server.tool + def query( + foo: int = Field(1, alias="bar"), + bar: str = Field("x", alias="baz"), + ) -> str: + """Query.""" + return "ok" + + collected = _tools_from_server(server, area_map={}, axes=()) + assert collected is not None + assert [(p.name, p.type_str) for p in collected[0].params] == [ + ("bar", "int"), + ("baz", "str"), + ] + + +def test_a_stalled_transform_does_not_block_the_build( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: + """A transform is as able to stall as the listing it wraps. + + ``_PROVIDER_TIMEOUT`` bounded only the underlying ``_list_*`` call, so a + transform that never returns held the build open indefinitely. + """ + from fastmcp.server.transforms import Transform + + from sphinx_autodoc_fastmcp import _collector + + class Stalled(Transform): + async def list_tools(self, tools: t.Any) -> t.Any: + await asyncio.sleep(30) + return tools + + monkeypatch.setattr(_collector, "_PROVIDER_TIMEOUT", 0.5) + server: FastMCP = FastMCP("server") + + @server.tool + def local(a: int) -> str: + """Local.""" + return "ok" + + server.local_provider.add_transform(Stalled()) + + with caplog.at_level(logging.WARNING): + collected = [ + tool.name for tool in _iter_components(server) if isinstance(tool, _Tool) + ] + + assert collected == [] + assert any("failed to list" in rec.message for rec in caplog.records) From f1936104252ea1b130c232b368b9958d36590bae Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 06:00:18 -0500 Subject: [PATCH 27/49] fastmcp(fix): Resolve schema references why: Renamed tools lose model types when schemas contain local refs. what: - Resolve pointers and union members with cycle protection. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 37 ++++++++----- tests/ext/fastmcp/test_real_server.py | 52 ++++++++++++++++++- 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index bf354bcd..6743f1e3 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -224,23 +224,34 @@ def _render_default(value: t.Any) -> str: return json.dumps(value) -def _schema_type_text(prop: dict[str, t.Any]) -> str: - """Describe a schema property when no signature parameter names it. - - A ``Field(alias=...)`` publishes the alias, so the signature has no - parameter of that name and cannot supply a type. The schema's own type is - a weaker display than the Python annotation, but it beats an em dash. - """ +def _schema_type_text( + prop: dict[str, t.Any], + schema: dict[str, t.Any] | None = None, + refs: frozenset[str] = frozenset(), +) -> str: + """Describe a schema type, resolving local references and union members.""" + schema = prop if schema is None else schema + ref = prop.get("$ref") + if isinstance(ref, str) and ref.startswith("#/") and ref not in refs: + target: t.Any = schema + for part in ref[2:].split("/"): + target = ( + target.get(part.replace("~1", "/").replace("~0", "~")) + if isinstance(target, dict) + else None + ) + if isinstance(target, dict): + return _schema_type_text(target, schema, refs | {ref}) declared = prop.get("type") if isinstance(declared, str): return declared union = prop.get("anyOf") or prop.get("oneOf") or () parts = [ - str(member.get("type", "")) + _schema_type_text(member, schema, refs) for member in union - if isinstance(member, dict) and member.get("type") + if isinstance(member, dict) ] - return " | ".join(parts) + return " | ".join(part for part in parts if part) def _params_from_schema( @@ -300,7 +311,7 @@ def _params_from_schema( else "" ) if not annotation: - annotation = _schema_type_text(prop) + annotation = _schema_type_text(prop, schema) is_required = name in required rows.append( ParamInfo( @@ -373,7 +384,7 @@ def _return_from_schema(schema: t.Any) -> str: # a generated wrapper. FastMCP marks the ones it made. if schema.get("x-fastmcp-wrap-result") and isinstance(props, dict): inner = props["result"] - return _schema_type_text(inner) if isinstance(inner, dict) else "" + return _schema_type_text(inner, schema) if isinstance(inner, dict) else "" return _schema_type_text(schema) @@ -1003,7 +1014,7 @@ def _template_params_from_schema( for name, subschema in props.items(): if not isinstance(subschema, dict): subschema = {} - type_str = _schema_type_text(subschema) + type_str = _schema_type_text(subschema, schema) rows.append( PromptArgInfo( name=str(name), diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index 409112ff..a52d4fa2 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -42,7 +42,7 @@ ResourceTemplate as _ResourceTemplate, # noqa: E402 ) from fastmcp.tools import Tool as _Tool # noqa: E402 -from pydantic import Field # noqa: E402 +from pydantic import BaseModel, Field # noqa: E402 from typing_extensions import TypedDict # noqa: E402 @@ -56,6 +56,13 @@ class _Boxed(TypedDict): result: int +class _Point(BaseModel): + """A coordinate accepted by a tool.""" + + #: Horizontal coordinate. + x: int + + from mcp.types import Annotations, ToolAnnotations # noqa: E402 _LAST_MODIFIED = "2026-01-01T00:00:00Z" @@ -1458,3 +1465,46 @@ def local(a: int) -> str: assert collected == [] assert any("failed to list" in rec.message for rec in caplog.records) + + +def test_renamed_model_parameters_resolve_schema_references() -> None: + """Forwarding functions retain named model types and nullable unions.""" + from fastmcp.server.transforms import ToolTransform + from fastmcp.tools.tool_transform import ToolTransformConfig + + server: FastMCP = FastMCP("models") + + @server.tool + def locate(point: _Point, maybe: _Point | None) -> _Point: + """Locate a point.""" + return point + + server.add_transform(ToolTransform({"locate": ToolTransformConfig(name="lookup")})) + collected = _tools_from_server(server, area_map={}, axes=()) + assert collected is not None + assert [(p.name, p.type_str) for p in collected[0].params] == [ + ("point", "object"), + ("maybe", "object | null"), + ] + + +@pytest.mark.parametrize( + ("prop", "expected"), + [ + ({"$ref": "#/$defs/a~1b~0c"}, "integer"), + ({"oneOf": [{"$ref": "#/$defs/a~1b~0c"}, {"type": "null"}]}, "integer | null"), + ({"$ref": "#/$defs/cycle"}, ""), + ({"$ref": "#/$defs/missing"}, ""), + ], +) +def test_schema_references_are_local_and_cycle_safe( + prop: dict[str, t.Any], expected: str +) -> None: + """Resolve JSON pointers without following missing or recursive targets.""" + schema = { + "$defs": { + "a/b~c": {"type": "integer"}, + "cycle": {"$ref": "#/$defs/cycle"}, + } + } + assert _schema_type_text(prop, schema) == expected From d27e20d12f584a95d3d2a9c75b710fad9252ac91 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 06:00:25 -0500 Subject: [PATCH 28/49] fastmcp(fix): Retain provider discovery why: Aggregate subclasses can refresh components while listing. what: - Traverse inherited listings and invoke overridden listings. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 9 +++++-- tests/ext/fastmcp/test_real_server.py | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index 6743f1e3..aebae715 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -780,17 +780,22 @@ async def _provider_components( switched off or gated inside a mounted server would vanish from its documentation. """ + from fastmcp.server.providers.aggregate import AggregateProvider + + method_name = f"_list_{kind}" child = getattr(provider, "server", None) if child is not None: base = await _server_components(kind, child, depth + 1, path) elif getattr(provider, "_inner", None) is not None: base = await _provider_components(kind, provider._inner, depth, path) # noqa: SLF001 - elif getattr(provider, "providers", None) is not None: + elif isinstance(provider, AggregateProvider) and getattr( + type(provider), method_name + ) is getattr(AggregateProvider, method_name): # An aggregate holds providers of its own; asking it to list would # re-enter the filtered path for every one of them. base = await _gather(kind, provider, provider.providers, depth, path) else: - method = getattr(provider, f"_list_{kind}", None) + method = getattr(provider, method_name, None) # Bound the leaf that actually does the work. Bounding a subtree # discards the healthy components already gathered beneath it. base = ( diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index a52d4fa2..be11a3ac 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -12,6 +12,7 @@ import asyncio import logging +import pathlib import re import types import typing as t @@ -1508,3 +1509,29 @@ def test_schema_references_are_local_and_cycle_safe( } } assert _schema_type_text(prop, schema) == expected + + +def test_skill_discovery_refreshes_during_collection(tmp_path: pathlib.Path) -> None: + """Skills added after provider construction appear without a live listing.""" + from fastmcp.server.providers.skills import SkillsDirectoryProvider + + provider = SkillsDirectoryProvider(tmp_path, reload=True) + server: FastMCP = FastMCP("skills") + server.add_provider(provider, namespace="api") + skill = tmp_path / "fresh" + skill.mkdir() + (skill / "SKILL.md").write_text( + "---\nname: fresh\ndescription: Fresh skill\n---\n\nRead this skill.\n" + ) + + collected = list(_iter_components(server)) + served = asyncio.run(server.list_resources()) + templates = asyncio.run(server.list_resource_templates()) + assert served + assert templates + assert sorted(str(c.uri) for c in collected if isinstance(c, _Resource)) == sorted( + str(c.uri) for c in served + ) + assert sorted( + c.uri_template for c in collected if isinstance(c, _ResourceTemplate) + ) == sorted(c.uri_template for c in templates) From 481dbc54ce7174072df242fee468a0eaa36a2357 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 06:00:33 -0500 Subject: [PATCH 29/49] fastmcp(fix): Prefer input validation aliases why: Input aliases can collide with another signature parameter. what: - Match validation aliases before serialization aliases. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 4 +-- tests/ext/fastmcp/test_real_server.py | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index aebae715..4156fde7 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -291,8 +291,8 @@ def _params_from_schema( # parameter's default, and either spelling publishes the alias. carriers = (*t.get_args(annotation)[1:], param.default) for meta in carriers: - alias = getattr(meta, "alias", None) or getattr( - meta, "validation_alias", None + alias = getattr(meta, "validation_alias", None) or getattr( + meta, "alias", None ) if isinstance(alias, str) and alias: aliases[alias] = param_name diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index be11a3ac..9df0687a 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -1535,3 +1535,30 @@ def test_skill_discovery_refreshes_during_collection(tmp_path: pathlib.Path) -> assert sorted( c.uri_template for c in collected if isinstance(c, _ResourceTemplate) ) == sorted(c.uri_template for c in templates) + + +def test_validation_alias_owns_the_published_input() -> None: + """Input aliases take precedence over serialization aliases.""" + server: FastMCP = FastMCP("aliases") + + @server.tool + def annotated( + foo: t.Annotated[int, Field(alias="other", validation_alias="bar")], + bar: t.Annotated[str, Field(alias="baz")], + ) -> None: + """Accept aliased inputs.""" + + @server.tool + def defaults( + foo: int = Field(1, alias="other", validation_alias="bar"), + bar: str = Field("x", alias="baz"), + ) -> None: + """Accept aliased defaults.""" + + collected = _tools_from_server(server, area_map={}, axes=()) + assert collected is not None + for tool in collected: + assert [(p.name, p.type_str) for p in tool.params] == [ + ("bar", "int"), + ("baz", "str"), + ] From 4bfe0282a6468621144454be6449b6be5ea0d6a9 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 06:00:42 -0500 Subject: [PATCH 30/49] fastmcp(fix): Ignore forwarding variadics why: Forwarding kwargs can shadow a published tool argument. what: - Use schema types for variadic signature parameters. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 2 ++ tests/ext/fastmcp/test_real_server.py | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index 4156fde7..dbb9f7e6 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -307,6 +307,8 @@ def _params_from_schema( annotation = ( _strip_annotated(sig_param.annotation) if sig_param is not None + and sig_param.kind + not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD) and sig_param.annotation is not inspect.Parameter.empty else "" ) diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index 9df0687a..119426c3 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -968,7 +968,8 @@ def ns_hello(x: str) -> str: assert documented["ns_ns_hello"].docstring == "Decoy." -def test_a_transformed_argument_documents_its_served_type() -> None: +@pytest.mark.parametrize("name", ["x", "kwargs", "args"]) +def test_a_transformed_argument_documents_its_served_type(name: str) -> None: """A transform that retypes an argument owns the displayed type. The tool it was made from still says ``int``; the server publishes @@ -983,14 +984,14 @@ def square(x: int) -> int: transformed = _Tool.from_tool( _Tool.from_function(square), name="square_str", - transform_args={"x": ArgTransform(type=str)}, + transform_args={"x": ArgTransform(name=name, type=str)}, ) server: FastMCP = FastMCP("server") server.add_tool(transformed) collected = _tools_from_server(server, area_map={}, axes=()) assert collected is not None - assert [(p.name, p.type_str) for p in collected[0].params] == [("x", "string")] + assert [(p.name, p.type_str) for p in collected[0].params] == [(name, "string")] def test_one_failing_provider_does_not_abort_the_build( From cc045062f776caf2f18d041b20b953e3604c0346 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 06:00:46 -0500 Subject: [PATCH 31/49] fastmcp(test): Read Sphinx 8 inventory tuples why: Python 3.10 uses Sphinx 8, whose inventory entries are tuples. what: - Read canonical URIs from both supported inventory formats. --- tests/ext/fastmcp/test_component_linking.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/ext/fastmcp/test_component_linking.py b/tests/ext/fastmcp/test_component_linking.py index 33493afd..0f72d76e 100644 --- a/tests/ext/fastmcp/test_component_linking.py +++ b/tests/ext/fastmcp/test_component_linking.py @@ -310,8 +310,9 @@ def test_no_index_resource_keeps_single_canonical_home( """``:no-index:`` binds the canonical label to the page that omits it.""" inventory = _load_inventory(no_index_html) item = inventory["std:label"]["fastmcp-resource-hello"] - assert item.uri.startswith("canonical"), ( - f"canonical label should point at canonical page, got {item.uri!r}" + uri = item[2] if isinstance(item, tuple) else item.uri + assert uri.startswith("canonical"), ( + f"canonical label should point at canonical page, got {uri!r}" ) From 769e3c709ba657942da1da85944cdb3ed8a2aec5 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 06:34:48 -0500 Subject: [PATCH 32/49] docs(justfile) Clear the doctree cache on clean `rm -rf {{ builddir }}/*` leaves `.doctrees` and `.buildinfo`: bash does not match dotfiles with `*`. The next build is then incremental and skips unchanged documents, so warnings that only fire on first read do not appear and a warning count taken after `just clean` reads lower than a real build. The recipe's own second line already uses the un-globbed form. --- docs/justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/justfile b/docs/justfile index 9258bb2a..df948967 100644 --- a/docs/justfile +++ b/docs/justfile @@ -89,7 +89,7 @@ json: # Clean build directory and the vite-built theme assets [confirm] clean: - rm -rf {{ builddir }}/* + rm -rf {{ builddir }} rm -rf ../packages/gp-furo-theme/src/gp_furo_theme/theme/gp-furo/static/ # Build HTML help files From dc567018a29ae5d24ec42577df2d2b72f847cbda Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 06:39:58 -0500 Subject: [PATCH 33/49] layout(fix): Blend code into API headers why: Inline code kept a separate background when an API header was hovered. what: Make signature code transparent within the shared API header. --- .../src/sphinx_ux_autodoc_layout/_static/css/layout.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/sphinx-ux-autodoc-layout/src/sphinx_ux_autodoc_layout/_static/css/layout.css b/packages/sphinx-ux-autodoc-layout/src/sphinx_ux_autodoc_layout/_static/css/layout.css index 0885b861..0dd168d3 100644 --- a/packages/sphinx-ux-autodoc-layout/src/sphinx_ux_autodoc_layout/_static/css/layout.css +++ b/packages/sphinx-ux-autodoc-layout/src/sphinx_ux_autodoc_layout/_static/css/layout.css @@ -185,6 +185,10 @@ dl.gp-sphinx-api-container > dt.gp-sphinx-api-header > .gp-sphinx-api-layout--mo } /* ── Signature row ──────────────────────────────────── */ +.gp-sphinx-api-header .gp-sphinx-api-signature code.literal { + background: transparent; +} + dl.gp-sphinx-api-container > dt.gp-sphinx-api-header .gp-sphinx-api-signature { flex: 1 1 auto; font-family: var(--font-stack--monospace); From 84bdd7bd4789cc6f3654b8c1a6009bd714204de4 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 07:05:07 -0500 Subject: [PATCH 34/49] docs(fix): Stop idle preview rebuilds why: pnpm temporary probes modify the watched Vite directory and trigger another Sphinx build. what: Ignore those exact build events in start and design while retaining source-file watching. --- docs/justfile | 2 ++ tests/docs/test_docs_policy.py | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/docs/justfile b/docs/justfile index df948967..845bc405 100644 --- a/docs/justfile +++ b/docs/justfile @@ -193,6 +193,7 @@ start: --re-ignore '/\.pytest_cache(/|$)' \ --re-ignore '/dist(/|$)' \ --re-ignore '/_build(/|$)' \ + --re-ignore '/packages/gp-furo-theme/web(/_tmp_[^/]+)?$' \ --re-ignore '/packages/[^/]+/src/[^/]+/theme/[^/]+/static(/|$)' # Design mode: watch static files and disable incremental builds @@ -206,4 +207,5 @@ design: --re-ignore '/\.pytest_cache(/|$)' \ --re-ignore '/dist(/|$)' \ --re-ignore '/_build(/|$)' \ + --re-ignore '/packages/gp-furo-theme/web(/_tmp_[^/]+)?$' \ --re-ignore '/packages/[^/]+/src/[^/]+/theme/[^/]+/static(/|$)' diff --git a/tests/docs/test_docs_policy.py b/tests/docs/test_docs_policy.py index 08cab406..40d152f6 100644 --- a/tests/docs/test_docs_policy.py +++ b/tests/docs/test_docs_policy.py @@ -163,6 +163,31 @@ def test_contributing_docs_use_copyable_documentation_commands() -> None: assert offenders == [] +@pytest.mark.parametrize("recipe", ["start", "design"]) +def test_preview_ignores_build_events_but_watches_sources(recipe: str) -> None: + """Build events stay ignored while editable theme files remain watched.""" + text = (DOCS_ROOT / "justfile").read_text(encoding="utf-8") + body = text.split(f"\n{recipe}:\n", maxsplit=1)[1].split("\n\n", maxsplit=1)[0] + patterns = [re.compile(p) for p in re.findall(r"--re-ignore '([^']+)'", body)] + web = (REPO_ROOT / "packages" / "gp-furo-theme" / "web").as_posix() + + for path in (web, f"{web}/_tmp_123_abcdef"): + assert any(p.search(path) for p in patterns), ( + f"build event triggers {recipe}: {path}" + ) + + for source in ( + "src/styles/index.css", + "src/scripts/furo.ts", + "vite.config.ts", + "package.json", + ): + path = f"{web}/{source}" + assert not any(p.search(path) for p in patterns), ( + f"source ignored by {recipe}: {path}" + ) + + def test_console_blocks_contain_one_prompted_command() -> None: """Each console block has one copyable command prompt.""" offenders: list[str] = [] From 04c57ee21ff5c242c62b2b072b1cc6ebacd700e5 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 09:25:03 -0500 Subject: [PATCH 35/49] fastmcp(fix): Quiet server/module overlap why: Live-server tools already have documented precedence over module entries, so reporting each overlap as a duplicate adds build noise. what: - Skip module entries whose names the server owns. - Verify both collection modes preserve server metadata and module fallbacks while genuine module collisions still warn. --- .../src/sphinx_autodoc_fastmcp/_collector.py | 6 +- tests/ext/fastmcp/test_real_server.py | 124 ++++++++++++------ 2 files changed, 85 insertions(+), 45 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index dbb9f7e6..63f9df7b 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -581,11 +581,11 @@ def collect_tools(app: Sphinx) -> None: if info is not None: collector_tools.append(info) - # Server-collected tools win on a shared name; a module entry naming the - # same tool is reported like any other collision rather than overwritten - # in silence. Module entries fill the gaps. + # Server tools take precedence; only collisions among module-only tools warn. collected: dict[str, ToolInfo] = dict(served_by_name) for collected_tool in collector_tools: + if collected_tool.name in served_by_name: + continue _index_by_unique_name(collected, collected_tool.name, collected_tool, "tool") app.env.fastmcp_tools = collected # type: ignore[attr-defined] diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index 119426c3..adc2f8e8 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -14,6 +14,7 @@ import logging import pathlib import re +import sys import types import typing as t import warnings @@ -21,6 +22,7 @@ import pytest from sphinx_autodoc_fastmcp._collector import ( + ToolCollector, _annotation_hints, _index_by_unique_name, _iter_components, @@ -566,66 +568,104 @@ def hello(a: int) -> str: ) -def test_a_module_tool_colliding_with_a_served_tool_is_reported( +@pytest.mark.parametrize("mode", ["register", "introspect"]) +def test_a_served_tool_quietly_takes_precedence_over_a_module_tool( + mode: str, + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: - """The server wins a shared name, and the module entry is warned about. - - Merging with a bare dict update discarded the module entry in silence, - bypassing the collision report every other same-kind duplicate gets. - """ - import sys + """Server metadata wins; module tools fill gaps without overlap warnings.""" + module_server = FastMCP("module") + @module_server.tool def list_sessions(server: str) -> list[str]: """Module copy.""" return [] - t.cast(t.Any, list_sessions).__fastmcp__ = types.SimpleNamespace( - name="list_sessions", title="List", tags=set(), annotations=None - ) + @module_server.tool + def module_only() -> str: + """Module-only tool.""" + return "module" + + def register(collector: ToolCollector) -> None: + collector.tool()(list_sessions) + collector.tool()(module_only) + module = types.ModuleType("collision_mod") module.list_sessions = list_sessions # type: ignore[attr-defined] - sys.modules["collision_mod"] = module + module.module_only = module_only # type: ignore[attr-defined] + module.register = register # type: ignore[attr-defined] + monkeypatch.setitem(sys.modules, "collision_mod", module) app: FastMCP = FastMCP("served") - @app.tool - def list_sessions_served(server: str) -> list[str]: - """Served copy.""" - return [] - - app.local_provider.remove_tool("list_sessions_served") - @app.tool(name="list_sessions") def served(server: str) -> list[str]: """Served copy.""" return [] - class _Env: - pass - - class _Config: - fastmcp_server_module = "x" - fastmcp_tool_modules = ["collision_mod"] - fastmcp_area_map: dict[str, str] = {} - fastmcp_axes: tuple[t.Any, ...] = () - fastmcp_collector_mode = "introspect" - - class _App: - config = _Config() - env = _Env() - - fake = _App() - fake._fastmcp_server_cache = ("x", app) # type: ignore[attr-defined] - try: - with caplog.at_level(logging.WARNING): - collect_tools(t.cast(t.Any, fake)) - finally: - del sys.modules["collision_mod"] - - documented = t.cast(t.Any, fake.env).fastmcp_tools + fake = types.SimpleNamespace( + config=types.SimpleNamespace( + fastmcp_server_module="x", + fastmcp_tool_modules=["collision_mod"], + fastmcp_area_map={}, + fastmcp_axes=(), + fastmcp_collector_mode=mode, + ), + env=types.SimpleNamespace(), + _fastmcp_server_cache=("x", app), + ) + with caplog.at_level(logging.WARNING, logger="sphinx_autodoc_fastmcp._collector"): + collect_tools(t.cast(t.Any, fake)) + + documented = fake.env.fastmcp_tools + assert set(documented) == {"list_sessions", "module_only"} + assert documented["list_sessions"].func is served assert documented["list_sessions"].docstring == "Served copy." - assert any("duplicate tool name" in rec.message for rec in caplog.records) + assert documented["module_only"].func is module_only + assert not any("duplicate tool name" in rec.getMessage() for rec in caplog.records) + + +@pytest.mark.parametrize("mode", ["register", "introspect"]) +def test_duplicate_module_tool_names_still_warn( + mode: str, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + """Module collisions retain the first entry and report the lost tool.""" + for module_name in ("first_module", "second_module"): + server = FastMCP(module_name) + + @server.tool + def same() -> str: + return "value" + + def register( + collector: ToolCollector, tool: t.Callable[[], str] = same + ) -> None: + collector.tool()(tool) + + module = types.ModuleType(module_name) + module.same = same # type: ignore[attr-defined] + module.register = register # type: ignore[attr-defined] + monkeypatch.setitem(sys.modules, module_name, module) + + app = types.SimpleNamespace( + config=types.SimpleNamespace( + fastmcp_server_module="", + fastmcp_tool_modules=["first_module", "second_module"], + fastmcp_area_map={}, + fastmcp_axes=(), + fastmcp_collector_mode=mode, + ), + env=types.SimpleNamespace(), + ) + with caplog.at_level(logging.WARNING, logger="sphinx_autodoc_fastmcp._collector"): + collect_tools(t.cast(t.Any, app)) + + assert list(app.env.fastmcp_tools) == ["same"] + assert app.env.fastmcp_tools["same"].module_name == "first_module" + assert any("duplicate tool name 'same'" in r.getMessage() for r in caplog.records) def test_a_child_transform_applies_before_its_mount_namespace() -> None: From cbe9e8f225d07cf511076dfe572238868542630b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 09:26:56 -0500 Subject: [PATCH 36/49] fastmcp(fix): Use Sphinx collection warnings why: Collector diagnostics bypassed Sphinx, allowing strict builds to succeed without recording or counting collection warnings. what: - Route collector warnings through Sphinx with fastmcp categories. - Give name collisions a duplicate subtype for selective suppression. - Exercise strict CLI exit status and warning files, and document the warning policy alongside the collector configuration. --- .github/CONTRIBUTING.md | 3 + .../packages/sphinx-autodoc-fastmcp/how-to.md | 6 ++ .../src/sphinx_autodoc_fastmcp/_collector.py | 24 ++++++- tests/ext/fastmcp/test_fastmcp.py | 6 +- tests/ext/fastmcp/test_fastmcp_integration.py | 69 +++++++++++++++++++ tests/ext/fastmcp/test_real_server.py | 8 ++- 6 files changed, 109 insertions(+), 7 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 984bbef3..95a2e558 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -107,6 +107,9 @@ convention the linters do not enforce. `NullHandler` in library `__init__.py` files. Never configure handlers, levels, or formatters in library code — that is the application's job. +- Sphinx build diagnostics use `sphinx.util.logging.getLogger(__name__)` + so warnings reach `-W`, `-w` and `suppress_warnings`. Give warnings a + `type` and, where useful, a `subtype` for selective suppression. - Use lazy formatting — `logger.debug("msg %s", val)`, not an f-string — so interpolation is skipped when the level is filtered and log aggregators group messages by template instead of by literal string. diff --git a/docs/packages/sphinx-autodoc-fastmcp/how-to.md b/docs/packages/sphinx-autodoc-fastmcp/how-to.md index 5da6ce00..38290d8e 100644 --- a/docs/packages/sphinx-autodoc-fastmcp/how-to.md +++ b/docs/packages/sphinx-autodoc-fastmcp/how-to.md @@ -174,3 +174,9 @@ ensure components registered lazily appear in the docs; FastMCP's default FastMCP keys tools and prompts by name while permitting two registrations to share one, so both are served. The docs index holds one entry per name: it keeps the first and warns, naming the collision. + +Server/module overlap follows the documented precedence without warning. +Collector warnings use Sphinx's warning stream, so `-W` fails the build and +`-w` records them. Name collisions use the `fastmcp.duplicate` category; +other collection warnings use `fastmcp`. Set +`suppress_warnings = ["fastmcp.duplicate"]` to suppress only name collisions. diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py index 63f9df7b..8ac52b1d 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py @@ -7,12 +7,12 @@ import importlib import inspect import json -import logging import re import threading import typing as t from sphinx.application import Sphinx +from sphinx.util import logging as sphinx_logging from sphinx_autodoc_fastmcp._models import ( DEFAULT_AXES, @@ -36,7 +36,7 @@ normalize_annotation_text, ) -logger = logging.getLogger(__name__) +logger = sphinx_logging.getLogger(__name__) class ToolCollector: @@ -208,6 +208,8 @@ def _index_by_unique_name( "and skipping the rest", kind, name, + type="fastmcp", + subtype="duplicate", ) return index[name] = info @@ -475,7 +477,9 @@ def _tools_from_server( from fastmcp.tools import Tool as _Tool except ImportError: # pragma: no cover - defensive logger.warning( - "sphinx_autodoc_fastmcp: could not import fastmcp Tool", exc_info=True + "sphinx_autodoc_fastmcp: could not import fastmcp Tool", + exc_info=True, + type="fastmcp", ) return None return [ @@ -513,6 +517,7 @@ def collect_tools(app: Sphinx) -> None: logger.warning( "sphinx_autodoc_fastmcp: unknown fastmcp_collector_mode %r; using 'register'", mode, + type="fastmcp", ) mode = "register" @@ -535,6 +540,7 @@ def collect_tools(app: Sphinx) -> None: if not served_by_name and not modules: logger.warning( "sphinx_autodoc_fastmcp: fastmcp_tool_modules is empty; no tools collected", + type="fastmcp", ) app.env.fastmcp_tools = {} # type: ignore[attr-defined] return @@ -555,6 +561,7 @@ def collect_tools(app: Sphinx) -> None: "sphinx_autodoc_fastmcp: failed to load tool module %s", dotted, exc_info=True, + type="fastmcp", ) collector_tools = collector.tools else: @@ -567,6 +574,7 @@ def collect_tools(app: Sphinx) -> None: "sphinx_autodoc_fastmcp: failed to import %s", dotted, exc_info=True, + type="fastmcp", ) continue for _name, obj in inspect.getmembers(mod): @@ -615,6 +623,7 @@ def _resolve_server_instance(dotted: str) -> t.Any | None: logger.warning( "sphinx_autodoc_fastmcp: fastmcp_server_module %r has no attribute", dotted, + type="fastmcp", ) return None try: @@ -624,6 +633,7 @@ def _resolve_server_instance(dotted: str) -> t.Any | None: "sphinx_autodoc_fastmcp: could not import server module %s", module_path, exc_info=True, + type="fastmcp", ) return None obj = getattr(mod, attr, None) @@ -637,6 +647,7 @@ def _resolve_server_instance(dotted: str) -> t.Any | None: "sphinx_autodoc_fastmcp: calling %s() failed", dotted, exc_info=True, + type="fastmcp", ) return None if getattr(obj, "local_provider", None) is None: @@ -648,6 +659,7 @@ def _resolve_server_instance(dotted: str) -> t.Any | None: "sphinx_autodoc_fastmcp: %s did not resolve to a FastMCP instance " "(no local_provider attribute); prompts/resources will be empty", dotted, + type="fastmcp", ) return None # Always invoke the server's register-all hook when one is exported. @@ -688,6 +700,7 @@ def _resolve_server_instance(dotted: str) -> t.Any | None: "skipping server", dotted, exc_info=True, + type="fastmcp", ) return None return obj @@ -767,6 +780,7 @@ async def _gather( type(provider).__name__, kind.replace("_", " "), exc_info=True, + type="fastmcp", ) return out @@ -1112,6 +1126,7 @@ def collect_prompts_and_resources(app: Sphinx) -> None: "sphinx_autodoc_fastmcp: fastmcp_server_module %r did not resolve " "to a FastMCP instance; prompts/resources will be empty", server_dotted, + type="fastmcp", ) else: try: @@ -1126,6 +1141,7 @@ def collect_prompts_and_resources(app: Sphinx) -> None: logger.warning( "sphinx_autodoc_fastmcp: could not import fastmcp types", exc_info=True, + type="fastmcp", ) else: for component in _iter_components(server): @@ -1172,6 +1188,8 @@ def _index_by_name(name_index: dict[str, str], name: str, key: str, kind: str) - existing, key, existing, + type="fastmcp", + subtype="duplicate", ) return name_index[name] = key diff --git a/tests/ext/fastmcp/test_fastmcp.py b/tests/ext/fastmcp/test_fastmcp.py index a76a3d48..3970209d 100644 --- a/tests/ext/fastmcp/test_fastmcp.py +++ b/tests/ext/fastmcp/test_fastmcp.py @@ -159,13 +159,15 @@ def test_resolve_server_warns_when_attr_is_not_fastmcp( fake_module.mcp = bare_obj # type: ignore[attr-defined] monkeypatch.setitem(sys.modules, "fake_fastmcp_bare2", fake_module) - with caplog.at_level(logging.WARNING, logger="sphinx_autodoc_fastmcp"): + with caplog.at_level( + logging.WARNING, logger="sphinx.sphinx_autodoc_fastmcp._collector" + ): _resolve_server_instance("fake_fastmcp_bare2:mcp") matched = [ r for r in caplog.records - if r.name == "sphinx_autodoc_fastmcp._collector" + if r.name == "sphinx.sphinx_autodoc_fastmcp._collector" and "local_provider" in r.getMessage() ] assert len(matched) == 1 diff --git a/tests/ext/fastmcp/test_fastmcp_integration.py b/tests/ext/fastmcp/test_fastmcp_integration.py index b53c8715..06641841 100644 --- a/tests/ext/fastmcp/test_fastmcp_integration.py +++ b/tests/ext/fastmcp/test_fastmcp_integration.py @@ -4,6 +4,8 @@ import logging import pathlib +import subprocess +import sys import textwrap import typing as t @@ -15,6 +17,7 @@ SharedSphinxResult, SphinxScenario, build_shared_sphinx_result, + copy_scenario_tree, read_output, ) @@ -86,6 +89,72 @@ def list_sessions(server: str, limit: int = 20) -> str: ) +@pytest.fixture(scope="module") +def duplicate_warning_builds( + tmp_path_factory: pytest.TempPathFactory, +) -> dict[bool, tuple[int, str]]: + """Build a duplicate registration with strict and suppressed diagnostics.""" + root = tmp_path_factory.mktemp("fastmcp-duplicate-warnings") + results: dict[bool, tuple[int, str]] = {} + for suppressed in (False, True): + build_root = root / str(suppressed) + conf = _CONF_PY + '\nfastmcp_tool_modules = ["demo_tools", "demo_tools"]\n' + if suppressed: + conf += 'suppress_warnings = ["fastmcp.duplicate"]\n' + source = copy_scenario_tree( + root / "cache", + SphinxScenario( + files=( + ScenarioFile("demo_tools.py", _MODULE_SOURCE), + ScenarioFile( + "conf.py", + conf.replace("__SCENARIO_SRCDIR__", SCENARIO_SRCDIR_TOKEN), + substitute_srcdir=True, + ), + ScenarioFile("index.rst", _INDEX_RST), + ), + ), + build_root, + ) + warning_file = build_root / "warnings.txt" + result = subprocess.run( + [ + sys.executable, + "-m", + "sphinx", + "-W", + "--keep-going", + "-b", + "dummy", + "-w", + str(warning_file), + str(source), + str(build_root / "output"), + ], + capture_output=True, + text=True, + check=False, + ) + assert warning_file.exists(), result.stderr + results[suppressed] = (result.returncode, warning_file.read_text()) + return results + + +@pytest.mark.integration +@pytest.mark.parametrize("suppressed", [False, True]) +def test_collector_warnings_obey_sphinx_warning_policy( + duplicate_warning_builds: dict[bool, tuple[int, str]], suppressed: bool +) -> None: + """A duplicate fails -W and reaches -w unless its category is suppressed.""" + returncode, warnings = duplicate_warning_builds[suppressed] + assert returncode == (0 if suppressed else 1) + if suppressed: + assert warnings == "" + else: + assert "duplicate tool name 'list_sessions'" in warnings + assert "[fastmcp.duplicate]" in warnings + + @pytest.fixture(scope="module") def fastmcp_html_result( tmp_path_factory: pytest.TempPathFactory, diff --git a/tests/ext/fastmcp/test_real_server.py b/tests/ext/fastmcp/test_real_server.py index adc2f8e8..a51da485 100644 --- a/tests/ext/fastmcp/test_real_server.py +++ b/tests/ext/fastmcp/test_real_server.py @@ -615,7 +615,9 @@ def served(server: str) -> list[str]: env=types.SimpleNamespace(), _fastmcp_server_cache=("x", app), ) - with caplog.at_level(logging.WARNING, logger="sphinx_autodoc_fastmcp._collector"): + with caplog.at_level( + logging.WARNING, logger="sphinx.sphinx_autodoc_fastmcp._collector" + ): collect_tools(t.cast(t.Any, fake)) documented = fake.env.fastmcp_tools @@ -660,7 +662,9 @@ def register( ), env=types.SimpleNamespace(), ) - with caplog.at_level(logging.WARNING, logger="sphinx_autodoc_fastmcp._collector"): + with caplog.at_level( + logging.WARNING, logger="sphinx.sphinx_autodoc_fastmcp._collector" + ): collect_tools(t.cast(t.Any, app)) assert list(app.env.fastmcp_tools) == ["same"] From 828a3a3bb6e37414d4a7ce938aec142256853a14 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 09:35:33 -0500 Subject: [PATCH 37/49] ci(fix): Select each matrix Python in uv why: The checked-in .python-version made every QA job use Python 3.14, including jobs labeled 3.10 through 3.13. what: - Set UV_PYTHON from the job matrix for dependency sync and checks. --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8524853e..e64c5c5a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,6 +18,7 @@ jobs: # Sphinx silently skips the missing static entries (no -W in qa). env: SPHINX_VITE_BUILDER_SKIP: "1" + UV_PYTHON: ${{ matrix.python-version }} strategy: fail-fast: false matrix: From 6f31f84c0366e4ab1dc731d4f2cbd96468cf0d44 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 09:44:22 -0500 Subject: [PATCH 38/49] tests(fix): Use TypedDict backport on 3.11 why: Python 3.10 and 3.11 omit the TypedDict base metadata needed to inherit key descriptions. The fixture assumed Python 3.12 behavior. what: - Use typing_extensions.TypedDict for this fixture before Python 3.12 - Document the backport requirement for inherited key descriptions - Verify the assertion still fails when base traversal is removed --- docs/packages/sphinx-autodoc-typehints-gp/how-to.md | 4 ++++ tests/ext/typehints_gp/test_documented_fields.py | 11 ++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/packages/sphinx-autodoc-typehints-gp/how-to.md b/docs/packages/sphinx-autodoc-typehints-gp/how-to.md index ac2e4415..a0e63072 100644 --- a/docs/packages/sphinx-autodoc-typehints-gp/how-to.md +++ b/docs/packages/sphinx-autodoc-typehints-gp/how-to.md @@ -90,6 +90,10 @@ Write the description on the class that *declares* the name. A subclass that inherits the field inherits the description with it, so a base class documenting forty fields does not oblige each subclass to repeat them. +On Python 3.10 and 3.11, use `typing_extensions.TypedDict` to inherit +key descriptions. The standard-library `TypedDict` does not retain the +base-class metadata needed to locate those descriptions until Python 3.12. + ### What happens to a name you describe nowhere A field nobody describes reaches the page as a bare name with a type and diff --git a/tests/ext/typehints_gp/test_documented_fields.py b/tests/ext/typehints_gp/test_documented_fields.py index d15b0b18..3c2ab8f4 100644 --- a/tests/ext/typehints_gp/test_documented_fields.py +++ b/tests/ext/typehints_gp/test_documented_fields.py @@ -2188,7 +2188,12 @@ def test_showing_undocumented_class_vars_restores_them( from __future__ import annotations import dataclasses - import typing as t + import sys + + if sys.version_info >= (3, 12): + from typing import TypedDict + else: + from typing_extensions import TypedDict @dataclasses.dataclass @@ -2209,7 +2214,7 @@ class Options(ServerBase): """Container for every option.""" - class KeyBase(t.TypedDict): + class KeyBase(TypedDict): """Keys a subclass builds on.""" directory: str @@ -2268,7 +2273,7 @@ def test_a_field_described_by_a_base_class_carries_that_description( def test_a_key_described_by_a_base_typed_dict_carries_that_description( inherited_description_html_result: SharedSphinxResult, ) -> None: - """A TypedDict base is reachable only through ``__orig_bases__``.""" + """The typing backport preserves base metadata before Python 3.12.""" html = read_output(inherited_description_html_result, "index.html") assert "Path for the worktree." in html From 329bf737cdeca08bb05a93da035a5a71d5c6eff2 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 09:53:22 -0500 Subject: [PATCH 39/49] types(fix): Type-check against Sphinx 8.1 and tomli why: The matrix now selects each Python, so mypy runs against the newest Sphinx each interpreter can install. Sphinx 8.1 on 3.10 declares resolve_any_xref returning list[tuple[str, Element]] and 8.2 narrows it to list[tuple[str, reference]]; list is invariant, so no single annotation satisfies both supertypes. tomli is installed below 3.11 and absent above it, so its import ignore is needed in one environment and unused in the other, and the cast on its parsed result is redundant only where it is typed. what: Pair each environment-dependent ignore with unused-ignore, and replace the tomllib cast with a typed local. Annotations only, no runtime change. --- docs/_ext/package_reference.py | 2 +- .../src/sphinx_autodoc_argparse/domain.py | 2 +- .../src/sphinx_autodoc_docutils/domain.py | 2 +- .../src/sphinx_autodoc_sphinx/domain.py | 2 +- scripts/ci/bump_version.py | 2 +- scripts/ci/package_tools.py | 5 +++-- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/_ext/package_reference.py b/docs/_ext/package_reference.py index b792ca19..7f097b8c 100644 --- a/docs/_ext/package_reference.py +++ b/docs/_ext/package_reference.py @@ -80,7 +80,7 @@ if sys.version_info >= (3, 11): import tomllib else: - import tomli as tomllib # type: ignore[import-not-found] + import tomli as tomllib # type: ignore[import-not-found, unused-ignore] logger = logging.getLogger(__name__) diff --git a/packages/sphinx-autodoc-argparse/src/sphinx_autodoc_argparse/domain.py b/packages/sphinx-autodoc-argparse/src/sphinx_autodoc_argparse/domain.py index 86768e96..707b21cc 100644 --- a/packages/sphinx-autodoc-argparse/src/sphinx_autodoc_argparse/domain.py +++ b/packages/sphinx-autodoc-argparse/src/sphinx_autodoc_argparse/domain.py @@ -340,7 +340,7 @@ def resolve_xref( target, ) - def resolve_any_xref( + def resolve_any_xref( # type: ignore[override, unused-ignore] self, env: BuildEnvironment, fromdocname: str, diff --git a/packages/sphinx-autodoc-docutils/src/sphinx_autodoc_docutils/domain.py b/packages/sphinx-autodoc-docutils/src/sphinx_autodoc_docutils/domain.py index 449adfda..b1d2d557 100644 --- a/packages/sphinx-autodoc-docutils/src/sphinx_autodoc_docutils/domain.py +++ b/packages/sphinx-autodoc-docutils/src/sphinx_autodoc_docutils/domain.py @@ -336,7 +336,7 @@ def resolve_xref( target, ) - def resolve_any_xref( + def resolve_any_xref( # type: ignore[override, unused-ignore] self, env: BuildEnvironment, fromdocname: str, diff --git a/packages/sphinx-autodoc-sphinx/src/sphinx_autodoc_sphinx/domain.py b/packages/sphinx-autodoc-sphinx/src/sphinx_autodoc_sphinx/domain.py index 5d7a816d..9cc35034 100644 --- a/packages/sphinx-autodoc-sphinx/src/sphinx_autodoc_sphinx/domain.py +++ b/packages/sphinx-autodoc-sphinx/src/sphinx_autodoc_sphinx/domain.py @@ -301,7 +301,7 @@ def resolve_xref( target, ) - def resolve_any_xref( + def resolve_any_xref( # type: ignore[override, unused-ignore] self, env: BuildEnvironment, fromdocname: str, diff --git a/scripts/ci/bump_version.py b/scripts/ci/bump_version.py index 337faff6..04eba202 100644 --- a/scripts/ci/bump_version.py +++ b/scripts/ci/bump_version.py @@ -23,7 +23,7 @@ if sys.version_info >= (3, 11): import tomllib else: - import tomli as tomllib # type: ignore[import-not-found] + import tomli as tomllib # type: ignore[import-not-found, unused-ignore] try: from packaging.version import InvalidVersion, Version diff --git a/scripts/ci/package_tools.py b/scripts/ci/package_tools.py index d8d208a9..a61ce239 100644 --- a/scripts/ci/package_tools.py +++ b/scripts/ci/package_tools.py @@ -17,7 +17,7 @@ if sys.version_info >= (3, 11): import tomllib else: - import tomli as tomllib # type: ignore[import-not-found] + import tomli as tomllib # type: ignore[import-not-found, unused-ignore] @dataclass(frozen=True) @@ -61,7 +61,8 @@ def _load_toml(path: pathlib.Path) -> dict[str, t.Any]: Parsed TOML data. """ with path.open("rb") as handle: - return t.cast("dict[str, t.Any]", tomllib.load(handle)) + data: dict[str, t.Any] = tomllib.load(handle) + return data def _root_project(root: pathlib.Path) -> dict[str, t.Any]: From 921bcfbb92769bad87c8400aea562e95d9ea84d7 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 09:53:30 -0500 Subject: [PATCH 40/49] theme(fix): Register the theme path as a string why: Sphinx 8.1's add_html_theme takes a str; 8.2 widened it to accept os.PathLike. Passing a Path failed the type check on the newest Sphinx that Python 3.10 can install. what: Convert the path at the call site, matching gp-furo-theme, which already does this, and follow the fake through its test and doctest. --- .../sphinx-gp-theme/src/sphinx_gp_theme/__init__.py | 6 +++--- tests/test_theme.py | 11 +++-------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/sphinx-gp-theme/src/sphinx_gp_theme/__init__.py b/packages/sphinx-gp-theme/src/sphinx_gp_theme/__init__.py index 3cf49961..4f2b6ca7 100644 --- a/packages/sphinx-gp-theme/src/sphinx_gp_theme/__init__.py +++ b/packages/sphinx-gp-theme/src/sphinx_gp_theme/__init__.py @@ -64,8 +64,8 @@ def setup(app: Sphinx) -> dict[str, bool | str]: -------- >>> class FakeApp: ... def __init__(self) -> None: - ... self.calls: list[tuple[str, pathlib.Path]] = [] - ... def add_html_theme(self, name: str, theme_path: pathlib.Path) -> None: + ... self.calls: list[tuple[str, str]] = [] + ... def add_html_theme(self, name: str, theme_path: str) -> None: ... self.calls.append((name, theme_path)) >>> fake = FakeApp() >>> metadata = setup(fake) # type: ignore[arg-type] @@ -74,7 +74,7 @@ def setup(app: Sphinx) -> dict[str, bool | str]: >>> metadata["parallel_read_safe"] True """ - app.add_html_theme("sphinx-gp-theme", get_theme_path()) + app.add_html_theme("sphinx-gp-theme", str(get_theme_path())) return { "parallel_read_safe": True, "parallel_write_safe": True, diff --git a/tests/test_theme.py b/tests/test_theme.py index 842a195f..e220dce6 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -2,13 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING - from sphinx_gp_theme import get_theme_path, setup -if TYPE_CHECKING: - import pathlib - def test_theme_path_exists() -> None: """Theme directory exists.""" @@ -74,14 +69,14 @@ def test_theme_setup_registers_theme() -> None: class FakeApp: def __init__(self) -> None: - self.calls: list[tuple[str, pathlib.Path]] = [] + self.calls: list[tuple[str, str]] = [] - def add_html_theme(self, name: str, theme_path: pathlib.Path) -> None: + def add_html_theme(self, name: str, theme_path: str) -> None: self.calls.append((name, theme_path)) app = FakeApp() metadata = setup(app) # type: ignore[arg-type] - assert app.calls == [("sphinx-gp-theme", get_theme_path())] + assert app.calls == [("sphinx-gp-theme", str(get_theme_path()))] assert metadata["parallel_read_safe"] is True assert metadata["parallel_write_safe"] is True From 05c18d69540dc7002e5ef6c3cc10ee3b953b5504 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 09:53:38 -0500 Subject: [PATCH 41/49] typehints(fix): Read autodoc annotations on Sphinx 8.1 why: Sphinx 8.2 moved autodoc's per-document annotation store from env.temp_data["annotations"] to env.current_document.autodoc_annotations. The extension read the new attribute unconditionally while declaring sphinx>=8.1 and requires-python>=3.10, and 8.1 is the newest Sphinx that 3.10 can install. Reading it there raises AttributeError, so every type the extension records is lost: eight integration tests error out on 3.10 once the matrix actually selects that interpreter. The type checker reported this as a missing attribute, but it is a crash, not an annotation gap. what: Read the 8.2 attribute when present and fall back to the 8.1 mapping, returning the live store in both so recorded annotations are not dropped. Cover each store with a regression test; the fallback test asserts writes land in temp_data, which a copy would silently lose. --- .../sphinx_autodoc_typehints_gp/extension.py | 36 ++++++++++++++++--- tests/ext/typehints_gp/test_unit.py | 35 ++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/packages/sphinx-autodoc-typehints-gp/src/sphinx_autodoc_typehints_gp/extension.py b/packages/sphinx-autodoc-typehints-gp/src/sphinx_autodoc_typehints_gp/extension.py index 00af3b76..1da324b0 100644 --- a/packages/sphinx-autodoc-typehints-gp/src/sphinx_autodoc_typehints_gp/extension.py +++ b/packages/sphinx-autodoc-typehints-gp/src/sphinx_autodoc_typehints_gp/extension.py @@ -43,6 +43,36 @@ _MODULE_IMPORTS: dict[str, dict[str, str]] = {} +def _autodoc_annotations(env: BuildEnvironment) -> dict[str, dict[str, str]]: + """Return autodoc's per-document annotation store. + + Sphinx 8.2 moved the store from ``env.temp_data["annotations"]`` to + ``env.current_document.autodoc_annotations``; Sphinx 8.1, the newest + release installable on Python 3.10, has ``temp_data`` only. + + Parameters + ---------- + env : BuildEnvironment + The Sphinx build environment. + + Returns + ------- + dict[str, dict[str, str]] + Mapping of object name to that object's annotations. + """ + current_document = getattr(env, "current_document", None) + if current_document is not None: + return t.cast( + "dict[str, dict[str, str]]", + current_document.autodoc_annotations, + ) + temp_data: t.Any = env.temp_data + return t.cast( + "dict[str, dict[str, str]]", + temp_data.setdefault("annotations", {}), + ) + + def get_module_imports(module_name: str) -> dict[str, str]: """Extract all import aliases from a module's source code. @@ -406,9 +436,7 @@ def record_typehints( aliases = get_module_imports(module_name) - doc_annotations = app.env.current_document.autodoc_annotations.setdefault( - name, {} - ) + doc_annotations = _autodoc_annotations(app.env).setdefault(name, {}) from sphinx.util.typing import stringify_annotation @@ -553,7 +581,7 @@ def merge_typehints( except KeyError: return - annotations = app.env.current_document.autodoc_annotations + annotations = _autodoc_annotations(app.env) if not annotations.get(fullname): return diff --git a/tests/ext/typehints_gp/test_unit.py b/tests/ext/typehints_gp/test_unit.py index b7d35e99..5f170cb3 100644 --- a/tests/ext/typehints_gp/test_unit.py +++ b/tests/ext/typehints_gp/test_unit.py @@ -27,10 +27,45 @@ process_numpy_docstring, ) from sphinx_autodoc_typehints_gp.extension import ( + _autodoc_annotations, get_module_imports, resolve_annotation_string, ) +# --------------------------------------------------------------------------- +# _autodoc_annotations (Sphinx 8.1 / 8.2 annotation-store compatibility) +# --------------------------------------------------------------------------- + + +def test_autodoc_annotations_reads_the_sphinx_82_attribute() -> None: + """Sphinx 8.2 keeps the store on ``env.current_document``.""" + + class CurrentDocument: + autodoc_annotations = {"f": {"x": "int"}} + + class Env: + current_document = CurrentDocument() + + assert _autodoc_annotations(Env()) == {"f": {"x": "int"}} # type: ignore[arg-type] + + +def test_autodoc_annotations_falls_back_to_sphinx_81_temp_data() -> None: + """Sphinx 8.1 has no ``current_document``; the store lives in temp_data. + + Writes through the returned mapping must land in ``temp_data`` -- a copy + would silently drop every annotation the extension records on 8.1. + """ + + class Env: + def __init__(self) -> None: + self.temp_data: dict[str, object] = {} + + env = Env() + store = _autodoc_annotations(env) # type: ignore[arg-type] + store["f"] = {"x": "int"} + assert env.temp_data == {"annotations": {"f": {"x": "int"}}} + + # --------------------------------------------------------------------------- # get_module_imports / resolve_annotation_string (individual — not fixture-based) # --------------------------------------------------------------------------- From 8c5bea408974d64db3a60b1dcd323ba69a52a32b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 09:54:28 -0500 Subject: [PATCH 42/49] tests(fix): Support Python 3.10 and Sphinx 8.1 why: The real Python matrix exposed fixtures that assumed StrEnum, NotRequired and InventoryFile.loads were available everywhere. what: - Exercise a string enum and backported NotRequired on Python 3.10 - Read inventories through the existing Sphinx 8.1-compatible API - Preserve all signature parity and inventory assertions --- .../test_sphinx_pytest_fixtures_integration.py | 10 ++++------ tests/ext/typehints_gp/test_documented_fields.py | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/ext/pytest_fixtures/test_sphinx_pytest_fixtures_integration.py b/tests/ext/pytest_fixtures/test_sphinx_pytest_fixtures_integration.py index 5515308d..d799d040 100644 --- a/tests/ext/pytest_fixtures/test_sphinx_pytest_fixtures_integration.py +++ b/tests/ext/pytest_fixtures/test_sphinx_pytest_fixtures_integration.py @@ -169,13 +169,11 @@ def test_default_html_outputs_smoke(default_html_result) -> None: """The default HTML build emits badge markup, inventory, and genindex entries.""" index_html = read_output(default_html_result, "index.html") genindex_html = read_output(default_html_result, "genindex.html") - inv = InventoryFile.loads( - (default_html_result.outdir / "objects.inv").read_bytes(), - uri="", - ) + with (default_html_result.outdir / "objects.inv").open("rb") as handle: + inv = InventoryFile.load(handle, "", lambda base, target: target) - assert "py:fixture" in inv.data - assert any("my_server" in name for name in inv.data["py:fixture"]) + assert "py:fixture" in inv + assert any("my_server" in name for name in inv["py:fixture"]) for css_class in ( SAB.BADGE_GROUP, diff --git a/tests/ext/typehints_gp/test_documented_fields.py b/tests/ext/typehints_gp/test_documented_fields.py index 3c2ab8f4..7d78999e 100644 --- a/tests/ext/typehints_gp/test_documented_fields.py +++ b/tests/ext/typehints_gp/test_documented_fields.py @@ -2542,7 +2542,7 @@ class Safety(enum.Enum): READONLY = "readonly" -class Tier(enum.StrEnum): +class Tier(StrEnum): \"\"\"A shape under test.{tier} \"\"\" @@ -2588,7 +2588,7 @@ class Payload(t.TypedDict): \"\"\" label: str - nickname: t.NotRequired[str] + nickname: NotRequired[str] class Registry: @@ -2670,8 +2670,18 @@ def _parity_section(entries: tuple[tuple[str, str, str], ...]) -> str: import dataclasses import enum + import sys import typing as t + if sys.version_info >= (3, 11): + from enum import StrEnum + from typing import NotRequired + else: + from typing_extensions import NotRequired + + class StrEnum(str, enum.Enum): + pass + """ ) From 0d35019011fe0f1f10f95beaf2bb93cd3aafb295 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 10:58:01 -0500 Subject: [PATCH 43/49] fastmcp(fix): Link the tool summary relative to its page why: The summary directive built refuri by interpolating the configured fastmcp_area_map value, so the link resolved against whatever directory the page happened to sit in. On this project's own docs that 404s twice: from /gallery/ it resolves to /gallery/packages/sphinx-autodoc-fastmcp/examples/, and from the examples page it appends the path to itself. Nothing catches it -- these are generated hrefs, not cross-references, so -W stays green. The area map is also the wrong source. It is a configured guess at where a tool is documented; the registered label knows where the card actually is. what: Emit the same placeholder node the {tool} role emits and let resolve_tool_refs do the work. It resolves through the canonical label and calls builder.get_relative_uri with the real fromdocname, which the directive cannot do at parse time. A tool with no card now renders as plain literal text rather than a link to a page that may not exist. Two collision fixtures asserted the old behaviour and are updated: one expected href="api/#..." in a scenario with no "api" document -- a broken link encoded as a passing test -- and now asserts that raw area path never appears; the other counts two canonical in-page links because the summary resolves the same way the inline toolref does. Verified: all 283 assets on the built site resolve 200, where two 404'd before; mypy clean under 3.10/Sphinx 8.1.3 and 3.14/Sphinx 8.2.3. --- .../src/sphinx_autodoc_fastmcp/_directives.py | 15 +++- tests/ext/fastmcp/test_fastmcp_integration.py | 85 ++++++++++++++++++- 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py index badc4813..3dc81fe3 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py @@ -33,6 +33,7 @@ make_table, parse_rst_inline, ) +from sphinx_autodoc_fastmcp._roles import _tool_ref_placeholder from sphinx_autodoc_typehints_gp import ( build_annotation_display_paragraph, build_annotation_paragraph, @@ -451,9 +452,17 @@ def run(self) -> list[nodes.Node]: rows: list[list[str | nodes.Node]] = [] for tool in sorted(group_tools, key=lambda x: x.name): first_line = first_paragraph(tool.docstring) - ref = nodes.reference("", "", internal=True) - ref["refuri"] = f"{tool.area}/#{_component_ids('tool', tool.name)[0]}" - ref += nodes.literal("", tool.name) + # Defer to ``resolve_tool_refs``: it resolves through the + # registered label, so the link points at the card's real + # document and is made relative to the page being written. + # Building ``refuri`` here would have neither -- ``tool.area`` + # is a configured guess, and at parse time there is no + # ``fromdocname`` to be relative to. + ref = _tool_ref_placeholder( + "", + reftarget=tool.name.replace("_", "-"), + show_badge=False, + ) rows.append( [ make_para(ref), diff --git a/tests/ext/fastmcp/test_fastmcp_integration.py b/tests/ext/fastmcp/test_fastmcp_integration.py index 06641841..d57397d6 100644 --- a/tests/ext/fastmcp/test_fastmcp_integration.py +++ b/tests/ext/fastmcp/test_fastmcp_integration.py @@ -361,21 +361,28 @@ class CollisionAnchorFixture(t.NamedTuple): ), # The toolref link wraps the tool name in ; the bare {ref} # link wraps the label title in — the - # trailing tag disambiguates the two resolution paths. + # trailing tag disambiguates the two resolution paths. Two matches: + # the inline toolref, and the summary row, which resolves through the + # same path and lands on the same in-page anchor because the card is + # on this page. CollisionAnchorFixture( test_id="toolref-targets-canonical-anchor", needle='class="reference internal" href="#fastmcp-tool-delete-buffer"> SharedSphinxResult: + """Build a summary directive one directory below the tool card.""" + cache_root = tmp_path_factory.mktemp("fastmcp-nested-summary") + scenario = SphinxScenario( + files=( + ScenarioFile("buffer_tools.py", _COLLISION_MODULE_SOURCE), + ScenarioFile( + "conf.py", + _COLLISION_CONF_PY.replace( + "__SCENARIO_SRCDIR__", SCENARIO_SRCDIR_TOKEN + ), + substitute_srcdir=True, + ), + ScenarioFile("index.rst", _NESTED_INDEX_RST), + ScenarioFile("api.rst", _NESTED_API_RST), + ScenarioFile("sub/summary.rst", _NESTED_SUMMARY_RST), + ), + ) + return build_shared_sphinx_result( + cache_root, + scenario, + purge_modules=("buffer_tools",), + ) + + +def test_tool_summary_link_is_relative_to_the_rendering_page( + fastmcp_nested_summary_result: SharedSphinxResult, +) -> None: + """The summary link resolves from the page it is rendered on.""" + html = read_output(fastmcp_nested_summary_result, "sub/summary.html") + assert 'href="../api.html#fastmcp-tool-delete-buffer"' in html From 1362256cfdc83f926f3df6e30f33b68191a535ce Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 10:58:49 -0500 Subject: [PATCH 44/49] fastmcp(fix): Match reserved axis names regardless of case why: The guard compared the declared axis name to COMPONENT_KINDS verbatim, so fastmcp_axes with an axis named "Tool" cleared it. The anchors it then generates go through make_id, which lower-cases, so they collide with the canonical tool ids exactly as a lower-case "tool" axis would -- the guard let through the case it exists to catch. what: Case-fold before the membership test, and cover the capitalised form in the doctest. --- .../src/sphinx_autodoc_fastmcp/_models.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_models.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_models.py index 197be3a2..f11f9d0b 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_models.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_models.py @@ -156,12 +156,17 @@ def _is_reserved_axis_name(name: str) -> bool: Examples -------- + The anchors are built with ``make_id``, which lower-cases, so a + capitalised axis collides exactly as the lower-case one does: + >>> _is_reserved_axis_name("capability") False >>> _is_reserved_axis_name("tool") True + >>> _is_reserved_axis_name("Tool") + True """ - if name not in COMPONENT_KINDS: + if name.casefold() not in COMPONENT_KINDS: return False logger.warning( "sphinx_autodoc_fastmcp: fastmcp_axes declares an axis named %r, which " From 44431dd9db05907eee130ba85fc805436a88bb5c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 10:59:33 -0500 Subject: [PATCH 45/49] fastmcp(docs): Describe how the collector actually reads a server why: The fastmcp_server_module description told users the collector reads local_provider._components directly. It has not done that for some time -- it walks the server's providers, descends into mounted servers and aggregates, and applies each provider's transforms, all under a timeout. The string is shipped confval help, so the inaccuracy renders into every consumer's documentation. `local_provider._components` now appears nowhere in the package except that sentence. what: Describe the behaviour instead of the internals. Naming a private attribute is what let the text rot without anything noticing; the replacement says what the collector enumerates and why, which stays true across refactors of how it gets there. --- .../src/sphinx_autodoc_fastmcp/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/__init__.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/__init__.py index c6d8fd44..4dc9eb7a 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/__init__.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/__init__.py @@ -178,11 +178,12 @@ def setup(app: Sphinx) -> dict[str, t.Any]: "env", description=( '``"pkg.module:attribute"`` path to a live ``FastMCP`` ' - "instance. When set, the collector reads " - "``local_provider._components`` directly so docs enumerate " - "the same surface as the running server: tools, prompts, " - "resources and resource templates. Tools read this way take " - "precedence over ``fastmcp_tool_modules``." + "instance. When set, the collector walks the server's " + "providers -- mounted servers, aggregates and the transforms " + "each applies -- so docs enumerate the same surface the " + "server serves: tools, prompts, resources and resource " + "templates. Tools read this way take precedence over " + "``fastmcp_tool_modules``." ), ) From 7f8d862b9c0542b8763f32ddf157a984ef5111af Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 11:01:17 -0500 Subject: [PATCH 46/49] fastmcp(fix): Stop reporting unclaimable bare aliases why: Sphinx seeds genindex, modindex and search into the standard domain before any document is read. A tool named after one of them can never claim its bare-slug alias, so the collision warning fires on every build, forever, with no configuration that avoids it and nothing for an author to do. This extension already treats the case as expected: the comment above the canonical-first lookup in _transforms.py says such a tool still links to its own card precisely because the built-ins occupy those labels first. Warning about a condition we document as handled is the wrong severity. It matters now rather than later. These diagnostics are moving onto Sphinx's logger, and consumer docs builds are gaining -W. A consumer with a tool named `search` would get a permanently red gate for a name collision that degrades nothing. what: Skip the warning when the existing claimant is one of Sphinx's seeded labels, identified by its initial_data entry rather than by name alone. Registration still declines, so the canonical id remains the only anchor. A collision with another document's label is actionable and still warns. --- .../src/sphinx_autodoc_fastmcp/_directives.py | 17 ++++++ tests/ext/fastmcp/test_fastmcp.py | 59 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py index 3dc81fe3..5946be98 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py @@ -109,6 +109,17 @@ def _component_ids(kind: str, name: str) -> tuple[str, list[str]]: return canonical, aliases +#: Labels Sphinx's ``StandardDomain`` seeds before any document is read, from +#: its ``initial_data`` (``sphinx/domains/std/__init__.py``). A tool whose bare +#: slug is one of these can never claim the alias, on any project, on every +#: build -- so reporting it as a collision is noise with no available fix. +_SPHINX_SEEDED_LABELS: dict[str, tuple[str, str]] = { + "genindex": ("genindex", ""), + "modindex": ("py-modindex", ""), + "search": ("search", ""), +} + + def _register_alias_if_free( env: BuildEnvironment, *, @@ -161,6 +172,12 @@ def _register_alias_if_free( existing_doc = existing[0] existing_id = existing[1] if (existing_doc, existing_id) != (env.docname, target_id): + if _SPHINX_SEEDED_LABELS.get(alias) == (existing_doc, existing_id): + # Expected, and already handled: roles resolve the canonical + # ``fastmcp--`` id first, so the card still + # resolves. Nothing is degraded and nobody can act on it -- + # the alias is unclaimable by construction. + return False logger.warning( "sphinx_autodoc_fastmcp: bare alias %r for %s already claimed " "by %s#%s; using canonical id only", diff --git a/tests/ext/fastmcp/test_fastmcp.py b/tests/ext/fastmcp/test_fastmcp.py index 3970209d..046c99af 100644 --- a/tests/ext/fastmcp/test_fastmcp.py +++ b/tests/ext/fastmcp/test_fastmcp.py @@ -13,6 +13,7 @@ from sphinx_autodoc_fastmcp._badges import build_axis_badge, build_tool_badge_group from sphinx_autodoc_fastmcp._collector import _resolve_server_instance from sphinx_autodoc_fastmcp._css import _CSS +from sphinx_autodoc_fastmcp._directives import _register_alias_if_free from sphinx_autodoc_fastmcp._parsing import ( extract_params, first_paragraph, @@ -383,3 +384,61 @@ def test_every_component_kind_is_reserved_as_an_axis_name() -> None: for kind in COMPONENT_KINDS: canonical, _aliases = _component_ids(kind, "x") assert canonical == f"fastmcp-{kind}-x" + + +def _alias_env(docname: str, labels: dict[str, tuple[str, ...]]) -> t.Any: + """Return a minimal env whose standard domain holds *labels*.""" + std = types.SimpleNamespace(labels=labels, anonlabels={}) + return types.SimpleNamespace( + docname=docname, + domains=types.SimpleNamespace(standard_domain=std), + ) + + +def test_sphinx_seeded_label_collision_is_not_reported( + caplog: pytest.LogCaptureFixture, +) -> None: + """A tool named after a Sphinx built-in label collides silently. + + Sphinx seeds ``genindex``, ``modindex`` and ``search`` before any + document is read, so such a tool can never claim its bare alias -- and + does not need to, because roles resolve the canonical id first. The + condition is unfixable and harmless, so it must not be a warning: it + would fire on every build of every project with such a tool. + """ + env = _alias_env("mcp/tools", {"search": ("search", "", "Search Page")}) + + with caplog.at_level(logging.WARNING): + registered = _register_alias_if_free( + env, + alias="search", + target_id="fastmcp-tool-search", + display_name="search", + kind="tool", + ) + + assert registered is False + assert not [r for r in caplog.records if "already claimed" in r.getMessage()] + + +def test_foreign_label_collision_is_still_reported( + caplog: pytest.LogCaptureFixture, +) -> None: + """A collision with another document's label stays a warning. + + Unlike the seeded built-ins, this one is actionable: the author can + rename the heading or the tool. + """ + env = _alias_env("api", {"delete-buffer": ("guide", "delete-buffer", "Delete")}) + + with caplog.at_level(logging.WARNING): + registered = _register_alias_if_free( + env, + alias="delete-buffer", + target_id="fastmcp-tool-delete-buffer", + display_name="delete_buffer", + kind="tool", + ) + + assert registered is False + assert [r for r in caplog.records if "already claimed" in r.getMessage()] From a706188e077ab4d30cd73dd6eddbd8eb23319114 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 11:05:14 -0500 Subject: [PATCH 47/49] fastmcp(fix): Route every warning through Sphinx why: c34c51ea moved the collector onto sphinx.util.logging and documented that collector warnings obey -W. The extension's other three modules were left on stdlib logging, so the eight diagnostics they raise reached stderr and nothing else: invisible to -W, absent from the -w warnings file, and unreachable by suppress_warnings. A consumer building with -W could print a warning and still exit 0, which is exactly what agentgrep's docs build did. what: Convert _directives.py, _models.py and _transforms.py to sphinx.util.logging and tag each warning with the condition class it reports: alias, axis, config, xref. One subtype per class rather than per message, so suppress_warnings stays usable. Documented as a table in the how-to alongside the existing fastmcp.duplicate. The unmatched-axis test now asserts on Sphinx's warning stream rather than caplog -- reaching that stream is the whole point of the change, and Sphinx swaps logging handlers during a build so caplog cannot observe it. Reverting either module to stdlib logging fails it. --- .../packages/sphinx-autodoc-fastmcp/how-to.md | 21 +++++++++++++---- .../src/sphinx_autodoc_fastmcp/_directives.py | 8 +++++-- .../src/sphinx_autodoc_fastmcp/_models.py | 11 +++++++-- .../src/sphinx_autodoc_fastmcp/_transforms.py | 10 ++++++-- tests/ext/fastmcp/test_fastmcp_integration.py | 23 ++++++++++--------- tests/ext/fastmcp/test_transforms.py | 8 +++---- 6 files changed, 56 insertions(+), 25 deletions(-) diff --git a/docs/packages/sphinx-autodoc-fastmcp/how-to.md b/docs/packages/sphinx-autodoc-fastmcp/how-to.md index 38290d8e..49851fc4 100644 --- a/docs/packages/sphinx-autodoc-fastmcp/how-to.md +++ b/docs/packages/sphinx-autodoc-fastmcp/how-to.md @@ -176,7 +176,20 @@ share one, so both are served. The docs index holds one entry per name: it keeps the first and warns, naming the collision. Server/module overlap follows the documented precedence without warning. -Collector warnings use Sphinx's warning stream, so `-W` fails the build and -`-w` records them. Name collisions use the `fastmcp.duplicate` category; -other collection warnings use `fastmcp`. Set -`suppress_warnings = ["fastmcp.duplicate"]` to suppress only name collisions. + +Every warning this extension raises goes through Sphinx's warning stream, so +`-W` fails the build on them and `-w` records them. Each carries a category +you can suppress individually through `suppress_warnings`: + +| Category | Raised when | +| --- | --- | +| `fastmcp.duplicate` | Two components claim one name | +| `fastmcp.alias` | A tool's bare-slug alias is already claimed by another document's label | +| `fastmcp.axis` | An axis is unusable, or a tool matches no term on one | +| `fastmcp.config` | A `fastmcp_axes` entry is malformed | +| `fastmcp.xref` | A cross-reference cannot resolve, or resolves away from its canonical section | + +Suppressing the parent `fastmcp` category silences all of them. A tool named +after one of Sphinx's built-in labels (`genindex`, `modindex`, `search`) +raises nothing: it can never claim the bare alias, cross-references resolve +the canonical id first, and there is no action an author could take. diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py index 5946be98..e07ee605 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_directives.py @@ -2,11 +2,11 @@ from __future__ import annotations -import logging import typing as t from docutils import nodes from docutils.parsers.rst import directives +from sphinx.util import logging as sphinx_logging from sphinx.util.docutils import SphinxDirective if t.TYPE_CHECKING: @@ -50,7 +50,7 @@ build_api_table_section, ) -logger = logging.getLogger(__name__) +logger = sphinx_logging.getLogger(__name__) def _register_section_label( @@ -185,6 +185,8 @@ def _register_alias_if_free( display_name, existing_doc, existing_id, + type="fastmcp", + subtype="alias", ) return False @@ -448,6 +450,8 @@ def run(self) -> list[nodes.Node]: len(unassigned), axis.name, ", ".join(sorted(tool.name for tool in unassigned)), + type="fastmcp", + subtype="axis", ) result_nodes: list[nodes.Node] = [] diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_models.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_models.py index f11f9d0b..d04f06c2 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_models.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_models.py @@ -2,11 +2,12 @@ from __future__ import annotations -import logging import typing as t from dataclasses import dataclass, field -logger = logging.getLogger(__name__) +from sphinx.util import logging as sphinx_logging + +logger = sphinx_logging.getLogger(__name__) #: Sources an axis can read a tool's term from. ``tags`` matches declared #: terms against ``tool.tags``; ``annotations`` derives one from the MCP @@ -122,6 +123,8 @@ def _coerce_term(value: t.Any) -> Term | None: logger.warning( "sphinx_autodoc_fastmcp: toolset term %r has no 'term'; skipping it", value, + type="fastmcp", + subtype="config", ) return None return Term( @@ -179,6 +182,8 @@ def _is_reserved_axis_name(name: str) -> bool: name, name, f"{name}-kind", + type="fastmcp", + subtype="axis", ) return True @@ -221,6 +226,8 @@ def coerce_axes(value: t.Any) -> tuple[Axis, ...]: "sphinx_autodoc_fastmcp: fastmcp_axes entry %r has no 'name'; " "skipping it", entry, + type="fastmcp", + subtype="config", ) continue if _is_reserved_axis_name(name): diff --git a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_transforms.py b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_transforms.py index dac4aae6..882617e5 100644 --- a/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_transforms.py +++ b/packages/sphinx-autodoc-fastmcp/src/sphinx_autodoc_fastmcp/_transforms.py @@ -2,11 +2,11 @@ from __future__ import annotations -import logging import re from docutils import nodes from sphinx.application import Sphinx +from sphinx.util import logging as sphinx_logging from sphinx_autodoc_fastmcp._badges import ( active_axes, @@ -22,7 +22,7 @@ from sphinx_ux_autodoc_layout import API, api_component from sphinx_ux_badges import SAB -logger = logging.getLogger(__name__) +logger = sphinx_logging.getLogger(__name__) def _tool_content_container(section: nodes.section) -> nodes.Element: @@ -206,6 +206,8 @@ def resolve_tool_refs( labelid, canonical, tool_name, + type="fastmcp", + subtype="xref", ) newnode = nodes.reference("", "", internal=True) @@ -218,6 +220,8 @@ def resolve_tool_refs( "sphinx_autodoc_fastmcp: failed to resolve URI for %s -> %s", fromdocname, todocname, + type="fastmcp", + subtype="xref", ) newnode["refuri"] = "#" + labelid newnode["classes"].append("reference") @@ -313,6 +317,8 @@ def resolve_component_refs( "sphinx_autodoc_fastmcp: failed to resolve URI for %s -> %s", fromdocname, todocname, + type="fastmcp", + subtype="xref", ) newnode["refuri"] = "#" + labelid newnode["classes"].append("reference") diff --git a/tests/ext/fastmcp/test_fastmcp_integration.py b/tests/ext/fastmcp/test_fastmcp_integration.py index d57397d6..1027a15f 100644 --- a/tests/ext/fastmcp/test_fastmcp_integration.py +++ b/tests/ext/fastmcp/test_fastmcp_integration.py @@ -2,7 +2,6 @@ from __future__ import annotations -import logging import pathlib import subprocess import sys @@ -472,20 +471,22 @@ def test_tool_role_omits_the_badge_for_a_tag_outside_the_vocabulary( @pytest.mark.integration def test_the_summary_warns_when_it_drops_an_unmatched_tool( tmp_path_factory: pytest.TempPathFactory, - caplog: pytest.LogCaptureFixture, ) -> None: """A tool the summary cannot place must not vanish without a trace.""" cache_root = tmp_path_factory.mktemp("fastmcp-unmatched-toolset-warn") - with caplog.at_level(logging.WARNING, logger="sphinx_autodoc_fastmcp"): - build_shared_sphinx_result( - cache_root, - _unmatched_scenario(), - purge_modules=("demo_tools",), - ) + result = build_shared_sphinx_result( + cache_root, + _unmatched_scenario(), + purge_modules=("demo_tools",), + ) - messages = "\n".join(record.message for record in caplog.records) - assert "omitted from fastmcp-tool-summary" in messages - assert "list_sessions" in messages + # Assert on Sphinx's own warning stream rather than a stdlib handler: + # reaching it is what makes the diagnostic visible to -W, to the + # warnings file and to suppress_warnings. Sphinx swaps logging handlers + # for the duration of a build, so caplog cannot see this at all. + assert "omitted from fastmcp-tool-summary" in result.warnings + assert "[fastmcp.axis]" in result.warnings + assert "list_sessions" in result.warnings @pytest.mark.integration diff --git a/tests/ext/fastmcp/test_transforms.py b/tests/ext/fastmcp/test_transforms.py index 4d37a1ef..23975f70 100644 --- a/tests/ext/fastmcp/test_transforms.py +++ b/tests/ext/fastmcp/test_transforms.py @@ -77,7 +77,7 @@ def test_resolve_tool_refs_warns_when_known_tool_has_no_canonical_home( caplog: pytest.LogCaptureFixture, ) -> None: """A known tool resolving to a foreign label (reserved ``search``) warns.""" - with caplog.at_level(logging.WARNING, logger="sphinx_autodoc_fastmcp._transforms"): + with caplog.at_level(logging.WARNING, logger="sphinx.sphinx_autodoc_fastmcp._transforms"): container = _resolve_single_tool_ref( reftarget="search", labels={"search": ("search", "", "Search Page")}, @@ -85,7 +85,7 @@ def test_resolve_tool_refs_warns_when_known_tool_has_no_canonical_home( ) records = [ - r for r in caplog.records if r.name == "sphinx_autodoc_fastmcp._transforms" + r for r in caplog.records if r.name == "sphinx.sphinx_autodoc_fastmcp._transforms" ] assert len(records) == 1 message = records[0].getMessage() @@ -99,7 +99,7 @@ def test_resolve_tool_refs_silent_when_canonical_label_present( caplog: pytest.LogCaptureFixture, ) -> None: """No warning when the canonical ``fastmcp-tool-`` label exists.""" - with caplog.at_level(logging.WARNING, logger="sphinx_autodoc_fastmcp._transforms"): + with caplog.at_level(logging.WARNING, logger="sphinx.sphinx_autodoc_fastmcp._transforms"): container = _resolve_single_tool_ref( reftarget="search", labels={ @@ -114,7 +114,7 @@ def test_resolve_tool_refs_silent_when_canonical_label_present( ) warnings = [ - r for r in caplog.records if r.name == "sphinx_autodoc_fastmcp._transforms" + r for r in caplog.records if r.name == "sphinx.sphinx_autodoc_fastmcp._transforms" ] assert warnings == [] reference = container[0] From ebfad6fb96ffca93b02949b60c6327c46c263dca Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 11:10:02 -0500 Subject: [PATCH 48/49] fastmcp(fix): Format the converted transforms test why: 97057762 updated the logger name in this test without reformatting it, so two lines exceeded the limit and ruff format failed every qa job. what: Reformat. No behaviour change. --- tests/ext/fastmcp/test_transforms.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/ext/fastmcp/test_transforms.py b/tests/ext/fastmcp/test_transforms.py index 23975f70..bc89bb55 100644 --- a/tests/ext/fastmcp/test_transforms.py +++ b/tests/ext/fastmcp/test_transforms.py @@ -77,7 +77,9 @@ def test_resolve_tool_refs_warns_when_known_tool_has_no_canonical_home( caplog: pytest.LogCaptureFixture, ) -> None: """A known tool resolving to a foreign label (reserved ``search``) warns.""" - with caplog.at_level(logging.WARNING, logger="sphinx.sphinx_autodoc_fastmcp._transforms"): + with caplog.at_level( + logging.WARNING, logger="sphinx.sphinx_autodoc_fastmcp._transforms" + ): container = _resolve_single_tool_ref( reftarget="search", labels={"search": ("search", "", "Search Page")}, @@ -85,7 +87,9 @@ def test_resolve_tool_refs_warns_when_known_tool_has_no_canonical_home( ) records = [ - r for r in caplog.records if r.name == "sphinx.sphinx_autodoc_fastmcp._transforms" + r + for r in caplog.records + if r.name == "sphinx.sphinx_autodoc_fastmcp._transforms" ] assert len(records) == 1 message = records[0].getMessage() @@ -99,7 +103,9 @@ def test_resolve_tool_refs_silent_when_canonical_label_present( caplog: pytest.LogCaptureFixture, ) -> None: """No warning when the canonical ``fastmcp-tool-`` label exists.""" - with caplog.at_level(logging.WARNING, logger="sphinx.sphinx_autodoc_fastmcp._transforms"): + with caplog.at_level( + logging.WARNING, logger="sphinx.sphinx_autodoc_fastmcp._transforms" + ): container = _resolve_single_tool_ref( reftarget="search", labels={ @@ -114,7 +120,9 @@ def test_resolve_tool_refs_silent_when_canonical_label_present( ) warnings = [ - r for r in caplog.records if r.name == "sphinx.sphinx_autodoc_fastmcp._transforms" + r + for r in caplog.records + if r.name == "sphinx.sphinx_autodoc_fastmcp._transforms" ] assert warnings == [] reference = container[0] From c74b7c194716609fad9ba34324cc18c34b126617 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 6 Sep 2026 11:26:54 -0500 Subject: [PATCH 49/49] docs(CHANGES) Summarize FastMCP improvements why: Describe the documentation changes users can expect. what: - Cover live server tools, resource metadata, warnings and links. - Note the Sphinx compatibility, preview and API header fixes. --- CHANGES | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/CHANGES b/CHANGES index f8ec89de..e1a9f39c 100644 --- a/CHANGES +++ b/CHANGES @@ -18,6 +18,20 @@ $ uv add gp-sphinx --prerelease allow +### What's new + +#### `sphinx-autodoc-fastmcp`: Document live server tools + +Set `fastmcp_server_module` to document mounted and renamed tools with +the names, parameters and metadata clients receive. Disabled tools stay +documented. Server entries take precedence over module scanning without +duplicate warnings. (#78) + +#### `sphinx-autodoc-fastmcp`: Show resource metadata + +Resource and resource-template cards now show their intended audience, +priority and last-modified information when supplied. (#78) + ### Fixes #### `sphinx-gp-llms`: A Markdown twin link only where a twin exists @@ -66,6 +80,39 @@ build stayed clean. Tool roles now resolve the canonical the link. A tool that appears only under `:no-index:` — and so has no canonical cross-reference home — now warns instead of mis-linking. (#60) +#### `sphinx-autodoc-fastmcp`: Preserve metadata with FastMCP 4 + +Tool hints and prompt argument descriptions render correctly with +FastMCP 4. Parameter types stay readable without annotation metadata, +and generated schema instructions stay out of descriptions. (#78) + +#### `sphinx-autodoc-fastmcp`: Honor Sphinx warning settings + +Extension warnings now follow Sphinx's warning and suppression settings, +so strict builds fail on unsuppressed warnings. Duplicate component +names are reported while the first entry is retained. (#78) + +#### `sphinx-autodoc-fastmcp`: Keep tool links on the right page + +Tool summary links resolve from nested pages. Tools named after built-in +Sphinx pages avoid unnecessary alias warnings, and reserved axis names +are rejected regardless of capitalization. (#78) + +#### `sphinx-autodoc-typehints-gp`: Preserve Sphinx 8.1 descriptions + +Parameter and return descriptions remain visible with Sphinx 8.1, +including builds on Python 3.10. (#78) + +#### Documentation preview: Stop repeated idle rebuilds + +The documentation preview rebuilds when sources change and stays idle +otherwise. Cleaning the docs also clears cached doctrees. (#78) + +#### `sphinx-ux-autodoc-layout`: Blend code into API headers + +Inline code in API signatures uses the header background, including +when the header is hovered. (#78) + ## gp-sphinx 0.1.0a38 (2026-08-30) gp-sphinx 0.1.0a38 lets a project say what its MCP tools are, in its own