diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9dd97e1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,33 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: / + schedule: + interval: weekly + groups: + minor-and-patch: + patterns: ["*"] + update-types: ["minor", "patch"] + - package-ecosystem: pip + directory: /packages/agentic-fabric + schedule: + interval: weekly + groups: + minor-and-patch: + patterns: ["*"] + update-types: ["minor", "patch"] + - package-ecosystem: pip + directory: /packages/pytest-agentic-fabric + schedule: + interval: weekly + groups: + minor-and-patch: + patterns: ["*"] + update-types: ["minor", "patch"] + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + actions: + patterns: ["*"] diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml new file mode 100644 index 0000000..9e22edc --- /dev/null +++ b/.github/workflows/automerge.yml @@ -0,0 +1,31 @@ +name: Automerge + +# Enables GitHub native auto-merge on dependabot + release-please PRs. +# Native auto-merge waits for required checks + any required reviews, +# then squash-merges without further action. This workflow does not check +# out PR code; pull_request_target is used only to enable auto-merge from +# the base workflow context. +on: + pull_request_target: + types: [opened, reopened, synchronize, ready_for_review] + +permissions: + contents: write + pull-requests: write + +jobs: + automerge: + name: Enable auto-merge + runs-on: ubuntu-latest + if: > + github.event.pull_request.user.login == 'dependabot[bot]' || + ( + github.event.pull_request.user.login == 'github-actions[bot]' && + startsWith(github.event.pull_request.head.ref, 'release-please--') + ) + steps: + - name: Enable auto-merge (squash) + env: + GH_TOKEN: ${{ github.token }} + PR_URL: ${{ github.event.pull_request.html_url }} + run: gh pr merge --auto --squash "$PR_URL" diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..c75ca2e --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,132 @@ +name: CD + +on: + workflow_dispatch: + inputs: + tag: + description: Release tag to publish and deploy + required: true + type: string + package: + description: Workspace package to publish + required: true + type: string + +permissions: {} + +jobs: + test: + name: Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ inputs.tag }} + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: ${{ matrix.python-version }} + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + version: "0.11.19" + enable-cache: true + - run: uv sync --all-packages --all-extras --dev + - run: uv tool install tox --with tox-uv --with tox-gh + - run: tox -e "py$(echo '${{ matrix.python-version }}' | tr -d '.')" + + quality: + name: Quality, docs, and build + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ inputs.tag }} + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: "3.13" + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + version: "0.11.19" + enable-cache: true + - run: uv sync --all-packages --all-extras --dev + - run: uv tool install tox --with tox-uv --with tox-gh + - run: tox -e lint,typecheck,audit,examples,coverage,plugin,docs,build + + publish-package: + name: Publish package to PyPI + needs: [test, quality] + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ inputs.tag }} + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: "3.13" + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + version: "0.11.19" + enable-cache: true + - name: Validate package input + env: + PACKAGE: ${{ inputs.package }} + run: | + case "$PACKAGE" in + agentic-fabric|pytest-agentic-fabric) ;; + *) echo "unsupported package: $PACKAGE" >&2; exit 1 ;; + esac + - name: Build package + env: + PACKAGE: ${{ inputs.package }} + run: uv build --package "$PACKAGE" --out-dir dist --clear + - run: uv publish --trusted-publishing always dist/* + + publish-docs: + name: Publish docs to GitHub Pages + if: inputs.package == 'agentic-fabric' + needs: publish-package + runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ inputs.tag }} + persist-credentials: false + - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 + with: + enablement: true + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: "3.13" + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + version: "0.11.19" + enable-cache: true + - run: uv sync --all-packages --all-extras --dev + - run: uv tool install tox --with tox-uv --with tox-gh + - run: tox -e docs + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: docs/_build/html + - id: deployment + name: Deploy docs + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4d32611 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: {} + +jobs: + test: + name: Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: ${{ matrix.python-version }} + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + version: "0.11.19" + enable-cache: true + - run: uv sync --all-packages --all-extras --dev + - run: uv tool install tox --with tox-uv --with tox-gh + - run: tox -e "py$(echo '${{ matrix.python-version }}' | tr -d '.')" + + quality: + name: Quality, docs, build + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: "3.13" + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + version: "0.11.19" + enable-cache: true + - run: uv sync --all-packages --all-extras --dev + - run: uv tool install tox --with tox-uv --with tox-gh + - run: tox -e lint,typecheck,audit,examples,coverage,plugin,docs,build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..98a82c7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: Release + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: {} + +jobs: + release-please: + name: Release Please + runs-on: ubuntu-latest + permissions: + actions: write + contents: write + pull-requests: write + steps: + - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 + id: release + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + - name: Dispatch agentic-fabric continuous delivery + if: steps.release.outputs['packages/agentic-fabric--release_created'] == 'true' + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ steps.release.outputs['packages/agentic-fabric--tag_name'] }} + run: gh workflow run cd.yml --repo "$GH_REPO" --ref main -f tag="$RELEASE_TAG" -f package="agentic-fabric" + - name: Dispatch pytest-agentic-fabric continuous delivery + if: steps.release.outputs['packages/pytest-agentic-fabric--release_created'] == 'true' + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ steps.release.outputs['packages/pytest-agentic-fabric--tag_name'] }} + run: gh workflow run cd.yml --repo "$GH_REPO" --ref main -f tag="$RELEASE_TAG" -f package="pytest-agentic-fabric" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..74f2111 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.DS_Store +.coverage +.mypy_cache/ +.pytest_cache/ +.ruff_cache/ +.tox/ +.venv/ +__pycache__/ +build/ +dist/ +htmlcov/ +*.egg-info/ +*.pyc +docs/_build/ +docs/apidocs/ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..443381e --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,4 @@ +{ + "packages/agentic-fabric": "1.0.0", + "packages/pytest-agentic-fabric": "0.1.0" +} diff --git a/AGENTIC_REINFORCEMENT.md b/AGENTIC_REINFORCEMENT.md new file mode 100644 index 0000000..ef6bc3b --- /dev/null +++ b/AGENTIC_REINFORCEMENT.md @@ -0,0 +1,48 @@ +# Agentic Reinforcement + +This repository creates the runtime- and agent-aware superclass on top of +`VendorData`. + +## AgenticData Contract + +- `AgenticData` extends `VendorData`, not `ExtendedData` directly. +- Preserve vendor-layer semantics while adding runtime context, fabric agent registry + behavior, runner dispatch, and agent-facing tool exposure. +- The local `_VendorDataBase` fallback is an importability shim, not a second + architecture. Keep it minimal and temporary in spirit. + +## What This Repository Owns + +- runtime discovery and selection +- runner registries and runtime adapters +- LangChain, CrewAI, LangGraph, Strands, and MCP-facing tool wrappers +- agent-facing tool catalogs built from vendor capabilities +- fabric agent, session, and agent orchestration context + +## What This Repository Does Not Own + +- base data primitives +- generic file/workflow/logging primitives +- provider SDK integrations +- provider availability/install guidance beyond delegating to `vendor-fabric` +- reimplementation of the `VendorData` provider dispatch layer + +## Hand-Off From Vendor Fabric + +- Pull provider capability metadata and typed callables from `vendor-fabric`. +- Decide here how those capabilities become framework tools and runtime-visible + surfaces. +- If something exists only because an AI framework wants a specific wrapper + type, keep it here. +- Do not push AI-framework shims back down into `vendor-fabric`. +- Do not bypass `vendor-fabric` to import SecretSync Go bindings or invoke the + Go runtime directly from this layer. + +## Superclass Role + +- `AgenticData` is the superclass downstream domain-specific agent/session/task + objects should extend. +- Downstream apps should build on `AgenticData` rather than recreating provider + and runtime composition from scratch. +- Agentic behavior is additive over the vendor layer, which is additive over + the base data layer. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3954d6c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,66 @@ +# Repository Instructions + +This repository contains the `agentic-fabric` Python workspace. + +## Scope + +- `packages/agentic-fabric`: framework-agnostic agent discovery, runtime + selection, runner adapters, agent-facing tool wrappers, and `AgenticData` +- `packages/pytest-agentic-fabric`: pytest fixtures and mocks for agentic + runtime, tool, and framework tests +- `docs/`: Sphinx/Furo documentation and generated API reference + +## Layer Boundaries + +- `extended-data` owns base data containers, logging, input handling, files, + redaction, and generic workflows. +- `vendor-fabric` owns vendor connectors, provider-backed sync, SecretSync + Python facade/capability surfaces, provider capability metadata, and provider + dispatch. +- `agentic-fabric` owns runtime discovery, runner selection, framework + adapters, agent-facing tool wrappers, MCP surfaces, and orchestration. + +`AgenticData` extends `VendorData`; it should not bypass the vendor layer or +recreate provider dispatch. Keep SecretSync access routed through +`vendor-fabric` capabilities rather than importing the Go binding directly. + +## Preferred Commands + +```bash +uv sync --all-packages --all-extras --dev +tox -e lint +tox -e typecheck +tox -e audit +tox -e py311,py312,py313,py314 +tox -e coverage +tox -e plugin +tox -e examples +tox -e docs +tox -e build +``` + +Use `uvx --with tox-uv tox -e ` when tox is not installed locally. + +Do not set `skip_missing_interpreters = true`. Python 3.11, 3.12, 3.13, and +3.14 are all part of the supported release contract. + +## Expectations + +- Keep README files, Sphinx docs, examples, tests, and implementation aligned. +- Keep optional framework imports lazy and registry-backed. +- Keep provider-backed tools routed through `vendor-fabric`; do not call vendor + SDKs directly from agent runtime code. +- Use configured logging or Python logging/warnings/exceptions in library + runtime paths. CLI commands and examples may write user-facing output. +- Prefer durable package guidance over branch-specific migration instructions. + +## Release Flow + +Releases are managed by release-please: + +- `ci.yml` validates pull requests. +- `release.yml` opens and maintains the release-please PR. +- `cd.yml` publishes packages to PyPI and deploys Sphinx/Furo docs to GitHub + Pages after releases are created. + +Do not hand-edit versions or tags outside release-please work. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0de494e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 jbcom + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..311c20f --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Agentic Fabric Workspace + +This repository is the public `jbcom/agentic-fabric` workspace. + +Documentation: [jonbogaty.com/agentic-fabric](https://jonbogaty.com/agentic-fabric/) + +Read `AGENTS.md` first in Codex sessions; it contains durable repository +guidance, validation commands, and package boundary rules. + +The implemented surface includes `AgenticData`, lazy runtime registry metadata, +capability decorators, vendor-tool references, and the sibling +`pytest-agentic-fabric` package. diff --git a/SECRETS_SYNC_ALIGNMENT.md b/SECRETS_SYNC_ALIGNMENT.md new file mode 100644 index 0000000..10de882 --- /dev/null +++ b/SECRETS_SYNC_ALIGNMENT.md @@ -0,0 +1,38 @@ +# SecretSync Alignment + +This file locks the intended SecretSync boundary for `agentic-fabric`. + +## Canonical Stack + +1. `secrets-sync` owns the Go runtime and gopy binding source. +2. `vendor-fabric` owns the Python facade, credential handoff, and + `VendorData`-level capability surface. +3. `agentic-fabric` owns runtime selection, tool wrapping, and orchestration. + +## Binding Contract + +- PyPI distribution: `secrets-sync-python-binding` +- Python import/module: `secrets_sync` +- This repository should not import `secrets_sync` directly; it should consume + SecretSync through `vendor-fabric`. + +## This Repository's Role + +- Turn SecretSync-related vendor capabilities into framework-visible tools for + CrewAI, LangGraph, Strands, MCP, and related runtimes. +- Preserve the superclass chain `ExtendedData -> VendorData -> AgenticData`. +- Keep install guidance honest: vendor extras and agent runtime extras are + separate concerns unless an explicit passthrough extra is later added. + +## Required Direction + +- Access SecretSync through `VendorData` and `vendor-fabric` capabilities. +- Keep agent/session abstractions additive over the vendor layer. +- Let runners and tool registries decide how SecretSync becomes usable in a + given runtime without re-owning provider logic here. + +## Forbidden Drift + +- Do not import or bind directly to the SecretSync Go runtime from here. +- Do not own credential handoff, provider clients, or binding plumbing here. +- Do not document fake aggregate extras that do not actually exist. diff --git a/docs/_static/agentic-fabric.css b/docs/_static/agentic-fabric.css new file mode 100644 index 0000000..6eea12c --- /dev/null +++ b/docs/_static/agentic-fabric.css @@ -0,0 +1 @@ +/* Placeholder for project-specific docs styling. */ diff --git a/docs/agentic-workflows.rst b/docs/agentic-workflows.rst new file mode 100644 index 0000000..744dd14 --- /dev/null +++ b/docs/agentic-workflows.rst @@ -0,0 +1,117 @@ +Agentic Workflows +================= + +Fabric agent definitions are YAML-first and runtime-neutral unless placed in a +framework-specific directory. + +.. code:: yaml + + name: example + fabric_agents: + reviewer: + description: Review implementation quality + agents: config/agents.yaml + tasks: config/tasks.yaml + +``agentic-fabric`` resolves the fabric agent, selects a runner, and delegates +execution to the installed framework. The public API is intentionally +small: + +.. code:: python + + from agentic_fabric import discover_packages, get_fabric_agent_config, run_fabric_agent_auto + + packages = discover_packages() + config = get_fabric_agent_config(packages["example"], "reviewer") + result = run_fabric_agent_auto(config, inputs={"topic": "release readiness"}) + +For stateful orchestration, register the same config with +``AgenticData``: + +.. code:: python + + from agentic_fabric import AgenticData + + session = AgenticData(fabric_agents={"reviewer": config}) + session.use_runtime("langgraph", strict=False) + result = session.run_reviewer({"topic": "release readiness"}) + +When no framework is installed, discovery and metadata inspection still +work. Execution reports the unavailable runtime instead of failing +during import. + +Runtime Extras +-------------- + +The public runtime registry exposes install guidance for each optional +framework: + +.. code:: python + + from agentic_fabric import get_framework_info + + for runtime in get_framework_info(): + print(runtime["name"], runtime["available"], runtime["install"]) + +Supported runtime extras are ``langgraph`` and ``strands``. The CrewAI adapter +is lazy and can use an externally installed CrewAI runtime, but +``agentic-fabric`` does not publish a CrewAI extra while CrewAI's ChromaDB +dependency path has an upstream critical advisory with no patched version. +Install the runtime you actually execute; the package does not publish an +aggregate AI or all-frameworks extra. ``mcp`` and ``scraping`` remain focused +optional tool surfaces. Local CLI runners are configured profiles over external +executables and do not require a Python extra: + +.. code:: bash + + agentic-fabric list-runners + agentic-fabric run --runner claude-code --input "Review this package" + +Tool Resolution +--------------- + +Fabric agent tool entries are resolved lazily. Built-in filesystem tool aliases, +``mcp://filesystem/...`` aliases, and ``vendor://provider/operation`` +references do not import optional framework or vendor packages until the +tool is used. + +For Python tools, prefer registering a factory in application startup: + +.. code:: python + + from agentic_fabric.tools.registry import register_tool_factory + + register_tool_factory("MyTool", lambda: MyTool(), aliases=("my-tool",)) + +Fully qualified ``module:attribute`` and ``package.module.ClassName`` +references are allowed for ``agentic_fabric`` modules by default. External +module imports must be explicitly allowlisted: + +.. code:: bash + + export AGENTIC_FABRIC_TOOL_IMPORT_ALLOWLIST="my_company.tools,shared_agents." + +Manifest paths for ``agents``, ``tasks``, and ``knowledge`` are resolved +relative to the fabric agent config directory and cannot escape it. Filesystem +tools resolve symlinks before reading or writing and keep writes inside +their configured workspace directories. + +MCP Adapters +------------ + +The ``mcp`` extra installs the MCP transport dependency. Provider +implementation still belongs to ``vendor-fabric``; this package only owns the +runtime-visible adapter layer: + +.. code:: bash + + agentic-fabric-vendor-mcp + agentic-fabric-meshy-mcp + +``agentic-fabric-vendor-mcp`` exposes credential-free vendor catalog tools and +public ``vendor-fabric`` data methods. ``agentic-fabric-meshy-mcp`` adapts the +Meshy capability definitions from ``vendor_fabric.meshy.tools`` into MCP tool +metadata. Install the matching ``vendor-fabric`` package and provider extras +in the same environment before running provider-backed MCP tools. Startup +errors include the install guidance plus the original import failure so a +missing provider extra is distinguishable from a missing adapter dependency. diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 0000000..db162fd --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,10 @@ +API Reference +============= + +This section is generated from the published workspace import surfaces, +``agentic_fabric`` and ``pytest_agentic_fabric``, with ``sphinx-autodoc2``. + +.. toctree:: + :maxdepth: 2 + + ../apidocs/index diff --git a/docs/architecture.rst b/docs/architecture.rst new file mode 100644 index 0000000..7d575d2 --- /dev/null +++ b/docs/architecture.rst @@ -0,0 +1,142 @@ +Architecture Plan +================= + +This page records the local plan for ``agentic-fabric``. It is the +source of truth for agent runtime architecture in this repository. + +Boundary +-------- + +``agentic-fabric`` owns agent and fabric agent orchestration: + +- discovery and manifest loading +- universal fabric agent composition +- runner adapters for optional agent frameworks +- tool resolution for built-in, MCP-style, fully qualified, and + vendor-backed tools +- agent-facing tool catalogs built from ``vendor-fabric`` capability + metadata +- agent capability dispatch over the data and vendor layers +- pytest support for agent runtime tests + +``extended-data`` owns base data primitives, logging, input handling, +files, redaction, and generic workflows. ``vendor-fabric`` owns vendor +API connectors, provider-backed sync, SecretSync Python facade/capability +surfaces, provider capability metadata, and provider dispatch. + +AgenticData +----------- + +``AgenticData`` extends the vendor layer in the same way ``VendorData`` +extends ``ExtendedData``. The full-stack inheritance line is: + +.. code:: text + + ExtendedData -> VendorData -> AgenticData + +``AgenticData`` owns agent runtime context. It does not reimplement data +containers or provider sync. Those behaviors come from inherited layers +when ``vendor-fabric`` is installed, and from an import-safe fallback +otherwise. + +.. code:: python + + from agentic_fabric import AgenticData + + + session = AgenticData({"task": "summarize"}) + session.use_runtime("langgraph", strict=False) + +That shape lets the agent layer carry data, provider, and runtime context +together: + +.. code:: python + + session.register_fabric_agent("repo_summarizer", fabric_agent_config) + result = session.run_fabric_agent("repo_summarizer", repo="jbcom/extended-data") + +Agent Capability Registry +------------------------- + +Agent and runtime capabilities should use normal Python mechanisms: + +- ``typing.Protocol`` for runner and tool contracts +- abstract base classes for shared runner behavior +- ``@agent_capability(...)`` or ``@tool_capability(...)`` decorators on + concrete runtime/tool methods +- ``__init_subclass__`` to collect capability metadata through + inheritance +- read-only capability mappings for inspection and deterministic + dispatch +- ``__getattr__`` only as a convenience over declared capabilities +- ``__dir__`` for autocomplete and developer inspection + +The implementation lives in ``agentic_fabric.capabilities``: + +- ``@agent_capability(...)`` +- ``@runtime_capability(...)`` +- ``@tool_capability(...)`` +- ``AgentCapabilityProviderMixin`` + +Do not use custom dunder capability names. The capability registry is +plain Python metadata collected at class creation. + +Runtime Selection +----------------- + +Runtime frameworks are optional and should be discovered lazily. The +core package should import without CrewAI, LangGraph, Strands, or vendor +SDKs being imported eagerly. + +Runtime metadata lives in ``agentic_fabric.runners.registry``. The public +compatibility API remains in ``agentic_fabric.core.decomposer``: +``detect_framework()``, ``get_runner()``, ``get_available_frameworks()``, +and ``get_framework_info()``. + +Runtime selection follows this precedence: + +1. an explicit runtime argument from the caller +2. a runtime already active on ``AgenticData`` +3. a runtime required by the fabric agent manifest or framework-specific + config directory +4. the first installed runtime in documented priority order +5. a clear unavailable-runtime error with install guidance + +Manifest-required runtimes are also compatibility constraints. If a caller +explicitly requests ``langgraph`` for a fabric agent that requires +``crewai``, or an active runtime conflicts with that requirement, +``AgenticData`` raises a configuration error rather than silently selecting +another framework. + +Vendor Tooling +-------------- + +Vendor-backed tools should go through ``AgenticData``, ``VendorData``, +and ``vendor-fabric`` capabilities. Agent code should not call cloud SDKs +directly. Agent tools that need repository files, Slack messages, Vault +secrets, or S3 objects should route through provider capabilities +declared by ``vendor-fabric``. + +Lazy vendor tool references are supported with +``vendor://provider/operation`` and ``vendor:provider:operation`` names. +When ``vendor-fabric`` is installed, ``AgenticData.vendor_tools()`` reads +``VendorData.capabilities()`` and returns lazy agent-facing wrappers for +those provider operations. + +Testing Package +--------------- + +``pytest-agentic-fabric`` is a sibling package in this repository. It +provides runtime availability fixtures, framework mocking helpers, minimal +fabric agent/workspace fixtures, credential skip fixtures, and opt-in E2E +controls. + +Provider fixtures belong in ``pytest-vendor-fabric``. Base data fixtures +belong in ``pytest-extended-data``. + +Validation Contract +------------------- + +Every public behavior needs tests and docs in the same change. Python +3.11, 3.12, 3.13, and 3.14 must pass without skipped missing +interpreters. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..18f25e6 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,39 @@ +"""Sphinx configuration for agentic-fabric.""" + +from __future__ import annotations + + +project = "Agentic Fabric" +author = "Jon Bogaty" +copyright = "2026, Jon Bogaty" # noqa: A001 + +extensions = [ + "myst_parser", + "autodoc2", + "sphinx_copybutton", +] + +source_suffix = { + ".rst": "restructuredtext", +} + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] +html_theme = "furo" +html_static_path = ["_static"] +html_css_files = ["agentic-fabric.css"] + +autodoc2_packages = [ + { + "path": "../packages/agentic-fabric/src/agentic_fabric", + "module": "agentic_fabric", + }, + { + "path": "../packages/pytest-agentic-fabric/src/pytest_agentic_fabric", + "module": "pytest_agentic_fabric", + }, +] +autodoc2_output_dir = "apidocs" +autodoc2_render_plugin = "rst" +autodoc2_docstring_parser_regexes = [(r".*", "myst")] +autodoc2_hidden_objects = ["dunder", "private"] diff --git a/docs/development.rst b/docs/development.rst new file mode 100644 index 0000000..3d3a697 --- /dev/null +++ b/docs/development.rst @@ -0,0 +1,46 @@ +Development +=========== + +Use tox for local validation: + +.. code:: bash + + tox -e lint + tox -e typecheck + tox -e audit + tox -e py311 + tox -e py312 + tox -e py313 + tox -e py314 + tox -e coverage + tox -e plugin + tox -e examples + tox -e docs + tox -e build + +Optional framework and vendor tests should be marked and should skip +cleanly when the relevant extra is not installed. + +Use ``pytest-agentic-fabric`` fixtures for reusable runtime test setup: + +.. code:: python + + def test_runtime(agentic_fabric_mocker, agentic_fabric_agent_config): + agentic_fabric_mocker.mock_langgraph() + assert agentic_fabric_agent_config["name"] == "test_fabric_agent" + +Override ``agentic_fabric_agent_config`` when a test needs a custom +discoverable workspace. The ``agentic_workspace`` fixture writes that config +into a temporary ``packages/sample/.fabric`` tree with matching manifest, +agents, and tasks YAML files. + +Published helper fixtures include ``fabric_mocker``/``agentic_fabric_mocker``, +``mock_agentic_frameworks``, ``mock_crewai``, ``mock_langgraph``, +``mock_strands``, ``simple_agent_config``, ``simple_task_config``, +``simple_fabric_agent_config``, ``multi_agent_fabric_agent_config``, +``fabric_agent_with_knowledge``, and ``temp_fabric_dir``. Use those fixtures +instead of private test modules so downstream packages exercise the same +pytest plugin surface that is published with the workspace. + +Before deleting old monorepo code, first prove this repository owns the +moved surface with docs, tests, package metadata, and release workflows. diff --git a/docs/getting-started.rst b/docs/getting-started.rst new file mode 100644 index 0000000..f616c26 --- /dev/null +++ b/docs/getting-started.rst @@ -0,0 +1,76 @@ +Getting Started +=============== + +Install the core package when you only need discovery, configuration loading, +lazy runner selection, local CLI runner profiles, and framework-neutral file +tools: + +.. code:: bash + + pip install agentic-fabric + +Install the runtime extras you actually use: + +.. code:: bash + + pip install "agentic-fabric[langgraph]" + pip install "agentic-fabric[strands]" + pip install "agentic-fabric[mcp]" + pip install "agentic-fabric[scraping]" + +``mcp`` exposes MCP integration dependencies. ``scraping`` installs the +first-party requests/BeautifulSoup scraping helpers. Vendor passthrough extras +are deferred until ``vendor-fabric`` is published with a stable optional-extra +contract. There is no aggregate AI or all-frameworks extra. Test, typing, docs, +and dev extras are reserved for repository validation and package maintenance. + +Local CLI runners do not require a Python extra. They shell out to external +executables that you install separately: + +.. code:: bash + + agentic-fabric list-runners --json + agentic-fabric run --runner aider --input "Add validation to auth.py" + +Built-in filesystem tools also stay in the core package without depending on +CrewAI or Pydantic. Runtime adapters add framework-specific wrapping only when +their optional dependencies are installed. + +.. code:: python + + from agentic_fabric import detect_framework, get_framework_info, get_runner + + runtimes = get_framework_info() + framework = detect_framework() + runner = get_runner(framework) + +Fabric agent definitions can live in ``.fabric/``, ``.crewai/``, ``.langgraph/``, +or ``.strands/`` directories. A ``.fabric/`` directory is +framework-agnostic. A framework-specific directory requires that +runtime. + +If a required runtime is unavailable, runtime errors include the matching +install guidance. Automatic selection follows CrewAI, then LangGraph, then +Strands, but explicit runtime selection always wins unless the fabric agent is stored +in a framework-specific directory. + +Use ``AgenticData`` when runtime context should travel with data and +registered fabric agent definitions: + +.. code:: python + + from agentic_fabric import AgenticData + + session = AgenticData({"repo": "jbcom/agentic-fabric"}) + session.register_fabric_agent("reviewer", fabric_agent_config) + result = session.run_fabric_agent("reviewer", runtime="crewai") + +CrewAI Dependency Note +---------------------- + +The CrewAI adapter remains lazy and source-compatible, but +``agentic-fabric`` does not publish a CrewAI extra while current CrewAI +releases depend on ChromaDB releases covered by an upstream critical advisory +with no patched version. Install CrewAI separately only after reviewing that +advisory state. Core, local CLI, LangGraph, Strands, MCP, and first-party +scraping installs do not include CrewAI. diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..e63b8ac --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,39 @@ +Agentic Fabric +============== + +``agentic-fabric`` is the standalone orchestration package for +framework-agnostic agent fabrics. It discovers YAML fabric agent definitions, +selects an installed runtime, and runs the same fabric agent on CrewAI, +LangGraph, Strands, or local CLI runners. + +.. code:: bash + + pip install agentic-fabric + pip install "agentic-fabric[langgraph]" + pip install "agentic-fabric[strands]" + pip install "agentic-fabric[mcp]" + pip install "agentic-fabric[scraping]" + +Local CLI runners are part of the core install because they shell out to +external executables and require no third-party Python framework. LangGraph, +Strands, MCP, scraping helpers, and vendor providers are opt-in extras. CrewAI +support stays lazy but CrewAI itself is an external install while its ChromaDB +dependency path has an upstream critical advisory with no patched version. + +Vendor-backed passthrough extras are deferred until ``vendor-fabric`` is +published with a stable optional-extra contract. Until then, vendor references +stay lazy and report install guidance at use time. + +Core imports stay lightweight. Optional frameworks and vendor SDKs are +loaded only when a runner, tool, or adapter is resolved. + +.. toctree:: + :maxdepth: 2 + + getting-started + architecture + agentic-workflows + vendor-fabric + pillars + development + api/index diff --git a/docs/pillars.rst b/docs/pillars.rst new file mode 100644 index 0000000..a7370bb --- /dev/null +++ b/docs/pillars.rst @@ -0,0 +1,54 @@ +Pillars +======= + +Declare Once +------------ + +Fabric agent definitions should be portable across supported runtimes. Framework +specificity belongs in adapters and optional extras, not in the basic +fabric agent schema. + +Data And Context Move Together +------------------------------ + +``AgenticData`` should carry the current extended data value, active +provider context, active runtime context, logging context, and +tool/capability registry. It should extend ``VendorData``; it should not +duplicate ``ExtendedData`` or provider behavior. + +Lazy by Default +--------------- + +Optional frameworks, vendor SDKs, and tool packages must be imported +only when their runner or tool is used. + +Capabilities Over Boilerplate +----------------------------- + +Agent runners and tools should declare capabilities with decorators +collected by ``__init_subclass__``. Dynamic facade methods are allowed +only when they route to declared capabilities. + +Clear Boundaries +---------------- + +This package does not own data primitives or vendor APIs. It composes +``extended-data`` and ``vendor-fabric`` when those capabilities are +installed. Provider connectors and dispatch stay in ``vendor-fabric``; +framework-specific and agent-facing wrappers built from those capabilities +stay in this package. + +Testable Adapters +----------------- + +Every runner and tool adapter should have unit coverage for resolution, +availability, and failure behavior. End-to-end tests can stay optional +and marked by runtime. + +Frameworks Are Optional, Contracts Are Not +------------------------------------------ + +CrewAI, LangGraph, Strands, and local CLI runners can be optional +installs. Their availability, missing dependency messages, fixture +behavior, and runtime selection order must still be documented and +tested. diff --git a/docs/vendor-fabric.rst b/docs/vendor-fabric.rst new file mode 100644 index 0000000..01eb114 --- /dev/null +++ b/docs/vendor-fabric.rst @@ -0,0 +1,54 @@ +Vendor Fabric Integration +========================= + +``agentic-fabric`` does not declare passthrough ``vendor-fabric`` extras +until ``vendor-fabric`` is published and resolvable from PyPI. The core +package still imports without vendor SDKs installed. + +The integration rule is simple: vendor IO, provider capability metadata, +and the SecretSync Python facade/capability surface belong in +``vendor-fabric``. The SecretSync Go runtime and gopy binding source +belong in ``secrets-sync``. Fabric agent orchestration, runtime selection, and +agent-facing framework wrappers belong here. + +``AgenticData`` subclasses ``VendorData`` when ``vendor-fabric`` is +installed. Until then, it keeps runtime context importable and raises +clear guidance for vendor-backed operations. + +Vendor-backed tools use lazy references: + +.. code:: python + + from agentic_fabric.tools.registry import resolve_tool + + tool = resolve_tool("vendor://github/get_file") + result = tool(path="README.md") + +Those wrappers route through ``AgenticData.call`` and ``VendorData`` +capabilities. Agent code should not import cloud SDKs or provider +clients directly. + +When ``vendor-fabric`` is installed, downstream agent/session classes can +also expose a provider catalog from the superclass: + +.. code:: python + + from agentic_fabric import AgenticData + + + session = AgenticData() + github_tools = session.vendor_tools("github", include_unavailable=False) + +``vendor_tools()`` reads ``VendorData.capabilities()`` and converts each +capability route into a lazy ``VendorCapabilityTool``. The provider +connectors, install availability, and dispatch behavior still come from +``vendor-fabric``. + +MCP ownership follows the same boundary. ``agentic-fabric`` owns the +``agentic-fabric-vendor-mcp`` and ``agentic-fabric-meshy-mcp`` adapter entry +points because MCP is an agent runtime transport. The underlying connector +classes, provider methods, Meshy capability metadata, credentials, and network +calls remain in ``vendor-fabric`` and are imported lazily only when the MCP +server is created. If those lazy imports fail, the adapter reports the +``agentic-fabric[mcp]`` or ``vendor-fabric[...]`` install guidance and preserves +the original import failure for provider-extra debugging. diff --git a/packages/agentic-fabric/CHANGELOG.md b/packages/agentic-fabric/CHANGELOG.md new file mode 100644 index 0000000..3e40bc1 --- /dev/null +++ b/packages/agentic-fabric/CHANGELOG.md @@ -0,0 +1,40 @@ +# CHANGELOG + + + +## v1.0.0 (2025-12-25) + +### Initial Stable Release + +Framework-agnostic AI fabric agent orchestration is now production-ready! + +### Features + +- **Multi-Framework Support**: Run fabric agents on CrewAI, LangGraph, or Strands +- **Auto-Detection**: Automatically selects best available framework +- **Universal YAML Format**: Define once, run anywhere +- **Single-Agent CLI Runners**: Support for aider, claude-code, ollama, and more +- **MCP Adapters**: Expose vendor-fabric catalog/data methods and Meshy capability metadata over MCP +- **Knowledge Base Integration**: Load domain knowledge from markdown files +- **Agent Archetypes**: Reusable agent templates +- **Comprehensive Testing**: 122+ unit tests, E2E test suite + +### Breaking Changes + +- **Minimum Python version increased from 3.10 to 3.11** + - Required for CrewAI 1.5.0+ compatibility + - Python 3.11, 3.12, 3.13, 3.14 now supported + +### Documentation + +- Complete API documentation with Sphinx +- Quick start guide and architecture overview +- jbcom dark theme branding applied +- Integration guides for vendor-fabric + +### Internal + +- Full test coverage across all runners +- Ruff linting and formatting +- Type hints with mypy +- Tox for multi-version testing diff --git a/packages/agentic-fabric/README.md b/packages/agentic-fabric/README.md new file mode 100644 index 0000000..e3f12d1 --- /dev/null +++ b/packages/agentic-fabric/README.md @@ -0,0 +1,224 @@ +# agentic-fabric + +[![PyPI version](https://img.shields.io/pypi/v/agentic-fabric.svg)](https://pypi.org/project/agentic-fabric/) +[![CI](https://github.com/jbcom/agentic-fabric/actions/workflows/ci.yml/badge.svg)](https://github.com/jbcom/agentic-fabric/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Python](https://img.shields.io/pypi/pyversions/agentic-fabric.svg)](https://pypi.org/project/agentic-fabric/) + +Framework-agnostic agent fabric orchestration. Declare fabric agents once in YAML, then +run them on CrewAI, LangGraph, Strands, or local CLI runners without changing a +single fabric agent definition. Runtime frameworks are optional and are detected lazily +from what is installed. + +[Documentation](https://jonbogaty.com/agentic-fabric/) | [Source](https://github.com/jbcom/agentic-fabric) | [Issues](https://github.com/jbcom/agentic-fabric/issues) + +## Installation + +```bash +# Core discovery, loading, runner selection, local CLI, and neutral file tools +pip install agentic-fabric + +# With a specific framework +pip install "agentic-fabric[langgraph]" +pip install "agentic-fabric[strands]" + +# Non-framework optional surfaces +pip install "agentic-fabric[mcp]" +pip install "agentic-fabric[scraping]" +``` + +Local CLI runners do not require a Python extra. Install the external CLI +(`aider`, `claude`, `codex`, `ollama`, or a custom executable) and inspect +profiles with `agentic-fabric list-runners --json`. + +Vendor-backed passthrough extras will be added after the upstream +`vendor-fabric` optional-extra contract is published and stable. Until then, +vendor references stay lazy and report install guidance at use time. + +There is no aggregate AI extra. Install exactly the framework or provider path +you use. The CrewAI adapter remains lazy, but `agentic-fabric` does not publish +a CrewAI extra while CrewAI depends on ChromaDB releases covered by an upstream +critical advisory with no patched version. Install CrewAI separately only after +reviewing that advisory state. Core, local-CLI, and first-party scraping +installs are unaffected. + +## Quick Start + +### 1. Define a Fabric Agent (YAML) + +```yaml +# .fabric/fabric_agents/analyzer/agents.yaml +code_reviewer: + role: Senior Code Reviewer + goal: Find bugs and improvements + backstory: Expert at code analysis +``` + +```yaml +# .fabric/fabric_agents/analyzer/tasks.yaml +review_code: + description: Review the provided code for issues + expected_output: List of findings with severity + agent: code_reviewer +``` + +### 2. Run It + +```python +from pathlib import Path + +from agentic_fabric import detect_framework, get_fabric_agent_config, run_fabric_agent_auto + +# See what framework is available +framework = detect_framework() + +# Load a fabric agent manifest discovered in a package or workspace +config = get_fabric_agent_config(Path(".fabric"), "analyzer") + +# Auto-detect best framework and run +result = run_fabric_agent_auto(config, inputs={"code": "..."}) +``` + +Or from the CLI: + +```bash +agentic-fabric run my-package analyzer --input "Review this code: ..." +``` + +### 3. Use a Specific Runner + +```python +from agentic_fabric import get_runner + +runner = get_runner("langgraph") # Force LangGraph +fabric_agent = runner.build_fabric_agent(config) +result = runner.run(fabric_agent, inputs) +``` + +### 4. Carry Runtime Context with Data + +```python +from agentic_fabric import AgenticData, get_framework_info + +print(get_framework_info()) + +session = AgenticData({"repo": "jbcom/agentic-fabric"}) +session.register_fabric_agent("reviewer", config) +result = session.run_fabric_agent("reviewer", runtime="crewai") +``` + +## Key Features + +- Framework agnostic: one fabric agent definition, multiple runtime backends. +- Lazy imports: core package import does not require CrewAI, LangGraph, + Strands, or vendor SDKs. +- Framework-neutral file tools: built-in filesystem tools can be resolved + without installing CrewAI or Pydantic; framework adapters add schema wrappers + only when their optional dependencies are present. +- Focused extras: `langgraph`, `strands`, `mcp`, `scraping`, + `tests`, `typing`, `docs`, and `dev`. +- `AgenticData`: carries data, registered fabric agents, active runtime selection, and + vendor-layer context together. +- Capability decorators: runners and tools expose declared capabilities through + read-only metadata and deterministic dispatch. +- Tool resolution: built-in, vendor URI, and registered factories are preferred; + external dynamic imports require `AGENTIC_FABRIC_TOOL_IMPORT_ALLOWLIST`. +- Vendor tool catalogs: `AgenticData.vendor_tools()` adapts inherited + `VendorData` capability metadata into agent-facing tools without importing + provider SDKs directly. +- YAML-first: fabric agent configuration in YAML, not Python boilerplate. +- Hierarchical orchestration: `ManagerAgent` delegates across fabric agents. +- Package discovery: finds `.fabric/`, `.crewai/`, `.langgraph/`, and + `.strands/` directories. +- Vendor passthrough extras are deferred until `vendor-fabric` is published + with a stable optional-extra contract. +- CLI and library: use from the command line or import as a module. + +## Framework Priority + +1. CrewAI, if installed. +2. LangGraph, if CrewAI is unavailable. +3. Strands, if neither CrewAI nor LangGraph is available. + +You can always force a specific runner with `get_runner("langgraph")` or +`agentic-fabric run --framework langgraph`. + +If the selected runtime is not installed, errors point to the matching +`agentic-fabric[...]` extra. Framework-specific config directories also enforce +their runtime: a fabric agent in `.langgraph/` will not silently run on CrewAI. + +## Local CLI Runners + +For single-agent coding tools, use the `--runner` CLI path: + +```bash +agentic-fabric list-runners --json +agentic-fabric run --runner aider --input "Add validation to auth.py" +agentic-fabric run --runner ollama --model deepseek-coder --input "Explain this module" +``` + +Profiles are loaded from the packaged `local_cli_profiles.yaml`, validated +before use, and rejected on POSIX systems if the profiles file is group- or +world-writable. + +## MCP Adapters + +The `mcp` extra installs the MCP transport dependency and enables two console +entry points: + +```bash +agentic-fabric-vendor-mcp +agentic-fabric-meshy-mcp +``` + +`agentic-fabric-vendor-mcp` exposes credential-free vendor catalog tools and +public `vendor-fabric` data methods. `agentic-fabric-meshy-mcp` converts +Meshy capability metadata from `vendor-fabric[meshy]` into MCP tools. Both +servers import provider code lazily; install the matching `vendor-fabric` +package/extras in the same environment before running provider-backed tools. +If provider startup fails, the adapter error includes the `agentic-fabric[mcp]` +or `vendor-fabric[...]` install guidance plus the original import failure so +missing provider extras are visible. + +## Repository Boundary + +- `extended-data` owns base data containers, logging, input handling, files, + redaction, and generic workflows. +- `vendor-fabric` owns vendor connectors, provider-backed sync, the SecretSync + Python facade/capability surfaces, provider capability metadata, and provider + dispatch. +- `agentic-fabric` owns fabric agent discovery, runner selection, framework adapters, + agent-facing tool wrappers, and orchestration. + +Full guides and API documentation are published at +[jonbogaty.com/agentic-fabric](https://jonbogaty.com/agentic-fabric/). +`AGENTS.md` contains durable repository guidance for Codex sessions. + +## Documentation + +The docs are built with Sphinx, Furo, and sphinx-autodoc2: + +```bash +tox -e docs +``` + +Local validation: + +```bash +uv sync --all-packages --all-extras --dev +tox -e lint +tox -e typecheck +tox -e audit +tox -e py311 +tox -e py312 +tox -e py313 +tox -e py314 +tox -e coverage +tox -e plugin +tox -e examples +tox -e build +``` + +## License + +MIT diff --git a/packages/agentic-fabric/examples/README.md b/packages/agentic-fabric/examples/README.md new file mode 100644 index 0000000..75d9408 --- /dev/null +++ b/packages/agentic-fabric/examples/README.md @@ -0,0 +1,18 @@ +# Examples + +These examples are intentionally executable in CI. They avoid live LLM calls +and optional framework imports unless explicitly noted, so they also serve as +integration checks for the public API. + +```bash +python examples/discovery_workflow.py +python examples/tool_registry.py +python examples/runtime_context.py +python examples/mcp_adapters.py +``` + +The bundled `sample_workspace/` directory demonstrates a framework-agnostic +`.fabric/` layout. + +`mcp_adapters.py` shows the agentic-fabric-owned MCP entry points and a +client configuration shape without importing the MCP SDK or provider packages. diff --git a/packages/agentic-fabric/examples/__init__.py b/packages/agentic-fabric/examples/__init__.py new file mode 100644 index 0000000..288b1ae --- /dev/null +++ b/packages/agentic-fabric/examples/__init__.py @@ -0,0 +1 @@ +"""Runnable agentic-fabric examples.""" diff --git a/packages/agentic-fabric/examples/discovery_workflow.py b/packages/agentic-fabric/examples/discovery_workflow.py new file mode 100644 index 0000000..eaae24a --- /dev/null +++ b/packages/agentic-fabric/examples/discovery_workflow.py @@ -0,0 +1,49 @@ +"""Discover and inspect a framework-agnostic fabric workspace.""" + +from __future__ import annotations + +import json +import sys + +from pathlib import Path +from typing import Any + +from agentic_fabric.core.discovery import discover_packages, get_fabric_agent_config, load_manifest + + +DEFAULT_WORKSPACE = Path(__file__).parent / "sample_workspace" + + +def summarize_workspace(workspace_root: Path = DEFAULT_WORKSPACE) -> dict[str, Any]: + """Return a deterministic summary of fabric agents in a workspace.""" + packages = discover_packages(workspace_root=workspace_root) + summary: dict[str, Any] = {"workspace": str(workspace_root), "packages": {}} + + for package_name, config_dir in sorted(packages.items()): + manifest = load_manifest(config_dir) + fabric_agent_summaries = {} + for fabric_agent_name in sorted(manifest.get("fabric_agents", {})): + fabric_agent_config = get_fabric_agent_config(config_dir, fabric_agent_name) + fabric_agent_summaries[fabric_agent_name] = { + "description": fabric_agent_config.get("description", ""), + "required_framework": fabric_agent_config.get("required_framework"), + "agents": sorted(fabric_agent_config.get("agents", {})), + "tasks": sorted(fabric_agent_config.get("tasks", {})), + } + + summary["packages"][package_name] = { + "config_dir": config_dir.name, + "fabric_agents": fabric_agent_summaries, + } + + return summary + + +def main() -> None: + """Print the sample workspace summary as JSON.""" + sys.stdout.write(json.dumps(summarize_workspace(), indent=2, sort_keys=True)) + sys.stdout.write("\n") + + +if __name__ == "__main__": + main() diff --git a/packages/agentic-fabric/examples/mcp_adapters.py b/packages/agentic-fabric/examples/mcp_adapters.py new file mode 100644 index 0000000..8a428e8 --- /dev/null +++ b/packages/agentic-fabric/examples/mcp_adapters.py @@ -0,0 +1,45 @@ +"""Inspect MCP adapter entry points without importing optional providers.""" + +from __future__ import annotations + +import json + +from typing import Any + +from agentic_fabric.tools import meshy_mcp, vendor_mcp + + +def inspect_mcp_adapters() -> dict[str, Any]: + """Return deterministic MCP adapter configuration examples.""" + return { + "extra": "mcp", + "entry_points": { + "vendor": "agentic-fabric-vendor-mcp", + "meshy": "agentic-fabric-meshy-mcp", + }, + "install_guidance": { + "mcp": vendor_mcp.MCP_INSTALL_MESSAGE, + "vendor": vendor_mcp.VENDOR_INSTALL_MESSAGE, + "meshy": meshy_mcp.VENDOR_INSTALL_MESSAGE, + }, + "client_config": { + "mcpServers": { + "vendor-fabric": { + "command": "agentic-fabric-vendor-mcp", + }, + "meshy": { + "command": "agentic-fabric-meshy-mcp", + "env": {"MESHY_API_KEY": ""}, + }, + } + }, + } + + +def main() -> None: + """Print MCP adapter metadata as JSON.""" + print(json.dumps(inspect_mcp_adapters(), indent=2, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/packages/agentic-fabric/examples/runtime_context.py b/packages/agentic-fabric/examples/runtime_context.py new file mode 100644 index 0000000..1b4b85a --- /dev/null +++ b/packages/agentic-fabric/examples/runtime_context.py @@ -0,0 +1,27 @@ +"""Inspect AgenticData runtime context without requiring optional frameworks.""" + +from __future__ import annotations + +import json + +from typing import Any + +from agentic_fabric import AgenticData, get_framework_info + + +def inspect_runtime_context() -> dict[str, Any]: + """Return a serializable summary of runtime context behavior.""" + data = AgenticData({"task": "review"}) + data.register_fabric_agent("reviewer", {"name": "reviewer", "agents": {}, "tasks": {}}) + data.use_runtime("crewai", strict=False) + + return { + "active_runtime": data.active_runtime, + "registered_fabric_agents": sorted(data.fabric_agents), + "runtime_names": [item["name"] for item in get_framework_info()], + "vendor_fabric_available": data.vendor_fabric_available, + } + + +if __name__ == "__main__": + print(json.dumps(inspect_runtime_context(), indent=2)) diff --git a/packages/agentic-fabric/examples/sample_workspace/packages/review/.fabric/fabric_agents/implementation_review/agents.yaml b/packages/agentic-fabric/examples/sample_workspace/packages/review/.fabric/fabric_agents/implementation_review/agents.yaml new file mode 100644 index 0000000..2314467 --- /dev/null +++ b/packages/agentic-fabric/examples/sample_workspace/packages/review/.fabric/fabric_agents/implementation_review/agents.yaml @@ -0,0 +1,9 @@ +reviewer: + role: Senior Python reviewer + goal: Identify correctness, packaging, and migration risks. + backstory: Experienced Python library maintainer focused on testable APIs. + +documenter: + role: Documentation reviewer + goal: Ensure examples, README content, and API docs match implementation behavior. + backstory: Maintains developer-facing documentation for reusable Python packages. diff --git a/packages/agentic-fabric/examples/sample_workspace/packages/review/.fabric/fabric_agents/implementation_review/tasks.yaml b/packages/agentic-fabric/examples/sample_workspace/packages/review/.fabric/fabric_agents/implementation_review/tasks.yaml new file mode 100644 index 0000000..ef8d002 --- /dev/null +++ b/packages/agentic-fabric/examples/sample_workspace/packages/review/.fabric/fabric_agents/implementation_review/tasks.yaml @@ -0,0 +1,9 @@ +review_code: + description: Review the submitted implementation for defects and migration gaps. + expected_output: Findings with severity, file references, and suggested fixes. + agent: reviewer + +review_docs: + description: Confirm documentation and examples describe the implementation accurately. + expected_output: Documentation alignment notes and missing examples. + agent: documenter diff --git a/packages/agentic-fabric/examples/sample_workspace/packages/review/.fabric/manifest.yaml b/packages/agentic-fabric/examples/sample_workspace/packages/review/.fabric/manifest.yaml new file mode 100644 index 0000000..6ae11d3 --- /dev/null +++ b/packages/agentic-fabric/examples/sample_workspace/packages/review/.fabric/manifest.yaml @@ -0,0 +1,7 @@ +name: review +description: Framework-agnostic review fabric_agent example. +fabric_agents: + implementation_review: + description: Review an implementation for correctness and migration readiness. + agents: fabric_agents/implementation_review/agents.yaml + tasks: fabric_agents/implementation_review/tasks.yaml diff --git a/packages/agentic-fabric/examples/tool_registry.py b/packages/agentic-fabric/examples/tool_registry.py new file mode 100644 index 0000000..a4521c4 --- /dev/null +++ b/packages/agentic-fabric/examples/tool_registry.py @@ -0,0 +1,29 @@ +"""Resolve tools without importing optional framework packages.""" + +from __future__ import annotations + +import json + +from typing import Any + +from agentic_fabric.tools.registry import resolve_tool + + +def inspect_registry() -> dict[str, Any]: + """Return deterministic registry resolution examples.""" + framework_dirs = resolve_tool("agentic_fabric.core.discovery:FRAMEWORK_DIRS") + unresolved_mcp = resolve_tool("mcp://git/execute_command") + + return { + "framework_dirs": framework_dirs, + "mcp_git_available": unresolved_mcp is not None, + } + + +def main() -> None: + """Print registry inspection as JSON.""" + print(json.dumps(inspect_registry(), indent=2, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/packages/agentic-fabric/pyproject.toml b/packages/agentic-fabric/pyproject.toml new file mode 100644 index 0000000..c46b787 --- /dev/null +++ b/packages/agentic-fabric/pyproject.toml @@ -0,0 +1,183 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "agentic-fabric" +version = "1.0.0" +description = "Framework-agnostic agent fabric orchestration for CrewAI, LangGraph, Strands, local CLI runners, and vendor-fabric tools" +requires-python = ">=3.11" +license = { text = "MIT" } +readme = "README.md" +authors = [{ name = "Jon Bogaty", email = "jon@jonbogaty.com" }] +maintainers = [{ name = "Jon Bogaty", email = "jon@jonbogaty.com" }] +keywords = ["crewai", "langgraph", "strands", "ai-agents", "multi-agent", "orchestration", "vendor-fabric"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Typing :: Typed", +] +dependencies = [ + "pyyaml>=6.0.3", +] + +[project.optional-dependencies] +langgraph = ["langgraph>=0.2.0", "langchain-anthropic>=0.3.0"] +strands = ["strands-agents>=0.1.0"] +mcp = ["mcp>=1.0.0"] +scraping = ["beautifulsoup4>=4.12.0", "requests>=2.31.0"] +tests = [ + "coverage[toml]>=7.14.3", + "pytest-agentic-fabric[tests]", + "pytest-asyncio>=1.4.0", + "pytest-cov>=7.1.0", + "pytest-mock>=3.15.1", + "pytest>=9.1.1", + "pytest-timeout>=2.4.0", + "pytest-xdist>=3.8.0", +] +typing = [ + "mypy>=2.1.0", + "types-PyYAML>=6.0.12.20260518", + "types-requests>=2.33.0.20260518", +] +dev = [ + "agentic-fabric[tests,typing]", + "ruff>=0.15.20", +] +docs = [ + "furo>=2025.12.19", + "myst-parser>=5.1.0,<6.0.0", + "sphinx>=8.2.3,<9.0.0", + "sphinx-autodoc2>=0.5.0", + "sphinx-copybutton>=0.5.2", +] + +[project.scripts] +agentic-fabric = "agentic_fabric.main:main" +agentic-fabric-meshy-mcp = "agentic_fabric.tools.meshy_mcp:main" +agentic-fabric-vendor-mcp = "agentic_fabric.tools.vendor_mcp:main" + +[project.urls] +Documentation = "https://jonbogaty.com/agentic-fabric/" +Issues = "https://github.com/jbcom/agentic-fabric/issues" +Source = "https://github.com/jbcom/agentic-fabric" +Changelog = "https://github.com/jbcom/agentic-fabric/blob/main/CHANGELOG.md" + +[tool.hatch.build.targets.wheel] +packages = ["src/agentic_fabric"] + +[tool.pytest.ini_options] +addopts = ["-ra", "--strict-markers", "--strict-config"] +testpaths = ["tests"] +xfail_strict = true +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +timeout = 30 +timeout_method = "thread" +markers = [ + "crewai: tests that require CrewAI runtime dependencies", + "e2e: end-to-end tests that require external runtime dependencies", + "langgraph: tests that require LangGraph runtime dependencies", + "strands: tests that require Strands runtime dependencies", + "vendor_fabric: tests that require vendor-fabric runtime dependencies", +] + +[tool.coverage.run] +branch = true +parallel = true +source = ["agentic_fabric"] + +[tool.coverage.report] +show_missing = true +skip_covered = true + +[tool.ruff] +target-version = "py311" +line-length = 120 +src = ["src", "tests", "examples"] + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + "ANN", + "ARG", + "BLE001", + "C901", + "COM", + "D105", + "D205", + "D415", + "D417", + "DTZ007", + "E402", + "E501", + "EM", + "ERA001", + "FBT", + "FIX", + "G", + "PERF", + "PLC0415", + "PLR0124", + "PLR0911", + "PLR0912", + "PLR0913", + "PLR0915", + "PLR2004", + "PLW0603", + "PTH", + "RET505", + "RUF012", + "S105", + "S110", + "S310", + "S603", + "SLF001", + "TCH", + "TD", + "TID252", + "TRY003", + "TRY300", + "TRY301", + "TRY401", +] + +[tool.ruff.lint.per-file-ignores] +"examples/**" = ["T201"] +"tests/**" = ["B018", "BLE", "D", "DTZ", "INP001", "LOG001", "N802", "N803", "N806", "PLC0415", "PT", "S", "SIM117", "TRY"] +"src/agentic_fabric/main.py" = ["T201"] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.isort] +lines-between-types = 1 +lines-after-imports = 2 + +[tool.mypy] +python_version = "3.11" +pretty = true +show_error_codes = true +ignore_missing_imports = true +warn_return_any = false +warn_unused_configs = true +exclude = ["tests/e2e/.*", "tests/.*/conftest.py"] + +[[tool.mypy.overrides]] +module = [ + "bs4", + "crewai", + "crewai.*", + "langchain_anthropic", + "langgraph", + "langgraph.*", + "strands", +] +ignore_missing_imports = true diff --git a/packages/agentic-fabric/src/agentic_fabric/__init__.py b/packages/agentic-fabric/src/agentic_fabric/__init__.py new file mode 100644 index 0000000..d0a9a5b --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/__init__.py @@ -0,0 +1,90 @@ +"""agentic-fabric: Framework-agnostic AI fabric orchestration. + +Declare fabric agents once, run on CrewAI, LangGraph, or Strands. + +Usage: + from agentic_fabric.core.decomposer import run_fabric_agent_auto, get_runner, detect_framework + from agentic_fabric.core.discovery import discover_packages, get_fabric_agent_config + from agentic_fabric.core.manager import ManagerAgent + + # Auto-detect framework and run a fabric agent + packages = discover_packages() + config = get_fabric_agent_config(packages["my-package"], "reviewer") + result = run_fabric_agent_auto(config, inputs={"task": "..."}) + + # Or get a specific runner + runner = get_runner("crewai") # or "langgraph", "strands" + fabric_agent = runner.build_fabric_agent(config) + result = runner.run(fabric_agent, inputs) + + # Or use a hierarchical manager agent + class MyManager(ManagerAgent): + def __init__(self): + super().__init__(fabric_agents={ + "design": "design_review", + "implementation": "implementation_review" + }) + + async def execute_workflow(self, task): + design = await self.delegate_async("design", task) + return await self.delegate_async("implementation", design) +""" + +from __future__ import annotations + +from importlib.metadata import PackageNotFoundError, version + + +try: + __version__ = version("agentic-fabric") +except PackageNotFoundError: + __version__ = "0.0.0" + +# Core exports - framework-agnostic functionality +from agentic_fabric.agentic_data import AgenticData +from agentic_fabric.capabilities import ( + AgentCapabilityProviderMixin, + AgentCapabilitySpec, + agent_capability, + runtime_capability, + tool_capability, +) +from agentic_fabric.core.decomposer import ( + compose_fabric_agent, + detect_framework, + get_available_frameworks, + get_framework_info, + get_runner, + is_framework_available, + run_fabric_agent_auto, +) +from agentic_fabric.core.discovery import ( + discover_all_framework_configs, + discover_packages, + get_fabric_agent_config, + list_fabric_agents, +) +from agentic_fabric.core.manager import ManagerAgent + + +__all__ = [ + "AgentCapabilityProviderMixin", + "AgentCapabilitySpec", + "AgenticData", + "ManagerAgent", + "__version__", + "agent_capability", + "compose_fabric_agent", + "detect_framework", + "discover_all_framework_configs", + "discover_packages", + "get_available_frameworks", + "get_fabric_agent_config", + "get_framework_info", + "get_runner", + "is_framework_available", + "list_fabric_agents", + "run_fabric_agent_auto", + "runtime_capability", + "tool_capability", +] diff --git a/packages/agentic-fabric/src/agentic_fabric/__main__.py b/packages/agentic-fabric/src/agentic_fabric/__main__.py new file mode 100644 index 0000000..a6b8c51 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/__main__.py @@ -0,0 +1,9 @@ +"""Allow running as: python -m agentic_fabric.""" + +from __future__ import annotations + +from agentic_fabric.main import main + + +if __name__ == "__main__": + main() diff --git a/packages/agentic-fabric/src/agentic_fabric/agentic_data.py b/packages/agentic-fabric/src/agentic_fabric/agentic_data.py new file mode 100644 index 0000000..c76c658 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/agentic_data.py @@ -0,0 +1,265 @@ +"""Agent runtime facade over the vendor and data layers.""" + +from __future__ import annotations + +from collections.abc import Mapping +from types import MappingProxyType +from typing import Any, ClassVar + +from agentic_fabric.runners.registry import RuntimeUnavailableError, install_command, runtime_info, runtime_names + + +try: # pragma: no cover - exercised when vendor-fabric is installed by consumers + from vendor_fabric.vendor_data import VendorData as _VendorDataBase +except ImportError: # pragma: no cover - default in this workspace until vendor-fabric is published + _VENDOR_FABRIC_AVAILABLE = False + + class _VendorDataBase: # type: ignore[no-redef] + """Small fallback that keeps AgenticData importable without vendor-fabric.""" + + def __init__(self, value: Any = None, **_: Any) -> None: + self._agentic_value = value + self._active_provider: str | None = None + + @property + def value(self) -> Any: + """Return the wrapped value.""" + return self._agentic_value + + @property + def active_provider(self) -> str | None: + """Return the active vendor provider, when available.""" + return self._active_provider + + def as_builtin(self) -> Any: + """Return the wrapped value unchanged.""" + return self._agentic_value + + def cast(self, value: Any) -> _VendorDataBase: + """Replace the wrapped value.""" + self._agentic_value = value + return self + + def open(self, provider_id: str, *, strict: bool = True, **_: Any) -> _VendorDataBase: + """Record a provider or raise install guidance when strict.""" + if strict: + msg = ( + f"Provider '{provider_id}' requires vendor-fabric. " + "Install vendor-fabric after it is published, then reinstall agentic-fabric." + ) + raise ImportError(msg) + self._active_provider = provider_id + return self + + def call(self, operation: str, *_: Any, **__: Any) -> Any: + """Raise clear guidance for vendor-backed operations.""" + msg = f"Vendor operation '{operation}' requires vendor-fabric." + raise ImportError(msg) + +else: + _VENDOR_FABRIC_AVAILABLE = True + + +class AgenticData(_VendorDataBase): + """VendorData extension with active runtime and fabric agent registry context.""" + + runtime_priority: ClassVar[tuple[str, ...]] = tuple(runtime_names()) + + def __init__( + self, + value: Any = None, + *, + fabric: Any | None = None, + fabric_agents: Mapping[str, Mapping[str, Any]] | None = None, + logger: Any | None = None, + active_runtime: str | None = None, + **fabric_kwargs: Any, + ) -> None: + """Initialize data, registered fabric agents, and optional active runtime.""" + super().__init__(value, fabric=fabric, logger=logger, **fabric_kwargs) + self._fabric_agents: dict[str, dict[str, Any]] = { + name: dict(config) for name, config in (fabric_agents or {}).items() + } + self._active_runtime: str | None = None + if active_runtime is not None: + self.use_runtime(active_runtime, strict=False) + + @property + def fabric_agents(self) -> Mapping[str, Mapping[str, Any]]: + """Return registered fabric agent definitions.""" + return MappingProxyType(self._fabric_agents) + + @property + def active_runtime(self) -> str | None: + """Return the active runtime name, when one has been selected.""" + return self._active_runtime + + @property + def vendor_fabric_available(self) -> bool: + """Return whether this import is backed by a real VendorData class.""" + return _VENDOR_FABRIC_AVAILABLE + + def cast(self, value: Any) -> AgenticData: + """Mutate the wrapped data while preserving runtime context.""" + super().cast(value) + return self + + def register_fabric_agent(self, name: str, fabric_agent_config: Mapping[str, Any]) -> AgenticData: + """Register a named fabric agent config for ``run_fabric_agent`` lookup.""" + self._fabric_agents[name] = dict(fabric_agent_config) + return self + + def unregister_fabric_agent(self, name: str) -> AgenticData: + """Remove a named fabric agent config if it exists.""" + self._fabric_agents.pop(name, None) + return self + + def use_runtime(self, runtime: str, *, strict: bool = True) -> AgenticData: + """Select the active runtime for future agent calls.""" + normalized = runtime.strip().lower() + if normalized == "auto": + normalized = self.select_runtime() + if strict and not self.is_runtime_available(normalized): + raise RuntimeUnavailableError(normalized, install_command(normalized)) + self._active_runtime = normalized + return self + + def clear_runtime(self) -> AgenticData: + """Clear the active runtime selection.""" + self._active_runtime = None + return self + + def runtimes(self) -> list[dict[str, Any]]: + """Return runtime registry metadata with availability.""" + info = runtime_info() + return list(info) if isinstance(info, list) else [info] + + def runtime_info(self, runtime: str | None = None) -> list[dict[str, Any]] | dict[str, Any]: + """Return runtime metadata with current availability.""" + return runtime_info(runtime) + + def is_runtime_available(self, runtime: str) -> bool: + """Return whether a runtime is importable in the current environment.""" + from agentic_fabric.core.decomposer import is_framework_available + + return is_framework_available(runtime) + + def select_runtime( + self, + runtime: str | None = None, + *, + fabric_agent_config: Mapping[str, Any] | None = None, + ) -> str: + """Select a runtime using explicit, active, manifest, then auto priority.""" + required_runtime = _manifest_runtime(fabric_agent_config) + requested = _normalize_runtime(runtime) + active = _normalize_runtime(self._active_runtime) + + if requested is not None and required_runtime is not None and requested != required_runtime: + msg = f"Fabric agent requires {required_runtime} but {requested} was requested" + raise ValueError(msg) + + if requested is not None: + return _require_available(requested) + + if active is not None: + if required_runtime is not None and active != required_runtime: + msg = f"Active runtime {active} conflicts with fabric agent requirement {required_runtime}" + raise ValueError(msg) + return _require_available(active) + + if required_runtime is not None: + return _require_available(required_runtime) + + from agentic_fabric.core.decomposer import detect_framework + + return detect_framework() + + def run_fabric_agent( + self, + fabric_agent: str | Mapping[str, Any], + inputs: Mapping[str, Any] | None = None, + *, + runtime: str | None = None, + **input_kwargs: Any, + ) -> str: + """Run a registered fabric agent by name or a direct fabric agent config.""" + from agentic_fabric.core.decomposer import run_fabric_agent_auto + + fabric_agent_config = self._lookup_fabric_agent(fabric_agent) + merged_inputs = dict(inputs or {}) + merged_inputs.update(input_kwargs) + selected_runtime = self.select_runtime(runtime, fabric_agent_config=fabric_agent_config) + return run_fabric_agent_auto(fabric_agent_config, inputs=merged_inputs, framework=selected_runtime) + + def call_runtime(self, capability: str, *args: Any, runtime: str | None = None, **kwargs: Any) -> Any: + """Call a declared capability on a selected runtime runner.""" + from agentic_fabric.core.decomposer import get_runner + + selected_runtime = self.select_runtime(runtime) + runner = get_runner(selected_runtime) + return runner.call_capability(capability, *args, **kwargs) + + def vendor_tools( + self, + provider: str | None = None, + *, + include_unavailable: bool = True, + ) -> list[Any]: + """Return agent-facing tools built from inherited vendor capabilities.""" + from agentic_fabric.tools.vendor import vendor_capability_tools + + return vendor_capability_tools(self, provider=provider, include_unavailable=include_unavailable) + + def __getattr__(self, name: str) -> Any: + """Expose ``run_`` helpers while preserving VendorData dispatch.""" + if name.startswith("run_"): + fabric_agent_name = name.removeprefix("run_") + if fabric_agent_name in self._fabric_agents: + return lambda *args, **kwargs: self.run_fabric_agent(fabric_agent_name, *args, **kwargs) + + try: + return super().__getattr__(name) # type: ignore[misc] + except AttributeError: + raise AttributeError(f"{type(self).__name__!s} has no attribute {name!r}") from None + + def __dir__(self) -> list[str]: + """Include dynamic registered-fabric-agent helpers in introspection.""" + dynamic = [f"run_{name}" for name in self._fabric_agents] + return sorted({*super().__dir__(), *dynamic}) + + def _lookup_fabric_agent(self, fabric_agent: str | Mapping[str, Any]) -> dict[str, Any]: + """Return a fabric agent config from a name or direct mapping.""" + if isinstance(fabric_agent, Mapping): + return dict(fabric_agent) + try: + return dict(self._fabric_agents[fabric_agent]) + except KeyError as exc: + options = ", ".join(sorted(self._fabric_agents)) or "none registered" + msg = f"Unknown fabric agent '{fabric_agent}'. Registered fabric agents: {options}" + raise KeyError(msg) from exc + + +def _normalize_runtime(runtime: str | None) -> str | None: + """Normalize runtime names and treat auto as no explicit choice.""" + if runtime is None: + return None + normalized = runtime.strip().lower() + return None if normalized == "auto" else normalized + + +def _manifest_runtime(fabric_agent_config: Mapping[str, Any] | None) -> str | None: + """Read runtime requirements from a fabric agent manifest/config mapping.""" + if fabric_agent_config is None: + return None + runtime = fabric_agent_config.get("required_framework") or fabric_agent_config.get("runtime") or fabric_agent_config.get("framework") + return _normalize_runtime(str(runtime)) if runtime else None + + +def _require_available(runtime: str) -> str: + """Return a runtime or raise install guidance.""" + from agentic_fabric.core.decomposer import is_framework_available + + if not is_framework_available(runtime): + raise RuntimeUnavailableError(runtime, install_command(runtime)) + return runtime diff --git a/packages/agentic-fabric/src/agentic_fabric/base/__init__.py b/packages/agentic-fabric/src/agentic_fabric/base/__init__.py new file mode 100644 index 0000000..af28da8 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/base/__init__.py @@ -0,0 +1,16 @@ +"""Base module - reusable agent archetypes and shared tools.""" + +from __future__ import annotations + +from agentic_fabric.tools.file_tools import ( + DirectoryListTool, + GameCodeReaderTool, + GameCodeWriterTool, +) + + +__all__ = [ + "DirectoryListTool", + "GameCodeReaderTool", + "GameCodeWriterTool", +] diff --git a/packages/agentic-fabric/src/agentic_fabric/base/archetypes.yaml b/packages/agentic-fabric/src/agentic_fabric/base/archetypes.yaml new file mode 100644 index 0000000..8278632 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/base/archetypes.yaml @@ -0,0 +1,75 @@ +# Reusable Agent Archetypes +# +# These are base templates that packages can extend in their own agents.yaml. +# Use the 'extends' field to inherit from an archetype. +# +# Example usage in packages/mypackage/.fabric/fabric_agents/builder/agents.yaml: +# my_engineer: +# extends: senior_engineer +# variables: +# language: Python +# backstory: | +# {base} +# Additional context specific to my package... + +archetypes: + senior_engineer: + role: "Senior {language} Engineer" + goal: > + Write production-quality {language} code following best practices + and existing patterns in the codebase. + backstory: > + You are a senior developer with 10+ years of experience. + You always read existing code before writing new code. + You follow project conventions exactly. + You write clean, well-documented code with proper types. + + qa_engineer: + role: "Quality Assurance Engineer" + goal: > + Review code for errors, security issues, type safety problems, + and deviations from project conventions. + backstory: > + You specialize in code review with an eye for: + - Type safety and proper interface usage + - Missing imports that would cause errors + - Inconsistent naming conventions + - Performance anti-patterns + - Security vulnerabilities + + technical_lead: + role: "Technical Lead" + goal: > + Ensure all code is complete, correct, and properly integrated + with the existing architecture. + backstory: > + You are the technical lead with final approval authority. + You verify: + - Code fulfills the original requirement + - Architecture patterns are followed + - Code integrates with existing systems + - No regressions or breaking changes + + architect: + role: "Software Architect" + goal: > + Design scalable, maintainable system architectures and + ensure code follows established patterns. + backstory: > + You are a software architect focused on: + - Clean architecture principles + - SOLID design patterns + - Separation of concerns + - Extensibility and maintainability + + designer: + role: "System Designer" + goal: > + Create detailed designs and specifications for new features + that align with project goals. + backstory: > + You are a system designer who: + - Translates requirements into technical designs + - Considers edge cases and error handling + - Documents design decisions + - Balances complexity with maintainability diff --git a/packages/agentic-fabric/src/agentic_fabric/capabilities.py b/packages/agentic-fabric/src/agentic_fabric/capabilities.py new file mode 100644 index 0000000..77ddc6a --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/capabilities.py @@ -0,0 +1,130 @@ +"""Capability metadata for agent runners and tools.""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from types import MappingProxyType +from typing import Any, ClassVar, TypeVar, cast + + +F = TypeVar("F", bound=Callable[..., Any]) + + +@dataclass(frozen=True) +class AgentCapabilitySpec: + """A declared agent/runtime/tool capability.""" + + name: str + kind: str = "runtime" + aliases: tuple[str, ...] = () + description: str = "" + + def as_dict(self) -> dict[str, Any]: + """Return serializable capability metadata.""" + return { + "name": self.name, + "kind": self.kind, + "aliases": list(self.aliases), + "description": self.description, + } + + +def agent_capability( + name: str, + *, + kind: str = "runtime", + aliases: tuple[str, ...] = (), + description: str = "", +) -> Callable[[F], F]: + """Declare an agent-facing capability on a method.""" + + def decorate(method: F) -> F: + specs = list(getattr(method, "_agentic_capabilities", ())) + specs.append( + AgentCapabilitySpec( + name=name, + kind=kind, + aliases=aliases, + description=description, + ) + ) + cast("Any", method)._agentic_capabilities = tuple(specs) + return method + + return decorate + + +def runtime_capability( + name: str, + *, + aliases: tuple[str, ...] = (), + description: str = "", +) -> Callable[[F], F]: + """Declare a runner/runtime capability.""" + return agent_capability(name, kind="runtime", aliases=aliases, description=description) + + +def tool_capability( + name: str, + *, + aliases: tuple[str, ...] = (), + description: str = "", +) -> Callable[[F], F]: + """Declare a tool capability.""" + return agent_capability(name, kind="tool", aliases=aliases, description=description) + + +class AgentCapabilityProviderMixin: + """Collect decorated capability declarations through inheritance.""" + + agent_capabilities: ClassVar[Mapping[str, AgentCapabilitySpec]] + agent_capability_methods: ClassVar[Mapping[str, str]] + + def __init_subclass__(cls, **kwargs: Any) -> None: + """Collect decorated methods across the full MRO.""" + super().__init_subclass__(**kwargs) + capabilities: dict[str, AgentCapabilitySpec] = {} + methods: dict[str, str] = {} + + for owner in reversed(cls.__mro__): + for method_name, member in getattr(owner, "__dict__", {}).items(): + specs = cast("tuple[AgentCapabilitySpec, ...]", getattr(member, "_agentic_capabilities", ())) + for spec in specs: + for public_name in (spec.name, *spec.aliases): + capabilities[public_name] = spec + methods[public_name] = method_name + + cls.agent_capabilities = MappingProxyType(capabilities) + cls.agent_capability_methods = MappingProxyType(methods) + + @classmethod + def list_capabilities(cls, *, kind: str | None = None) -> tuple[AgentCapabilitySpec, ...]: + """Return declared capabilities, optionally filtered by kind.""" + seen: set[int] = set() + capabilities = [] + for capability in cls.agent_capabilities.values(): + marker = id(capability) + if marker in seen: + continue + seen.add(marker) + capabilities.append(capability) + if kind is not None: + capabilities = [capability for capability in capabilities if capability.kind == kind] + return tuple(capabilities) + + def call_capability(self, name: str, *args: Any, **kwargs: Any) -> Any: + """Dispatch one declared capability by public name.""" + method_name = self.agent_capability_methods.get(name) + if method_name is None: + msg = f"Unknown agent capability: {name}" + raise AttributeError(msg) + method = getattr(self, method_name) + if not callable(method): + msg = f"Agent capability {name!r} is not callable" + raise TypeError(msg) + return method(*args, **kwargs) + + def capability_map(self) -> Mapping[str, AgentCapabilitySpec]: + """Return read-only capability metadata for this instance.""" + return self.agent_capabilities diff --git a/packages/agentic-fabric/src/agentic_fabric/config/__init__.py b/packages/agentic-fabric/src/agentic_fabric/config/__init__.py new file mode 100644 index 0000000..dcef26f --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/config/__init__.py @@ -0,0 +1,10 @@ +"""Configuration for CrewAI agents.""" + +from __future__ import annotations + +from agentic_fabric.config.llm import get_llm + + +__all__ = [ + "get_llm", +] diff --git a/packages/agentic-fabric/src/agentic_fabric/config/agents.yaml b/packages/agentic-fabric/src/agentic_fabric/config/agents.yaml new file mode 100644 index 0000000..a8d8b6a --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/config/agents.yaml @@ -0,0 +1,101 @@ + +technical_director: + role: "Technical Director & Workflow Coordinator" + goal: "Orchestrate Alpha→Work→Omega workflow, delegate to specialists, validate deliverables" + backstory: | + You are an experienced technical director who understands how to decompose complex + game development into vertical slices. You validate each slice before proceeding, + ensure proper handoffs between specialists, and maintain architectural coherence. + You understand when to parallelize work and when sequential validation is critical. + llm: "claude-3-7-sonnet-20250219" + max_iter: 40 + max_reasoning_attempts: 5 + allow_delegation: true + tools: + - "mcp://git/execute_command" + - "mcp://filesystem/read_file" + - "mcp://filesystem/write_file" + - "mcp://conport/get_conport_schema" + +project_manager: + role: "Project Manager (Alpha Agent - Context Reader)" + goal: "Analyze projectbrief.md and ConPort, identify blockers, validate prerequisites" + backstory: | + You start every workflow by reading complete project context. You identify missing + information, blockers, dependencies, and required resources. You create actionable + checklists and validation criteria for downstream tasks. + llm: "claude-3-5-sonnet-20241022" + max_iter: 25 + tools: + - "mcp://conport/get_conport_schema" + - "mcp://filesystem/read_file" + - "mcp://git/execute_command" + +ecs_architect: + role: "ECS & Data Architecture Lead" + goal: "Design type-safe ECS schemas, validate data contracts, ensure zero TypeScript errors" + backstory: | + You specialize in Entity Component System design with Miniplex and TypeScript. + You create minimal, focused schemas that solve ONE problem well. You validate + against existing patterns and ensure backward compatibility. + llm: "claude-3-5-sonnet-20241022" + max_iter: 30 + max_reasoning_attempts: 3 + tools: + - "mcp://filesystem/read_file" + - "mcp://filesystem/write_file" + - "mcp://git/execute_command" + +rendering_engineer: + role: "3D Graphics & Rendering Engineer" + goal: "Build performant React Three Fiber scenes, optimize shaders, maintain 60fps" + backstory: | + You create stunning R3F/Three.js visuals with GLSL shaders while maintaining + 60fps on mobile. You understand the post-processing pipeline requirements and + mobile performance constraints. + llm: "claude-3-5-sonnet-20241022" + max_iter: 30 + max_reasoning_attempts: 3 + tools: + - "mcp://filesystem/read_file" + - "mcp://filesystem/write_file" + +integration_specialist: + role: "Frontend Integration Specialist" + goal: "Connect backend deliverables to R3F frontend, validate end-to-end flow" + backstory: | + You specialize in integrating backend systems with React Three Fiber frontends. + You create adapters, validate type contracts, and ensure smooth data flow from + ECS to rendering pipeline. + llm: "claude-3-5-sonnet-20241022" + max_iter: 30 + tools: + - "mcp://filesystem/read_file" + - "mcp://filesystem/write_file" + +qa_validator: + role: "Quality Assurance & Validation Specialist" + goal: "Validate deliverables against acceptance criteria, run tests, verify performance" + backstory: | + You validate that deliverables meet all acceptance criteria. You run unit tests, + check TypeScript compilation, measure performance, and create validation reports. + You identify gaps before human-in-the-loop review. + llm: "claude-3-5-sonnet-20241022" + max_iter: 25 + tools: + - "mcp://filesystem/read_file" + - "mcp://git/execute_command" + - "mcp://playwright/execute" + +technical_writer: + role: "Technical Writer (Omega Agent - Context Writer)" + goal: "Document decisions, update ConPort, create integration guides" + backstory: | + You end every workflow by updating ConPort with decisions and progress. + You create clear, actionable documentation that the next agent or human + can use to continue work. + llm: "claude-3-5-sonnet-20241022" + max_iter: 25 + tools: + - "mcp://conport/get_conport_schema" + - "mcp://filesystem/write_file" diff --git a/packages/agentic-fabric/src/agentic_fabric/config/llm.py b/packages/agentic-fabric/src/agentic_fabric/config/llm.py new file mode 100644 index 0000000..0ed1561 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/config/llm.py @@ -0,0 +1,233 @@ +"""LLM Configuration for CrewAI agents. + +Uses Anthropic Claude directly via ANTHROPIC_API_KEY. +Falls back to OpenRouter if OPENROUTER_API_KEY is set. +""" + +from __future__ import annotations + +import os + +from dataclasses import dataclass +from enum import Enum + + +try: + from crewai import LLM +except ImportError: + # Allow module to load even if crewai not installed (for testing) + LLM = None # type: ignore[assignment] + + +class LLMProvider(Enum): + """Available LLM providers.""" + + ANTHROPIC = "anthropic" + OPENROUTER = "openrouter" + + +@dataclass(frozen=True) +class LLMConfig: + """Configuration for specific LLM use cases.""" + + model: str + temperature: float + description: str + + +# Model identifiers +_CLAUDE_HAIKU_45 = "claude-haiku-4-5-20251001" +_CLAUDE_SONNET_45 = "claude-sonnet-4-5-20250929" +_CLAUDE_SONNET_4 = "claude-sonnet-4-20250514" +_CLAUDE_OPUS_4 = "claude-opus-4-20250514" + +# Default model - Claude Haiku 4.5 for fast, cost-effective operations +DEFAULT_MODEL = _CLAUDE_HAIKU_45 + +# Alternative models +MODELS = { + "haiku": _CLAUDE_HAIKU_45, + "sonnet": _CLAUDE_SONNET_45, + "sonnet-4": _CLAUDE_SONNET_4, + "opus": _CLAUDE_OPUS_4, + # OpenRouter fallbacks + "openrouter-auto": "openrouter/auto", + "openrouter-haiku": "openrouter/anthropic/claude-haiku-4.5", +} + +_OPENROUTER_MODEL_MAP = { + _CLAUDE_HAIKU_45: "openrouter/anthropic/claude-haiku-4.5", + _CLAUDE_SONNET_45: "openrouter/anthropic/claude-sonnet-4.5", + _CLAUDE_SONNET_4: "openrouter/anthropic/claude-sonnet-4", + _CLAUDE_OPUS_4: "openrouter/anthropic/claude-opus-4", + "haiku": "openrouter/anthropic/claude-haiku-4.5", + "sonnet": "openrouter/anthropic/claude-sonnet-4.5", + "sonnet-4": "openrouter/anthropic/claude-sonnet-4", + "opus": "openrouter/anthropic/claude-opus-4", +} + +# Predefined configurations for common use cases +LLM_CONFIGS = { + "reasoning": LLMConfig(model=_CLAUDE_OPUS_4, temperature=0.3, description="Optimized for complex reasoning tasks"), + "creative": LLMConfig( + model=_CLAUDE_SONNET_45, + temperature=0.8, + description="Optimized for creative content generation", + ), + "code": LLMConfig( + model=_CLAUDE_SONNET_45, + temperature=0.2, + description="Optimized for code generation and analysis", + ), + "default": LLMConfig( + model=_CLAUDE_HAIKU_45, + temperature=0.7, + description="Balanced configuration for general use", + ), +} + + +def get_llm(model: str = DEFAULT_MODEL, temperature: float = 0.7, provider: LLMProvider | None = None) -> LLM | None: + """Get configured LLM instance for CrewAI agents. + + Args: + model: Model identifier. Defaults to claude-haiku-4-5-20251001 (DEFAULT_MODEL) + temperature: Sampling temperature (0.0-1.0). Lower = more focused, + higher = more creative. + provider: Force specific provider (ANTHROPIC or OPENROUTER). + If None, auto-detects based on available API keys. + + Available models: + - claude-haiku-4-5-20251001 (default - fast, cost-effective) + - claude-sonnet-4-5-20250929 (best for code and creative) + - claude-sonnet-4-20250514 (capable general purpose) + - claude-opus-4-20250514 (most capable) + - openrouter/auto (fallback via OpenRouter) + + Returns: + Configured LLM instance, or None if no API key set + + Note: + Tries ANTHROPIC_API_KEY first, falls back to OPENROUTER_API_KEY. + Returns None if neither is set, allowing CrewAI to use its default. + + Example: + >>> llm = get_llm() # Uses Claude Haiku 4.5 + >>> llm = get_llm("claude-opus-4-20250514", temperature=0.3) + >>> llm = get_llm(provider=LLMProvider.OPENROUTER) + """ + if LLM is None: + return None + + # Determine provider + anthropic_key = os.getenv("ANTHROPIC_API_KEY") + openrouter_key = os.getenv("OPENROUTER_API_KEY") + + # Force provider if specified + if provider == LLMProvider.ANTHROPIC: + if not anthropic_key: + return None + return _create_anthropic_llm(model, temperature, anthropic_key) + + if provider == LLMProvider.OPENROUTER: + if not openrouter_key: + return None + return _create_openrouter_llm(model, temperature, openrouter_key) + + # Auto-detect: Try Anthropic first for direct Claude models + if anthropic_key and not model.startswith("openrouter/"): + return _create_anthropic_llm(model, temperature, anthropic_key) + + # Fall back to OpenRouter + if openrouter_key: + return _create_openrouter_llm(model, temperature, openrouter_key) + + return None + + +def _create_anthropic_llm(model: str, temperature: float, api_key: str) -> LLM: + """Create Anthropic LLM instance.""" + return LLM( + model=model, + api_key=api_key, + temperature=temperature, + ) + + +def _create_openrouter_llm(model: str, temperature: float, api_key: str) -> LLM: + """Create OpenRouter LLM instance.""" + # Convert model name to OpenRouter format if needed + if not model.startswith("openrouter/"): + model = _OPENROUTER_MODEL_MAP.get(model, MODELS.get(model, MODELS.get("openrouter-auto", "openrouter/auto"))) + + return LLM( + model=model, + api_key=api_key, + base_url="https://openrouter.ai/api/v1", + temperature=temperature, + ) + + +def get_llm_or_raise(model: str = DEFAULT_MODEL, temperature: float = 0.7, provider: LLMProvider | None = None) -> LLM: + """Get configured LLM instance, raising if API key not set. + + Use this when you need to ensure an LLM is available. + + Args: + model: Model identifier (see get_llm for options) + temperature: Sampling temperature (0.0-1.0) + provider: Force specific provider (optional) + + Returns: + Configured LLM instance + + Raises: + ValueError: If neither ANTHROPIC_API_KEY nor OPENROUTER_API_KEY is set + """ + llm = get_llm(model, temperature, provider) + if llm is None: + raise ValueError( + "ANTHROPIC_API_KEY or OPENROUTER_API_KEY environment variable must be set. " + "Get Anthropic key at https://console.anthropic.com/ or " + "OpenRouter key at https://openrouter.ai/" + ) + return llm + + +def get_llm_for_task(task: str) -> LLM | None: + """Get LLM configured for a specific task type. + + Args: + task: Task type - one of: 'reasoning', 'creative', 'code', 'default' + + Returns: + Configured LLM instance, or None if no API key set + + Raises: + ValueError: If task type is unknown + + Example: + >>> llm = get_llm_for_task('code') # Low temp, optimized for code + >>> llm = get_llm_for_task('creative') # High temp, creative output + """ + if task not in LLM_CONFIGS: + raise ValueError(f"Unknown task type: {task}. Available: {', '.join(LLM_CONFIGS.keys())}") + + config = LLM_CONFIGS[task] + return get_llm(config.model, config.temperature) + + +# Convenience functions for specific use cases (backward compatibility) +def get_reasoning_llm() -> LLM | None: + """Get LLM optimized for complex reasoning tasks.""" + return get_llm_for_task("reasoning") + + +def get_creative_llm() -> LLM | None: + """Get LLM optimized for creative tasks.""" + return get_llm_for_task("creative") + + +def get_code_llm() -> LLM | None: + """Get LLM optimized for code generation.""" + return get_llm_for_task("code") diff --git a/packages/agentic-fabric/src/agentic_fabric/config/tasks.yaml b/packages/agentic-fabric/src/agentic_fabric/config/tasks.yaml new file mode 100644 index 0000000..41b8d16 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/config/tasks.yaml @@ -0,0 +1,177 @@ + +# ------------------------------------------------------------------------- +# PHASE 0: ALPHA - Context Loading & Validation +# ------------------------------------------------------------------------- +alpha_context_load: + description: | + ALPHA TASK: Load complete project context before any work begins. + + Actions: + 1. Read projectbrief.md for project goals and constraints + 2. Query ConPort schema to understand current state + 3. Read .ruler/ files for architectural patterns + 4. Identify any missing prerequisites or blockers + + Validation Criteria: + - All context files are accessible + - No critical blockers identified + - Prerequisites are documented + + Output Format: Markdown checklist with: + - [ ] Context item loaded + - [ ] Prerequisite validated + - [ ] Blocker identified (if any) + expected_output: | + Markdown report with: + ## Context Loaded + - List of files read + - Current project state summary + + ## Prerequisites + - [ ] Checklist of validated prerequisites + + ## Blockers (if any) + - Description of any blockers + + ## Ready to Proceed + - Yes/No with reasoning + agent: project_manager + human_input: false + +# ------------------------------------------------------------------------- +# PHASE 1: FOUNDATION - Parallel schema validation +# ------------------------------------------------------------------------- +validate_ecs_component_schemas: + description: | + Validate existing ECS component schemas in src/ecs/. + + Focus Areas: + 1. TypeScript compilation with zero errors + 2. Adherence to Miniplex patterns + 3. Type exports are complete and correct + 4. Component schemas are minimal and focused + + DO NOT create new components. ONLY validate existing ones. + + Validation Steps: + 1. Run `pnpm run build` to check TypeScript compilation + 2. Check each component file against patterns + 3. Verify type exports exist + 4. Document any issues found + + If issues found: Create a remediation plan, do NOT fix yet. + expected_output: | + ## Validation Report + + ### TypeScript Compilation + - Status: PASS/FAIL + - Errors: (list if any) + + ### Pattern Compliance + - AIComponent.ts: PASS/FAIL (reasoning) + - AnimationComponent.ts: PASS/FAIL (reasoning) + - [continue for all components] + + ### Type Exports + - Complete: YES/NO + - Missing: (list if any) + + ### Remediation Plan (if needed) + - Issue 1: [description] → [fix] + - Issue 2: [description] → [fix] + + ### Ready for Next Phase + - YES/NO with reasoning + agent: ecs_architect + context: [alpha_context_load] + async_execution: false + +validate_dfu_data_contracts: + description: | + Validate Daggerfall Unity data mappings in shared/backend/dfu_analysis/. + + Focus Areas: + 1. biomes.json structure matches BiomeContract.ts + 2. species.json structure matches SpeciesContract.ts + 3. Data completeness (no missing required fields) + 4. Type safety between backend and frontend contracts + + DO NOT modify contracts yet. ONLY validate. + + Validation Steps: + 1. Load shared/contracts/*.ts + 2. Load shared/backend/dfu_analysis/mappings/*.json + 3. Verify structural alignment + 4. Check for missing data + 5. Document any mismatches + expected_output: | + ## Contract Validation Report + + ### BiomeContract Alignment + - Structure matches: YES/NO + - Missing fields: (list if any) + - Type mismatches: (list if any) + + ### SpeciesContract Alignment + - Structure matches: YES/NO + - Missing fields: (list if any) + - Type mismatches: (list if any) + + ### Data Completeness + - Biomes: X/Y complete + - Species: X/Y complete + + ### Remediation Plan (if needed) + - Issue 1: [description] → [fix] + + ### Ready for Next Phase + - YES/NO with reasoning + agent: ecs_architect + context: [alpha_context_load] + async_execution: false + +# ------------------------------------------------------------------------- +# OMEGA: Documentation & Context Recording +# ------------------------------------------------------------------------- +omega_record_progress: + description: | + OMEGA TASK: Record all decisions, progress, and learnings into ConPort. + + Actions: + 1. Summarize all completed work + 2. Document architectural decisions made + 3. Record patterns discovered + 4. Update ConPort with current state + 5. Create handoff notes for next phase + + This runs AFTER every major phase completion. + expected_output: | + ## ConPort Update + + ### Work Completed + - Phase 1: [summary] + - Phase 2: [summary] + - Phase 3: [summary] + + ### Architectural Decisions + - Decision 1: [what, why, alternatives considered] + - Decision 2: [what, why, alternatives considered] + + ### Patterns Discovered + - Pattern 1: [description, when to use] + - Pattern 2: [description, when to use] + + ### Current State + - ECS schemas: [status] + - Prototypes: [list with status] + - Technical debt: [list] + + ### Handoff Notes for Next Phase + - Prerequisites: [checklist] + - Context to review: [files] + - Open questions: [list] + + ### ConPort Updated + - YES/NO with timestamp + agent: technical_writer + context: [validate_ecs_component_schemas, validate_dfu_data_contracts] diff --git a/packages/agentic-fabric/src/agentic_fabric/core/__init__.py b/packages/agentic-fabric/src/agentic_fabric/core/__init__.py new file mode 100644 index 0000000..034dec0 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/core/__init__.py @@ -0,0 +1,18 @@ +"""Core fabric engine for package discovery, loading, and execution.""" + +from __future__ import annotations + +from agentic_fabric.core.discovery import discover_packages, get_fabric_agent_config, load_manifest +from agentic_fabric.core.loader import load_fabric_agent_from_config +from agentic_fabric.core.manager import ManagerAgent +from agentic_fabric.core.runner import run_fabric_agent + + +__all__ = [ + "ManagerAgent", + "discover_packages", + "get_fabric_agent_config", + "load_fabric_agent_from_config", + "load_manifest", + "run_fabric_agent", +] diff --git a/packages/agentic-fabric/src/agentic_fabric/core/decomposer.py b/packages/agentic-fabric/src/agentic_fabric/core/decomposer.py new file mode 100644 index 0000000..a86cf2a --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/core/decomposer.py @@ -0,0 +1,278 @@ +"""Framework decomposition - auto-detect and select AI framework. + +This module provides the core capability of agentic-fabric: declaring fabric agents +once and running them on CrewAI, LangGraph, or Strands depending on what's +installed. It also supports single-agent CLI runners for simpler tasks. + +Usage: + from agentic_fabric.core.decomposer import get_runner, detect_framework + + # Auto-detect best framework for multi-agent + framework = detect_framework() + + # Get runner for that framework + runner = get_runner(framework) + + # Or let it auto-select + runner = get_runner() # Uses best available + + # Get single-agent CLI runner + from agentic_fabric.core.decomposer import get_cli_runner + runner = get_cli_runner("aider") +""" + +from __future__ import annotations + +import importlib + +from typing import TYPE_CHECKING, Any + +from agentic_fabric.runners.registry import get_runtime_spec, runtime_info, runtime_names + + +if TYPE_CHECKING: + from agentic_fabric.runners.base import BaseRunner + from agentic_fabric.runners.single_agent_runner import SingleAgentRunner + +# Framework detection cache +_framework_cache: dict[str, bool] = {} + +# Framework priority (first available wins) +FRAMEWORK_PRIORITY = runtime_names() + + +def is_framework_available(framework: str) -> bool: + """Check if a framework is installed and importable. + + Args: + framework: Framework name (crewai, langgraph, strands) + + Returns: + True if framework is available + """ + if framework in _framework_cache: + return _framework_cache[framework] + + if framework not in FRAMEWORK_PRIORITY: + _framework_cache[framework] = False + return False + + try: + spec = get_runtime_spec(framework) + importlib.import_module(spec.import_name) + _framework_cache[framework] = True + return True + except ImportError: + _framework_cache[framework] = False + return False + + +def detect_framework(preferred: str | None = None) -> str: + """Detect the best available AI framework. + + Args: + preferred: Optional preferred framework. If available, use it. + + Returns: + Name of the best available framework. + + Raises: + RuntimeError: If no frameworks are installed. + """ + # Check preferred first + if preferred and preferred != "auto" and is_framework_available(preferred): + return preferred + # Fall through to auto-detect if preferred not available + + # Auto-detect based on priority + for framework in FRAMEWORK_PRIORITY: + if is_framework_available(framework): + return framework + + install_options = "\n".join(f" {_get_install_command(framework)}" for framework in FRAMEWORK_PRIORITY) + raise RuntimeError(f"No AI frameworks installed. Install one of:\n{install_options}") + + +def get_available_frameworks() -> list[str]: + """Get list of all available frameworks. + + Returns: + List of framework names that are installed. + """ + return [f for f in FRAMEWORK_PRIORITY if is_framework_available(f)] + + +def get_runner(framework: str | None = None) -> BaseRunner: + """Get a runner for the specified or auto-detected framework. + + Args: + framework: Framework name or None for auto-detect. + + Returns: + Runner instance for the framework. + + Raises: + RuntimeError: If framework not available. + ValueError: If unknown framework specified. + """ + if framework is None or framework == "auto": + framework = detect_framework() + + try: + spec = get_runtime_spec(framework) + except ValueError as exc: + raise ValueError(f"Unknown framework: {framework}. Options: {FRAMEWORK_PRIORITY}") from exc + + module = importlib.import_module(spec.runner_module) + runner_cls = getattr(module, spec.runner_class) + return runner_cls() + + +def get_cli_runner( + profile: str | dict[str, Any], + model: str | None = None, +) -> SingleAgentRunner: + """Get a single-agent CLI runner for the specified profile. + + Args: + profile: Profile name (e.g., "aider", "claude-code", "ollama") or + custom config dict. + model: Optional model override. + + Returns: + LocalCLIRunner instance for the profile. + + Raises: + ValueError: If profile not found. + FileNotFoundError: If profiles file missing. + + Examples: + # Use built-in profile + runner = get_cli_runner("aider") + result = runner.run("Add error handling to auth.py") + + # Use with model override + runner = get_cli_runner("ollama", model="deepseek-coder") + result = runner.run("Fix the bug") + + # Use custom config + runner = get_cli_runner({ + "command": "my-tool", + "task_flag": "--task", + "auto_approve": "--yes", + }) + """ + from agentic_fabric.runners.local_cli_runner import LocalCLIRunner + + return LocalCLIRunner(profile, model=model) + + +def get_available_cli_runners() -> list[str]: + """Get list of available CLI runner profiles. + + Returns: + List of profile names (e.g., ["aider", "claude-code", "ollama"]). + """ + from agentic_fabric.runners.local_cli_runner import LocalCLIRunner + + return LocalCLIRunner.get_available_profiles() + + +def get_framework_info(framework: str | None = None) -> list[dict[str, Any]] | dict[str, Any]: + """Return lazy runtime registry metadata with current availability.""" + return runtime_info(framework) + + +def is_cli_runner_available(profile: str) -> bool: + """Check if a CLI runner profile is available (tool installed). + + Args: + profile: Profile name to check. + + Returns: + True if the tool is installed and accessible. + """ + try: + runner = get_cli_runner(profile) + return runner.is_available() + except (ValueError, FileNotFoundError): + return False + + +def _resolve_required_framework(fabric_agent_config: dict[str, Any], framework: str | None = None) -> str | None: + """Return the framework selected after honoring fabric-agent-level requirements.""" + required_framework = fabric_agent_config.get("required_framework") + if not required_framework: + return framework + + if framework and framework not in (required_framework, "auto"): + raise ValueError( + f"Fabric agent requires {required_framework} (defined in .{required_framework}/ directory) " + f"but {framework} was requested" + ) + + if not is_framework_available(required_framework): + raise RuntimeError( + f"Fabric agent requires {required_framework} but it's not installed. " + f"Install with: {_get_install_command(required_framework)}" + ) + return required_framework + + +def compose_fabric_agent( + fabric_agent_config: dict[str, Any], + framework: str | None = None, +) -> Any: + """Compose a fabric agent configuration into a runtime-specific object. + + This is the core function that converts a framework-agnostic fabric agent + definition into a runnable fabric agent for the target framework. + + Args: + fabric_agent_config: Fabric agent configuration from loader. + framework: Target framework or None for auto-detect. + If fabric_agent_config has required_framework, that takes precedence. + + Returns: + Runtime-specific fabric agent object ready to run. + + Raises: + RuntimeError: If required framework is not available. + """ + runner = get_runner(_resolve_required_framework(fabric_agent_config, framework)) + return runner.build_fabric_agent(fabric_agent_config) + + +def _get_install_command(framework: str) -> str: + """Get the package-extra install command for a framework.""" + try: + return get_runtime_spec(framework).install + except ValueError: + return f"pip install {framework}" + + +# Convenience function for simple use cases +def run_fabric_agent_auto( + fabric_agent_config: dict[str, Any], + inputs: dict[str, Any] | None = None, + framework: str | None = None, +) -> str: + """Run a fabric agent using the best available framework. + + Args: + fabric_agent_config: Fabric agent configuration from loader. + inputs: Optional inputs for the fabric agent. + framework: Optional framework override. If fabric_agent_config has + required_framework (from .crewai/.strands/.langgraph dir), + that takes precedence. + + Returns: + Fabric agent output as string. + + Raises: + RuntimeError: If required framework is not available. + ValueError: If requested framework conflicts with required framework. + """ + runner = get_runner(_resolve_required_framework(fabric_agent_config, framework)) + fabric_agent = runner.build_fabric_agent(fabric_agent_config) + return runner.run(fabric_agent, inputs or {}) diff --git a/packages/agentic-fabric/src/agentic_fabric/core/discovery.py b/packages/agentic-fabric/src/agentic_fabric/core/discovery.py new file mode 100644 index 0000000..7cf534d --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/core/discovery.py @@ -0,0 +1,354 @@ +"""Discovery module - finds packages with fabric agent configuration directories. + +Supports framework-specific configuration directories: +- .fabric/ - framework-agnostic fabric configuration +- .crewai/ - CrewAI-specific configurations +- .langgraph/ - LangGraph-specific configurations +- .strands/ - Strands-specific configurations + +The discovery order prefers the agnostic fabric before runtime-specific +directories. +""" + +from __future__ import annotations + +import logging + +from pathlib import Path +from typing import Any + +import yaml + + +logger = logging.getLogger(__name__) + +# Framework directory names in priority order +# .fabric is framework-agnostic (can run on any available framework) +# Framework-specific dirs enforce that framework +FRAMEWORK_DIRS = [".fabric", ".crewai", ".langgraph", ".strands"] + +# Mapping from directory name to framework name +# .fabric maps to None (framework-agnostic, auto-detect at runtime) +DIR_TO_FRAMEWORK: dict[str, str | None] = { + ".fabric": None, # Framework-agnostic + ".crewai": "crewai", + ".langgraph": "langgraph", + ".strands": "strands", +} + +# Mapping from framework name to directory name +# None (agnostic) maps to .fabric +FRAMEWORK_TO_DIR: dict[str | None, str] = { + None: ".fabric", + "crewai": ".crewai", + "langgraph": ".langgraph", + "strands": ".strands", +} + + +def get_workspace_root() -> Path: + """Get the workspace root directory. + + Walks up from the current file to find the root (where pyproject.toml is). + """ + current = Path(__file__).resolve() + for parent in current.parents: + if (parent / "pyproject.toml").exists() and (parent / "packages").exists(): + return parent + # Fallback to current working directory + return Path.cwd() + + +def discover_packages( + workspace_root: Path | None = None, + framework: str | None = None, +) -> dict[str, Path]: + """Discover all packages with fabric agent configuration directories. + + Args: + workspace_root: Root of the workspace. If None, auto-detected. + framework: Optional framework to filter by (crewai, langgraph, strands). + If None, returns first found directory per package. + + Returns: + Dict mapping package name to its config directory path. + """ + if workspace_root is None: + workspace_root = get_workspace_root() + + packages = {} + + # Determine which directories to look for + if framework: + dir_name = FRAMEWORK_TO_DIR.get(framework) + if dir_name is None: + msg = f"Unknown framework: {framework}" + raise ValueError(msg) + dirs_to_check = [dir_name] + else: + dirs_to_check = FRAMEWORK_DIRS + + # Check packages/ directory + packages_dir = workspace_root / "packages" + if packages_dir.is_dir(): + for pkg_dir in packages_dir.iterdir(): + if not pkg_dir.is_dir(): + continue + + # Try each framework directory in priority order + for dir_name in dirs_to_check: + config_dir = pkg_dir / dir_name + if config_dir.is_dir() and (config_dir / "manifest.yaml").exists(): + packages[pkg_dir.name] = config_dir + break # Use first found (highest priority) + + # Also check workspace root for standalone projects + for dir_name in dirs_to_check: + config_dir = workspace_root / dir_name + if config_dir.is_dir() and (config_dir / "manifest.yaml").exists(): + # Use the workspace name or a default + pkg_name = workspace_root.name or "default" + if pkg_name not in packages: + packages[pkg_name] = config_dir + break + + return packages + + +def discover_all_framework_configs( + workspace_root: Path | None = None, +) -> dict[str, dict[str | None, Path]]: + """Discover all framework-specific config directories for all packages. + + This finds ALL framework directories, not just the first one per package. + + Args: + workspace_root: Root of the workspace. If None, auto-detected. + + Returns: + Dict mapping package name to dict of framework -> config_dir. + Example: {"sample": {"crewai": Path(...), "strands": Path(...)}} + Framework can be None for framework-agnostic .fabric/ directories. + """ + if workspace_root is None: + workspace_root = get_workspace_root() + + packages: dict[str, dict[str | None, Path]] = {} + + # Check packages/ directory + packages_dir = workspace_root / "packages" + if packages_dir.is_dir(): + for pkg_dir in packages_dir.iterdir(): + if not pkg_dir.is_dir(): + continue + + pkg_configs: dict[str | None, Path] = {} + for dir_name in FRAMEWORK_DIRS: + config_dir = pkg_dir / dir_name + if config_dir.is_dir() and (config_dir / "manifest.yaml").exists(): + framework = DIR_TO_FRAMEWORK[dir_name] + pkg_configs[framework] = config_dir + + if pkg_configs: + packages[pkg_dir.name] = pkg_configs + + # Also check workspace root + root_configs: dict[str | None, Path] = {} + for dir_name in FRAMEWORK_DIRS: + config_dir = workspace_root / dir_name + if config_dir.is_dir() and (config_dir / "manifest.yaml").exists(): + framework = DIR_TO_FRAMEWORK[dir_name] + root_configs[framework] = config_dir + + if root_configs: + pkg_name = workspace_root.name or "default" + if pkg_name not in packages: + packages[pkg_name] = root_configs + + return packages + + +def load_manifest(config_dir: Path) -> dict[str, Any]: + """Load a package's fabric manifest. + + Args: + config_dir: Path to the fabric or runtime-specific config directory. + + Returns: + Parsed manifest as a dictionary. + """ + manifest_path = config_dir / "manifest.yaml" + with open(manifest_path, encoding="utf-8") as f: + result = yaml.safe_load(f) + if result is None: + return {} + if not isinstance(result, dict): + msg = f"{manifest_path} must contain a mapping at the document root" + raise TypeError(msg) + return result + + +def _load_yaml_mapping(path: Path, description: str) -> dict[str, Any]: + """Load a required YAML mapping from disk.""" + if not path.is_file(): + msg = f"{description} file not found: {path}" + raise FileNotFoundError(msg) + + result = yaml.safe_load(path.read_text(encoding="utf-8")) + if result is None: + return {} + if not isinstance(result, dict): + msg = f"{path} must contain a mapping of {description.lower()}" + raise TypeError(msg) + return result + + +def _resolve_config_path(config_dir: Path, relative_path: str) -> Path: + """Resolve a manifest path and require it to stay below the config dir.""" + candidate = Path(relative_path) + if candidate.is_absolute() or ".." in candidate.parts: + msg = f"Manifest path must be relative to {config_dir.name}: {relative_path}" + raise ValueError(msg) + + root = config_dir.resolve() + resolved = (root / candidate).resolve(strict=False) + if not resolved.is_relative_to(root): + msg = f"Manifest path escapes config directory: {relative_path}" + raise ValueError(msg) + return resolved + + +def get_framework_from_config_dir(config_dir: Path) -> str | None: + """Determine the required framework from a config directory path. + + Args: + config_dir: Path to a framework config directory (.crewai, .strands, etc.) + + Returns: + Framework name if directory indicates a specific framework, None otherwise. + """ + dir_name = config_dir.name + return DIR_TO_FRAMEWORK.get(dir_name) + + +def get_fabric_agent_config(config_dir: Path, fabric_agent_name: str) -> dict: + """Load a specific fabric agent's configuration. + + Args: + config_dir: Path to the config directory (.fabric/, .crewai/, .strands/, .langgraph/). + fabric_agent_name: Name of the fabric agent to load. + + Returns: + Dict with agents, tasks, knowledge_paths, and required_framework. + The required_framework field indicates which framework MUST be used + if the config is in a framework-specific directory. + + Raises: + ValueError: If fabric agent not found in manifest. + """ + manifest = load_manifest(config_dir) + fabric_agents = manifest.get("fabric_agents", {}) + fabric_agent_config = fabric_agents.get(fabric_agent_name) + + if not fabric_agent_config: + available = list(fabric_agents.keys()) + raise ValueError(f"Fabric agent '{fabric_agent_name}' not found. Available: {available}") + + agents_rel = fabric_agent_config.get("agents") + tasks_rel = fabric_agent_config.get("tasks") + if not agents_rel or not tasks_rel: + missing = "agents" if not agents_rel else "tasks" + msg = f"Fabric agent '{fabric_agent_name}' is missing required key: {missing}" + raise ValueError(msg) + + # Load agents and tasks YAML + agents_path = _resolve_config_path(config_dir, agents_rel) + tasks_path = _resolve_config_path(config_dir, tasks_rel) + + agents = _load_yaml_mapping(agents_path, "Agents") + tasks = _load_yaml_mapping(tasks_path, "Tasks") + + # Resolve knowledge paths + knowledge_paths = [] + for kp in fabric_agent_config.get("knowledge", []): + full_path = _resolve_config_path(config_dir, kp) + if full_path.is_dir(): + knowledge_paths.append(full_path) + + # Determine required framework from directory name + # If config is in .crewai/, it MUST run on CrewAI, etc. + required_framework = get_framework_from_config_dir(config_dir) + + # Also check manifest-level preferred_framework (can be overridden by dir) + manifest_framework = fabric_agent_config.get("preferred_framework") + if ( + manifest_framework + and manifest_framework != "auto" + and required_framework + and manifest_framework != required_framework + ): + # Manifest can specify preference, but directory takes precedence + logger.warning( + f"Warning: fabric agent '{fabric_agent_name}' specifies preferred_framework={manifest_framework} " + f"but is in {config_dir.name}/ directory which requires {required_framework}" + ) + + # Get LLM config from manifest + llm_config = manifest.get("llm", fabric_agent_config.get("llm", {})) + + return { + "name": fabric_agent_name, + "description": fabric_agent_config.get("description", ""), + "agents": agents, + "tasks": tasks, + "knowledge_paths": knowledge_paths, + "manifest": manifest, + "config_dir": config_dir, + # Framework enforcement + "required_framework": required_framework, + "preferred_framework": manifest_framework, + # LLM configuration + "llm": llm_config, + } + + +def list_fabric_agents( + package_name: str | None = None, + framework: str | None = None, +) -> dict[str, list[dict]]: + """List all available fabric agents, optionally filtered by package or framework. + + Args: + package_name: If provided, only list fabric agents for this package. + framework: If provided, only list fabric agents that can run on this framework. + + Returns: + Dict mapping package name to list of fabric agent info dicts. + Each fabric agent dict includes: + - name: Fabric agent name + - description: Fabric agent description + - required_framework: Framework required (if in framework-specific dir) + """ + packages = discover_packages(framework=framework) + result = {} + + for pkg_name, config_dir in packages.items(): + if package_name and pkg_name != package_name: + continue + + manifest = load_manifest(config_dir) + required_framework = get_framework_from_config_dir(config_dir) + + fabric_agents = [] + for fabric_agent_name, fabric_agent_config in manifest.get("fabric_agents", {}).items(): + fabric_agents.append( + { + "name": fabric_agent_name, + "description": fabric_agent_config.get("description", ""), + "required_framework": required_framework, + "preferred_framework": fabric_agent_config.get("preferred_framework"), + } + ) + result[pkg_name] = fabric_agents + + return result diff --git a/packages/agentic-fabric/src/agentic_fabric/core/loader.py b/packages/agentic-fabric/src/agentic_fabric/core/loader.py new file mode 100644 index 0000000..ef5e6e8 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/core/loader.py @@ -0,0 +1,202 @@ +"""Loader module - creates CrewAI Crew objects from YAML configurations. + +Note: This module imports CrewAI lazily to allow the core package to be +installed without requiring CrewAI. For framework-agnostic usage, prefer +using the decomposer module with runners instead. +""" + +from __future__ import annotations + +import logging + +from pathlib import Path +from typing import TYPE_CHECKING, Any + + +if TYPE_CHECKING: + from crewai import Agent, Crew, Task + +from agentic_fabric.tools.registry import resolve_tools + + +logger = logging.getLogger(__name__) + + +def _has_non_whitespace_content(file_path: Path) -> bool: + """Return whether a text file contains any non-whitespace content.""" + with file_path.open(encoding="utf-8") as handle: + while chunk := handle.read(4096): + if chunk.strip(): + return True + return False + + +def load_knowledge_sources(knowledge_paths: list[Path]) -> list: + """Load knowledge sources from the specified paths. + + Args: + knowledge_paths: List of paths to knowledge directories. + + Returns: + List of TextFileKnowledgeSource objects. + + Raises: + ImportError: If crewai is not installed. + """ + from crewai.knowledge.source.text_file_knowledge_source import TextFileKnowledgeSource + + sources = [] + + for knowledge_path in knowledge_paths: + if not knowledge_path.is_dir(): + continue + + # Load .md and .ts files + for ext in ["*.md", "*.ts", "*.tsx", "*.py"]: + for file_path in knowledge_path.rglob(ext): + try: + if _has_non_whitespace_content(file_path): + sources.append( + TextFileKnowledgeSource( + file_paths=[str(file_path)], + ) + ) + except (OSError, ValueError) as exc: + logger.warning("Could not load knowledge source %s: %s", file_path, exc) + + return sources + + +def create_agent_from_config( + agent_name: str, + agent_config: dict, + tools: list | None = None, +) -> Agent: + """Create an Agent from YAML configuration. + + Args: + agent_name: Name/key of the agent. + agent_config: Agent configuration dict with role, goal, backstory. + tools: Optional list of tools to give the agent. + + Returns: + Configured Agent instance. + + Raises: + ImportError: If crewai is not installed. + """ + from crewai import Agent + + from agentic_fabric.config.llm import get_llm + + return Agent( + role=agent_config.get("role", agent_name), + goal=agent_config.get("goal", ""), + backstory=agent_config.get("backstory", ""), + llm=get_llm(), + tools=tools or [], + allow_delegation=agent_config.get("allow_delegation", False), + verbose=True, + ) + + +def create_task_from_config( + task_name: str, + task_config: dict, + agent: Agent, +) -> Task: + """Create a Task from YAML configuration. + + Args: + task_name: Name/key of the task. + task_config: Task configuration dict with description, expected_output. + agent: Agent to assign to this task. + + Returns: + Configured Task instance. + + Raises: + ImportError: If crewai is not installed. + """ + from crewai import Task + + return Task( + description=task_config.get("description", ""), + expected_output=task_config.get("expected_output", ""), + agent=agent, + ) + + +def load_fabric_agent_from_config(fabric_agent_config: dict) -> Crew: + """Load a complete Crew from configuration. + + Args: + fabric_agent_config: Configuration dict from get_fabric_agent_config(). + + Returns: + Configured Crew instance ready to kickoff. + + Raises: + ImportError: If crewai is not installed. + """ + from crewai import Crew, Process + + from agentic_fabric.tools.file_tools import ( + DirectoryListTool, + GameCodeReaderTool, + GameCodeWriterTool, + ) + + # Initialize tools + code_writer = GameCodeWriterTool() + code_reader = GameCodeReaderTool() + dir_lister = DirectoryListTool() + all_tools = [code_writer, code_reader, dir_lister] + read_tools = [code_reader, dir_lister] + + # Create agents + agents_config = fabric_agent_config.get("agents", {}) + agents: dict[str, Any] = {} + + for agent_name, agent_cfg in agents_config.items(): + resolved_tools = resolve_tools(agent_cfg.get("tools", [])) + + # Preserve the older role-based fallback when no concrete tool could be resolved. + if resolved_tools: + tools = resolved_tools + else: + tools = all_tools if "engineer" in agent_name.lower() or "developer" in agent_name.lower() else read_tools + + agents[agent_name] = create_agent_from_config(agent_name, agent_cfg, tools) + + # Create tasks + tasks_config = fabric_agent_config.get("tasks", {}) + tasks = [] + + for task_name, task_cfg in tasks_config.items(): + # Find the agent for this task + agent_name = task_cfg.get("agent") + if not agent_name: + raise ValueError(f"Task '{task_name}' is missing an 'agent' assignment.") + + agent = agents.get(agent_name) + if not agent: + raise ValueError( + f"Agent '{agent_name}' for task '{task_name}' not found. Available agents: {list(agents.keys())}" + ) + + tasks.append(create_task_from_config(task_name, task_cfg, agent)) + + # Load knowledge sources + knowledge_sources = load_knowledge_sources(fabric_agent_config.get("knowledge_paths", [])) + + # Create and return the fabric_agent + return Crew( + agents=list(agents.values()), + tasks=tasks, + process=Process.sequential, + planning=True, + memory=True, + knowledge_sources=knowledge_sources or None, + verbose=True, + ) diff --git a/packages/agentic-fabric/src/agentic_fabric/core/manager.py b/packages/agentic-fabric/src/agentic_fabric/core/manager.py new file mode 100644 index 0000000..240f35f --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/core/manager.py @@ -0,0 +1,285 @@ +"""Hierarchical manager agent for fabric agent orchestration. + +This module provides a manager agent that can orchestrate multiple fabric agents, +enabling complex workflows with smart delegation, sequential and parallel +execution, and human-in-the-loop checkpoints. + +Example: + ```python + from agentic_fabric.core.manager import ManagerAgent + + class GameDevManager(ManagerAgent): + '''Manager that orchestrates game development fabric agents.''' + + def __init__(self): + super().__init__( + fabric_agents={ + "design": "gameplay_design", + "implementation": "ecs_implementation", + "assets": "asset_pipeline", + "qa": "qa_validation", + } + ) + + async def execute_workflow(self, task: str): + # Sequential execution + design_result = await self.delegate_async("design", task) + + # Parallel execution + impl_result, asset_result = await asyncio.gather( + self.delegate_async("implementation", design_result), + self.delegate_async("assets", design_result) + ) + + # Final QA + return await self.delegate_async("qa", { + "implementation": impl_result, + "assets": asset_result, + }) + ``` +""" + +from __future__ import annotations + +import asyncio +import logging + +from pathlib import Path +from typing import Any + +from agentic_fabric.core.decomposer import run_fabric_agent_auto +from agentic_fabric.core.discovery import discover_packages, get_fabric_agent_config + + +logger = logging.getLogger(__name__) + + +class ManagerAgent: + """Base class for hierarchical manager agents. + + A manager agent orchestrates multiple specialized fabric agents to accomplish + complex tasks that require coordination between different domains or phases. + + Attributes: + fabric_agents: Dict mapping fabric agent role names to fabric agent names in packages. + package_name: Optional package name if all fabric agents are in one package. + workspace_root: Optional workspace root for fabric agent discovery. + """ + + def __init__( + self, + fabric_agents: dict[str, str], + package_name: str | None = None, + workspace_root: Path | None = None, + ): + """Initialize the manager agent. + + Args: + fabric_agents: Dict mapping role names to fabric agent names (e.g., {"design": "game_design"}). + package_name: Optional package name if all fabric agents are in the same package. + workspace_root: Optional workspace root for discovering packages. + """ + self.fabric_agents = fabric_agents + self.package_name = package_name + self.workspace_root = workspace_root + self._packages_cache: dict[str, Path] | None = None + self._fabric_agent_config_cache: dict[str, dict[str, Any]] = {} + + def _get_packages(self) -> dict[str, Path]: + """Get discovered packages, using cache if available.""" + if self._packages_cache is None: + self._packages_cache = discover_packages(self.workspace_root) + return self._packages_cache + + def delegate( + self, + fabric_agent_role: str, + inputs: dict[str, Any] | str, + framework: str | None = None, + ) -> str: + """Delegate a task to a specific fabric agent synchronously. + + Args: + fabric_agent_role: Role name from the fabric_agents dict (e.g., "design"). + inputs: Input dict or string to pass to the fabric agent. + framework: Optional framework override. + + Returns: + Fabric agent output as a string. + + Raises: + ValueError: If fabric_agent_role not found in fabric_agents mapping. + """ + if fabric_agent_role not in self.fabric_agents: + raise ValueError(f"Unknown fabric agent role '{fabric_agent_role}'. Available: {list(self.fabric_agents.keys())}") + + fabric_agent_name = self.fabric_agents[fabric_agent_role] + + # Convert string inputs to dict (create new dict to avoid mutation) + if isinstance(inputs, str): + inputs = {"task": inputs} + + # Check fabric agent config cache first for performance + if fabric_agent_name in self._fabric_agent_config_cache: + cached_config = self._fabric_agent_config_cache[fabric_agent_name] + return run_fabric_agent_auto(cached_config, inputs=inputs, framework=framework) + + # Get fabric agent configuration + fabric_agent_config: dict[str, Any] + if self.package_name: + # Use specified package + packages = self._get_packages() + if self.package_name not in packages: + raise ValueError(f"Package '{self.package_name}' not found. Available: {list(packages.keys())}") + config_dir = packages[self.package_name] + fabric_agent_config = get_fabric_agent_config(config_dir, fabric_agent_name) + else: + # Auto-discover package containing the fabric agent + packages = self._get_packages() + found_config: dict[str, Any] | None = None + for pkg_dir in packages.values(): + try: + found_config = get_fabric_agent_config(pkg_dir, fabric_agent_name) + break + except ValueError: + continue + + if found_config is None: + raise ValueError( + f"Fabric agent '{fabric_agent_name}' not found in any package. " + f"Available packages: {list(packages.keys())}" + ) + fabric_agent_config = found_config + + # Cache the config for future calls + self._fabric_agent_config_cache[fabric_agent_name] = fabric_agent_config + return run_fabric_agent_auto(fabric_agent_config, inputs=inputs, framework=framework) + + async def delegate_async( + self, + fabric_agent_role: str, + inputs: dict[str, Any] | str, + framework: str | None = None, + ) -> str: + """Delegate a task to a specific fabric agent asynchronously. + + This runs the fabric agent in a thread pool to avoid blocking the event loop. + + Args: + fabric_agent_role: Role name from the fabric_agents dict (e.g., "design"). + inputs: Input dict or string to pass to the fabric agent. + framework: Optional framework override. + + Returns: + Fabric agent output as a string. + """ + loop = asyncio.get_running_loop() + return await loop.run_in_executor(None, self.delegate, fabric_agent_role, inputs, framework) + + async def delegate_parallel( + self, + delegations: list[tuple[str, dict[str, Any] | str]], + framework: str | None = None, + ) -> list[str]: + """Delegate tasks to multiple fabric agents in parallel. + + Args: + delegations: List of (fabric_agent_role, inputs) tuples. + framework: Optional framework override for all fabric agents. + + Returns: + List of fabric_agent outputs in the same order as delegations. + + Example: + ```python + results = await manager.delegate_parallel([ + ("design", "Create game concept"), + ("assets", "Generate placeholder assets"), + ]) + design_result, assets_result = results + ``` + """ + tasks = [self.delegate_async(fabric_agent_role, inputs, framework) for fabric_agent_role, inputs in delegations] + return await asyncio.gather(*tasks) + + def delegate_sequential( + self, + delegations: list[tuple[str, dict[str, Any] | str]], + framework: str | None = None, + ) -> list[str]: + """Delegate tasks to multiple fabric agents sequentially. + + Args: + delegations: List of (fabric_agent_role, inputs) tuples. + framework: Optional framework override for all fabric agents. + + Returns: + List of fabric_agent outputs in the same order as delegations. + + Example: + ```python + results = manager.delegate_sequential([ + ("design", "Create game concept"), + ("implementation", "Implement the design"), + ("qa", "Test the implementation"), + ]) + ``` + """ + results = [] + for fabric_agent_role, inputs in delegations: + result = self.delegate(fabric_agent_role, inputs, framework) + results.append(result) + return results + + def checkpoint( + self, + message: str, + result: Any, + auto_approve: bool = False, + ) -> tuple[bool, Any]: + """Create a human-in-the-loop checkpoint. + + Args: + message: Message to display to the human reviewer. + result: Current result to review. + auto_approve: If True, automatically approve without waiting. + + Returns: + Tuple of (approved, result). If not approved, result may be modified. + + Note: + In this base implementation, checkpoints are logged but auto-approved. + Subclasses can override to implement actual HITL workflows. + """ + logger.info("=" * 60) + logger.info("CHECKPOINT: %s", message) + logger.info("=" * 60) + # Log result type only to avoid exposing sensitive data (CWE-532) + logger.info("Result type: %s", type(result).__name__) + logger.info("=" * 60) + + if auto_approve: + logger.info("Auto-approved (auto_approve=True)") + return True, result + + # Base implementation auto-approves + logger.info("Auto-approved (base implementation)") + return True, result + + async def execute_workflow(self, task: str, **kwargs: Any) -> str: + """Execute the manager's workflow. + + This is the main entry point for the manager agent. Subclasses should + override this method to implement their specific orchestration logic. + + Args: + task: The main task to accomplish. + **kwargs: Additional keyword arguments. + + Returns: + Final result as a string. + + Raises: + NotImplementedError: If not overridden by subclass. + """ + raise NotImplementedError("Subclasses must implement execute_workflow() to define their orchestration logic") diff --git a/packages/agentic-fabric/src/agentic_fabric/core/runner.py b/packages/agentic-fabric/src/agentic_fabric/core/runner.py new file mode 100644 index 0000000..8dbadc0 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/core/runner.py @@ -0,0 +1,70 @@ +"""Runner module - executes fabric agents with inputs.""" + +from __future__ import annotations + +from pathlib import Path + +from agentic_fabric.core.discovery import discover_packages, get_fabric_agent_config +from agentic_fabric.core.loader import load_fabric_agent_from_config + + +def run_fabric_agent( + package_name: str, + fabric_agent_name: str, + inputs: dict | None = None, + workspace_root: Path | None = None, +) -> str: + """Run a fabric agent from a package with the given inputs. + + Args: + package_name: Name of the package. + fabric_agent_name: Name of the fabric agent to run (e.g., 'game_builder'). + inputs: Optional dict of inputs to pass to the fabric agent. + workspace_root: Optional workspace root path. + + Returns: + The fabric agent's output as a string. + + Raises: + ValueError: If package or fabric agent not found. + """ + # Discover packages + packages = discover_packages(workspace_root) + + if package_name not in packages: + available = list(packages.keys()) + raise ValueError(f"Package '{package_name}' not found. Available: {available}") + + config_dir = packages[package_name] + + # Load fabric agent configuration + fabric_agent_config = get_fabric_agent_config(config_dir, fabric_agent_name) + + # Build the fabric agent + fabric_agent = load_fabric_agent_from_config(fabric_agent_config) + + # Run it + result = fabric_agent.kickoff(inputs=inputs or {}) + + return result.raw if hasattr(result, "raw") else str(result) + + +def run_fabric_agent_from_path( + config_dir: Path, + fabric_agent_name: str, + inputs: dict | None = None, +) -> str: + """Run a fabric agent directly from a fabric config directory path. + + Args: + config_dir: Path to the .fabric/ or runtime-specific config directory. + fabric_agent_name: Name of the fabric agent to run. + inputs: Optional dict of inputs to pass to the fabric agent. + + Returns: + The fabric agent's output as a string. + """ + fabric_agent_config = get_fabric_agent_config(config_dir, fabric_agent_name) + fabric_agent = load_fabric_agent_from_config(fabric_agent_config) + result = fabric_agent.kickoff(inputs=inputs or {}) + return result.raw if hasattr(result, "raw") else str(result) diff --git a/packages/agentic-fabric/src/agentic_fabric/fabric_agents/__init__.py b/packages/agentic-fabric/src/agentic_fabric/fabric_agents/__init__.py new file mode 100644 index 0000000..77a96a8 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/fabric_agents/__init__.py @@ -0,0 +1 @@ +"""Built-in fabric agent definitions.""" diff --git a/packages/agentic-fabric/src/agentic_fabric/fabric_agents/connector_builder/__init__.py b/packages/agentic-fabric/src/agentic_fabric/fabric_agents/connector_builder/__init__.py new file mode 100644 index 0000000..1968cc7 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/fabric_agents/connector_builder/__init__.py @@ -0,0 +1 @@ +"""Connector-builder fabric agent.""" diff --git a/packages/agentic-fabric/src/agentic_fabric/fabric_agents/connector_builder/config/agents.yaml b/packages/agentic-fabric/src/agentic_fabric/fabric_agents/connector_builder/config/agents.yaml new file mode 100644 index 0000000..31c2671 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/fabric_agents/connector_builder/config/agents.yaml @@ -0,0 +1,16 @@ +doc_scraper: + role: API Documentation Scraper + goal: Extract endpoint information from online API documentation + tools: + - ScrapeWebsiteTool + - CrawlWebsiteTool + +api_analyzer: + role: API Structure Analyst + goal: Create a structured, machine-readable representation of the API from scraped documentation + +code_generator: + role: Python HTTP Client Generator + goal: Generate a vendor-fabric compatible Python HTTP client from a structured API representation + tools: + - FileWriteTool diff --git a/packages/agentic-fabric/src/agentic_fabric/fabric_agents/connector_builder/config/tasks.yaml b/packages/agentic-fabric/src/agentic_fabric/fabric_agents/connector_builder/config/tasks.yaml new file mode 100644 index 0000000..da9c510 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/fabric_agents/connector_builder/config/tasks.yaml @@ -0,0 +1,14 @@ +scrape_docs: + description: Scrape the API documentation from the provided URL to extract endpoint information. + expected_output: Raw text containing API endpoint details, schemas, and authentication methods. + agent: doc_scraper + +analyze_api: + description: Analyze the raw documentation text and create a structured representation of the API. + expected_output: A JSON object detailing API endpoints, methods, parameters, and schemas. + agent: api_analyzer + +generate_code: + description: Generate Python code for a vendor-fabric compatible HTTP client based on the structured API data in the {output_dir} directory. + expected_output: A set of Python files (models.py, client.py, exceptions.py, __init__.py) in the specified output directory. + agent: code_generator diff --git a/packages/agentic-fabric/src/agentic_fabric/fabric_agents/connector_builder/connector_builder_fabric.py b/packages/agentic-fabric/src/agentic_fabric/fabric_agents/connector_builder/connector_builder_fabric.py new file mode 100644 index 0000000..257424e --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/fabric_agents/connector_builder/connector_builder_fabric.py @@ -0,0 +1,108 @@ +"""Connector builder fabric agent. + +This script creates and initializes the ``connector_builder`` fabric agent, which is +designed to automatically generate HTTP connector code by scraping API +documentation. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from agentic_fabric.runners.registry import install_command +from agentic_fabric.tools.registry import resolve_tools +from agentic_fabric.utils import load_config + + +Agent: Any | None = None +Crew: Any | None = None +Task: Any | None = None + + +def _load_crewai_classes() -> tuple[Any, Any, Any]: + """Load CrewAI classes only when the connector-builder fabric agent is used.""" + global Agent, Crew, Task + + if Agent is None or Crew is None or Task is None: + try: + from crewai import Agent as ImportedAgent + from crewai import Crew as ImportedCrew + from crewai import Task as ImportedTask + except ImportError as exc: + msg = f"ConnectorBuilderFabricAgent requires CrewAI. Install with: {install_command('crewai')}" + raise RuntimeError(msg) from exc + + if Agent is None: + Agent = ImportedAgent + if Crew is None: + Crew = ImportedCrew + if Task is None: + Task = ImportedTask + + return Agent, Crew, Task + + +class ConnectorBuilderFabricAgent: + """Manage the agents and tasks for the connector builder fabric agent. + + This class loads agent and task configurations from YAML files, instantiates + the necessary CrewAI components, and provides a method to execute the + fabric agent's workflow. + + Attributes: + fabric_agent: An instance of the CrewAI Crew configured with agents and + tasks for connector building. + """ + + def __init__(self, output_dir: str = "output"): + """Initializes the ConnectorBuilderFabricAgent. + + Loads agent and task configurations from YAML files, creates Agent + and Task objects, and assembles them into a Crew. + + Args: + output_dir: The directory where the generated connector code + will be saved. + """ + agent_cls, crew_cls, task_cls = _load_crewai_classes() + config_dir = Path(__file__).parent / "config" + agent_config = load_config(config_dir / "agents.yaml") + task_config = load_config(config_dir / "tasks.yaml") + + def build_agent(name: str) -> Any: + config = agent_config[name].copy() + config["tools"] = resolve_tools(config.get("tools", [])) + return agent_cls(**config) + + # Create Agents + self.doc_scraper = build_agent("doc_scraper") + self.api_analyzer = build_agent("api_analyzer") + self.code_generator = build_agent("code_generator") + + # Create Tasks + self.scrape_docs = task_cls(**task_config["scrape_docs"]) + self.analyze_api = task_cls(**task_config["analyze_api"]) + + generate_code_config = task_config["generate_code"].copy() + generate_code_config["description"] = generate_code_config["description"].format(output_dir=output_dir) + self.generate_code = task_cls(**generate_code_config) + + self.fabric_agent = crew_cls( + agents=[self.doc_scraper, self.api_analyzer, self.code_generator], + tasks=[self.scrape_docs, self.analyze_api, self.generate_code], + verbose=True, + ) + + def kickoff(self, inputs: dict) -> str: + """Start the fabric agent's execution with the given inputs. + + Args: + inputs: A dictionary containing the necessary inputs for the + fabric agent's tasks, such as the URL of the API documentation. + + Returns: + A string representing the result of the fabric agent's execution. + """ + result = self.fabric_agent.kickoff(inputs=inputs) + return result.raw if hasattr(result, "raw") else str(result) diff --git a/packages/agentic-fabric/src/agentic_fabric/main.py b/packages/agentic-fabric/src/agentic_fabric/main.py new file mode 100644 index 0000000..10c377a --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/main.py @@ -0,0 +1,591 @@ +"""Main entry point for agentic-fabric - framework-agnostic fabric agent runner. + +This is a generic fabric agent runner that discovers and executes fabric agents +defined in packages' .fabric/, .crewai/, .langgraph/, or .strands/ directories. + +Usage: + # List all available packages with fabric agents + agentic-fabric list + agentic-fabric list --json # JSON output for external tools + + # List fabric agents in a specific package + agentic-fabric list my-package + + # Run a fabric agent + agentic-fabric run my-package reviewer --input "Review this code" + agentic-fabric run my-package reviewer --input "..." --json # JSON output + + # Run with input from file + agentic-fabric run my-package reviewer --file tasks.md + + # Show fabric agent details + agentic-fabric info my-package reviewer --json +""" + +from __future__ import annotations + +import argparse +import json +import sys +import time + +from pathlib import Path + +from agentic_fabric.core.discovery import discover_packages, get_fabric_agent_config, list_fabric_agents + + +def cmd_list(args): + """List available packages and fabric agents.""" + framework = getattr(args, "framework", None) + use_json = getattr(args, "json", False) + + fabric_agents_by_package = list_fabric_agents( + args.package if hasattr(args, "package") else None, + framework=framework, + ) + + if use_json: + # Flatten to list for JSON output + all_fabric_agents = [] + for pkg_name, fabric_agents in fabric_agents_by_package.items(): + for fabric_agent in fabric_agents: + all_fabric_agents.append( + { + "package": pkg_name, + "name": fabric_agent["name"], + "description": fabric_agent.get("description", ""), + "required_framework": fabric_agent.get("required_framework"), + } + ) + print(json.dumps({"fabric_agents": all_fabric_agents}, indent=2)) + return + + if not fabric_agents_by_package: + print("No packages with fabric agent configuration directories found.") + print("\nTo add fabric agents to a package, create one of:") + print(" packages//.fabric/manifest.yaml # Framework-agnostic") + print(" packages//.crewai/manifest.yaml # CrewAI-specific") + print(" packages//.langgraph/manifest.yaml # LangGraph-specific") + print(" packages//.strands/manifest.yaml # Strands-specific") + return + + print("=" * 60) + print("AVAILABLE FABRIC AGENTS") + print("=" * 60) + + for pkg_name, fabric_agents in fabric_agents_by_package.items(): + print(f"\n📦 {pkg_name}") + for fabric_agent in fabric_agents: + desc = fabric_agent.get("description", "") + framework_info = "" + if fabric_agent.get("required_framework"): + framework_info = f" [{fabric_agent['required_framework']}]" + print(f" • {fabric_agent['name']}{framework_info}: {desc}") + + +def cmd_run(args): + """Run a specific fabric agent or single-agent task.""" + use_json = getattr(args, "json", False) + start_time = time.time() + + # Check if using single-agent runner + if hasattr(args, "runner") and args.runner: + return _cmd_run_single_agent(args, use_json, start_time) + + # Multi-agent fabric agent execution requires package and fabric_agent. + if not args.package or not args.fabric_agent: + if use_json: + print( + json.dumps( + { + "success": False, + "error": "Package and fabric_agent are required for multi-agent execution. " + "Use --runner for single-agent tasks.", + "duration_ms": int((time.time() - start_time) * 1000), + } + ) + ) + else: + print("❌ Error: Package and fabric_agent are required for multi-agent execution.") + print("Use --runner for single-agent tasks or provide both package and fabric_agent.") + sys.exit(2) + + # Multi-agent fabric agent execution + from agentic_fabric.core.decomposer import detect_framework, run_fabric_agent_auto + + if not use_json: + print("=" * 60) + print(f"🚀 Running {args.package}/{args.fabric_agent}") + print("=" * 60) + + try: + # Get input + if args.file: + input_text = Path(args.file).read_text(encoding="utf-8") + elif args.input: + input_text = args.input + else: + input_text = "" + except OSError as e: + duration_ms = int((time.time() - start_time) * 1000) + if use_json: + print(json.dumps({"success": False, "error": str(e), "duration_ms": duration_ms})) + else: + print(f"❌ Error: {e}") + sys.exit(2) + + inputs = {"spec": input_text, "component_spec": input_text, "input": input_text} + + # Discover package and load config + packages = discover_packages() + if args.package not in packages: + if use_json: + print( + json.dumps( + { + "success": False, + "error": f"Package '{args.package}' not found", + "available_packages": list(packages.keys()), + "duration_ms": int((time.time() - start_time) * 1000), + } + ) + ) + else: + print(f"❌ Package '{args.package}' not found.") + print(f"Available: {list(packages.keys())}") + sys.exit(2) # Exit code 2 = configuration error + + config_dir = packages[args.package] + + try: + fabric_agent_config = get_fabric_agent_config(config_dir, args.fabric_agent) + + # Determine framework + required = fabric_agent_config.get("required_framework") + requested = args.framework if args.framework != "auto" else None + framework_used = required or requested or detect_framework() + + if not use_json: + if required: + print(f"📋 Framework: {required} (required by .{required}/ directory)") + elif requested: + print(f"📋 Framework: {requested} (requested)") + else: + print(f"📋 Framework: {framework_used} (auto-detected)") + + result = run_fabric_agent_auto( + fabric_agent_config, + inputs=inputs, + framework=framework_used, + ) + + duration_ms = int((time.time() - start_time) * 1000) + + if use_json: + print( + json.dumps( + { + "success": True, + "output": result, + "framework_used": framework_used, + "duration_ms": duration_ms, + } + ) + ) + else: + print("\n" + "=" * 60) + print("📄 RESULT") + print("=" * 60) + print(result) + + except (ValueError, RuntimeError) as e: + duration_ms = int((time.time() - start_time) * 1000) + if use_json: + print( + json.dumps( + { + "success": False, + "error": str(e), + "duration_ms": duration_ms, + } + ) + ) + else: + print(f"❌ Error: {e}") + sys.exit(1) # Exit code 1 = fabric agent execution failed + + +def _cmd_run_single_agent(args, use_json: bool, start_time: float): + """Run a task with a single-agent CLI runner.""" + from agentic_fabric.core.decomposer import get_available_cli_runners, get_cli_runner + + if not use_json: + print("=" * 60) + print(f"🤖 Running single-agent: {args.runner}") + print("=" * 60) + + try: + # Get input + if args.file: + input_text = Path(args.file).read_text(encoding="utf-8") + elif args.input: + input_text = args.input + else: + input_text = "" + except OSError as e: + duration_ms = int((time.time() - start_time) * 1000) + if use_json: + print(json.dumps({"success": False, "error": str(e), "duration_ms": duration_ms})) + else: + print(f"❌ Error: {e}") + sys.exit(2) + + if not input_text: + if use_json: + print( + json.dumps( + { + "success": False, + "error": "No input provided. Use --input or --file", + "duration_ms": int((time.time() - start_time) * 1000), + } + ) + ) + else: + print("❌ Error: No input provided. Use --input or --file") + sys.exit(2) + + # Get working directory if package specified + working_dir = None + if hasattr(args, "package") and args.package: + packages = discover_packages() + if args.package in packages: + # Use package directory as working dir + working_dir = str(packages[args.package].parent) + + try: + # Get runner + runner = get_cli_runner( + args.runner, + model=getattr(args, "model", None), + ) + + # Check if available + if not runner.is_available(): + available = get_available_cli_runners() + if use_json: + print( + json.dumps( + { + "success": False, + "error": f"Runner '{args.runner}' not available (tool not installed)", + "available_runners": available, + "duration_ms": int((time.time() - start_time) * 1000), + } + ) + ) + else: + print(f"❌ Runner '{args.runner}' not available (tool not installed)") + print(f"\nAvailable runners: {', '.join(available)}") + print("\nTo install, check: agentic-fabric list-runners --json") + sys.exit(2) + + if not use_json: + runner_label = getattr(getattr(runner, "config", None), "name", None) or args.runner + print(f"📋 Runner: {runner_label}") + if working_dir: + print(f"📁 Working dir: {working_dir}") + + # Run the task + result = runner.run( + task=input_text, + working_dir=working_dir, + auto_approve=getattr(args, "auto_approve", True), + ) + + duration_ms = int((time.time() - start_time) * 1000) + + if use_json: + print( + json.dumps( + { + "success": True, + "output": result, + "runner": args.runner, + "duration_ms": duration_ms, + } + ) + ) + else: + print("\n" + "=" * 60) + print("📄 RESULT") + print("=" * 60) + print(result) + + except (ValueError, RuntimeError, FileNotFoundError) as e: + duration_ms = int((time.time() - start_time) * 1000) + if use_json: + print( + json.dumps( + { + "success": False, + "error": str(e), + "duration_ms": duration_ms, + } + ) + ) + else: + print(f"❌ Error: {e}") + sys.exit(1) + + +def cmd_info(args): + """Show detailed info about a fabric agent.""" + use_json = getattr(args, "json", False) + packages = discover_packages() + + if args.package not in packages: + if use_json: + print( + json.dumps( + { + "error": f"Package '{args.package}' not found", + "available_packages": list(packages.keys()), + } + ) + ) + else: + print(f"❌ Package '{args.package}' not found.") + print(f"Available: {list(packages.keys())}") + sys.exit(2) + + config_dir = packages[args.package] + + try: + config = get_fabric_agent_config(config_dir, args.fabric_agent) + except ValueError as e: + if use_json: + print(json.dumps({"error": str(e)})) + else: + print(f"❌ {e}") + sys.exit(2) + + if use_json: + print( + json.dumps( + { + "package": args.package, + "name": args.fabric_agent, + "description": config.get("description", ""), + "required_framework": config.get("required_framework"), + "agents": [ + {"name": name, "role": cfg.get("role", name)} for name, cfg in config.get("agents", {}).items() + ], + "tasks": [ + {"name": name, "description": cfg.get("description", "")} + for name, cfg in config.get("tasks", {}).items() + ], + "knowledge_paths": config.get("knowledge_paths", []), + }, + indent=2, + ) + ) + return + + print("=" * 60) + print(f"FABRIC AGENT: {args.package}/{args.fabric_agent}") + print("=" * 60) + print(f"\nDescription: {config.get('description', 'N/A')}") + + print("\n📋 Agents:") + for name, cfg in config.get("agents", {}).items(): + role = cfg.get("role", name) + print(f" • {name}: {role}") + + print("\n📝 Tasks:") + for name, cfg in config.get("tasks", {}).items(): + desc = cfg.get("description", "")[:60] + print(f" • {name}: {desc}...") + + print("\n📚 Knowledge:") + for kp in config.get("knowledge_paths", []): + print(f" • {kp}") + + +def cmd_list_runners(args): + """List available single-agent CLI runners.""" + from agentic_fabric.core.decomposer import ( + get_available_cli_runners, + get_cli_runner, + ) + + use_json = getattr(args, "json", False) + + try: + profiles = get_available_cli_runners() + + if use_json: + runners_info = [] + for profile in profiles: + try: + runner = get_cli_runner(profile) + runners_info.append( + { + "name": profile, + "display_name": runner.config.name, + "description": runner.config.description, + "available": runner.is_available(), + "install_cmd": runner.config.install_cmd, + "docs_url": runner.config.docs_url, + "required_env": runner.get_required_env_vars(), + } + ) + except Exception as e: + # Skip profiles that fail to load, but warn the user + print( + f"Warning: Could not load profile '{profile}': {e}", + file=sys.stderr, + ) + continue + + print(json.dumps({"runners": runners_info}, indent=2)) + else: + print("=" * 60) + print("AVAILABLE SINGLE-AGENT CLI RUNNERS") + print("=" * 60) + print() + + for profile in profiles: + try: + runner = get_cli_runner(profile) + available = "✅" if runner.is_available() else "❌" + print(f"{available} {profile}: {runner.config.description}") + + if not runner.is_available(): + print(f" Install: {runner.config.install_cmd}") + + if runner.get_required_env_vars(): + print(f" Requires: {', '.join(runner.get_required_env_vars())}") + + print() + except Exception as e: + # Skip profiles that fail to load, but warn the user + print( + f"Warning: Could not load profile '{profile}': {e}", + file=sys.stderr, + ) + continue + + except FileNotFoundError as e: + if use_json: + print(json.dumps({"error": str(e)})) + else: + print(f"❌ Error: {e}") + sys.exit(2) + + +def main(): + """CLI entry point.""" + parser = argparse.ArgumentParser( + description="agentic-fabric - framework-agnostic fabric agent runner", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # List all packages with fabric agents + agentic-fabric list + agentic-fabric list --json # JSON output for external tools + + # List fabric agents in a package + agentic-fabric list my-package + + # List available single-agent runners + agentic-fabric list-runners + agentic-fabric list-runners --json + + # Run a multi-agent fabric agent + agentic-fabric run my-package reviewer --input "Review this code" + agentic-fabric run my-package reviewer --input "..." --json # JSON output + + # Run with single-agent CLI runner + agentic-fabric run --runner aider --input "Add error handling to auth.py" + agentic-fabric run --runner claude-code --input "Refactor the database module" + agentic-fabric run --runner ollama --input "Fix the bug" --model deepseek-coder + + # Show fabric agent details + agentic-fabric info my-package reviewer --json + +Exit codes: + 0 - Success + 1 - Fabric agent execution failed + 2 - Configuration error (package/fabric_agent not found) + """, + ) + + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # List command + list_parser = subparsers.add_parser("list", help="List available fabric agents") + list_parser.add_argument("package", nargs="?", help="Package to list fabric agents for") + list_parser.add_argument( + "--framework", + choices=["crewai", "langgraph", "strands"], + help="Filter fabric agents by framework", + ) + list_parser.add_argument("--json", action="store_true", help="Output as JSON (for external tools)") + + # List runners command + list_runners_parser = subparsers.add_parser("list-runners", help="List available single-agent CLI runners") + list_runners_parser.add_argument("--json", action="store_true", help="Output as JSON (for external tools)") + + # Run command + run_parser = subparsers.add_parser("run", help="Run a fabric agent or single-agent task") + run_parser.add_argument("package", nargs="?", help="Package name (e.g., my-package)") + run_parser.add_argument("fabric_agent", nargs="?", help="Fabric agent name (e.g., reviewer)") + run_parser.add_argument("--input", "-i", help="Input specification") + run_parser.add_argument("--file", "-f", help="Read input from file") + run_parser.add_argument( + "--framework", + choices=["auto", "crewai", "langgraph", "strands"], + default="auto", + help="Framework to use (auto=detect, or specify). " + "Note: If the fabric agent is in a framework-specific directory, that takes precedence.", + ) + run_parser.add_argument( + "--runner", + help="Single-agent CLI runner to use (e.g., aider, claude-code, ollama). " + "When specified, package/fabric_agent are optional and task is run with the CLI tool.", + ) + run_parser.add_argument( + "--model", + help="Model to use with single-agent runner (if supported by the tool)", + ) + run_parser.add_argument( + "--auto-approve", + action=argparse.BooleanOptionalAction, + default=True, + help="Auto-approve changes (default: true). Use --no-auto-approve to disable.", + ) + run_parser.add_argument("--json", action="store_true", help="Output as JSON (for external tools)") + + # Info command + info_parser = subparsers.add_parser("info", help="Show fabric agent details") + info_parser.add_argument("package", help="Package name") + info_parser.add_argument("fabric_agent", help="Fabric agent name") + info_parser.add_argument("--json", action="store_true", help="Output as JSON (for external tools)") + + args = parser.parse_args() + + if args.command == "list": + cmd_list(args) + elif args.command == "list-runners": + cmd_list_runners(args) + elif args.command == "run": + cmd_run(args) + elif args.command == "info": + cmd_info(args) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/packages/agentic-fabric/src/agentic_fabric/runners/__init__.py b/packages/agentic-fabric/src/agentic_fabric/runners/__init__.py new file mode 100644 index 0000000..f4cc18b --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/runners/__init__.py @@ -0,0 +1,32 @@ +"""Framework-specific runners for agentic-fabric. + +Each runner implements the same interface but targets a different AI framework: +- CrewAIRunner: Full-featured, best for complex fabric_agents +- LangGraphRunner: Graph-based flows, good for conditional logic +- StrandsRunner: Lightweight, AWS-native + +Single-agent runners provide simpler execution without multi-agent overhead: +- LocalCLIRunner: Universal runner for any CLI-based coding agent (aider, claude-code, ollama, etc.) + +Usage: + # Multi-agent fabric_agents + from agentic_fabric.runners import get_runner + + runner = get_runner("crewai") # Or "langgraph", "strands", "auto" + fabric_agent = runner.build_fabric_agent(config) + result = runner.run(fabric_agent, inputs) + + # Single-agent CLI runners + from agentic_fabric.core.decomposer import get_cli_runner + + runner = get_cli_runner("aider") + result = runner.run("Add error handling to auth.py") +""" + +from __future__ import annotations + +from agentic_fabric.core.decomposer import get_cli_runner, get_framework_info, get_runner +from agentic_fabric.runners.registry import RuntimeSpec, runtime_names, runtime_specs + + +__all__ = ["RuntimeSpec", "get_cli_runner", "get_framework_info", "get_runner", "runtime_names", "runtime_specs"] diff --git a/packages/agentic-fabric/src/agentic_fabric/runners/base.py b/packages/agentic-fabric/src/agentic_fabric/runners/base.py new file mode 100644 index 0000000..7a0d08c --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/runners/base.py @@ -0,0 +1,370 @@ +"""Base runner interface for framework-specific implementations. + +All runners must implement this interface to ensure consistent behavior +across CrewAI, LangGraph, and Strands. + +# Runner Implementation Guide + +To create a new runner for a framework: + +1. **Subclass BaseRunner**: + ```python + from agentic_fabric.runners.base import BaseRunner + + class MyFrameworkRunner(BaseRunner): + framework_name = "myframework" + ``` + +2. **Implement required methods**: + - `build_fabric_agent()` - Convert universal config to a runtime-native fabric agent + - `run()` - Execute the fabric agent and return string output + - `build_agent()` - Create framework-specific agent + - `build_task()` - Create framework-specific task + +3. **Register in decomposer** (core/decomposer.py): + - Add to `FRAMEWORK_PRIORITY` + - Add case to `get_runner()` + +4. **Add optional dependency** (pyproject.toml): + ```toml + [project.optional-dependencies] + myframework = ["myframework-package>=1.0"] + ``` + +# Example Runner + +```python +class SimpleRunner(BaseRunner): + framework_name = "simple" + + def __init__(self): + try: + import simple_framework + except ImportError as e: + raise RuntimeError("simple_framework not installed") from e + + def build_fabric_agent(self, fabric_agent_config): + # Use dict to allow lookup by agent name + agents = { + name: self.build_agent(cfg) + for name, cfg in fabric_agent_config.get("agents", {}).items() + } + + tasks = [] + for task_name, task_cfg in fabric_agent_config.get("tasks", {}).items(): + agent_name = task_cfg.get("agent") + agent = agents[agent_name] + tasks.append(self.build_task(task_cfg, agent)) + + return {"agents": list(agents.values()), "tasks": tasks} + + def run(self, fabric_agent, inputs): + # Execute and return string + return "Result from simple framework" + + def build_agent(self, agent_config, tools=None): + return {"role": agent_config["role"], "tools": tools or []} + + def build_task(self, task_config, agent): + return {"description": task_config["description"], "agent": agent} +``` +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any + +from agentic_fabric.capabilities import AgentCapabilityProviderMixin, runtime_capability + + +class BaseRunner(AgentCapabilityProviderMixin, ABC): + """Abstract base class for framework runners. + + Each framework runner converts agentic-fabric's universal fabric agent format + into framework-specific objects and executes them. + + Attributes: + framework_name: String identifier for this framework (e.g., "crewai", + "langgraph", "strands"). Used for logging and framework selection. + + Example: + Basic runner usage with auto-detection: + + ```python + from agentic_fabric.core.decomposer import get_runner + + # Auto-detect best available framework + runner = get_runner() + + fabric_agent_config = { + "name": "research_fabric_agent", + "agents": { + "researcher": { + "role": "Senior Researcher", + "goal": "Find comprehensive information", + "backstory": "Expert researcher with years of experience" + } + }, + "tasks": { + "research_task": { + "description": "Research the topic thoroughly", + "expected_output": "Detailed research report", + "agent": "researcher" + } + } + } + + # Build and run in one step + result = runner.build_and_run(fabric_agent_config, {"topic": "AI safety"}) + print(result) + ``` + """ + + framework_name: str = "base" + + @runtime_capability( + "build_fabric_agent", + description="Convert universal fabric agent configuration into a runtime-native fabric agent object.", + ) + @abstractmethod + def build_fabric_agent(self, fabric_agent_config: dict[str, Any]) -> Any: + """Build a framework-specific fabric agent from configuration. + + This is the main method that converts agentic-fabric's universal + YAML-based fabric agent configuration into the framework's native objects. + + Args: + fabric_agent_config: Universal fabric agent configuration dict containing: + - name (str): Fabric agent name for identification + - description (str): What the fabric agent does + - agents (dict): Dict mapping agent names to agent configs. + Each agent config has: + - role (str): Agent's role/title + - goal (str): What the agent aims to achieve + - backstory (str): Agent's background/expertise + - tools (list[str], optional): Tool names to enable + - llm (str, optional): Model override + - allow_delegation (bool, optional): Can delegate to others + - tasks (dict): Dict mapping task names to task configs. + Each task config has: + - description (str): What to do + - expected_output (str): What output looks like + - agent (str): Name of agent to execute this task + - context (list[str], optional): Names of prior tasks + - knowledge_paths (list[Path], optional): Directories with + knowledge files (.md, .txt, etc.) + - process (str, optional): "sequential" or "hierarchical" + - required_framework (str, optional): Enforced framework + + Returns: + Framework-specific fabric agent object. Type varies by framework: + - CrewAI: crewai.Crew + - LangGraph: Compiled StateGraph + - Strands: strands.Agent + + Raises: + ValueError: If configuration is invalid (e.g., unknown agent + referenced in task). + RuntimeError: If framework is not available. + + Example: + ```python + runner = CrewAIRunner() + fabric_agent = runner.build_fabric_agent({ + "name": "writer_fabric_agent", + "agents": { + "writer": { + "role": "Technical Writer", + "goal": "Create clear documentation", + "backstory": "10 years writing experience" + } + }, + "tasks": { + "write_docs": { + "description": "Write API documentation for {topic}", + "expected_output": "Markdown documentation file", + "agent": "writer" + } + } + }) + ``` + """ + + @runtime_capability("run", description="Execute a runtime-native fabric agent object.") + @abstractmethod + def run(self, fabric_agent: Any, inputs: dict[str, Any]) -> str: + r"""Execute the fabric agent with inputs. + + Runs the previously built fabric agent with the given inputs. This method + handles the framework-specific execution and normalizes the output + to a string. + + Args: + fabric_agent: Framework-specific fabric agent object from build_fabric_agent(). + Type depends on framework: + - CrewAI: crewai.Crew instance + - LangGraph: Compiled StateGraph + - Strands: strands.Agent instance + inputs: Input dict to pass to the fabric agent. Keys are typically + variable names used in task descriptions (e.g., {topic} + in description becomes inputs["topic"]). + + Returns: + Fabric agent output as a string. For complex outputs, the raw text + is returned; parsing is left to the caller. + + Raises: + RuntimeError: If fabric agent execution fails. + + Example: + ```python + # CrewAI runner example + result = runner.run(fabric_agent, {"topic": "machine learning"}) + print(result) # "## Machine Learning Report\\n\\n..." + + # LangGraph runner example + result = runner.run(graph, {"input": "analyze this code"}) + ``` + """ + + @runtime_capability("build_and_run", aliases=("run_fabric_agent",), description="Build and execute a fabric agent in one call.") + def build_and_run( + self, + fabric_agent_config: dict[str, Any], + inputs: dict[str, Any] | None = None, + ) -> str: + """Convenience method to build and run in one step. + + Args: + fabric_agent_config: Fabric agent configuration. + inputs: Optional inputs for the fabric agent. + + Returns: + Fabric agent output as string. + """ + fabric_agent = self.build_fabric_agent(fabric_agent_config) + return self.run(fabric_agent, inputs or {}) + + @runtime_capability("build_agent", description="Create one runtime-native agent object.") + @abstractmethod + def build_agent(self, agent_config: dict[str, Any], tools: list | None = None) -> Any: + """Build a framework-specific agent. + + Creates a single agent from the universal configuration. Called by + build_fabric_agent() for each agent in the fabric agent. + + Args: + agent_config: Agent configuration dict containing: + - role (str): Agent's role/title (e.g., "Senior Researcher") + - goal (str): What the agent aims to achieve + - backstory (str): Agent's background and expertise + - llm (str, optional): Model name to override default + - allow_delegation (bool, optional): Can delegate tasks + - verbose (bool, optional): Enable verbose output + tools: Optional list of tool instances to give the agent. + Tools should already be resolved/instantiated. + + Returns: + Framework-specific agent object: + - CrewAI: crewai.Agent + - LangGraph: Compiled ReAct agent graph + - Strands: strands.Agent + + Example: + ```python + # CrewAI + agent = runner.build_agent({ + "role": "Data Analyst", + "goal": "Analyze data and provide insights", + "backstory": "Expert in statistical analysis" + }, tools=[search_tool, calculator_tool]) + ``` + """ + + @runtime_capability("build_task", description="Create one runtime-native task object.") + @abstractmethod + def build_task(self, task_config: dict[str, Any], agent: Any) -> Any: + """Build a framework-specific task. + + Creates a single task from the universal configuration. Called by + build_fabric_agent() for each task in the fabric agent. + + Note: Some frameworks (like CrewAI) support additional parameters + beyond the base signature. Subclasses may extend this method with + optional parameters like `context` for task dependencies. + + Args: + task_config: Task configuration dict containing: + - description (str): What the task should accomplish. + Can include {placeholders} for runtime inputs. + - expected_output (str): Description of expected output format + - async_execution (bool, optional): Run asynchronously + - human_input (bool, optional): Request human feedback + agent: Framework-specific agent to assign to this task. + Must be the return value from build_agent(). + + Returns: + Framework-specific task object: + - CrewAI: crewai.Task + - LangGraph: dict (tasks are stored as config) + - Strands: dict (tasks are stored as config) + + Example: + ```python + # Basic task + task = runner.build_task({ + "description": "Research {topic} thoroughly", + "expected_output": "Comprehensive research report" + }, agent=researcher_agent) + + # CrewAI with context (extended signature) + task = crewai_runner.build_task( + task_config, + agent, + context=[prior_research_task] # CrewAI-specific + ) + ``` + """ + + @runtime_capability("get_llm", description="Return the runtime-compatible LLM object.") + def get_llm(self, model: str | None = None) -> Any: + """Get the LLM for this framework. + + Retrieves or creates an LLM instance. The default implementation + uses agentic-fabric's shared LLM configuration. Subclasses may + override for framework-specific LLM handling. + + Args: + model: Optional model name override. If None, uses DEFAULT_MODEL + from agentic_fabric.config.llm (usually claude-sonnet-4-20250514). + + Returns: + Framework-specific LLM object, or None if the LLM module + is not available (framework may have its own default). + - CrewAI: LiteLLM-compatible LLM + - LangGraph: langchain ChatModel + - Strands: Uses model_id directly + + Example: + ```python + # Use default model + llm = runner.get_llm() + + # Use specific model + llm = runner.get_llm("claude-haiku-4-5-20251001") + + # In LangGraphRunner (overridden) + llm = runner.get_llm("claude-sonnet-4-20250514") # Returns ChatAnthropic + ``` + """ + # Default implementation - subclasses can override + # Import lazily to avoid requiring crewai at module load time + try: + from agentic_fabric.config.llm import DEFAULT_MODEL, get_llm + + # Use default model if none specified to avoid AttributeError + return get_llm(model or DEFAULT_MODEL) + except ImportError: + # If llm module not available, return None (framework may have its own default) + return None diff --git a/packages/agentic-fabric/src/agentic_fabric/runners/crewai_runner.py b/packages/agentic-fabric/src/agentic_fabric/runners/crewai_runner.py new file mode 100644 index 0000000..2642f68 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/runners/crewai_runner.py @@ -0,0 +1,206 @@ +"""CrewAI runner implementation. + +This is the default and most full-featured runner. CrewAI provides: +- Hierarchical and sequential processes +- Memory and planning +- Knowledge sources +- Tool integration +- Delegation between agents +""" + +from __future__ import annotations + +import logging + +from pathlib import Path +from typing import Any + +from agentic_fabric.runners.base import BaseRunner +from agentic_fabric.runners.registry import install_command +from agentic_fabric.tools.registry import resolve_tools + + +logger = logging.getLogger(__name__) + + +class CrewAIRunner(BaseRunner): + """Runner that uses CrewAI for fabric agent execution.""" + + framework_name = "crewai" + + def __init__(self): + """Initialize CrewAI runner.""" + # Verify CrewAI is available + try: + import crewai # noqa: F401 + except ImportError as e: + raise RuntimeError(f"CrewAI not installed. Install with: {install_command(self.framework_name)}") from e + + def build_fabric_agent(self, fabric_agent_config: dict[str, Any]) -> Any: + """Build a CrewAI Crew from configuration. + + Args: + fabric_agent_config: Universal fabric agent configuration. + + Returns: + CrewAI Crew object. + """ + from crewai import Crew, Process + + # Build agents with their tools from config + agents_config = fabric_agent_config.get("agents", {}) + agents = {} + for agent_name, agent_cfg in agents_config.items(): + # Extract tools from agent config + agent_tools = self._resolve_tools(agent_cfg.get("tools", [])) + agents[agent_name] = self.build_agent(agent_cfg, tools=agent_tools) + + # Build tasks, tracking them for context dependencies + tasks_config = fabric_agent_config.get("tasks", {}) + tasks = [] + tasks_by_name: dict[str, Any] = {} + for task_name, task_cfg in tasks_config.items(): + agent_name = task_cfg.get("agent") + if not agent_name or agent_name not in agents: + raise ValueError(f"Task '{task_name}' has invalid agent: {agent_name}") + + # Build context from referenced tasks + context_tasks = [] + unresolved_context = [] + for ctx_name in task_cfg.get("context", []): + if ctx_name in tasks_by_name: + context_tasks.append(tasks_by_name[ctx_name]) + else: + unresolved_context.append(ctx_name) + + if unresolved_context: + logger.warning( + "Task '%s' references context tasks that are not yet available: %s", + task_name, + ", ".join(unresolved_context), + ) + + task = self.build_task(task_cfg, agents[agent_name], context=context_tasks) + tasks.append(task) + tasks_by_name[task_name] = task + + # Load knowledge sources + knowledge_sources = self._load_knowledge(fabric_agent_config.get("knowledge_paths", [])) + + # Determine process type + process_type = fabric_agent_config.get("process", "sequential") + process = Process.hierarchical if process_type == "hierarchical" else Process.sequential + + return Crew( + agents=list(agents.values()), + tasks=tasks, + process=process, + planning=fabric_agent_config.get("planning", True), + memory=fabric_agent_config.get("memory", True), + knowledge_sources=knowledge_sources or None, + verbose=fabric_agent_config.get("verbose", True), + ) + + def run(self, fabric_agent: Any, inputs: dict[str, Any]) -> str: + """Execute the CrewAI fabric agent. + + Args: + fabric_agent: CrewAI Crew object. + inputs: Inputs for the fabric agent. + + Returns: + Fabric agent output as string. + """ + result = fabric_agent.kickoff(inputs=inputs) + return result.raw if hasattr(result, "raw") else str(result) + + def build_agent(self, agent_config: dict[str, Any], tools: list | None = None) -> Any: + """Build a CrewAI Agent. + + Args: + agent_config: Agent configuration. + tools: Optional tools for the agent. + + Returns: + CrewAI Agent object. + """ + from crewai import Agent + + return Agent( + role=agent_config.get("role", "Agent"), + goal=agent_config.get("goal", ""), + backstory=agent_config.get("backstory", ""), + llm=self.get_llm(agent_config.get("llm")), + tools=tools or [], + allow_delegation=agent_config.get("allow_delegation", False), + verbose=True, + ) + + def build_task( + self, + task_config: dict[str, Any], + agent: Any, + context: list | None = None, + ) -> Any: + """Build a CrewAI Task. + + Args: + task_config: Task configuration. + agent: Agent to assign to the task. + context: Optional list of tasks this task depends on. + + Returns: + CrewAI Task object. + """ + from crewai import Task + + task_kwargs = { + "description": task_config.get("description", ""), + "expected_output": task_config.get("expected_output", ""), + "agent": agent, + } + + # Add context if provided (for task chaining) + if context: + task_kwargs["context"] = context + + return Task(**task_kwargs) + + def _resolve_tools(self, tool_names: list[str]) -> list: + """Resolve tool names to actual tool instances. + + Args: + tool_names: List of tool names from config. + + Returns: + List of tool instances. + """ + return resolve_tools(tool_names) + + def _load_knowledge(self, knowledge_paths: list[Path]) -> list: + """Load knowledge sources from paths. + + Args: + knowledge_paths: List of knowledge directory paths. + + Returns: + List of CrewAI knowledge source objects. + """ + from crewai.knowledge.source.text_file_knowledge_source import TextFileKnowledgeSource + + sources = [] + for knowledge_path_value in knowledge_paths: + knowledge_path = knowledge_path_value if isinstance(knowledge_path_value, Path) else Path(knowledge_path_value) + if not knowledge_path.is_dir(): + continue + + for ext in ["*.md", "*.txt", "*.py", "*.ts"]: + for file_path in knowledge_path.rglob(ext): + try: + content = file_path.read_text(encoding="utf-8") + if content.strip(): + sources.append(TextFileKnowledgeSource(file_paths=[str(file_path)])) + except OSError as exc: + logger.warning("Could not load knowledge file %s: %s", file_path, exc) + + return sources diff --git a/packages/agentic-fabric/src/agentic_fabric/runners/langgraph_runner.py b/packages/agentic-fabric/src/agentic_fabric/runners/langgraph_runner.py new file mode 100644 index 0000000..105755e --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/runners/langgraph_runner.py @@ -0,0 +1,136 @@ +"""LangGraph runner implementation. + +LangGraph excels at: +- Complex conditional flows +- State management +- Cycles and loops +- Integration with LangChain ecosystem +""" + +from __future__ import annotations + +from typing import Any + +from agentic_fabric.runners.base import BaseRunner +from agentic_fabric.runners.registry import install_command +from agentic_fabric.tools.adapters import resolve_langgraph_tools + + +class LangGraphRunner(BaseRunner): + """Runner that uses LangGraph for fabric agent execution.""" + + framework_name = "langgraph" + + def __init__(self): + """Initialize LangGraph runner.""" + try: + import langgraph # noqa: F401 + except ImportError as e: + raise RuntimeError(f"LangGraph not installed. Install with: {install_command(self.framework_name)}") from e + + def build_fabric_agent(self, fabric_agent_config: dict[str, Any]) -> Any: + """Build a LangGraph ReAct agent from fabric agent configuration. + + The current adapter condenses the universal fabric agent configuration into a + LangGraph prebuilt ReAct agent with the declared tools attached. + + Args: + fabric_agent_config: Universal fabric agent configuration. + + Returns: + LangGraph runnable agent. + """ + from langgraph.prebuilt import create_react_agent + + # For simple fabric agents, create a ReAct agent. + # More complex fabric agents could be converted to full StateGraphs. + + # Get LLM from config (respects the framework's configuration) + llm_config = fabric_agent_config.get("llm", {}) + model = llm_config.get("model") if isinstance(llm_config, dict) else llm_config + llm = self.get_llm(model) + + # Build tools declared by agents in the universal config + tools = self._build_tools_from_config(fabric_agent_config) + + return create_react_agent(llm, tools) + + def run(self, fabric_agent: Any, inputs: dict[str, Any]) -> str: + """Execute the LangGraph workflow. + + Args: + fabric_agent: Compiled LangGraph. + inputs: Inputs for the workflow. + + Returns: + Workflow output as string. + """ + # Convert inputs to messages format + user_message = inputs.get("input", inputs.get("task", str(inputs))) + + result = fabric_agent.invoke({"messages": [("user", user_message)]}) + + # Extract final message + messages = result.get("messages", []) + if messages: + final = messages[-1] + return final.content if hasattr(final, "content") else str(final) + + return str(result) + + def get_llm(self, model: str | None = None) -> Any: + """Get LangChain-compatible LLM. + + Args: + model: Optional model name override. + + Returns: + LangChain ChatAnthropic LLM. + """ + from langchain_anthropic import ChatAnthropic + + # Default to Claude Haiku 4.5 if no model specified. + default_model = "claude-haiku-4-5-20251001" + return ChatAnthropic(model=model or default_model) + + def build_agent(self, agent_config: dict[str, Any], tools: list | None = None) -> Any: + """Build a LangGraph-compatible agent. + + Args: + agent_config: Agent configuration. + tools: Optional tools. + + Returns: + LangGraph agent. + """ + from langgraph.prebuilt import create_react_agent + + # Get LLM from agent config if specified + llm = self.get_llm(agent_config.get("llm")) + return create_react_agent(llm, tools or []) + + def build_task(self, task_config: dict[str, Any], agent: Any) -> Any: + """Build a task representation for LangGraph. + + In LangGraph, tasks are typically represented as graph nodes or + prompts to agents. Returns a dict for now. + + Args: + task_config: Task configuration. + agent: Agent for the task. + + Returns: + Task configuration dict with agent reference. + """ + return { + "description": task_config.get("description", ""), + "expected_output": task_config.get("expected_output", ""), + "agent": agent, + } + + def _build_tools_from_config(self, fabric_agent_config: dict[str, Any]) -> list: + """Resolve configured agent tools into LangGraph-compatible tools.""" + tool_names: list[str] = [] + for agent_cfg in fabric_agent_config.get("agents", {}).values(): + tool_names.extend(agent_cfg.get("tools", [])) + return resolve_langgraph_tools(tool_names) diff --git a/packages/agentic-fabric/src/agentic_fabric/runners/local_cli_profiles.yaml b/packages/agentic-fabric/src/agentic_fabric/runners/local_cli_profiles.yaml new file mode 100644 index 0000000..f0711a3 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/runners/local_cli_profiles.yaml @@ -0,0 +1,169 @@ +# Local CLI Runner Profiles +# Verified CLI interfaces for single-agent coding tools +# +# These profiles define how to invoke various CLI coding agents. +# All flags verified from official documentation. + +profiles: + # ============================================ + # Aider - AI Pair Programming + # Docs: https://aider.chat/docs/config/options.html + # ============================================ + aider: + name: "Aider" + description: "AI pair programming in your terminal" + command: "aider" + task_flag: "--message" + auth_env: + - "OPENAI_API_KEY" + - "ANTHROPIC_API_KEY" + auto_approve: "--yes-always" + model_flag: "--model" + additional_flags: + - "--no-git" # Let agentic-fabric handle git + install_cmd: "pipx install aider-chat" + docs_url: "https://aider.chat/docs/config/options.html" + notes: "Supports OpenAI, Anthropic, Gemini, Ollama, and more" + + # ============================================ + # Claude Code - Anthropic's Coding Agent + # Docs: https://docs.anthropic.com/en/docs/claude-code/cli-usage + # ============================================ + claude-code: + name: "Claude Code" + description: "Anthropic's AI coding agent" + command: "claude" + task_flag: "--print" # -p also works + auth_env: + - "ANTHROPIC_API_KEY" + auto_approve: "--dangerously-skip-permissions" + structured_output: "--output-format json" + model_flag: "--model" + additional_flags: [] + install_cmd: "npm install -g @anthropic-ai/claude-code" + docs_url: "https://docs.anthropic.com/en/docs/claude-code/cli-usage" + notes: | + Flags verified: + - --print (-p): Query via SDK, then exit + - --dangerously-skip-permissions: Skip permission prompts + - --output-format json: JSON output + - --model: sonnet, opus, or full model name + - --max-turns: Limit agentic turns + - --verbose: Verbose logging + + # ============================================ + # OpenAI Codex CLI + # Docs: https://github.com/openai/codex/blob/main/docs/exec.md + # ============================================ + codex: + name: "OpenAI Codex" + description: "OpenAI's local coding agent" + command: "codex" + subcommand: "exec" # codex exec "task" + task_flag: "" # Positional after exec + auth_env: + - "OPENAI_API_KEY" # Or ChatGPT login + auto_approve: "--full-auto" # Allow file edits + structured_output: "--json" # JSONL streaming output + additional_flags: + - "--sandbox" + - "danger-full-access" # Full access mode + install_cmd: "npm install -g @openai/codex" + docs_url: "https://github.com/openai/codex" + notes: | + Non-interactive mode: codex exec "task" + Flags verified: + - --full-auto: Allow file edits + - --json: Stream JSON events + - --sandbox danger-full-access: Full access + - --output-schema: JSON schema for structured output + - -o/--output-last-message: Output file + + # ============================================ + # Kiro CLI - AWS AI Coding Assistant + # Docs: https://kiro.dev/docs/cli + # ============================================ + kiro: + name: "Kiro" + description: "AWS AI-assisted development CLI" + command: "kiro-cli" + task_flag: "" # Interactive by default, uses chat mode + auth_env: [] # Uses AWS credentials or Kiro login + auto_approve: null # Interactive tool + model_flag: null + additional_flags: + - "--agent" # Specify agent: kiro-cli --agent myagent + install_cmd: "curl -fsSL https://cli.kiro.dev/install | bash" + docs_url: "https://kiro.dev/docs/cli" + notes: | + Kiro is primarily interactive. Key features: + - --agent : Use specific agent + - --resume: Resume conversation + - /editor: Multi-line input + Best for: Spec-driven development, custom agents + + # ============================================ + # Ollama - Free Local LLMs + # Docs: https://github.com/ollama/ollama + # ============================================ + ollama: + name: "Ollama" + description: "Free local LLM execution" + command: "ollama" + subcommand: "run" # ollama run + task_flag: "" # Positional: ollama run codellama "task" + auth_env: [] # No auth needed - fully local + auto_approve: null # No confirmations + model_flag: null # Model is positional: ollama run + default_model: "codellama" + additional_flags: [] + install_cmd: "curl -fsSL https://ollama.com/install.sh | sh" + docs_url: "https://github.com/ollama/ollama" + notes: | + Usage: ollama run "prompt" + Popular coding models: + - codellama: Code-focused Llama + - deepseek-coder: DeepSeek's coding model + - qwen2.5-coder: Alibaba's coding model + - starcoder2: BigCode's model + Zero cost, fully local, no API keys + + # ============================================ + # Goose - Block's AI Coding Agent + # Docs: https://block.github.io/goose/docs/quickstart + # ============================================ + goose: + name: "Goose" + description: "Block's extensible AI coding agent" + command: "goose" + task_flag: "" # Interactive by default + auth_env: [] # Configured via goose configure + auto_approve: null # Interactive + model_flag: null + additional_flags: [] + install_cmd: "curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | bash" + docs_url: "https://block.github.io/goose/docs/quickstart" + notes: | + Goose is primarily interactive desktop/CLI app. + Setup: goose configure + Supports: Tetrate, OpenRouter, and other providers + Features: MCP servers, extensions, recipes + +# ============================================ +# Summary: Non-Interactive Execution +# ============================================ +# +# Tool | Non-Interactive Command +# --------------|-------------------------------------------------- +# aider | aider --message "task" --yes-always --no-git +# claude-code | claude -p "task" --dangerously-skip-permissions +# codex | codex exec "task" --full-auto +# ollama | ollama run codellama "task" +# kiro | (primarily interactive) +# goose | (primarily interactive) +# +# For agentic-fabric integration, we focus on: +# - aider (best non-interactive support) +# - claude-code (excellent --print mode) +# - codex (dedicated exec mode) +# - ollama (simple prompt-response) diff --git a/packages/agentic-fabric/src/agentic_fabric/runners/local_cli_runner.py b/packages/agentic-fabric/src/agentic_fabric/runners/local_cli_runner.py new file mode 100644 index 0000000..61790b2 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/runners/local_cli_runner.py @@ -0,0 +1,491 @@ +"""Universal local CLI runner for single-agent coding tools. + +This runner provides a universal interface for any CLI-based coding agent, +eliminating the need to write custom runners for each tool. Just define the +CLI flags in local_cli_profiles.yaml and it works. + +Supported tools (via profiles): +- Aider: AI pair programming +- Claude Code: Anthropic's coding agent +- OpenAI Codex: OpenAI's local agent +- Ollama: Free local LLMs (codellama, deepseek-coder, etc.) +- Custom: Define your own tool's CLI flags + +Benefits: +- Zero code to add new agents +- Consistent interface across all tools +- Automatic auth env var handling +- Configurable modes (auto-approve, structured output) +""" + +from __future__ import annotations + +import os +import re +import shlex +import stat +import subprocess + +from dataclasses import asdict, dataclass, field, fields, replace +from pathlib import Path +from typing import Any + +import yaml + +from agentic_fabric.runners.single_agent_runner import SingleAgentRunner + + +@dataclass +class LocalCLIConfig: + """Configuration for a CLI-based coding agent. + + This defines how to invoke any CLI tool that can execute coding tasks. + All fields map to the tool's command-line interface. + """ + + # Basic command configuration + command: str # Base command (e.g., "aider", "claude", "ollama") + task_flag: str # How to pass task (e.g., "--message", "--print", "" for positional) + + # Optional subcommand (e.g., "run" for "ollama run") + subcommand: str | None = None + + # Authentication + auth_env: list[str] = field(default_factory=list) # Required env vars + + # Mode controls + auto_approve: str | None = None # Auto-approve flag (e.g., "--yes-always") + structured_output: str | None = None # JSON output flag (e.g., "--json") + + # Model configuration + model_flag: str | None = None # How to specify model (e.g., "--model") + default_model: str | None = None # Default model if not specified + + # Working directory + working_dir_flag: str | None = None # Working dir flag (e.g., "--cwd") + + # Additional flags to always include + additional_flags: list[str] = field(default_factory=list) + + # Execution settings + timeout: int = 300 # Timeout in seconds + + # Metadata + name: str = "" + description: str = "" + install_cmd: str = "" + docs_url: str = "" + notes: str = "" + + +_CONFIG_FIELD_NAMES = {item.name for item in fields(LocalCLIConfig)} +_ENV_VAR_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") +_SHELL_CONTROL_TOKENS = (";", "&&", "||", "|", "`", "$(", "\n", "\r", "<", ">") + + +def _copy_config(config: LocalCLIConfig) -> LocalCLIConfig: + """Return an isolated copy of a CLI config, including mutable list fields.""" + return replace(config, auth_env=list(config.auth_env), additional_flags=list(config.additional_flags)) + + +def _validated_config(name: str, config: LocalCLIConfig | dict[str, Any]) -> LocalCLIConfig: + """Return a copied LocalCLIConfig after applying profile validation.""" + config_dict = asdict(config) if isinstance(config, LocalCLIConfig) else dict(config) + return LocalCLIConfig(**_validate_profile_config(name, config_dict)) + + +def _validate_profile_config(name: str, config_dict: dict[str, Any]) -> dict[str, Any]: + """Validate a local CLI profile loaded from YAML.""" + unknown_fields = set(config_dict) - _CONFIG_FIELD_NAMES + if unknown_fields: + msg = f"Profile '{name}' has unsupported fields: {sorted(unknown_fields)}" + raise ValueError(msg) + + command = config_dict.get("command") + task_flag = config_dict.get("task_flag") + if not isinstance(command, str) or not command.strip(): + msg = f"Profile '{name}' must define a non-empty command string" + raise ValueError(msg) + if not isinstance(task_flag, str): + msg = f"Profile '{name}' must define task_flag as a string" + raise TypeError(msg) + + command_parts = _split_cli_fragment(name, "command", command) + if not command_parts or any(_contains_shell_control(part) for part in command_parts): + msg = f"Profile '{name}' command is not a direct executable invocation" + raise ValueError(msg) + for part in command_parts: + _validate_cli_fragment(name, "command", part) + + if task_flag: + _validate_cli_fragment(name, "task_flag", task_flag) + + for field_name in ("auth_env", "additional_flags"): + value = config_dict.get(field_name, []) + if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value): + msg = f"Profile '{name}' field '{field_name}' must be a list of non-empty strings" + raise ValueError(msg) + if field_name == "auth_env": + invalid_vars = [item for item in value if not _ENV_VAR_RE.match(item)] + if invalid_vars: + msg = f"Profile '{name}' field 'auth_env' contains invalid environment variable names: {invalid_vars}" + raise ValueError(msg) + else: + for item in value: + _validate_cli_fragment(name, field_name, item) + + for field_name in ("subcommand", "auto_approve", "structured_output", "model_flag", "default_model", "working_dir_flag"): + value = config_dict.get(field_name) + if value is not None and not isinstance(value, str): + msg = f"Profile '{name}' field '{field_name}' must be a string when provided" + raise ValueError(msg) + if value: + _validate_cli_fragment(name, field_name, value) + + timeout = config_dict.get("timeout", 300) + if not isinstance(timeout, int) or timeout <= 0 or timeout > 3600: + msg = f"Profile '{name}' timeout must be between 1 and 3600 seconds" + raise ValueError(msg) + + return config_dict + + +def _contains_shell_control(value: str) -> bool: + """Return whether a config string contains shell control syntax.""" + return any(token in value for token in _SHELL_CONTROL_TOKENS) + + +def _validate_cli_fragment(profile_name: str, field_name: str, value: str) -> None: + """Reject shell control syntax in config-controlled CLI fragments.""" + if _contains_shell_control(value): + msg = f"Profile '{profile_name}' field '{field_name}' contains shell control syntax" + raise ValueError(msg) + if not _split_cli_fragment(profile_name, field_name, value): + msg = f"Profile '{profile_name}' field '{field_name}' must contain at least one CLI argument" + raise ValueError(msg) + + +def _split_cli_fragment(profile_name: str, field_name: str, value: str) -> list[str]: + """Split a config-controlled CLI fragment with profile-specific errors.""" + try: + return shlex.split(value) + except ValueError as exc: + msg = f"Profile '{profile_name}' field '{field_name}' is not valid shell-style syntax" + raise ValueError(msg) from exc + + +def _extend_cli_fragment(cmd: list[str], value: str | None) -> None: + """Append a validated config fragment as one or more subprocess arguments.""" + if value: + cmd.extend(shlex.split(value)) + + +def _validate_profiles_file_source(profiles_file: Path) -> None: + """Require the bundled profiles path to be a regular profile file.""" + if profiles_file.name != "local_cli_profiles.yaml": + msg = f"Unexpected profiles file name: {profiles_file}" + raise ValueError(msg) + if profiles_file.is_symlink() or not profiles_file.is_file(): + msg = f"Profiles file must be a regular file: {profiles_file}" + raise FileNotFoundError(msg) + + +def _validate_profiles_file_permissions(profiles_file: Path) -> None: + """Reject local CLI profile files writable by group or other users.""" + if os.name != "posix": + return + + mode = profiles_file.stat().st_mode + if mode & (stat.S_IWGRP | stat.S_IWOTH): + msg = f"Profiles file is writable by group or other users: {profiles_file}" + raise PermissionError(msg) + + +def _validate_subprocess_argument(field_name: str, value: str | None) -> None: + """Reject argument values that cannot be passed safely to subprocess.""" + if value is None: + return + if not isinstance(value, str): + msg = f"{field_name} must be a string" + raise TypeError(msg) + if "\x00" in value: + msg = f"{field_name} contains a NUL byte" + raise ValueError(msg) + + +class LocalCLIRunner(SingleAgentRunner): + """Universal runner for CLI-based coding agents. + + This runner can invoke any CLI tool that accepts a task/prompt and + executes it. Configuration is done via profiles (built-in or custom). + + Examples: + # Use built-in profile + runner = LocalCLIRunner("aider") + result = runner.run("Add error handling to auth.py") + + # Use custom config + config = LocalCLIConfig( + command="my-tool", + task_flag="--task", + auto_approve="--yes", + ) + runner = LocalCLIRunner(config) + result = runner.run("Fix the bug") + """ + + # Cache for loaded profiles + _profiles_cache: dict[str, LocalCLIConfig] | None = None + + def __init__( + self, + profile: str | LocalCLIConfig | dict[str, Any], + model: str | None = None, + ): + """Initialize the runner with a profile or custom config. + + Args: + profile: Profile name (e.g., "aider"), LocalCLIConfig object, + or dict of config parameters. + model: Optional model override (if tool supports model selection). + """ + if isinstance(profile, str): + # Load from built-in profiles + profiles = self._load_profiles() + if profile not in profiles: + available = list(profiles.keys()) + raise ValueError( + f"Unknown profile '{profile}'. Available: {available}\n" + f"Or provide a LocalCLIConfig object for custom tools." + ) + self.config = _copy_config(profiles[profile]) + elif isinstance(profile, dict): + # Convert dict to LocalCLIConfig + self.config = _validated_config("custom", profile) + elif isinstance(profile, LocalCLIConfig): + # Use provided LocalCLIConfig directly + self.config = _validated_config("custom", profile) + else: + msg = "profile must be a profile name, LocalCLIConfig, or config dict" + raise TypeError(msg) + + self.model = model + self.runner_name = f"local_cli_{self.config.command}" + + @classmethod + def _load_profiles(cls) -> dict[str, LocalCLIConfig]: + """Load CLI profiles from local_cli_profiles.yaml. + + Returns: + Dict mapping profile names to LocalCLIConfig objects. + """ + if cls._profiles_cache is not None: + return cls._profiles_cache + + # Find the profiles file + profiles_file = Path(__file__).parent / "local_cli_profiles.yaml" + if not profiles_file.is_file(): + raise FileNotFoundError( + f"Profiles file not found: {profiles_file}\nExpected local_cli_profiles.yaml in runners directory." + ) + _validate_profiles_file_source(profiles_file) + _validate_profiles_file_permissions(profiles_file) + + # Load and parse YAML + with open(profiles_file, encoding="utf-8") as f: + data = yaml.safe_load(f) + + if not isinstance(data, dict): + msg = f"Profiles file must contain a mapping: {profiles_file}" + raise TypeError(msg) + + profiles = {} + raw_profiles = data.get("profiles", {}) + if not isinstance(raw_profiles, dict): + msg = f"Profiles file 'profiles' key must contain a mapping: {profiles_file}" + raise TypeError(msg) + + for name, config_dict in raw_profiles.items(): + if not isinstance(name, str) or not isinstance(config_dict, dict): + msg = f"Profiles file has invalid profile entry: {name!r}" + raise TypeError(msg) + profiles[name] = LocalCLIConfig(**_validate_profile_config(name, config_dict)) + + cls._profiles_cache = profiles + return profiles + + @classmethod + def get_available_profiles(cls) -> list[str]: + """Get list of available profile names. + + Returns: + List of profile names that can be used. + """ + return list(cls._load_profiles().keys()) + + def is_available(self) -> bool: + """Check if the CLI tool is available (installed and accessible). + + Returns: + True if the command exists in PATH. + """ + from shutil import which + + # Check if command is in PATH + command = shlex.split(self.config.command)[0] + return which(command) is not None + + def get_required_env_vars(self) -> list[str]: + """Get list of required environment variables. + + Returns: + List of environment variable names from config.auth_env. + """ + return self.config.auth_env + + def run( + self, + task: str, + working_dir: str | None = None, + auto_approve: bool = True, + structured_output: bool = False, + model: str | None = None, + **kwargs: Any, + ) -> str: + """Execute a task using the configured CLI tool. + + Args: + task: The task to execute (e.g., "Add error handling"). + working_dir: Optional working directory for execution. + auto_approve: Whether to auto-approve changes (if supported). + structured_output: Whether to request JSON output (if supported). + model: Optional model override. + **kwargs: Additional tool-specific arguments. + + Returns: + Tool output as a string. + + Raises: + RuntimeError: If tool execution fails or required env vars missing. + subprocess.TimeoutExpired: If execution exceeds timeout. + """ + _validate_subprocess_argument("task", task) + _validate_subprocess_argument("working_dir", working_dir) + _validate_subprocess_argument("model", model) + + # Accept but don't use kwargs (reserved for future extensibility) + _ = kwargs + + # Check required environment variables + missing_vars = [] + for var in self.config.auth_env: + if var not in os.environ: + missing_vars.append(var) + + if missing_vars: + raise RuntimeError( + f"Missing required environment variables for {self.config.command}: " + f"{', '.join(missing_vars)}\n" + f"Set these before running: {', '.join(missing_vars)}" + ) + + # Build command + cmd = self._build_command( + task=task, + working_dir=working_dir, + auto_approve=auto_approve, + structured_output=structured_output, + model=model or self.model, + ) + + # Execute + try: + result = subprocess.run( + cmd, + cwd=working_dir, + capture_output=True, + text=True, + shell=False, + timeout=self.config.timeout, + env=os.environ.copy(), # Pass through environment + check=True, + ) + + return result.stdout + + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"Command failed with exit code {e.returncode}\n" + f"Command: {' '.join(e.cmd)}\n" + f"stdout: {e.stdout}\n" + f"stderr: {e.stderr}" + ) from e + except subprocess.TimeoutExpired as e: + raise RuntimeError(f"Command timed out after {self.config.timeout}s\nCommand: {' '.join(cmd)}") from e + + def _build_command( + self, + task: str, + working_dir: str | None, + auto_approve: bool, + structured_output: bool, + model: str | None, + ) -> list[str]: + """Build the command array for subprocess.run. + + Args: + task: The task to execute. + working_dir: Optional working directory. + auto_approve: Whether to include auto-approve flag. + structured_output: Whether to include structured output flag. + model: Optional model to use. + + Returns: + Command as list of strings. + """ + # Start with base command + cmd = shlex.split(self.config.command) + + # Add subcommand if present (e.g., "ollama run") + if self.config.subcommand: + _extend_cli_fragment(cmd, self.config.subcommand) + + # Add model if specified and tool supports it + # For positional model (like ollama), add before task + if model: + if self.config.model_flag: + _extend_cli_fragment(cmd, self.config.model_flag) + cmd.append(model) + else: + # Positional model (e.g., "ollama run ") + cmd.append(model) + elif self.config.default_model and not self.config.model_flag: + # Use default model for positional case + cmd.append(self.config.default_model) + + # Add task + if self.config.task_flag: + _extend_cli_fragment(cmd, self.config.task_flag) + cmd.append(task) + else: + # Positional argument + cmd.append(task) + + # Add auto-approve flag if requested and supported + if auto_approve and self.config.auto_approve: + _extend_cli_fragment(cmd, self.config.auto_approve) + + # Add structured output flag if requested and supported + if structured_output and self.config.structured_output: + _extend_cli_fragment(cmd, self.config.structured_output) + + # Add working directory if supported + if working_dir and self.config.working_dir_flag: + _extend_cli_fragment(cmd, self.config.working_dir_flag) + cmd.append(working_dir) + + # Add any additional flags + for flag in self.config.additional_flags: + _extend_cli_fragment(cmd, flag) + + return cmd diff --git a/packages/agentic-fabric/src/agentic_fabric/runners/registry.py b/packages/agentic-fabric/src/agentic_fabric/runners/registry.py new file mode 100644 index 0000000..1a1ccbb --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/runners/registry.py @@ -0,0 +1,165 @@ +"""Lazy runtime registry for optional agent frameworks.""" + +from __future__ import annotations + +import importlib + +from dataclasses import dataclass +from types import MappingProxyType +from typing import Any + + +@dataclass(frozen=True) +class RuntimeSpec: + """Metadata required to discover and instantiate one runtime.""" + + name: str + import_name: str + runner_module: str + runner_class: str + install: str + extra: str | None + description: str = "" + + def as_dict(self, *, available: bool) -> dict[str, Any]: + """Return serializable runtime metadata.""" + return { + "name": self.name, + "import_name": self.import_name, + "runner": f"{self.runner_module}:{self.runner_class}", + "install": self.install, + "extra": self.extra, + "available": available, + "description": self.description, + } + + +class RuntimeUnavailableError(RuntimeError): + """Raised when an optional runtime cannot be selected.""" + + def __init__(self, runtime: str, install: str) -> None: + """Initialize the error with runtime-specific install guidance.""" + super().__init__(f"Runtime '{runtime}' is not available. Install with: {install}") + self.runtime = runtime + self.install = install + + +_RUNTIME_SPECS: dict[str, RuntimeSpec] = {} +_AVAILABILITY_CACHE: dict[str, bool] = {} + + +def register_runtime(spec: RuntimeSpec) -> None: + """Register or replace one runtime spec.""" + _RUNTIME_SPECS[spec.name] = spec + _AVAILABILITY_CACHE.pop(spec.name, None) + + +def runtime_specs() -> MappingProxyType[str, RuntimeSpec]: + """Return registered runtime specs in priority order.""" + return MappingProxyType(_RUNTIME_SPECS) + + +def runtime_names() -> list[str]: + """Return registered runtime names in priority order.""" + return list(_RUNTIME_SPECS) + + +def get_runtime_spec(runtime: str) -> RuntimeSpec: + """Return a registered runtime spec.""" + try: + return _RUNTIME_SPECS[runtime] + except KeyError as exc: + options = ", ".join(runtime_names()) + msg = f"Unknown runtime: {runtime}. Options: [{options}]" + raise ValueError(msg) from exc + + +def clear_runtime_cache() -> None: + """Clear cached optional-runtime availability.""" + _AVAILABILITY_CACHE.clear() + + +def is_runtime_available(runtime: str) -> bool: + """Return whether a runtime import target is importable.""" + if runtime in _AVAILABILITY_CACHE: + return _AVAILABILITY_CACHE[runtime] + if runtime not in _RUNTIME_SPECS: + _AVAILABILITY_CACHE[runtime] = False + return False + + try: + importlib.import_module(_RUNTIME_SPECS[runtime].import_name) + except ImportError: + _AVAILABILITY_CACHE[runtime] = False + else: + _AVAILABILITY_CACHE[runtime] = True + return _AVAILABILITY_CACHE[runtime] + + +def available_runtimes() -> list[str]: + """Return available runtime names in priority order.""" + return [runtime for runtime in runtime_names() if is_runtime_available(runtime)] + + +def runtime_info(runtime: str | None = None) -> list[dict[str, Any]] | dict[str, Any]: + """Return runtime metadata with current availability.""" + if runtime is not None: + spec = get_runtime_spec(runtime) + return spec.as_dict(available=is_runtime_available(runtime)) + return [spec.as_dict(available=is_runtime_available(spec.name)) for spec in _RUNTIME_SPECS.values()] + + +def require_runtime(runtime: str) -> RuntimeSpec: + """Return a runtime spec or raise install guidance if unavailable.""" + spec = get_runtime_spec(runtime) + if not is_runtime_available(runtime): + raise RuntimeUnavailableError(runtime, spec.install) + return spec + + +def load_runner(runtime: str) -> Any: + """Instantiate a runner from the lazy registry.""" + spec = require_runtime(runtime) + module = importlib.import_module(spec.runner_module) + runner_cls = getattr(module, spec.runner_class) + return runner_cls() + + +def install_command(runtime: str) -> str: + """Return the install command for one registered runtime.""" + return get_runtime_spec(runtime).install + + +register_runtime( + RuntimeSpec( + name="crewai", + import_name="crewai", + runner_module="agentic_fabric.runners.crewai_runner", + runner_class="CrewAIRunner", + install="pip install crewai", + extra=None, + description="CrewAI orchestration through an externally installed CrewAI runtime.", + ) +) +register_runtime( + RuntimeSpec( + name="langgraph", + import_name="langgraph", + runner_module="agentic_fabric.runners.langgraph_runner", + runner_class="LangGraphRunner", + install='pip install "agentic-fabric[langgraph]"', + extra="langgraph", + description="LangGraph ReAct and graph workflows.", + ) +) +register_runtime( + RuntimeSpec( + name="strands", + import_name="strands", + runner_module="agentic_fabric.runners.strands_runner", + runner_class="StrandsRunner", + install='pip install "agentic-fabric[strands]"', + extra="strands", + description="AWS Strands single-agent workflow execution.", + ) +) diff --git a/packages/agentic-fabric/src/agentic_fabric/runners/single_agent_runner.py b/packages/agentic-fabric/src/agentic_fabric/runners/single_agent_runner.py new file mode 100644 index 0000000..bc22f8d --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/runners/single_agent_runner.py @@ -0,0 +1,75 @@ +"""Single-agent runner base class. + +Single-agent runners execute simple, sequential tasks without multi-agent +collaboration overhead. They're ideal for: +- Quick file edits +- Code generation +- Local development +- Sequential workflows + +Unlike multi-agent fabric agents, single-agent runners: +- Take a single task string as input +- Execute in one pass without delegation +- Return results directly +- Have minimal configuration overhead +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any + +from agentic_fabric.capabilities import AgentCapabilityProviderMixin, runtime_capability + + +class SingleAgentRunner(AgentCapabilityProviderMixin, ABC): + """Base class for single-agent runners. + + Single-agent runners provide a simpler execution model than multi-agent + fabric agents. They execute a single task and return the result, without the + complexity of agent collaboration, delegation, or hierarchical processes. + """ + + runner_name: str = "single_agent" + + @runtime_capability("run", description="Execute one single-agent task.") + @abstractmethod + def run( + self, + task: str, + working_dir: str | None = None, + **kwargs: Any, + ) -> str: + """Execute a single task and return the result. + + Args: + task: The task to execute (e.g., "Add error handling to auth.py"). + working_dir: Optional working directory for execution. + **kwargs: Additional runner-specific parameters. + + Returns: + Task output as a string. + + Raises: + RuntimeError: If execution fails. + """ + + @runtime_capability("is_available", description="Report whether this local runner can execute.") + def is_available(self) -> bool: + """Check if this runner is available (dependencies installed, etc.). + + Returns: + True if the runner can be used. + """ + # Default implementation - subclasses can override + return True + + @runtime_capability("required_env_vars", description="List environment variables required by this runner.") + def get_required_env_vars(self) -> list[str]: + """Get list of required environment variables. + + Returns: + List of environment variable names required by this runner. + """ + # Default implementation - subclasses can override + return [] diff --git a/packages/agentic-fabric/src/agentic_fabric/runners/strands_runner.py b/packages/agentic-fabric/src/agentic_fabric/runners/strands_runner.py new file mode 100644 index 0000000..5ff1021 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/runners/strands_runner.py @@ -0,0 +1,192 @@ +"""AWS Strands runner implementation. + +Strands is lightweight and AWS-native, good for: +- Simple agent workflows +- AWS Bedrock integration +- Minimal dependencies +- Plain Python function tools +""" + +from __future__ import annotations + +from typing import Any + +from agentic_fabric.runners.base import BaseRunner +from agentic_fabric.runners.registry import install_command +from agentic_fabric.tools.adapters import resolve_strands_tools + + +class StrandsRunner(BaseRunner): + """Runner that uses AWS Strands for fabric agent execution.""" + + framework_name = "strands" + + def __init__(self): + """Initialize Strands runner.""" + try: + import strands # noqa: F401 + except ImportError as e: + raise RuntimeError(f"Strands not installed. Install with: {install_command(self.framework_name)}") from e + + def build_fabric_agent(self, fabric_agent_config: dict[str, Any]) -> Any: + """Build a Strands agent from configuration. + + Strands uses a single-agent model, so we combine fabric agent tasks + into a system prompt for one capable agent. + + Args: + fabric_agent_config: Universal fabric agent configuration. + + Returns: + Strands Agent object. + """ + from strands import Agent + + # Build system prompt from fabric agent description and agent backstories. + system_prompt = self._build_system_prompt(fabric_agent_config) + + # Collect tools from tasks + tools = self._collect_tools(fabric_agent_config) + + # Get LLM/model configuration from fabric agent config. + llm_config = fabric_agent_config.get("llm", {}) + model_provider = self._get_model_provider(llm_config) + + agent_kwargs = { + "system_prompt": system_prompt, + "tools": tools, + } + + # Add model provider if configured + if model_provider: + agent_kwargs["model_id"] = model_provider + + return Agent(**agent_kwargs) + + def _get_model_provider(self, llm_config: dict | str | None) -> str | None: + """Get Strands-compatible model provider from LLM config. + + Args: + llm_config: LLM configuration from fabric agent config. + + Returns: + Model provider string for Strands, or None for default. + """ + if not llm_config: + return None + + if isinstance(llm_config, str): + return llm_config + + # Extract model from config dict + return llm_config.get("model") + + def run(self, fabric_agent: Any, inputs: dict[str, Any]) -> str: + """Execute the Strands agent. + + Args: + fabric_agent: Strands Agent object. + inputs: Inputs for the agent. + + Returns: + Agent output as string. + """ + # Convert inputs to prompt + prompt = inputs.get("input", inputs.get("task", str(inputs))) + + result = fabric_agent(prompt) + + return str(result) + + def build_agent(self, agent_config: dict[str, Any], tools: list | None = None) -> Any: + """Build a Strands agent. + + Args: + agent_config: Agent configuration. + tools: Optional tools. + + Returns: + Strands Agent object. + """ + from strands import Agent + + system_prompt = ( + f"You are a {agent_config.get('role', 'helpful assistant')}.\n\n" + f"Goal: {agent_config.get('goal', '')}\n\n" + f"Background: {agent_config.get('backstory', '')}" + ) + + return Agent( + system_prompt=system_prompt, + tools=tools or [], + ) + + def build_task(self, task_config: dict[str, Any], agent: Any) -> Any: + """Build a task for Strands. + + In Strands, tasks are prompts to the agent. Returns a dict + that can be used to construct prompts. + + Args: + task_config: Task configuration. + agent: Agent for the task. + + Returns: + Task configuration dict. + """ + return { + "description": task_config.get("description", ""), + "expected_output": task_config.get("expected_output", ""), + "agent": agent, + } + + def _build_system_prompt(self, fabric_agent_config: dict[str, Any]) -> str: + """Build a comprehensive system prompt from fabric agent config. + + Combines all agent roles, goals, and backstories into a single + capable system prompt. + + Args: + fabric_agent_config: Fabric agent configuration. + + Returns: + System prompt string. + """ + parts = [] + + # Add fabric agent description. + if fabric_agent_config.get("description"): + parts.append(f"# Your Purpose\n{fabric_agent_config['description']}") + + # Add agent capabilities + agents = fabric_agent_config.get("agents", {}) + if agents: + parts.append("\n# Your Capabilities") + for agent_name, agent_cfg in agents.items(): + role = agent_cfg.get("role", agent_name) + goal = agent_cfg.get("goal", "") + parts.append(f"\n## {role}") + if goal: + parts.append(f"Goal: {goal}") + + # Add task context + tasks = fabric_agent_config.get("tasks", {}) + if tasks: + parts.append("\n# Tasks You Can Perform") + for task_name, task_cfg in tasks.items(): + desc = task_cfg.get("description", "") + if desc: + # Only add ellipsis if actually truncated + if len(desc) > 200: + parts.append(f"\n- {task_name}: {desc[:200]}...") + else: + parts.append(f"\n- {task_name}: {desc}") + + return "\n".join(parts) + + def _collect_tools(self, fabric_agent_config: dict[str, Any]) -> list: + """Collect tools declared by agents in fabric agent configuration.""" + tool_names: list[str] = [] + for agent_cfg in fabric_agent_config.get("agents", {}).values(): + tool_names.extend(agent_cfg.get("tools", [])) + return resolve_strands_tools(tool_names) diff --git a/packages/agentic-fabric/src/agentic_fabric/tools/__init__.py b/packages/agentic-fabric/src/agentic_fabric/tools/__init__.py new file mode 100644 index 0000000..19005ea --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/tools/__init__.py @@ -0,0 +1,76 @@ +"""Custom tools for agentic-fabric. + +Tool modules are imported lazily so the core package can be imported without +pulling in optional framework dependencies like CrewAI or scraping extras. +""" + +from __future__ import annotations + +from contextlib import suppress +from importlib import import_module +from typing import TYPE_CHECKING, Any + + +if TYPE_CHECKING: + from agentic_fabric.tools.file_tools import DirectoryListTool, GameCodeReaderTool, GameCodeWriterTool + from agentic_fabric.tools.scraping_tools import CrawlWebsiteTool, ScrapeWebsiteTool + from agentic_fabric.tools.vendor import VendorCapabilityTool, vendor_capability_tools + + +def _load_attr(module_name: str, attr_name: str) -> Any: + module = import_module(module_name) + return getattr(module, attr_name) + + +def __getattr__(name: str) -> Any: + if name in {"DirectoryListTool", "GameCodeReaderTool", "GameCodeWriterTool"}: + return _load_attr("agentic_fabric.tools.file_tools", name) + if name in {"CrawlWebsiteTool", "ScrapeWebsiteTool"}: + return _load_attr("agentic_fabric.tools.scraping_tools", name) + if name in {"VendorCapabilityTool", "vendor_capability_tools"}: + return _load_attr("agentic_fabric.tools.vendor", name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def get_file_tools() -> list[Any]: + """Get the standard file manipulation tools.""" + return [ + __getattr__("GameCodeReaderTool")(), + __getattr__("GameCodeWriterTool")(), + __getattr__("DirectoryListTool")(), + ] + + +def get_scraping_tools() -> list[Any]: + """Get the web scraping tools.""" + return [ + __getattr__("ScrapeWebsiteTool")(), + __getattr__("CrawlWebsiteTool")(), + ] + + +def get_all_tools() -> list[Any]: + """Get all available tools.""" + tools = get_file_tools() + + with suppress(ImportError): + tools.extend(get_scraping_tools()) + + with suppress(AttributeError, ImportError, RuntimeError): + tools.extend(__getattr__("vendor_capability_tools")(include_unavailable=False)) + + return tools + + +__all__ = [ + "CrawlWebsiteTool", + "DirectoryListTool", + "GameCodeReaderTool", + "GameCodeWriterTool", + "ScrapeWebsiteTool", + "VendorCapabilityTool", + "get_all_tools", + "get_file_tools", + "get_scraping_tools", + "vendor_capability_tools", +] diff --git a/packages/agentic-fabric/src/agentic_fabric/tools/adapters.py b/packages/agentic-fabric/src/agentic_fabric/tools/adapters.py new file mode 100644 index 0000000..e0dff34 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/tools/adapters.py @@ -0,0 +1,128 @@ +"""Framework-specific adapters for configured tools.""" + +from __future__ import annotations + +import contextlib +import inspect +import logging +import re + +from typing import Any + +from agentic_fabric.tools.registry import resolve_tools + + +logger = logging.getLogger(__name__) + + +def _tool_name(tool_obj: Any) -> str: + """Build a framework-safe tool name.""" + raw_name = getattr(tool_obj, "name", None) or getattr(tool_obj, "__name__", None) or tool_obj.__class__.__name__ + cleaned = re.sub(r"[^0-9A-Za-z_]+", "_", str(raw_name)).strip("_") or "tool" + if cleaned[0].isdigit(): + cleaned = f"tool_{cleaned}" + return cleaned + + +def _tool_description(tool_obj: Any) -> str: + """Get the best available description for a tool object.""" + return str( + getattr(tool_obj, "description", None) + or inspect.getdoc(tool_obj) + or f"Execute the {_tool_name(tool_obj)} tool." + ) + + +def _invoke_tool(tool_obj: Any, kwargs: dict[str, Any]) -> Any: + """Execute a configured tool object.""" + if hasattr(tool_obj, "_run"): + return tool_obj._run(**kwargs) + if callable(tool_obj): + return tool_obj(**kwargs) + raise TypeError(f"Tool {tool_obj!r} is not callable") + + +def _build_runner(tool_obj: Any, name: str, description: str) -> Any: + """Create a plain callable wrapper for a configured tool.""" + target = getattr(tool_obj, "_run", tool_obj) + + def runner(**kwargs: Any) -> Any: + return _invoke_tool(tool_obj, kwargs) + + runner.__name__ = name + runner.__doc__ = description + if callable(target): + with contextlib.suppress(ValueError, TypeError): + runner.__signature__ = inspect.signature(target) # type: ignore[attr-defined] + return runner + + +def resolve_langgraph_tools(tool_names: list[str]) -> list[Any]: + """Resolve configured tool names to LangGraph-compatible tools.""" + if not tool_names: + return [] + + resolved_tools = resolve_tools(tool_names) + if not resolved_tools: + return [] + + try: + from langchain_core.tools import StructuredTool + except ImportError as exc: + logger.warning("LangGraph tool adaptation unavailable; skipping configured tools: %s", exc) + return [] + + adapted_tools: list[Any] = [] + for tool_obj in resolved_tools: + if hasattr(tool_obj, "invoke") and hasattr(tool_obj, "name"): + adapted_tools.append(tool_obj) + continue + + name = _tool_name(tool_obj) + description = _tool_description(tool_obj) + args_schema = getattr(tool_obj, "args_schema", None) + runner = _build_runner(tool_obj, name, description) + + adapted_tools.append( + StructuredTool.from_function( + func=runner, + name=name, + description=description, + args_schema=args_schema, + infer_schema=args_schema is None, + ) + ) + + return adapted_tools + + +def resolve_strands_tools(tool_names: list[str]) -> list[Any]: + """Resolve configured tool names to Strands-compatible tools.""" + if not tool_names: + return [] + + resolved_tools = resolve_tools(tool_names) + if not resolved_tools: + return [] + + try: + from strands import tool as strands_tool + except ImportError as exc: + logger.warning("Strands tool adaptation unavailable; skipping configured tools: %s", exc) + return [] + + adapted_tools: list[Any] = [] + for tool_obj in resolved_tools: + if hasattr(tool_obj, "TOOL_SPEC"): + adapted_tools.append(tool_obj) + continue + + name = _tool_name(tool_obj) + description = _tool_description(tool_obj) + args_schema = getattr(tool_obj, "args_schema", None) + input_schema = args_schema.model_json_schema() if args_schema is not None else None + runner = _build_runner(tool_obj, name, description) + + adapted_tools.append(strands_tool(name=name, description=description, inputSchema=input_schema)(runner)) + + return adapted_tools diff --git a/packages/agentic-fabric/src/agentic_fabric/tools/file_tools.py b/packages/agentic-fabric/src/agentic_fabric/tools/file_tools.py new file mode 100644 index 0000000..7e4e63f --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/tools/file_tools.py @@ -0,0 +1,332 @@ +"""Framework-neutral file manipulation tools for agents. + +These tools enable agents to read and write code to specific directories in +workspace package codebases. +""" + +from __future__ import annotations + +import os + +from pathlib import Path +from typing import Any + + +try: # pragma: no cover - exercised by consumers that install CrewAI + from crewai.tools import BaseTool as _BaseTool +except ImportError: + + class _BaseTool: # type: ignore[no-redef] + """Minimal base class used when no framework-specific tool base exists.""" + + +def _load_pydantic_schema_helpers() -> tuple[Any, Any]: # pragma: no cover + """Return Pydantic helpers when optional schema metadata is available.""" + try: + from pydantic import BaseModel, Field + except ImportError: + return None, None + return BaseModel, Field + + +_PYDANTIC_BASE_MODEL, _PYDANTIC_FIELD = _load_pydantic_schema_helpers() + + +def _find_workspace_root() -> Path | None: + """Search upward for workspace root (contains pyproject.toml with workspace).""" + current = Path(__file__).resolve().parent + for parent in [current, *list(current.parents)]: + pyproject = parent / "pyproject.toml" + if pyproject.exists(): + # Check if this is the workspace root (has packages/ directory) + packages_dir = parent / "packages" + if packages_dir.exists() and packages_dir.is_dir(): + return parent + return None + + +def get_workspace_root(package_name: str | None = None) -> Path: + """Get the workspace root directory for the target game code package. + + Returns packages/ as the workspace root, where the game code lives. + + Uses marker file search to find workspace root reliably, regardless of + where this module is installed or imported from. + + Args: + package_name: Name of the target package. If not provided, + uses TARGET_PACKAGE environment variable, or the only package + directory when the workspace has exactly one package. + + Returns: + Path to packages/ directory. + """ + # Determine the target package name + if package_name is None: + package_name = os.environ.get("TARGET_PACKAGE") + + # Find workspace root using marker file search + workspace_root = _find_workspace_root() + if workspace_root: + packages_dir = workspace_root / "packages" + if package_name is not None: + target_dir = packages_dir / package_name + if target_dir.exists(): + return target_dir + elif packages_dir.is_dir(): + package_dirs = sorted(path for path in packages_dir.iterdir() if path.is_dir()) + if len(package_dirs) == 1: + return package_dirs[0] + + # Fallback: try environment variable for root directory + env_root_var = f"{package_name.upper()}_ROOT" if package_name else None + if env_root_var and env_root_var in os.environ: + return Path(os.environ[env_root_var]).resolve() + + # Last fallback - current directory (shouldn't happen in normal use) + return Path.cwd() + + +# Allowed directories for writing relative to the selected package root. +ALLOWED_WRITE_DIRS = [ + "src/ecs", # ECS components, world, data + "src/ecs/data", # Species definitions, etc. + "src/ecs/systems", # ECS systems + "src/components", # React/R3F components + "src/components/ui", # UI components + "src/stores", # Zustand stores + "src/systems", # Non-ECS systems + "src/utils", # Utility functions + "src/types", # TypeScript types +] + +# Allowed file extensions +ALLOWED_EXTENSIONS = {".ts", ".tsx", ".json", ".md"} + + +def _clean_relative_path(path_value: str) -> str: + """Normalize a user-provided relative path string.""" + clean_path = path_value.strip().replace("\\", "/") + if not clean_path or clean_path.startswith("/") or ".." in Path(clean_path).parts: + raise ValueError(f"Path traversal not allowed in '{clean_path}'") + return clean_path + + +def _resolve_workspace_path(path_value: str) -> tuple[str, Path]: + """Resolve a user path and ensure it remains inside the workspace root.""" + clean_path = _clean_relative_path(path_value) + workspace_root = get_workspace_root().resolve() + full_path = (workspace_root / clean_path).resolve(strict=False) + if not full_path.is_relative_to(workspace_root): + raise ValueError(f"Path '{clean_path}' escapes workspace root") + return clean_path, full_path + + +def _is_allowed_write_path(clean_path: str) -> bool: + """Return whether a normalized path is under an allowed write directory.""" + return any(clean_path == allowed_dir or clean_path.startswith(f"{allowed_dir}/") for allowed_dir in ALLOWED_WRITE_DIRS) + + +def _build_pydantic_schema( + name: str, + doc: str, + fields_by_description: dict[str, str], +) -> Any: # pragma: no cover + """Build optional Pydantic schemas without making Pydantic a core dependency.""" + if _PYDANTIC_BASE_MODEL is None or _PYDANTIC_FIELD is None: + return None + namespace: dict[str, Any] = { + "__annotations__": dict.fromkeys(fields_by_description, str), + "__doc__": doc, + } + for field_name, description in fields_by_description.items(): + namespace[field_name] = _PYDANTIC_FIELD(description=description) + return type(name, (_PYDANTIC_BASE_MODEL,), namespace) + + +WriteFileInput = _build_pydantic_schema( + "WriteFileInput", + "Input schema for GameCodeWriterTool.", + { + "file_path": "Relative path from workspace root (e.g., 'src/ecs/data/NewComponent.ts')", + "content": "The TypeScript/TSX code content to write", + }, +) + + +class GameCodeWriterTool(_BaseTool): # type: ignore[misc,valid-type] + """Tool for writing code files to a game package codebase. + + This tool is restricted to specific directories to ensure agents + only modify appropriate parts of the codebase. + """ + + name: str = "Write Game Code File" + description: str = """ + Write a code file to the target game codebase (e.g., packages/). + + The target package is configurable via TARGET_PACKAGE environment variable. + + ALLOWED DIRECTORIES: + - src/ecs - ECS components, world definition + - src/ecs/data - Species data, biome configs + - src/ecs/systems - ECS systems + - src/components - React Three Fiber components + - src/components/ui - UI components (menus, HUD) + - src/stores - Zustand state stores + - src/systems - Non-ECS game systems + - src/utils - Utility functions + - src/types - TypeScript type definitions + + ALLOWED EXTENSIONS: .ts, .tsx, .json, .md + + Example: + file_path: "src/ecs/data/species.ts" + content: "export const PREDATOR_SPECIES = { ... }" + """ + args_schema: Any = WriteFileInput + + def _run(self, file_path: str, content: str) -> str: + """Write the file content to the specified path.""" + try: + clean_path, full_path = _resolve_workspace_path(file_path) + + # Check allowed directories + if not _is_allowed_write_path(clean_path): + return f"Error: Path '{clean_path}' is not in an allowed directory. Allowed: {ALLOWED_WRITE_DIRS}" + + # Check extension + ext = Path(clean_path).suffix.lower() + if ext not in ALLOWED_EXTENSIONS: + return f"Error: Extension '{ext}' not allowed. Allowed: {ALLOWED_EXTENSIONS}" + + # Create parent directories + full_path.parent.mkdir(parents=True, exist_ok=True) + + # Write content + with open(full_path, "w", encoding="utf-8") as f: + f.write(content) + + return f"Successfully wrote {len(content)} bytes to {clean_path}" + + except PermissionError: + return f"Error: Permission denied writing to {file_path}" + except ValueError as e: + return f"Error: {e!s}" + except Exception as e: + return f"Error writing file: {e!s}" + + +ReadFileInput = _build_pydantic_schema( + "ReadFileInput", + "Input schema for GameCodeReaderTool.", + {"file_path": "Relative path from workspace root (e.g., 'src/ecs/components.ts')"}, +) + + +class GameCodeReaderTool(_BaseTool): # type: ignore[misc,valid-type] + """Tool for reading code files from a game package codebase. + + Use this to understand existing patterns before writing new code. + """ + + name: str = "Read Game Code File" + description: str = """ + Read a code file from the target package's codebase. + + The target package is determined by the TARGET_PACKAGE environment variable. + + Use this tool to: + - Understand existing patterns + - See how similar components are structured + - Check imports and dependencies + + Example: + file_path: "src/ecs/components.ts" + """ + args_schema: Any = ReadFileInput + + def _run(self, file_path: str) -> str: + """Read the file content from the specified path.""" + try: + clean_path, full_path = _resolve_workspace_path(file_path) + + if not full_path.exists(): + return f"Error: File not found: {clean_path}" + + if not full_path.is_file(): + return f"Error: Path is not a file: {clean_path}" + + # Limit file size + if full_path.stat().st_size > 100_000: # 100KB limit + return f"Error: File too large (>{100_000} bytes)" + + with open(full_path, encoding="utf-8") as f: + return f.read() + + except PermissionError: + return f"Error: Permission denied reading {file_path}" + except ValueError as e: + return f"Error: {e!s}" + except Exception as e: + return f"Error reading file: {e!s}" + + +ListDirInput = _build_pydantic_schema( + "ListDirInput", + "Input schema for DirectoryListTool.", + {"directory": "Relative directory path from workspace root (e.g., 'src/ecs')"}, +) + + +class DirectoryListTool(_BaseTool): # type: ignore[misc,valid-type] + """Tool for listing files in a directory. + + Use this to discover existing files and understand project structure. + """ + + name: str = "List Directory Contents" + description: str = """ + List files and subdirectories in the target package codebase. + + The target package is determined by the TARGET_PACKAGE environment variable. + + Use this to: + - Discover existing components + - Understand project structure + - Find files to read or reference + + Example: + directory: "src/ecs/data" + """ + args_schema: Any = ListDirInput + + def _run(self, directory: str) -> str: + """List directory contents.""" + try: + clean_path, full_path = _resolve_workspace_path(directory) + + if not full_path.exists(): + return f"Error: Directory not found: {clean_path}" + + if not full_path.is_dir(): + return f"Error: Path is not a directory: {clean_path}" + + entries = [] + for entry in sorted(full_path.iterdir()): + if entry.name.startswith("."): + continue + prefix = "📁" if entry.is_dir() else "📄" + entries.append(f"{prefix} {entry.name}") + + if not entries: + return f"Directory {clean_path} is empty" + + return f"Contents of {clean_path}:\n" + "\n".join(entries) + + except PermissionError: + return f"Error: Permission denied accessing {directory}" + except ValueError as e: + return f"Error: {e!s}" + except Exception as e: + return f"Error listing directory: {e!s}" diff --git a/packages/agentic-fabric/src/agentic_fabric/tools/meshy_mcp.py b/packages/agentic-fabric/src/agentic_fabric/tools/meshy_mcp.py new file mode 100644 index 0000000..9523b01 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/tools/meshy_mcp.py @@ -0,0 +1,214 @@ +"""MCP adapter for vendor-fabric Meshy capabilities. + +Meshy provider calls stay in vendor-fabric. This agentic-fabric module only +decides how those provider capabilities become MCP tools. +""" + +from __future__ import annotations + +import asyncio +import inspect +import json + +from collections.abc import Callable, Iterable, Mapping +from typing import Any, cast + + +MCP_INSTALL_MESSAGE = "MCP SDK not installed. Install with: pip install agentic-fabric[mcp]" +VENDOR_INSTALL_MESSAGE = ( + "vendor-fabric[meshy] is required for Meshy MCP adapters. Install it in the same environment." +) + + +def _install_error(message: str, error: ImportError) -> ImportError: + """Build install guidance without hiding the actual failed import.""" + detail = str(error) + if detail: + return ImportError(f"{message}\nOriginal import error: {detail}") + return ImportError(message) + + +def _require_meshy_tool_definitions() -> list[dict[str, Any]]: + """Load Meshy capability metadata from vendor-fabric lazily.""" + try: + from vendor_fabric.meshy.tools import TOOL_DEFINITIONS + except ImportError as exc: + raise _install_error(VENDOR_INSTALL_MESSAGE, exc) from exc + return list(TOOL_DEFINITIONS) + + +def _schema_for_definition(definition: Mapping[str, Any]) -> dict[str, Any]: + """Return an MCP input schema for one Meshy capability definition.""" + schema = definition.get("schema") + if schema is None: + return {"type": "object", "properties": {}, "required": []} + + model_json_schema = getattr(schema, "model_json_schema", None) + if callable(model_json_schema): + generated = model_json_schema() + return { + "type": "object", + "properties": generated.get("properties", {}), + "required": generated.get("required", []), + } + + return {"type": "object", "properties": {}, "required": []} + + +def _create_mcp_tools() -> list[tuple[Any, Callable[..., Any]]]: + """Create MCP tool definitions from Meshy provider capabilities.""" + try: + from mcp.types import Tool + except ImportError as exc: + raise ImportError(MCP_INSTALL_MESSAGE) from exc + + mcp_tools: list[tuple[Any, Callable[..., Any]]] = [] + for definition in _require_meshy_tool_definitions(): + func = definition["func"] + tool = Tool( + name=str(definition["name"]), + description=str(definition.get("description") or definition["name"]), + inputSchema=_schema_for_definition(definition), + ) + mcp_tools.append((tool, func)) + + return mcp_tools + + +def _to_builtin(value: Any) -> Any: + try: + from extended_data.containers import to_builtin + except ImportError: + if hasattr(value, "model_dump"): + return value.model_dump() + if isinstance(value, Mapping): + return {key: _to_builtin(item) for key, item in value.items()} + if isinstance(value, Iterable) and not isinstance(value, str | bytes | bytearray): + return [_to_builtin(item) for item in value] + return value + return to_builtin(value) + + +def _redact_sensitive_data(value: Any) -> Any: + try: + from extended_data.primitives.redaction import redact_sensitive_data + except ImportError: + return value + return redact_sensitive_data(value) + + +def _redact_sensitive_text(value: object, *, values: Iterable[Any] | None = None) -> str: + try: + from extended_data.primitives.redaction import redact_sensitive_text + except ImportError: + return str(value) + return redact_sensitive_text(value, values=values) + + +def _jsonable_tool_result(result: Any) -> Any: + """Lower Meshy tool results to JSON-compatible redacted data.""" + if hasattr(result, "model_dump"): + result = result.model_dump() + elif isinstance(result, Iterable) and not isinstance(result, str | bytes | bytearray | Mapping): + result = [item.model_dump() if hasattr(item, "model_dump") else item for item in result] + result = _to_builtin(result) + if isinstance(result, set | frozenset): + result = [_to_builtin(item) for item in result] + return _redact_sensitive_data(result) + + +def _tool_error_payload(error: object, *, values: Iterable[Any] | None = None) -> dict[str, str]: + """Return an MCP-safe error payload without raw secret values.""" + return {"error": _redact_sensitive_text(error, values=values)} + + +def _tool_payload_text(payload: Any) -> str: + """Return a serialized MCP text payload.""" + try: + from extended_data.io import wrap_raw_data_for_export + except ImportError: + return json.dumps(payload, indent=2, default=str) + return wrap_raw_data_for_export(payload, allow_encoding="json", indent_2=True) + + +def _tool_result_text(result: Any) -> str: + """Return a serialized Meshy MCP result.""" + return _tool_payload_text(_jsonable_tool_result(result)) + + +def create_server() -> Any: + """Create an MCP server with Meshy provider capabilities.""" + try: + from mcp.server import Server + except ImportError as exc: + raise ImportError(MCP_INSTALL_MESSAGE) from exc + + server = Server("meshy-ai") + mcp_tools = _create_mcp_tools() + tool_handlers = {tool.name: func for tool, func in mcp_tools} + tool_list = [tool for tool, _ in mcp_tools] + + tool_decorator = cast(Callable[[], Callable[[Callable[..., Any]], Callable[..., Any]]], server.list_tools) + call_decorator = cast(Callable[[], Callable[[Callable[..., Any]], Callable[..., Any]]], server.call_tool) + + @tool_decorator() + async def list_tools() -> list[Any]: + return tool_list + + @call_decorator() + async def call_tool(name: str, arguments: dict[str, Any] | None) -> list[Any]: + from mcp.types import TextContent + + tool_arguments = arguments or {} + handler = tool_handlers.get(name) + if not handler: + return [ + TextContent( + type="text", + text=_tool_payload_text(_tool_error_payload(f"Unknown tool: {name}")), + ) + ] + + try: + result = handler(**tool_arguments) + if inspect.isawaitable(result): + result = await result + return [TextContent(type="text", text=_tool_result_text(result))] + except Exception as exc: + return [ + TextContent( + type="text", + text=_tool_payload_text(_tool_error_payload(exc, values=tool_arguments.values())), + ) + ] + + return server + + +def run_server(server: Any | None = None) -> None: + """Run the Meshy MCP adapter over stdio.""" + try: + from mcp.server.stdio import stdio_server + except ImportError as exc: + raise ImportError(MCP_INSTALL_MESSAGE) from exc + + if server is None: + server = create_server() + + async def main() -> None: + async with stdio_server() as (read_stream, write_stream): + await server.run(read_stream, write_stream, server.create_initialization_options()) + + asyncio.run(main()) + + +def main() -> None: + """Entry point for the Meshy MCP adapter.""" + run_server() + + +if __name__ == "__main__": # pragma: no cover + main() + + +__all__ = ["create_server", "main", "run_server"] diff --git a/packages/agentic-fabric/src/agentic_fabric/tools/registry.py b/packages/agentic-fabric/src/agentic_fabric/tools/registry.py new file mode 100644 index 0000000..87bf507 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/tools/registry.py @@ -0,0 +1,165 @@ +"""Tool resolution helpers for configured fabric agent tools. + +This module turns YAML-declared tool names into instantiated tool objects +without forcing every optional dependency to import at module load time. +""" + +from __future__ import annotations + +import importlib +import logging +import os + +from collections.abc import Callable +from typing import Any + + +logger = logging.getLogger(__name__) + +ToolFactory = Callable[[], Any] + + +def _build_factory(module_name: str, attr_name: str) -> ToolFactory: + """Create a lazy factory for a tool class or callable.""" + + def factory() -> Any: + module = importlib.import_module(module_name) + tool = getattr(module, attr_name) + return tool() if callable(tool) else tool + + return factory + + +_TOOL_FACTORIES: dict[str, ToolFactory] = { + "GameCodeWriterTool": _build_factory("agentic_fabric.tools.file_tools", "GameCodeWriterTool"), + "GameCodeReaderTool": _build_factory("agentic_fabric.tools.file_tools", "GameCodeReaderTool"), + "DirectoryListTool": _build_factory("agentic_fabric.tools.file_tools", "DirectoryListTool"), + "ScrapeWebsiteTool": _build_factory("agentic_fabric.tools.scraping_tools", "ScrapeWebsiteTool"), + "CrawlWebsiteTool": _build_factory("agentic_fabric.tools.scraping_tools", "CrawlWebsiteTool"), +} + +_TOOL_ALIASES = { + "FileWriteTool": "GameCodeWriterTool", + "WriteFileTool": "GameCodeWriterTool", + "FileReadTool": "GameCodeReaderTool", + "ReadFileTool": "GameCodeReaderTool", + "ListDirectoryTool": "DirectoryListTool", + "ListFilesTool": "DirectoryListTool", + "mcp://filesystem/write_file": "GameCodeWriterTool", + "mcp://filesystem/read_file": "GameCodeReaderTool", + "mcp://filesystem/list_directory": "DirectoryListTool", +} + +_DEFAULT_IMPORT_PREFIXES = ("agentic_fabric.",) + + +def _canonical_tool_name(tool_name: str) -> str: + """Normalize aliases to a canonical tool identifier.""" + return _TOOL_ALIASES.get(tool_name, tool_name) + + +def register_tool_factory(tool_name: str, factory: ToolFactory, *, aliases: tuple[str, ...] = ()) -> None: + """Register a safe tool factory without using dynamic imports.""" + _TOOL_FACTORIES[tool_name] = factory + for alias in aliases: + _TOOL_ALIASES[alias] = tool_name + + +def _allowed_import_prefixes() -> tuple[str, ...]: + """Return module prefixes allowed for fully qualified tool references.""" + configured = os.environ.get("AGENTIC_FABRIC_TOOL_IMPORT_ALLOWLIST", "") + extra = tuple(prefix.strip() for prefix in configured.split(",") if prefix.strip()) + return (*_DEFAULT_IMPORT_PREFIXES, *extra) + + +def _is_import_allowed(module_name: str) -> bool: + """Return whether a fully qualified tool module may be imported.""" + return any(module_name == prefix.rstrip(".") or module_name.startswith(prefix) for prefix in _allowed_import_prefixes()) + + +def _resolve_vendor_tool(tool_name: str) -> Any | None: + """Resolve vendor-backed tool references without importing vendor SDKs.""" + if tool_name.startswith("vendor://"): + path = tool_name.removeprefix("vendor://") + provider, separator, operation = path.partition("/") + if provider and separator and operation: + from agentic_fabric.tools.vendor import VendorCapabilityTool + + return VendorCapabilityTool(provider, operation) + + if tool_name.startswith("vendor:"): + _, provider, operation = tool_name.split(":", 2) if tool_name.count(":") >= 2 else ("", "", "") + if provider and operation: + from agentic_fabric.tools.vendor import VendorCapabilityTool + + return VendorCapabilityTool(provider, operation) + + return None + + +def resolve_tool(tool_name: str) -> Any | None: + """Resolve a configured tool name to an instantiated tool object. + + Supports: + - built-in aliases like ``FileWriteTool`` + - selected filesystem MCP URIs + - fully-qualified ``module:attribute`` references + - fully-qualified ``package.module.ClassName`` references + """ + canonical_name = _canonical_tool_name(tool_name) + + vendor_tool = _resolve_vendor_tool(canonical_name) + if vendor_tool is not None: + return vendor_tool + + if canonical_name.startswith("mcp://"): + return None + + try: + if canonical_name in _TOOL_FACTORIES: + return _TOOL_FACTORIES[canonical_name]() + + if ":" in canonical_name: + module_name, attr_name = canonical_name.split(":", 1) + if not _is_import_allowed(module_name): + logger.warning("Skipping tool '%s': module is not in AGENTIC_FABRIC_TOOL_IMPORT_ALLOWLIST", tool_name) + return None + return _build_factory(module_name, attr_name)() + + if "." in canonical_name: + module_name, _, attr_name = canonical_name.rpartition(".") + if module_name and attr_name: + if not _is_import_allowed(module_name): + logger.warning("Skipping tool '%s': module is not in AGENTIC_FABRIC_TOOL_IMPORT_ALLOWLIST", tool_name) + return None + return _build_factory(module_name, attr_name)() + except (ImportError, AttributeError) as exc: + logger.warning("Failed to resolve tool '%s': %s", tool_name, exc) + return None + + return None + + +def resolve_tools(tool_names: list[str]) -> list[Any]: + """Resolve configured tool names to instantiated tool objects. + + Unknown or unavailable tools are skipped with a warning so that fabric agents can + continue to run with the capabilities available in the current environment. + """ + resolved: list[Any] = [] + seen: set[str] = set() + + for tool_name in tool_names: + canonical_name = _canonical_tool_name(tool_name) + if canonical_name in seen: + continue + + tool = resolve_tool(tool_name) + if tool is None: + logger.warning("Skipping unresolved tool '%s'", tool_name) + continue + + resolved.append(tool) + seen.add(canonical_name) + + return resolved diff --git a/packages/agentic-fabric/src/agentic_fabric/tools/scraping_tools.py b/packages/agentic-fabric/src/agentic_fabric/tools/scraping_tools.py new file mode 100644 index 0000000..7d6ccb9 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/tools/scraping_tools.py @@ -0,0 +1,148 @@ +"""Website scraping tools for CrewAI-compatible runners.""" + +from __future__ import annotations + +import logging + +from collections import deque +from ipaddress import ip_address +from urllib.parse import urljoin, urlparse + +import requests + +from bs4 import BeautifulSoup + + +log = logging.getLogger(__name__) + +MAX_CRAWL_PAGES = 25 +MAX_CRAWL_DEPTH = 2 +_ALLOWED_SCHEMES = {"http", "https"} +_BLOCKED_HOSTS = {"localhost"} + + +def _is_safe_crawl_url(url: str) -> bool: + """Return whether a URL is safe for direct crawler requests.""" + parsed = urlparse(url) + if parsed.scheme not in _ALLOWED_SCHEMES or not parsed.hostname: + return False + + host = parsed.hostname.lower().rstrip(".") + if host in _BLOCKED_HOSTS or host.endswith(".localhost"): + return False + + try: + address = ip_address(host) + except ValueError: + return True + + return not ( + address.is_private + or address.is_loopback + or address.is_link_local + or address.is_multicast + or address.is_reserved + or address.is_unspecified + ) + + +def _get_safe_response(url: str) -> requests.Response | None: + """Request a safe URL without following redirects to unvalidated hosts.""" + try: + response = requests.get(url, timeout=30, allow_redirects=False) + response.raise_for_status() + except requests.RequestException: + log.exception("Error requesting %s", url) + return None + + status_code = getattr(response, "status_code", None) + if isinstance(status_code, int) and 300 <= status_code < 400: + log.warning("Skipping redirected URL %s", url) + return None + return response + + +class ScrapeWebsiteTool: + """A small first-party website scraping tool with CrewAI-compatible shape.""" + + name: str = "ScrapeWebsiteTool" + description: str = "Scrape readable content from a URL." + + def _run(self, url: str) -> str: + """Scrape readable content from one safe HTTP(S) URL.""" + if not _is_safe_crawl_url(url): + log.warning("Skipping unsafe scrape URL %s", url) + return "" + + response = _get_safe_response(url) + if response is None: + return "" + + soup = BeautifulSoup(response.content, "html.parser") + return self._scrape_content(soup) + + def _scrape_content(self, soup: BeautifulSoup) -> str: + """Scrape readable content from a BeautifulSoup object.""" + for script_or_style in soup(["script", "style"]): + script_or_style.decompose() + return " ".join(t.strip() for t in soup.stripped_strings) + + +class CrawlWebsiteTool(ScrapeWebsiteTool): + """A tool for crawling a website and scraping its content. + + This tool extends ScrapeWebsiteTool to support crawling multiple pages + starting from a given URL. + """ + + name: str = "CrawlWebsiteTool" + description: str = "Crawl a website from a given URL and scrape its content." + + def _run(self, url: str) -> str: + """Crawls a website and returns the scraped content. + + Args: + url: The URL to start crawling from. + + Returns: + The scraped content of the website. + """ + if not _is_safe_crawl_url(url): + log.warning("Skipping unsafe crawl URL %s", url) + return "" + + scraped_content: list[str] = [] + visited_urls: set[str] = set() + queued_urls = {url} + urls_to_visit = deque([(url, 0)]) + base_netloc = urlparse(url).netloc + + while urls_to_visit and len(visited_urls) < MAX_CRAWL_PAGES: + current_url, depth = urls_to_visit.popleft() + queued_urls.discard(current_url) + visited_urls.add(current_url) + + response = _get_safe_response(current_url) + if response is None: + continue + soup = BeautifulSoup(response.content, "html.parser") + page_content = self._scrape_content(soup) + if page_content: + scraped_content.append(page_content) + + if depth >= MAX_CRAWL_DEPTH: + continue + + for link in soup.find_all("a", href=True): + next_url = urljoin(current_url, link["href"]) + if ( + next_url not in visited_urls + and next_url not in queued_urls + and len(visited_urls) + len(queued_urls) < MAX_CRAWL_PAGES + and urlparse(next_url).netloc == base_netloc + and _is_safe_crawl_url(next_url) + ): + urls_to_visit.append((next_url, depth + 1)) + queued_urls.add(next_url) + + return " ".join(scraped_content) diff --git a/packages/agentic-fabric/src/agentic_fabric/tools/vendor.py b/packages/agentic-fabric/src/agentic_fabric/tools/vendor.py new file mode 100644 index 0000000..9050272 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/tools/vendor.py @@ -0,0 +1,94 @@ +"""Vendor-backed agent tool wrappers.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + + +class VendorCapabilityTool: + """Generic tool that routes work through AgenticData/VendorData.""" + + def __init__( + self, + provider: str, + operation: str, + *, + data: Any | None = None, + description: str | None = None, + metadata: Any | None = None, + ) -> None: + """Initialize a lazy wrapper for one provider operation.""" + self.provider = provider + self.operation = operation + self.name = f"{provider}_{operation}".replace("-", "_") + self.description = description or f"Run vendor-fabric operation {operation!r} on provider {provider!r}." + self.data = data + self.metadata = metadata + + @classmethod + def from_metadata(cls, metadata: Any, *, data: Any | None = None) -> VendorCapabilityTool: + """Build a tool wrapper from one ``VendorData.capabilities()`` item.""" + provider = str(_metadata_value(metadata, "provider", "")).strip() + operation = str(_metadata_value(metadata, "operation", "")).strip() + if not provider or not operation: + msg = "Vendor capability metadata requires provider and operation" + raise ValueError(msg) + description = str(_metadata_value(metadata, "description", "")).strip() or None + return cls(provider, operation, data=data, description=description, metadata=metadata) + + def _run(self, **kwargs: Any) -> Any: + """Execute the vendor operation with keyword arguments.""" + data = self.data + if data is None: + from agentic_fabric.agentic_data import AgenticData + + data = AgenticData() + return data.call(self.operation, self.provider, **kwargs) + + def __call__(self, **kwargs: Any) -> Any: + """Allow framework adapters to treat this as a plain callable.""" + return self._run(**kwargs) + + +def vendor_capability_tools( + data: Any | None = None, + *, + provider: str | None = None, + include_unavailable: bool = True, +) -> list[VendorCapabilityTool]: + """Return agent-facing tools for capabilities exposed by ``VendorData``.""" + if data is None: + from agentic_fabric.agentic_data import AgenticData + + data = AgenticData() + + capabilities = getattr(data, "capabilities", None) + if not callable(capabilities): + return [] + + raw_capabilities = ( + capabilities(provider, include_unavailable=include_unavailable) + if provider is not None + else capabilities(include_unavailable=include_unavailable) + ) + + tools: list[VendorCapabilityTool] = [] + for capability in raw_capabilities: + try: + tools.append(VendorCapabilityTool.from_metadata(capability, data=data)) + except ValueError: + continue + return tools + + +def _metadata_value(metadata: Any, key: str, default: Any) -> Any: + """Read capability metadata from mappings or Extended Data-like objects.""" + if isinstance(metadata, Mapping): + return metadata.get(key, default) + + getter = getattr(metadata, "get", None) + if callable(getter): + return getter(key, default) + + return getattr(metadata, key, default) diff --git a/packages/agentic-fabric/src/agentic_fabric/tools/vendor_mcp.py b/packages/agentic-fabric/src/agentic_fabric/tools/vendor_mcp.py new file mode 100644 index 0000000..2cc7d1c --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/tools/vendor_mcp.py @@ -0,0 +1,338 @@ +"""MCP adapters for vendor-fabric provider capabilities. + +This module lives in agentic-fabric because MCP is a runtime-visible tool +transport. The provider implementation and capability metadata stay in +vendor-fabric and are imported lazily only when the MCP server is created. +""" + +from __future__ import annotations + +import asyncio +import builtins +import inspect +import json + +from collections.abc import Callable, Iterable, Mapping +from typing import Any, cast, get_origin, get_type_hints + + +MCP_INSTALL_MESSAGE = "MCP SDK not installed. Install with: pip install agentic-fabric[mcp]" +VENDOR_INSTALL_MESSAGE = ( + "vendor-fabric is required for vendor MCP adapters. Install vendor-fabric in the same environment." +) + + +def _install_error(message: str, error: ImportError) -> ImportError: + """Build install guidance without hiding the actual failed import.""" + detail = str(error) + if detail: + return ImportError(f"{message}\nOriginal import error: {detail}") + return ImportError(message) + + +def _require_vendor_fabric() -> tuple[Any, Callable[..., Any]]: + """Load vendor-fabric registry and surface helpers lazily.""" + try: + from vendor_fabric import registry + from vendor_fabric.surface import connector_data_methods + except ImportError as exc: + raise _install_error(VENDOR_INSTALL_MESSAGE, exc) from exc + return registry, connector_data_methods + + +def _get_method_schema(method: Callable[..., Any]) -> dict[str, Any]: + """Generate a JSON schema from a provider method signature.""" + sig = inspect.signature(method) + try: + type_hints = get_type_hints(method) + except Exception: + type_hints = {} + properties: dict[str, dict[str, Any]] = {} + required: list[str] = [] + + for name, param in sig.parameters.items(): + if name in {"self", "cls"}: + continue + + prop: dict[str, Any] = {"type": "string"} + ann = type_hints.get(name, param.annotation) + if ann != inspect.Parameter.empty: + if ann is int: + prop = {"type": "integer"} + elif ann is float: + prop = {"type": "number"} + elif ann is bool: + prop = {"type": "boolean"} + elif ann is list or get_origin(ann) is list: + prop = {"type": "array"} + elif ann is dict or get_origin(ann) is dict: + prop = {"type": "object"} + + if method.__doc__: + for line in method.__doc__.split("\n"): + if f"{name}:" in line.lower(): + prop["description"] = line.split(":", 1)[-1].strip() + break + + if param.default != inspect.Parameter.empty: + prop["default"] = param.default + else: + required.append(name) + + properties[name] = prop + + return {"type": "object", "properties": properties, "required": required} + + +def _get_public_methods( + connector_class: builtins.type[Any], + connector_data_methods: Callable[[builtins.type[Any]], list[tuple[str, Callable[..., Any]]]], +) -> list[tuple[str, Callable[..., Any]]]: + """Get public provider data methods for MCP exposure.""" + return connector_data_methods(connector_class) + + +def _connector_classes(registry: Any) -> Mapping[str, builtins.type[Any]]: + """Return connector classes for runtime method exposure. + + vendor-fabric currently keeps class discovery internal. The MCP adapter + isolates that private call so catalog tools still work if the public + surface changes before a stable method-list API is published. + """ + list_connector_classes = getattr(registry, "_list_connector_classes", None) + if not callable(list_connector_classes): + return {} + return cast(Mapping[str, builtins.type[Any]], list_connector_classes()) + + +def _catalog_tool_definitions(registry: Any) -> dict[str, dict[str, Any]]: + """Build credential-free vendor catalog MCP tools.""" + include_unavailable_schema: dict[str, Any] = { + "type": "object", + "properties": {"include_unavailable": {"type": "boolean", "default": True}}, + "required": [], + } + empty_schema: dict[str, Any] = {"type": "object", "properties": {}, "required": []} + name_schema: dict[str, Any] = { + "type": "object", + "properties": { + "name": {"type": "string"}, + "include_unavailable": {"type": "boolean", "default": True}, + }, + "required": ["name"], + } + category_schema: dict[str, Any] = { + "type": "object", + "properties": { + "category": {"type": "string"}, + "include_unavailable": {"type": "boolean", "default": True}, + }, + "required": ["category"], + } + capability_schema: dict[str, Any] = { + "type": "object", + "properties": { + "capability": {"type": "string"}, + "include_unavailable": {"type": "boolean", "default": True}, + }, + "required": ["capability"], + } + + return { + "fabric_vendors_list": { + "description": "List vendors registered in the fabric catalog.", + "parameters": include_unavailable_schema, + "handler": registry.list_connectors, + }, + "fabric_vendors_list_available": { + "description": "List vendors currently available in this environment.", + "parameters": empty_schema, + "handler": registry.list_available_connectors, + }, + "fabric_vendors_list_info": { + "description": "List vendor catalog metadata.", + "parameters": include_unavailable_schema, + "handler": registry.list_connector_info, + }, + "fabric_vendor_get_info": { + "description": "Get catalog metadata for one vendor.", + "parameters": name_schema, + "handler": registry.get_connector_info, + }, + "fabric_vendors_list_categories": { + "description": "List vendor categories in the fabric catalog.", + "parameters": include_unavailable_schema, + "handler": registry.list_connector_categories, + }, + "fabric_vendors_list_capabilities": { + "description": "List vendor capabilities in the fabric catalog.", + "parameters": include_unavailable_schema, + "handler": registry.list_connector_capabilities, + }, + "fabric_vendors_list_by_category": { + "description": "List vendor catalog entries for a category.", + "parameters": category_schema, + "handler": registry.list_connectors_by_category, + }, + "fabric_vendors_list_by_capability": { + "description": "List vendor catalog entries for a capability.", + "parameters": capability_schema, + "handler": registry.list_connectors_by_capability, + }, + } + + +def _to_builtin(value: Any) -> Any: + try: + from extended_data.containers import to_builtin + except ImportError: + if hasattr(value, "model_dump"): + return value.model_dump() + if isinstance(value, Mapping): + return {key: _to_builtin(item) for key, item in value.items()} + if isinstance(value, Iterable) and not isinstance(value, str | bytes | bytearray): + return [_to_builtin(item) for item in value] + return value + return to_builtin(value) + + +def _redact_sensitive_data(value: Any) -> Any: + try: + from extended_data.primitives.redaction import redact_sensitive_data + except ImportError: + return value + return redact_sensitive_data(value) + + +def _redact_sensitive_text(value: object, *, values: Iterable[Any] | None = None) -> str: + try: + from extended_data.primitives.redaction import redact_sensitive_text + except ImportError: + return str(value) + return redact_sensitive_text(value, values=values) + + +def _jsonable_tool_result(result: Any) -> Any: + """Lower provider tool results to JSON-compatible redacted data.""" + if hasattr(result, "model_dump"): + result = result.model_dump() + elif isinstance(result, Iterable) and not isinstance(result, str | bytes | bytearray | Mapping): + result = [item.model_dump() if hasattr(item, "model_dump") else item for item in result] + result = _to_builtin(result) + if isinstance(result, set | frozenset): + result = [_to_builtin(item) for item in result] + return _redact_sensitive_data(result) + + +def _tool_error_text(error: Exception, values: Iterable[Any] | None = None) -> str: + """Return an MCP-safe error string without raw secret values.""" + return f"Error: {type(error).__name__}: {_redact_sensitive_text(error, values=values)}" + + +def _unknown_tool_text(name: str) -> str: + """Return an MCP-safe unknown-tool diagnostic.""" + return f"Unknown tool: {_redact_sensitive_text(name)}" + + +def _tool_result_text(result: Any) -> str: + """Return a serialized MCP tool result.""" + payload = _jsonable_tool_result(result) + try: + from extended_data.io import wrap_raw_data_for_export + except ImportError: + return json.dumps(payload, indent=2, default=str) + return wrap_raw_data_for_export(payload, allow_encoding="json", indent_2=True, default=str) + + +def create_server() -> Any: + """Create an MCP server exposing vendor-fabric provider capabilities.""" + try: + from mcp.server import Server + from mcp.types import TextContent, Tool + except ImportError as exc: + raise ImportError(MCP_INSTALL_MESSAGE) from exc + + registry, connector_data_methods = _require_vendor_fabric() + server = Server("vendor-fabric") + tools: dict[str, dict[str, Any]] = _catalog_tool_definitions(registry) + + for connector_name, connector_class in _connector_classes(registry).items(): + for method_name, method in _get_public_methods(connector_class, connector_data_methods): + if method_name in {"close", "request", "get_input", "register_tool"}: + continue + + description = method.__doc__.split("\n")[0].strip() if method.__doc__ else "" + tools[f"{connector_name}_{method_name}"] = { + "connector": connector_name, + "method": method_name, + "description": description or f"{connector_name}.{method_name}()", + "parameters": _get_method_schema(method), + } + + tool_decorator = cast(Callable[[], Callable[[Callable[..., Any]], Callable[..., Any]]], server.list_tools) + call_decorator = cast(Callable[[], Callable[[Callable[..., Any]], Callable[..., Any]]], server.call_tool) + + @tool_decorator() + async def list_tools() -> list[Tool]: + return [ + Tool(name=name, description=tool["description"], inputSchema=tool["parameters"]) + for name, tool in tools.items() + ] + + @call_decorator() + async def call_tool(name: str, arguments: dict[str, Any] | None) -> list[TextContent]: + tool_arguments = arguments or {} + if name not in tools: + return [TextContent(type="text", text=_unknown_tool_text(name))] + + tool = tools[name] + handler = tool.get("handler") + if callable(handler): + try: + result = handler(**tool_arguments) + if inspect.iscoroutine(result): + result = await result + return [TextContent(type="text", text=_tool_result_text(result))] + except Exception as exc: + return [TextContent(type="text", text=_tool_error_text(exc, tool_arguments.values()))] + + try: + connector = registry.get_connector(tool["connector"]) + method = getattr(connector, tool["method"]) + result = method(**tool_arguments) + if inspect.iscoroutine(result): + result = await result + return [TextContent(type="text", text=_tool_result_text(result))] + except Exception as exc: + return [TextContent(type="text", text=_tool_error_text(exc, tool_arguments.values()))] + + return server + + +def run_server(server: Any | None = None) -> None: + """Run the vendor MCP adapter over stdio.""" + try: + from mcp.server.stdio import stdio_server + except ImportError as exc: + raise ImportError(MCP_INSTALL_MESSAGE) from exc + + if server is None: + server = create_server() + + async def run() -> None: + async with stdio_server() as (read_stream, write_stream): + await server.run(read_stream, write_stream, server.create_initialization_options()) + + asyncio.run(run()) + + +def main() -> None: + """Entry point for the vendor MCP adapter.""" + run_server() + + +if __name__ == "__main__": # pragma: no cover + main() + + +__all__ = ["create_server", "main", "run_server"] diff --git a/packages/agentic-fabric/src/agentic_fabric/utils/__init__.py b/packages/agentic-fabric/src/agentic_fabric/utils/__init__.py new file mode 100644 index 0000000..c649d85 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/utils/__init__.py @@ -0,0 +1,6 @@ +"""Utility helpers for agentic-fabric.""" + +from .files import load_config + + +__all__ = ["load_config"] diff --git a/packages/agentic-fabric/src/agentic_fabric/utils/files.py b/packages/agentic-fabric/src/agentic_fabric/utils/files.py new file mode 100644 index 0000000..2ed3391 --- /dev/null +++ b/packages/agentic-fabric/src/agentic_fabric/utils/files.py @@ -0,0 +1,25 @@ +"""File loading helpers.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any, cast + +import yaml + + +def load_config(path: Path | str) -> dict[str, Any]: + """Loads a YAML configuration file. + + Args: + path: The path to the YAML file. + + Returns: + A dictionary containing the configuration. + """ + with open(path, encoding="utf-8") as f: + loaded = yaml.safe_load(f) or {} + if not isinstance(loaded, dict): + msg = f"Expected mapping at top level of YAML config: {path}" + raise TypeError(msg) + return cast(dict[str, Any], loaded) diff --git a/packages/agentic-fabric/tests/__init__.py b/packages/agentic-fabric/tests/__init__.py new file mode 100644 index 0000000..5b3f2a0 --- /dev/null +++ b/packages/agentic-fabric/tests/__init__.py @@ -0,0 +1,3 @@ +"""Tests for agentic-fabric package.""" + +from __future__ import annotations diff --git a/packages/agentic-fabric/tests/conftest.py b/packages/agentic-fabric/tests/conftest.py new file mode 100644 index 0000000..1c544bb --- /dev/null +++ b/packages/agentic-fabric/tests/conftest.py @@ -0,0 +1,150 @@ +"""Pytest configuration for agentic-fabric tests.""" + +from __future__ import annotations + +import os + +from collections.abc import Generator +from pathlib import Path +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest + +from pytest_agentic_fabric.mocking import ( # noqa: F401 + fabric_mocker, + mock_agentic_frameworks, + mock_crewai, + mock_langgraph, + mock_strands, +) +from pytest_agentic_fabric.plugin import ( # noqa: F401 + check_api_key, + check_aws_credentials, + fabric_agent_with_knowledge, + multi_agent_fabric_agent_config, + simple_agent_config, + simple_fabric_agent_config, + simple_task_config, + temp_fabric_dir, +) + + +def pytest_addoption(parser: Any) -> None: + """Register test-suite options for live E2E coverage.""" + parser.addoption( + "--e2e", + action="store_true", + default=False, + help="Run E2E tests that require real framework runtimes and external credentials", + ) + parser.addoption( + "--framework", + action="store", + choices=("crewai", "langgraph", "strands"), + default=None, + help="Run E2E tests for a single framework", + ) + + +def pytest_collection_modifyitems(config: Any, items: list[pytest.Item]) -> None: + """Skip live E2E tests unless explicitly enabled and filter by framework.""" + e2e_enabled = config.getoption("--e2e") + framework_filter = config.getoption("--framework") + + skip_e2e = pytest.mark.skip(reason="E2E tests disabled; pass --e2e to run them") + skip_framework = pytest.mark.skip(reason=f"Test not selected by --framework={framework_filter}") + framework_markers = {"crewai", "langgraph", "strands"} + + for item in items: + if "e2e" in item.keywords and not e2e_enabled: + item.add_marker(skip_e2e) + + if framework_filter: + test_frameworks = framework_markers.intersection(item.keywords) + if test_frameworks and framework_filter not in test_frameworks: + item.add_marker(skip_framework) + + +@pytest.fixture(autouse=True) +def mock_llm_env(request: pytest.FixtureRequest) -> Generator[None, Any, None]: + """Set up test environment with mocked LLM credentials.""" + if "e2e" in request.node.keywords: + yield + return + + # Set dummy API keys for testing (will be mocked anyway) + with patch.dict( + os.environ, + { + "OPENAI_API_KEY": "sk-test-mock-key", + "ANTHROPIC_API_KEY": "sk-ant-test-mock-key", + "CREWAI_TESTING": "true", + }, + ): + yield + + +@pytest.fixture +def temp_workspace(tmp_path: Path) -> Path: + """Create a temporary workspace with package structure.""" + # Create packages directory structure + packages_dir = tmp_path / "packages" + packages_dir.mkdir() + + # Create a mock sample package with framework-agnostic .fabric structure + sample_dir = packages_dir / "sample" + sample_dir.mkdir() + + fabric_dir = sample_dir / ".fabric" + fabric_dir.mkdir() + + # Create minimal manifest (dict format, not list) + manifest = fabric_dir / "manifest.yaml" + manifest.write_text( + """ +name: sample +description: Test package +fabric_agents: + test_fabric_agent: + description: A test fabric agent + agents: fabric_agents/test_fabric_agent/agents.yaml + tasks: fabric_agents/test_fabric_agent/tasks.yaml +""", + encoding="utf-8", + ) + + # Create fabric_agents directory + fabric_agents_dir = fabric_dir / "fabric_agents" / "test_fabric_agent" + fabric_agents_dir.mkdir(parents=True) + + # Create minimal agent and task configs + (fabric_agents_dir / "agents.yaml").write_text( + """ +test_agent: + role: Test Agent + goal: Test goal + backstory: Test backstory +""", + encoding="utf-8", + ) + + (fabric_agents_dir / "tasks.yaml").write_text( + """ +test_task: + description: Test task description + expected_output: Test output + agent: test_agent +""", + encoding="utf-8", + ) + + return tmp_path + + +@pytest.fixture +def mock_fabric_agent_result() -> MagicMock: + """Create a mock fabric agent result.""" + result = MagicMock() + result.raw = {"output": "test output", "success": True} + return result diff --git a/packages/agentic-fabric/tests/e2e/README.md b/packages/agentic-fabric/tests/e2e/README.md new file mode 100644 index 0000000..cf1d444 --- /dev/null +++ b/packages/agentic-fabric/tests/e2e/README.md @@ -0,0 +1,203 @@ +# End-to-End Tests + +This directory contains end-to-end tests for framework runners (CrewAI, LangGraph, Strands). + +## Overview + +E2E tests verify that each framework runner works correctly with: +- Real LLM API calls +- Complete fabric_agent execution flows +- Multi-agent collaboration +- Knowledge source integration +- Tool usage + +## Running E2E Tests + +E2E tests are **disabled by default** to avoid: +- Unexpected API costs +- Network dependency in CI/CD +- Slow test execution + +### Run all E2E tests + +```bash +# Requires ANTHROPIC_API_KEY environment variable +export ANTHROPIC_API_KEY="your-api-key" +uv run pytest tests/e2e/ --e2e -v +``` + +### Run framework-specific tests + +```bash +# Only CrewAI tests +uv run pytest tests/e2e/ --e2e --framework=crewai -v + +# Only LangGraph tests (requires langgraph installed) +uv run pytest tests/e2e/ --e2e --framework=langgraph -v + +# Only Strands tests (requires strands-agents installed) +uv run pytest tests/e2e/ --e2e --framework=strands -v +``` + +### Run specific test + +```bash +uv run pytest tests/e2e/test_crewai_e2e.py::TestCrewAISimpleExecution::test_simple_crew_execution --e2e -v +``` + +## Environment Variables + +### Required + +- `ANTHROPIC_API_KEY` - For Claude LLM (CrewAI, LangGraph) + +### Optional (for Strands with AWS Bedrock) + +- `AWS_ACCESS_KEY_ID` - AWS credentials +- `AWS_SECRET_ACCESS_KEY` - AWS credentials +- `AWS_REGION` - AWS region (e.g., `us-west-2`) + +#### Bedrock Model IDs + +When configuring Strands tests with Bedrock, use the official model IDs from [Claude on Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-on-amazon-bedrock): + +| Model | Bedrock Model ID | +|-------|------------------| +| Claude Haiku 4.5 ⭐ | `anthropic.claude-haiku-4-5-20251001-v1:0` | +| Claude Sonnet 4.5 | `anthropic.claude-sonnet-4-5-20250929-v1:0` | +| Claude Sonnet 4 | `anthropic.claude-sonnet-4-20250514-v1:0` | + +**Recommended**: Use Claude Haiku 4.5 for E2E tests - it's fast, cost-effective, and capable. + +Example configuration: +```python +fabric_agent_config = { + "llm": { + "provider": "bedrock", + "model": "anthropic.claude-haiku-4-5-20251001-v1:0", + }, + # ... rest of config +} +``` + +## Test Structure + +```text +tests/e2e/ +├── conftest.py # E2E fixtures and pytest configuration +├── test_crewai_e2e.py # CrewAI runner E2E tests +├── test_langgraph_e2e.py # LangGraph runner E2E tests +└── test_strands_e2e.py # Strands runner E2E tests +``` + +## Test Coverage + +### CrewAI (`test_crewai_e2e.py`) + +- ✅ Simple single-agent fabric_agent execution +- ✅ Build and run convenience method +- ✅ Multi-agent fabric_agent collaboration +- ✅ Knowledge source integration +- ✅ Tool usage with agents + +### LangGraph (`test_langgraph_e2e.py`) + +- ✅ ReAct agent execution +- ✅ Build and run method +- ✅ Multi-step graph execution +- ✅ State management between nodes +- ✅ Tool usage with agents + +### Strands (`test_strands_e2e.py`) + +- ✅ Simple agent execution +- ✅ Build and run method +- ✅ Agent building from configuration +- ✅ Task building +- ✅ System prompt generation +- ✅ AWS Bedrock integration (if credentials available) +- ✅ Tool usage (basic) + +## CI/CD Integration + +E2E tests are **excluded from regular CI runs** in `.github/workflows/ci.yml`: + +```yaml +- name: Run tests + run: uv run pytest tests -v --ignore=tests/e2e +``` + +To run E2E tests in CI (e.g., on a schedule): + +```yaml +# .github/workflows/e2e.yml +name: E2E Tests +on: + schedule: + - cron: '0 0 * * 0' # Weekly on Sunday + workflow_dispatch: + +jobs: + e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: hynek/setup-cached-uv@v2 + - run: uv sync --extra tests --extra langgraph --extra strands + - run: uv pip install crewai + - run: uv run pytest tests/e2e/ --e2e -v + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} +``` + +## Writing New E2E Tests + +1. Mark tests with `@pytest.mark.e2e` and framework marker +2. Use `check_api_key` fixture to verify required credentials +3. Keep tests focused and minimal to reduce API costs +4. Use fixtures from `conftest.py` for common configurations + +Note: Tests marked with `@pytest.mark.e2e` are automatically skipped unless the `--e2e` flag is provided. + +Example: + +```python +@pytest.mark.e2e +@pytest.mark.crewai +def test_my_feature( + check_api_key: None, + simple_fabric_agent_config: dict[str, Any], +) -> None: + """Test my new feature.""" + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + runner = CrewAIRunner() + result = runner.build_and_run(simple_fabric_agent_config, {"input": "test"}) + assert result is not None +``` + +## Debugging + +If tests fail: + +1. Check API key is set: `echo $ANTHROPIC_API_KEY` +2. Verify framework is installed: `pip list | grep crewai` +3. Run with verbose output: `-vv` +4. Run single test to isolate issue +5. Check network connectivity to LLM APIs + +## Cost Considerations + +E2E tests make real API calls which incur costs: + +- **CrewAI tests**: ~5-10 tests × ~$0.01-0.05 per test = ~$0.05-0.50 +- **LangGraph tests**: ~5 tests × ~$0.01-0.05 per test = ~$0.05-0.25 +- **Strands tests**: ~6-7 tests × $0.01-0.05 per test = ~$0.06-0.35 + +**Total estimated cost per full E2E run**: ~$0.16-1.10 + +To minimize costs: +- Run E2E tests only when needed +- Use `--framework` filter for specific tests +- Run on schedule instead of per-PR +- Use test timeouts to prevent runaway costs diff --git a/packages/agentic-fabric/tests/e2e/__init__.py b/packages/agentic-fabric/tests/e2e/__init__.py new file mode 100644 index 0000000..e64e48f --- /dev/null +++ b/packages/agentic-fabric/tests/e2e/__init__.py @@ -0,0 +1,7 @@ +"""End-to-end tests for framework runners. + +These tests verify that each framework runner works with real fabric_agents and LLM calls. +They are disabled by default and require the --e2e flag to run. +""" + +from __future__ import annotations diff --git a/packages/agentic-fabric/tests/e2e/conftest.py b/packages/agentic-fabric/tests/e2e/conftest.py new file mode 100644 index 0000000..d804e5d --- /dev/null +++ b/packages/agentic-fabric/tests/e2e/conftest.py @@ -0,0 +1,21 @@ +"""E2E test configuration. + +Fixtures and CLI options are provided by pytest-agentic-fabric plugin. +This file is kept for any project-specific customizations. +""" + +from __future__ import annotations + +# All fixtures come from tests/conftest.py: +# - check_api_key +# - check_aws_credentials +# - simple_agent_config +# - simple_task_config +# - simple_fabric_agent_config +# - multi_agent_fabric_agent_config +# - fabric_agent_with_knowledge +# - temp_fabric_dir +# +# CLI options: +# - --e2e: Enable E2E tests +# - --framework=: Filter by framework diff --git a/packages/agentic-fabric/tests/e2e/test_crewai_e2e.py b/packages/agentic-fabric/tests/e2e/test_crewai_e2e.py new file mode 100644 index 0000000..c77d41c --- /dev/null +++ b/packages/agentic-fabric/tests/e2e/test_crewai_e2e.py @@ -0,0 +1,214 @@ +"""End-to-end tests for CrewAI runner. + +These tests verify that the CrewAI runner works correctly with real LLM calls. +They require: +- The --e2e flag to run +- ANTHROPIC_API_KEY environment variable +- CrewAI installed (pip install crewai[tools]) +""" + +from __future__ import annotations + +from typing import Any + +import pytest + + +# Check if CrewAI is available +try: + import crewai # noqa: F401 + + CREWAI_AVAILABLE = True +except ImportError: + CREWAI_AVAILABLE = False + +# Skip all tests if CrewAI is not installed +pytestmark = [ + pytest.mark.e2e, + pytest.mark.crewai, + pytest.mark.skipif(not CREWAI_AVAILABLE, reason="crewai not installed"), +] + + +class TestCrewAISimpleExecution: + """Test simple fabric_agent execution with CrewAI.""" + + def test_simple_crew_execution( + self, + check_api_key: None, + simple_fabric_agent_config: dict[str, Any], + ) -> None: + """Test execution of a simple single-agent, single-task fabric_agent. + + Args: + check_api_key: Fixture to check for required API keys. + simple_fabric_agent_config: Simple fabric_agent configuration fixture. + """ + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + runner = CrewAIRunner() + + # Build the fabric_agent + fabric_agent = runner.build_fabric_agent(simple_fabric_agent_config) + assert fabric_agent is not None + + # Run the fabric_agent with a simple input + inputs = {"input": "What is 2 + 2?"} + result = runner.run(fabric_agent, inputs) + + # Verify we got a response + assert result is not None + assert isinstance(result, str) + assert len(result) > 0 + # The answer should contain '4' somewhere + assert "4" in result + + def test_fabric_agent_build_and_run( + self, + check_api_key: None, + simple_fabric_agent_config: dict[str, Any], + ) -> None: + """Test the convenience build_and_run method. + + Args: + check_api_key: Fixture to check for required API keys. + simple_fabric_agent_config: Simple fabric_agent configuration fixture. + """ + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + runner = CrewAIRunner() + + # Use build_and_run convenience method + inputs = {"input": "What is the capital of France?"} + result = runner.build_and_run(simple_fabric_agent_config, inputs) + + # Verify we got a response about Paris + assert result is not None + assert isinstance(result, str) + assert len(result) > 0 + assert "Paris" in result or "paris" in result.lower() + + +class TestCrewAIMultiAgent: + """Test multi-agent collaboration with CrewAI.""" + + def test_multi_agent_fabric_agent( + self, + check_api_key: None, + multi_agent_fabric_agent_config: dict[str, Any], + ) -> None: + """Test multi-agent fabric_agent with sequential task execution. + + Args: + check_api_key: Fixture to check for required API keys. + multi_agent_fabric_agent_config: Multi-agent fabric_agent configuration fixture. + """ + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + runner = CrewAIRunner() + + # Build and run multi-agent fabric_agent + inputs = {"input": "Tell me about Python programming"} + result = runner.build_and_run(multi_agent_fabric_agent_config, inputs) + + # Verify we got a result from the collaborative effort + assert result is not None + assert isinstance(result, str) + assert len(result) > 0 + # Should mention Python in some form + assert "Python" in result or "python" in result.lower() + + +class TestCrewAIKnowledge: + """Test knowledge source integration with CrewAI.""" + + def test_knowledge_source_integration( + self, + check_api_key: None, + fabric_agent_with_knowledge: dict[str, Any], + ) -> None: + """Test fabric agent with knowledge sources loaded from files. + + Args: + check_api_key: Fixture to check for required API keys. + fabric_agent_with_knowledge: Fabric agent config with knowledge sources fixture. + """ + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + runner = CrewAIRunner() + + # Build and run fabric agent with knowledge. + inputs = {"question": "What color is mentioned in the knowledge base?"} + result = runner.build_and_run(fabric_agent_with_knowledge, inputs) + + # Verify the fabric agent accessed the knowledge. + assert result is not None + assert isinstance(result, str) + assert len(result) > 0 + # Should mention blue from the knowledge file + assert "blue" in result.lower() or "Blue" in result + + +class TestCrewAITools: + """Test tool usage with CrewAI.""" + + def test_tool_usage( + self, + check_api_key: None, + simple_agent_config: dict[str, Any], + ) -> None: + """Test fabric_agent with tools assigned to agents. + + Args: + check_api_key: Fixture to check for required API keys. + simple_agent_config: Simple agent configuration fixture. + """ + from crewai_tools import tool + + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + # Create a simple tool + @tool("get_test_data") + def get_test_data(query: str) -> str: + """Get test data for a query. + + Args: + query: The query string. + + Returns: + Test data response. + """ + # Use the query parameter in the response for completeness + return f"Test data for '{query}': The answer is 42" + + runner = CrewAIRunner() + + # Build agent with tool + agent = runner.build_agent(simple_agent_config, tools=[get_test_data]) + assert agent is not None + + # Build a simple task + task_config = { + "description": "Use the get_test_data tool to find the answer", + "expected_output": "The answer from the tool", + } + task = runner.build_task(task_config, agent) + assert task is not None + + # Create a minimal fabric_agent + from crewai import Crew, Process + + fabric_agent = Crew( + agents=[agent], + tasks=[task], + process=Process.sequential, + verbose=True, + ) + + # Run the fabric_agent + result = fabric_agent.kickoff() + + # Verify the tool was used + assert result is not None + result_str = result.raw if hasattr(result, "raw") else str(result) + assert "42" in result_str diff --git a/packages/agentic-fabric/tests/e2e/test_langgraph_e2e.py b/packages/agentic-fabric/tests/e2e/test_langgraph_e2e.py new file mode 100644 index 0000000..0fb0ee8 --- /dev/null +++ b/packages/agentic-fabric/tests/e2e/test_langgraph_e2e.py @@ -0,0 +1,225 @@ +"""End-to-end tests for LangGraph runner. + +These tests verify that the LangGraph runner works correctly with real LLM calls. +They require: +- The --e2e flag to run +- ANTHROPIC_API_KEY environment variable +- LangGraph installed (pip install langgraph langchain-anthropic) +""" + +from __future__ import annotations + +from typing import Any + +import pytest + + +# Check if LangGraph is available +try: + import langgraph # noqa: F401 + + LANGGRAPH_AVAILABLE = True +except ImportError: + LANGGRAPH_AVAILABLE = False + +# Skip all tests if LangGraph is not installed +pytestmark = [ + pytest.mark.e2e, + pytest.mark.langgraph, + pytest.mark.skipif(not LANGGRAPH_AVAILABLE, reason="langgraph not installed"), +] + + +class TestLangGraphReActAgent: + """Test ReAct agent execution with LangGraph.""" + + def test_react_agent_execution( + self, + check_api_key: None, + simple_fabric_agent_config: dict[str, Any], + ) -> None: + """Test simple ReAct agent execution. + + Args: + check_api_key: Fixture to check for required API keys. + simple_fabric_agent_config: Simple fabric_agent configuration fixture. + """ + from agentic_fabric.runners.langgraph_runner import LangGraphRunner + + runner = LangGraphRunner() + + # Build the agent/fabric_agent + agent = runner.build_fabric_agent(simple_fabric_agent_config) + assert agent is not None + + # Run with simple input + inputs = {"input": "What is 3 + 5?"} + result = runner.run(agent, inputs) + + # Verify we got a response + assert result is not None + assert isinstance(result, str) + assert len(result) > 0 + # Should contain '8' in the answer + assert "8" in result + + def test_build_and_run( + self, + check_api_key: None, + simple_fabric_agent_config: dict[str, Any], + ) -> None: + """Test the convenience build_and_run method. + + Args: + check_api_key: Fixture to check for required API keys. + simple_fabric_agent_config: Simple fabric_agent configuration fixture. + """ + from agentic_fabric.runners.langgraph_runner import LangGraphRunner + + runner = LangGraphRunner() + + # Use build_and_run convenience method + inputs = {"input": "What is the largest ocean on Earth?"} + result = runner.build_and_run(simple_fabric_agent_config, inputs) + + # Verify we got a response about Pacific Ocean + assert result is not None + assert isinstance(result, str) + assert len(result) > 0 + assert "Pacific" in result or "pacific" in result.lower() + + +class TestLangGraphFlow: + """Test graph flow execution with LangGraph.""" + + def test_graph_flow( + self, + check_api_key: None, + simple_agent_config: dict[str, Any], + ) -> None: + """Test multi-step graph execution with state passing. + + Args: + check_api_key: Fixture to check for required API keys. + simple_agent_config: Simple agent configuration fixture. + """ + from langgraph.prebuilt import create_react_agent + + from agentic_fabric.runners.langgraph_runner import LangGraphRunner + + runner = LangGraphRunner() + + # Build agent with tools + llm = runner.get_llm() + agent = create_react_agent(llm, []) + assert agent is not None + + # Run the agent with a task that requires reasoning + result = agent.invoke( + { + "messages": [ + ( + "user", + "If a train travels 60 miles in 1 hour, how far does it travel in 30 minutes?", + ) + ] + } + ) + + # Verify we got a result with messages + assert result is not None + assert "messages" in result + assert len(result["messages"]) > 0 + + # Extract the final message + final_message = result["messages"][-1] + content = final_message.content if hasattr(final_message, "content") else str(final_message) + + # Should contain '30' miles + assert "30" in content + + +class TestLangGraphMultiStepPrompt: + """Test multi-step prompt handling with LangGraph.""" + + def test_agent_handles_multi_step_prompt( + self, + check_api_key: None, + multi_agent_fabric_agent_config: dict[str, Any], + ) -> None: + """Test that the LangGraph agent can handle a multi-step prompt. + + This test verifies that the created ReAct agent can process a prompt + with multiple sequential instructions. + + Args: + check_api_key: Fixture to check for required API keys. + multi_agent_fabric_agent_config: Multi-agent fabric_agent configuration fixture. + """ + from agentic_fabric.runners.langgraph_runner import LangGraphRunner + + runner = LangGraphRunner() + + # Build a fabric_agent from config + fabric_agent = runner.build_fabric_agent(multi_agent_fabric_agent_config) + assert fabric_agent is not None + + # Execute with input that requires multi-step reasoning with distinct answers + inputs = {"input": "First, tell me what 5 + 3 equals, then tell me what 10 - 3 equals."} + result = runner.run(fabric_agent, inputs) + + # Verify the runner properly managed state through the workflow + assert result is not None + assert isinstance(result, str) + assert len(result) > 0 + # Should contain both answers + assert "8" in result + assert "7" in result + + +class TestLangGraphTools: + """Test tool usage with LangGraph.""" + + def test_agent_with_tools( + self, + check_api_key: None, + ) -> None: + """Test LangGraph agent with custom tools. + + Args: + check_api_key: Fixture to check for required API keys. + """ + from langchain_core.tools import tool + from langgraph.prebuilt import create_react_agent + + from agentic_fabric.runners.langgraph_runner import LangGraphRunner + + # Create a test tool + @tool + def get_magic_number() -> int: + """Get the magic number. + + Returns: + The magic number (always 42). + """ + return 42 + + runner = LangGraphRunner() + llm = runner.get_llm() + + # Create agent with tool + agent = create_react_agent(llm, [get_magic_number]) + + # Run agent that should use the tool + result = agent.invoke({"messages": [("user", "What is the magic number? Use the get_magic_number tool.")]}) + + # Verify we got a result + assert result is not None + assert "messages" in result + + # Extract final message + final_message = result["messages"][-1] + content = final_message.content if hasattr(final_message, "content") else str(final_message) + + # Should mention 42 + assert "42" in content diff --git a/packages/agentic-fabric/tests/e2e/test_strands_e2e.py b/packages/agentic-fabric/tests/e2e/test_strands_e2e.py new file mode 100644 index 0000000..13af4ac --- /dev/null +++ b/packages/agentic-fabric/tests/e2e/test_strands_e2e.py @@ -0,0 +1,264 @@ +"""End-to-end tests for Strands runner. + +These tests verify that the Strands runner works correctly with real LLM calls. +They require: +- The --e2e flag to run +- AWS credentials configured (for Bedrock integration) +- Strands installed (pip install strands-agents) + +Note: Some tests may be skipped if AWS Bedrock is not configured. +""" + +from __future__ import annotations + +import os + +from typing import Any + +import pytest + + +# Check if Strands is available +try: + import strands # noqa: F401 + + STRANDS_AVAILABLE = True +except ImportError: + STRANDS_AVAILABLE = False + +# Skip all tests if Strands is not installed +pytestmark = [ + pytest.mark.e2e, + pytest.mark.strands, + pytest.mark.skipif(not STRANDS_AVAILABLE, reason="strands not installed"), +] + + +class TestStrandsAgentExecution: + """Test basic agent execution with Strands.""" + + def test_strands_agent_execution( + self, + check_aws_credentials: None, + simple_fabric_agent_config: dict[str, Any], + ) -> None: + """Test simple Strands agent execution. + + Args: + check_aws_credentials: Fixture to check for required AWS credentials. + simple_fabric_agent_config: Simple fabric_agent configuration fixture. + """ + from agentic_fabric.runners.strands_runner import StrandsRunner + + runner = StrandsRunner() + + # Build the agent from fabric_agent config + agent = runner.build_fabric_agent(simple_fabric_agent_config) + assert agent is not None + + # Run with simple input + inputs = {"input": "What is 7 + 3?"} + result = runner.run(agent, inputs) + + # Verify we got a response + assert result is not None + assert isinstance(result, str) + assert len(result) > 0 + # Should contain '10' in the answer + assert "10" in result + + def test_build_and_run( + self, + check_aws_credentials: None, + simple_fabric_agent_config: dict[str, Any], + ) -> None: + """Test the convenience build_and_run method. + + Args: + check_aws_credentials: Fixture to check for required AWS credentials. + simple_fabric_agent_config: Simple fabric_agent configuration fixture. + """ + from agentic_fabric.runners.strands_runner import StrandsRunner + + runner = StrandsRunner() + + # Use build_and_run convenience method + inputs = {"input": "What is the smallest prime number?"} + result = runner.build_and_run(simple_fabric_agent_config, inputs) + + # Verify we got a response + assert result is not None + assert isinstance(result, str) + assert len(result) > 0 + # Should mention 2 as the smallest prime + assert "2" in result + + +class TestStrandsAgentBuilder: + """Test Strands agent building from configuration.""" + + def test_build_agent( + self, + check_aws_credentials: None, + simple_agent_config: dict[str, Any], + ) -> None: + """Test building a Strands agent from configuration. + + Args: + check_aws_credentials: Fixture to check for required AWS credentials. + simple_agent_config: Simple agent configuration fixture. + """ + from agentic_fabric.runners.strands_runner import StrandsRunner + + runner = StrandsRunner() + + # Build agent + agent = runner.build_agent(simple_agent_config) + assert agent is not None + + # Test the agent with a simple query + result = agent("What is the capital of Italy?") + assert result is not None + result_str = str(result) + assert "Rome" in result_str or "rome" in result_str.lower() + + def test_build_task( + self, + check_aws_credentials: None, + simple_agent_config: dict[str, Any], + simple_task_config: dict[str, Any], + ) -> None: + """Test building a task for Strands. + + Args: + check_aws_credentials: Fixture to check for required AWS credentials. + simple_agent_config: Simple agent configuration fixture. + simple_task_config: Simple task configuration fixture. + """ + from agentic_fabric.runners.strands_runner import StrandsRunner + + runner = StrandsRunner() + + # Build agent first + agent = runner.build_agent(simple_agent_config) + + # Build task + task = runner.build_task(simple_task_config, agent) + assert task is not None + assert isinstance(task, dict) + assert "description" in task + assert "expected_output" in task + assert "agent" in task + + +class TestStrandsSystemPrompt: + """Test Strands system prompt generation.""" + + def test_system_prompt_generation( + self, + check_aws_credentials: None, + multi_agent_fabric_agent_config: dict[str, Any], + ) -> None: + """Test that system prompts are generated correctly from fabric_agent config. + + Args: + check_aws_credentials: Fixture to check for required AWS credentials. + multi_agent_fabric_agent_config: Multi-agent fabric_agent configuration fixture. + """ + from agentic_fabric.runners.strands_runner import StrandsRunner + + runner = StrandsRunner() + + # Build system prompt (internal method, testing through fabric_agent build) + agent = runner.build_fabric_agent(multi_agent_fabric_agent_config) + assert agent is not None + + # Run the agent to verify the system prompt is working + result = agent("Tell me about Python programming") + assert result is not None + result_str = str(result) + assert len(result_str) > 0 + + +class TestStrandsBedrockIntegration: + """Test Strands with AWS Bedrock (optional).""" + + @pytest.mark.skipif( + not os.environ.get("AWS_ACCESS_KEY_ID"), + reason="AWS credentials not configured", + ) + def test_bedrock_integration( + self, + check_aws_credentials: None, + simple_fabric_agent_config: dict[str, Any], + ) -> None: + """Test Strands with AWS Bedrock model provider. + + This test is skipped if AWS credentials are not available. + + Args: + check_aws_credentials: Fixture to check for required AWS credentials. + simple_fabric_agent_config: Simple fabric_agent configuration fixture. + """ + from agentic_fabric.runners.strands_runner import StrandsRunner + + # Add Bedrock model configuration + bedrock_config = { + **simple_fabric_agent_config, + "llm": { + "provider": "bedrock", + "model": "anthropic.claude-haiku-4-5-20251001-v1:0", + }, + } + + runner = StrandsRunner() + + # Build and run with Bedrock + inputs = {"input": "What is 5 * 6?"} + result = runner.build_and_run(bedrock_config, inputs) + + # Verify we got a response + assert result is not None + assert isinstance(result, str) + assert len(result) > 0 + # Should contain '30' in the answer + assert "30" in result + + +class TestStrandsTools: + """Test tool usage with Strands.""" + + def test_agent_with_tools( + self, + check_aws_credentials: None, + simple_agent_config: dict[str, Any], + ) -> None: + """Test Strands agent with custom tools. + + Args: + check_aws_credentials: Fixture to check for required AWS credentials. + simple_agent_config: Simple agent configuration fixture. + """ + from agentic_fabric.runners.strands_runner import StrandsRunner + + # Create a simple Python function as a tool + def get_secret_number() -> int: + """Get the secret number. + + Returns: + The secret number (always 99). + """ + return 99 + + runner = StrandsRunner() + + # Build agent with tool + agent = runner.build_agent(simple_agent_config, tools=[get_secret_number]) + assert agent is not None + + # Test execution with a prompt that encourages tool use + result = agent("Use the get_secret_number tool to find the secret number.") + assert result is not None + result_str = str(result) + # Should mention 99 if the tool was invoked + assert "99" in result_str diff --git a/packages/agentic-fabric/tests/test_agentic_data.py b/packages/agentic-fabric/tests/test_agentic_data.py new file mode 100644 index 0000000..f5f97b5 --- /dev/null +++ b/packages/agentic-fabric/tests/test_agentic_data.py @@ -0,0 +1,275 @@ +"""Tests for AgenticData runtime context and dispatch.""" + +from __future__ import annotations + +import importlib.util +import sys +import types + +from pathlib import Path +from typing import Any + +import pytest + +from agentic_fabric import AgenticData +from agentic_fabric.runners.registry import RuntimeUnavailableError + + +def test_agentic_data_preserves_value_and_runtime_context(monkeypatch: pytest.MonkeyPatch) -> None: + """AgenticData should keep data and active runtime context together.""" + monkeypatch.setattr("agentic_fabric.core.decomposer.is_framework_available", lambda runtime: runtime == "langgraph") + + data = AgenticData({"task": "summarize"}) + data.use_runtime("langgraph") + data.cast({"task": "rewrite"}) + + assert data.active_runtime == "langgraph" + assert data.as_builtin() == {"task": "rewrite"} + + +def test_use_runtime_strict_reports_install_guidance(monkeypatch: pytest.MonkeyPatch) -> None: + """Unavailable runtimes should raise a typed error with install guidance.""" + monkeypatch.setattr("agentic_fabric.core.decomposer.is_framework_available", lambda runtime: False) + + with pytest.raises(RuntimeUnavailableError, match="pip install crewai"): + AgenticData().use_runtime("crewai") + + +def test_select_runtime_prefers_explicit_runtime(monkeypatch: pytest.MonkeyPatch) -> None: + """Explicit runtime selection should override an active runtime.""" + monkeypatch.setattr("agentic_fabric.core.decomposer.is_framework_available", lambda runtime: runtime == "strands") + + data = AgenticData(active_runtime="langgraph") + + assert data.select_runtime("strands") == "strands" + + +def test_select_runtime_rejects_active_manifest_conflict(monkeypatch: pytest.MonkeyPatch) -> None: + """Active runtime and required manifest runtime should not silently conflict.""" + monkeypatch.setattr("agentic_fabric.core.decomposer.is_framework_available", lambda runtime: True) + data = AgenticData(active_runtime="langgraph") + + with pytest.raises(ValueError, match="conflicts"): + data.select_runtime(fabric_agent_config={"required_framework": "crewai"}) + + +def test_select_runtime_uses_active_runtime(monkeypatch: pytest.MonkeyPatch) -> None: + """An active runtime should be selected when no explicit runtime is requested.""" + monkeypatch.setattr("agentic_fabric.core.decomposer.is_framework_available", lambda runtime: runtime == "strands") + data = AgenticData(active_runtime="strands") + + assert data.select_runtime() == "strands" + + +def test_run_fabric_agent_uses_registered_config_and_runtime(monkeypatch: pytest.MonkeyPatch) -> None: + """Named fabric agents should route through run_fabric_agent_auto with merged inputs.""" + calls: list[tuple[dict[str, Any], dict[str, Any], str | None]] = [] + + def fake_run_fabric_agent_auto(fabric_agent_config: dict[str, Any], inputs: dict[str, Any], framework: str | None = None) -> str: + calls.append((fabric_agent_config, inputs, framework)) + return "done" + + monkeypatch.setattr("agentic_fabric.core.decomposer.is_framework_available", lambda runtime: runtime == "crewai") + monkeypatch.setattr("agentic_fabric.core.decomposer.run_fabric_agent_auto", fake_run_fabric_agent_auto) + + data = AgenticData(fabric_agents={"reviewer": {"name": "review", "agents": {}, "tasks": {}}}) + + result = data.run_reviewer({"code": "x"}, severity="high") + + assert result == "done" + assert calls == [({"name": "review", "agents": {}, "tasks": {}}, {"code": "x", "severity": "high"}, "crewai")] + + +def test_unknown_fabric_agent_lists_registered_names() -> None: + """Unknown fabric agent errors should include the registered names.""" + data = AgenticData(fabric_agents={"reviewer": {"name": "review"}}) + + with pytest.raises(KeyError, match="reviewer"): + data.run_fabric_agent("writer") + + +def test_fallback_vendor_guidance_when_vendor_fabric_missing() -> None: + """The no-vendor fallback should fail with install guidance.""" + data = AgenticData() + if data.vendor_fabric_available: + pytest.skip("vendor-fabric is installed in this environment") + + with pytest.raises(ImportError, match="vendor-fabric"): + data.open("github") + + +def test_fallback_vendor_records_provider_when_not_strict() -> None: + """The no-vendor fallback should still keep non-strict provider context.""" + data = AgenticData() + if data.vendor_fabric_available: + pytest.skip("vendor-fabric is installed in this environment") + + assert data.open("github", strict=False) is data + assert data.active_provider == "github" + + with pytest.raises(ImportError, match="Vendor operation 'sync' requires vendor-fabric"): + data.call("sync") + + +def test_agentic_data_import_branch_with_vendor_fabric(monkeypatch: pytest.MonkeyPatch) -> None: + """The module should subclass real VendorData when vendor-fabric is importable.""" + import agentic_fabric.agentic_data as current_agentic_data + + fake_vendor_fabric = types.ModuleType("vendor_fabric") + fake_vendor_data = types.ModuleType("vendor_fabric.vendor_data") + + class VendorData: + def __init__(self, value: Any = None, **_: Any) -> None: + self.value = value + + def cast(self, value: Any) -> VendorData: + self.value = value + return self + + fake_vendor_data.VendorData = VendorData + monkeypatch.setitem(sys.modules, "vendor_fabric", fake_vendor_fabric) + monkeypatch.setitem(sys.modules, "vendor_fabric.vendor_data", fake_vendor_data) + + spec = importlib.util.spec_from_file_location( + "_agentic_data_vendor_branch", + Path(current_agentic_data.__file__), + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + data = module.AgenticData("value") + + assert module._VENDOR_FABRIC_AVAILABLE is True + assert data.vendor_fabric_available is True + assert isinstance(data, VendorData) + assert data.value == "value" + + +def test_fabric_agents_are_read_only_and_unregister_is_chainable() -> None: + """Fabric agent registry snapshots should be immutable from the public property.""" + data = AgenticData(fabric_agents={"reviewer": {"name": "review"}}) + + with pytest.raises(TypeError): + data.fabric_agents["writer"] = {"name": "write"} # type: ignore[index] + + assert data.unregister_fabric_agent("reviewer") is data + assert data.fabric_agents == {} + assert data.unregister_fabric_agent("missing") is data + + +def test_use_runtime_auto_and_clear_runtime(monkeypatch: pytest.MonkeyPatch) -> None: + """The auto runtime token should select a concrete runtime and clear cleanly.""" + monkeypatch.setattr(AgenticData, "select_runtime", lambda self: "crewai") + + data = AgenticData().use_runtime(" auto ", strict=False) + + assert data.active_runtime == "crewai" + assert data.clear_runtime() is data + assert data.active_runtime is None + + +def test_runtime_metadata_methods_delegate_to_registry(monkeypatch: pytest.MonkeyPatch) -> None: + """Runtime metadata helpers should normalize registry responses.""" + monkeypatch.setattr( + "agentic_fabric.agentic_data.runtime_info", + lambda runtime=None: {"name": runtime or "crewai", "available": True}, + ) + + data = AgenticData() + + assert data.runtimes() == [{"name": "crewai", "available": True}] + assert data.runtime_info("langgraph") == {"name": "langgraph", "available": True} + + +def test_select_runtime_rejects_explicit_manifest_conflict(monkeypatch: pytest.MonkeyPatch) -> None: + """Explicit runtime and manifest runtime requirements should agree.""" + monkeypatch.setattr("agentic_fabric.core.decomposer.is_framework_available", lambda runtime: True) + data = AgenticData() + + with pytest.raises(ValueError, match="Fabric agent requires crewai but langgraph was requested"): + data.select_runtime("langgraph", fabric_agent_config={"runtime": "crewai"}) + + +def test_select_runtime_uses_required_runtime_and_auto_detection(monkeypatch: pytest.MonkeyPatch) -> None: + """Manifest requirements should win before auto-detection fallback.""" + monkeypatch.setattr("agentic_fabric.core.decomposer.is_framework_available", lambda runtime: True) + monkeypatch.setattr("agentic_fabric.core.decomposer.detect_framework", lambda: "strands") + data = AgenticData() + + assert data.select_runtime(fabric_agent_config={"framework": "langgraph"}) == "langgraph" + assert data.select_runtime() == "strands" + + +def test_select_runtime_reports_unavailable_requested_runtime(monkeypatch: pytest.MonkeyPatch) -> None: + """Unavailable explicit runtimes should raise install guidance.""" + monkeypatch.setattr("agentic_fabric.core.decomposer.is_framework_available", lambda runtime: False) + + with pytest.raises(RuntimeUnavailableError, match="pip install crewai"): + AgenticData().select_runtime("crewai") + + +def test_run_fabric_agent_accepts_direct_mapping(monkeypatch: pytest.MonkeyPatch) -> None: + """Direct fabric agent config mappings should bypass registry lookup.""" + calls: list[tuple[dict[str, Any], dict[str, Any], str | None]] = [] + + def fake_run_fabric_agent_auto(fabric_agent_config: dict[str, Any], inputs: dict[str, Any], framework: str | None = None) -> str: + calls.append((fabric_agent_config, inputs, framework)) + return "done" + + monkeypatch.setattr("agentic_fabric.core.decomposer.is_framework_available", lambda runtime: runtime == "crewai") + monkeypatch.setattr("agentic_fabric.core.decomposer.run_fabric_agent_auto", fake_run_fabric_agent_auto) + + result = AgenticData().run_fabric_agent({"name": "direct", "runtime": "crewai"}, {"a": 1}, b=2) + + assert result == "done" + assert calls == [({"name": "direct", "runtime": "crewai"}, {"a": 1, "b": 2}, "crewai")] + + +def test_call_runtime_delegates_to_selected_runner(monkeypatch: pytest.MonkeyPatch) -> None: + """Runtime capability calls should use the selected runner.""" + + class FakeRunner: + def call_capability(self, capability: str, *args: Any, **kwargs: Any) -> tuple[Any, ...]: + return (capability, args, kwargs) + + monkeypatch.setattr("agentic_fabric.core.decomposer.is_framework_available", lambda runtime: True) + monkeypatch.setattr("agentic_fabric.core.decomposer.get_runner", lambda runtime: FakeRunner()) + + assert AgenticData().call_runtime("run", 1, runtime="crewai", flag=True) == ("run", (1,), {"flag": True}) + + +def test_vendor_tools_delegate_to_vendor_capability_catalog(monkeypatch: pytest.MonkeyPatch) -> None: + """AgenticData should expose agent-facing tools from vendor capabilities.""" + calls: list[tuple[AgenticData, str | None, bool]] = [] + + def fake_vendor_capability_tools( + data: AgenticData, + *, + provider: str | None = None, + include_unavailable: bool = True, + ) -> list[str]: + calls.append((data, provider, include_unavailable)) + return ["tool"] + + monkeypatch.setattr("agentic_fabric.tools.vendor.vendor_capability_tools", fake_vendor_capability_tools) + + data = AgenticData() + + assert data.vendor_tools("github", include_unavailable=False) == ["tool"] + assert calls == [(data, "github", False)] + + +def test_dynamic_helpers_and_missing_attributes(monkeypatch: pytest.MonkeyPatch) -> None: + """Registered fabric agents should appear as dynamic run_ helpers.""" + monkeypatch.setattr("agentic_fabric.core.decomposer.is_framework_available", lambda runtime: True) + monkeypatch.setattr("agentic_fabric.core.decomposer.run_fabric_agent_auto", lambda fabric_agent_config, inputs, framework: "done") + + data = AgenticData(fabric_agents={"reviewer": {"name": "reviewer", "runtime": "crewai"}}) + + assert "run_reviewer" in dir(data) + assert data.run_reviewer() == "done" + + with pytest.raises(AttributeError, match="has no attribute 'missing'"): + data.missing diff --git a/packages/agentic-fabric/tests/test_capabilities.py b/packages/agentic-fabric/tests/test_capabilities.py new file mode 100644 index 0000000..893b8ce --- /dev/null +++ b/packages/agentic-fabric/tests/test_capabilities.py @@ -0,0 +1,100 @@ +"""Tests for agent capability metadata and dispatch.""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from agentic_fabric.capabilities import ( + AgentCapabilityProviderMixin, + AgentCapabilitySpec, + agent_capability, + tool_capability, +) +from agentic_fabric.runners.base import BaseRunner + + +class TestCapabilityCollection: + """Capability decorators should be collected at class creation.""" + + def test_base_runner_declares_runtime_capabilities(self) -> None: + """Runner methods should expose read-only capability metadata.""" + + class TestRunner(BaseRunner): + def build_fabric_agent(self, fabric_agent_config: dict[str, Any]) -> Any: + return fabric_agent_config + + def run(self, fabric_agent: Any, inputs: dict[str, Any]) -> str: + return f"{fabric_agent}:{inputs}" + + def build_agent(self, agent_config: dict[str, Any], tools: list | None = None) -> Any: + return agent_config + + def build_task(self, task_config: dict[str, Any], agent: Any) -> Any: + return task_config, agent + + runner = TestRunner() + + assert "build_fabric_agent" in runner.capability_map() + assert "run_fabric_agent" in runner.capability_map() + assert runner.call_capability("run_fabric_agent", {"name": "fabric_agent"}, {"task": "go"}) == "{'name': 'fabric_agent'}:{'task': 'go'}" + + with pytest.raises(TypeError): + runner.capability_map()["new"] = runner.capability_map()["run"] # type: ignore[index] + + def test_subclass_decorator_adds_custom_capability(self) -> None: + """Custom decorated methods should be dispatchable by alias.""" + + class CustomProvider: + @agent_capability("summarize", aliases=("summary",), description="Summarize text.") + def summarize(self, text: str) -> str: + return text.upper() + + from agentic_fabric.capabilities import AgentCapabilityProviderMixin + + class TestProvider(AgentCapabilityProviderMixin, CustomProvider): + pass + + provider = TestProvider() + + assert provider.agent_capabilities["summary"].name == "summarize" + assert provider.call_capability("summary", "hello") == "HELLO" + + def test_capability_metadata_and_kind_filtering(self) -> None: + """Capability metadata should serialize and filter by kind.""" + + class ToolProvider(AgentCapabilityProviderMixin): + @tool_capability("read_file", aliases=("read",), description="Read a file.") + def read_file(self) -> str: + return "content" + + provider = ToolProvider() + specs = provider.list_capabilities(kind="tool") + + assert specs == (provider.agent_capabilities["read_file"],) + assert specs[0].as_dict() == { + "name": "read_file", + "kind": "tool", + "aliases": ["read"], + "description": "Read a file.", + } + assert AgentCapabilitySpec("run").as_dict()["kind"] == "runtime" + + def test_capability_dispatch_reports_unknown_and_noncallable(self) -> None: + """Capability dispatch should fail clearly for invalid names and methods.""" + + class BrokenProvider(AgentCapabilityProviderMixin): + @agent_capability("broken") + def broken(self) -> str: + return "ok" + + provider = BrokenProvider() + + with pytest.raises(AttributeError, match="Unknown agent capability: missing"): + provider.call_capability("missing") + + provider.broken = "not callable" # type: ignore[method-assign] + + with pytest.raises(TypeError, match="not callable"): + provider.call_capability("broken") diff --git a/packages/agentic-fabric/tests/test_cli_commands.py b/packages/agentic-fabric/tests/test_cli_commands.py new file mode 100644 index 0000000..4da9464 --- /dev/null +++ b/packages/agentic-fabric/tests/test_cli_commands.py @@ -0,0 +1,673 @@ +"""Focused tests for CLI command helpers.""" + +from __future__ import annotations + +import json + +from pathlib import Path +from types import SimpleNamespace +from typing import Any +from unittest.mock import patch + +import pytest + +from agentic_fabric import main as cli + + +class FakeCliRunner: + """Small CLI runner stand-in used by command tests.""" + + def __init__(self, *, available: bool = True, result: str = "runner output") -> None: + self.config = SimpleNamespace( + name="Fake Runner", + description="Runs fake tasks", + install_cmd="install fake", + docs_url="https://example.com/fake", + ) + self.available = available + self.result = result + self.calls: list[dict[str, Any]] = [] + + def is_available(self) -> bool: + return self.available + + def get_required_env_vars(self) -> list[str]: + return ["FAKE_API_KEY"] + + def run(self, **kwargs: Any) -> str: + self.calls.append(kwargs) + return self.result + + +def test_cmd_list_text_displays_framework_marked_fabric_agents(capsys: pytest.CaptureFixture[str]) -> None: + args = SimpleNamespace(package=None, framework=None, json=False) + + with patch.object( + cli, + "list_fabric_agents", + return_value={"pkg": [{"name": "reviewer", "description": "reviews code", "required_framework": "crewai"}]}, + ): + cli.cmd_list(args) + + output = capsys.readouterr().out + assert "AVAILABLE FABRIC AGENTS" in output + assert "pkg" in output + assert "reviewer [crewai]: reviews code" in output + + +def test_cmd_list_json_flattens_packages(capsys: pytest.CaptureFixture[str]) -> None: + args = SimpleNamespace(package="pkg", framework="langgraph", json=True) + + with patch.object( + cli, + "list_fabric_agents", + return_value={"pkg": [{"name": "builder", "description": "builds", "required_framework": "langgraph"}]}, + ): + cli.cmd_list(args) + + assert json.loads(capsys.readouterr().out) == { + "fabric_agents": [ + { + "package": "pkg", + "name": "builder", + "description": "builds", + "required_framework": "langgraph", + } + ] + } + + +def test_cmd_run_json_success_auto_detects_framework( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + args = SimpleNamespace( + json=True, + runner=None, + package="pkg", + fabric_agent="reviewer", + file=None, + input="review this", + framework="auto", + ) + + with ( + patch.object(cli, "discover_packages", return_value={"pkg": tmp_path / ".fabric"}), + patch.object(cli, "get_fabric_agent_config", return_value={"name": "reviewer"}), + patch("agentic_fabric.core.decomposer.detect_framework", return_value="crewai"), + patch("agentic_fabric.core.decomposer.run_fabric_agent_auto", return_value="done") as run_fabric_agent_auto, + ): + cli.cmd_run(args) + + data = json.loads(capsys.readouterr().out) + assert data["success"] is True + assert data["output"] == "done" + assert data["framework_used"] == "crewai" + run_fabric_agent_auto.assert_called_once_with( + {"name": "reviewer"}, + inputs={"spec": "review this", "component_spec": "review this", "input": "review this"}, + framework="crewai", + ) + + +def test_cmd_run_json_uses_file_input_and_requested_framework( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + input_file = tmp_path / "task.txt" + input_file.write_text("from file", encoding="utf-8") + args = SimpleNamespace( + json=True, + runner=None, + package="pkg", + fabric_agent="reviewer", + file=str(input_file), + input=None, + framework="strands", + ) + + with ( + patch.object(cli, "discover_packages", return_value={"pkg": tmp_path / ".strands"}), + patch.object(cli, "get_fabric_agent_config", return_value={"name": "reviewer"}), + patch("agentic_fabric.core.decomposer.run_fabric_agent_auto", return_value="done") as run_fabric_agent_auto, + ): + cli.cmd_run(args) + + data = json.loads(capsys.readouterr().out) + assert data["success"] is True + assert data["framework_used"] == "strands" + assert run_fabric_agent_auto.call_args.kwargs["inputs"]["input"] == "from file" + assert run_fabric_agent_auto.call_args.kwargs["framework"] == "strands" + + +def test_cmd_run_dispatches_single_agent_runner(capsys: pytest.CaptureFixture[str]) -> None: + args = SimpleNamespace(json=False, runner="fake", package=None, fabric_agent=None) + calls: list[tuple[SimpleNamespace, bool]] = [] + + with patch.object(cli, "_cmd_run_single_agent", side_effect=lambda args, use_json, start_time: calls.append((args, use_json))): + cli.cmd_run(args) + + assert calls == [(args, False)] + assert capsys.readouterr().out == "" + + +def test_cmd_run_json_reports_missing_package(capsys: pytest.CaptureFixture[str]) -> None: + args = SimpleNamespace( + json=True, + runner=None, + package="missing", + fabric_agent="reviewer", + file=None, + input="task", + framework="auto", + ) + + with ( + patch.object(cli, "discover_packages", return_value={"known": Path(".fabric")}), + pytest.raises(SystemExit) as exc_info, + ): + cli.cmd_run(args) + + assert exc_info.value.code == 2 + data = json.loads(capsys.readouterr().out) + assert data["success"] is False + assert data["available_packages"] == ["known"] + + +def test_cmd_run_json_reports_execution_errors( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + args = SimpleNamespace( + json=True, + runner=None, + package="pkg", + fabric_agent="reviewer", + file=None, + input="task", + framework="auto", + ) + + with ( + patch.object(cli, "discover_packages", return_value={"pkg": tmp_path / ".fabric"}), + patch.object(cli, "get_fabric_agent_config", side_effect=ValueError("bad fabric agent")), + pytest.raises(SystemExit) as exc_info, + ): + cli.cmd_run(args) + + assert exc_info.value.code == 1 + assert json.loads(capsys.readouterr().out)["error"] == "bad fabric agent" + + +@pytest.mark.parametrize("use_json", [True, False]) +def test_cmd_run_reports_file_read_errors(capsys: pytest.CaptureFixture[str], use_json: bool) -> None: + """Multi-agent CLI runs should report unreadable input files.""" + args = SimpleNamespace( + json=use_json, + runner=None, + package="pkg", + fabric_agent="reviewer", + file="/definitely/missing/task.txt", + input=None, + framework="auto", + ) + + with pytest.raises(SystemExit) as exc_info: + cli.cmd_run(args) + + assert exc_info.value.code == 2 + output = capsys.readouterr().out + if use_json: + assert json.loads(output)["success"] is False + else: + assert "Error:" in output + + +def test_cmd_run_text_reports_missing_arguments(capsys: pytest.CaptureFixture[str]) -> None: + args = SimpleNamespace(json=False, runner=None, package=None, fabric_agent=None) + + with pytest.raises(SystemExit) as exc_info: + cli.cmd_run(args) + + assert exc_info.value.code == 2 + assert "Package and fabric_agent are required" in capsys.readouterr().out + + +def test_cmd_run_json_reports_missing_arguments(capsys: pytest.CaptureFixture[str]) -> None: + args = SimpleNamespace(json=True, runner=None, package=None, fabric_agent=None) + + with pytest.raises(SystemExit) as exc_info: + cli.cmd_run(args) + + assert exc_info.value.code == 2 + assert json.loads(capsys.readouterr().out)["error"].startswith("Package and fabric_agent are required") + + +def test_cmd_run_text_reports_missing_package(capsys: pytest.CaptureFixture[str]) -> None: + args = SimpleNamespace( + json=False, + runner=None, + package="missing", + fabric_agent="reviewer", + file=None, + input="task", + framework="auto", + ) + + with ( + patch.object(cli, "discover_packages", return_value={"known": Path(".fabric")}), + pytest.raises(SystemExit) as exc_info, + ): + cli.cmd_run(args) + + assert exc_info.value.code == 2 + output = capsys.readouterr().out + assert "Package 'missing' not found" in output + assert "known" in output + + +def test_cmd_run_text_uses_empty_input_and_requested_or_auto_framework( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + args = SimpleNamespace( + json=False, + runner=None, + package="pkg", + fabric_agent="reviewer", + file=None, + input=None, + framework="langgraph", + ) + calls: list[dict[str, Any]] = [] + + def fake_run_fabric_agent_auto(fabric_agent_config: dict[str, Any], inputs: dict[str, str], framework: str | None = None) -> str: + calls.append({"fabric_agent_config": fabric_agent_config, "inputs": inputs, "framework": framework}) + return "done" + + with ( + patch.object(cli, "discover_packages", return_value={"pkg": tmp_path / ".fabric"}), + patch.object(cli, "get_fabric_agent_config", return_value={"name": "reviewer"}), + patch("agentic_fabric.core.decomposer.run_fabric_agent_auto", side_effect=fake_run_fabric_agent_auto), + ): + cli.cmd_run(args) + + output = capsys.readouterr().out + assert "Framework: langgraph (requested)" in output + assert calls[0]["inputs"] == {"spec": "", "component_spec": "", "input": ""} + assert calls[0]["framework"] == "langgraph" + + args.framework = "auto" + with ( + patch.object(cli, "discover_packages", return_value={"pkg": tmp_path / ".fabric"}), + patch.object(cli, "get_fabric_agent_config", return_value={"name": "reviewer"}), + patch("agentic_fabric.core.decomposer.detect_framework", return_value="strands"), + patch("agentic_fabric.core.decomposer.run_fabric_agent_auto", return_value="done"), + ): + cli.cmd_run(args) + + assert "Framework: strands (auto-detected)" in capsys.readouterr().out + + +def test_cmd_run_text_success_and_error_paths(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + args = SimpleNamespace( + json=False, + runner=None, + package="pkg", + fabric_agent="reviewer", + file=None, + input="task", + framework="auto", + ) + + with ( + patch.object(cli, "discover_packages", return_value={"pkg": tmp_path / ".fabric"}), + patch.object(cli, "get_fabric_agent_config", return_value={"name": "reviewer", "required_framework": "crewai"}), + patch("agentic_fabric.core.decomposer.run_fabric_agent_auto", return_value="done"), + ): + cli.cmd_run(args) + + output = capsys.readouterr().out + assert "Running pkg/reviewer" in output + assert "Framework: crewai" in output + assert "done" in output + + with ( + patch.object(cli, "discover_packages", return_value={"pkg": tmp_path / ".fabric"}), + patch.object(cli, "get_fabric_agent_config", side_effect=RuntimeError("boom")), + pytest.raises(SystemExit) as exc_info, + ): + cli.cmd_run(args) + + assert exc_info.value.code == 1 + assert "Error: boom" in capsys.readouterr().out + + +def test_cmd_info_json_success(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + args = SimpleNamespace(package="pkg", fabric_agent="reviewer", json=True) + config = { + "description": "Reviews code", + "required_framework": "crewai", + "agents": {"reviewer": {"role": "Code Reviewer"}}, + "tasks": {"review": {"description": "Review the change"}}, + "knowledge_paths": ["knowledge"], + } + + with ( + patch.object(cli, "discover_packages", return_value={"pkg": tmp_path / ".fabric"}), + patch.object(cli, "get_fabric_agent_config", return_value=config), + ): + cli.cmd_info(args) + + data = json.loads(capsys.readouterr().out) + assert data["package"] == "pkg" + assert data["required_framework"] == "crewai" + assert data["agents"] == [{"name": "reviewer", "role": "Code Reviewer"}] + assert data["tasks"] == [{"name": "review", "description": "Review the change"}] + + +def test_cmd_info_json_reports_config_error(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + args = SimpleNamespace(package="pkg", fabric_agent="missing", json=True) + + with ( + patch.object(cli, "discover_packages", return_value={"pkg": tmp_path / ".fabric"}), + patch.object(cli, "get_fabric_agent_config", side_effect=ValueError("missing fabric agent")), + pytest.raises(SystemExit) as exc_info, + ): + cli.cmd_info(args) + + assert exc_info.value.code == 2 + assert json.loads(capsys.readouterr().out) == {"error": "missing fabric agent"} + + +def test_cmd_info_json_reports_missing_package(capsys: pytest.CaptureFixture[str]) -> None: + args = SimpleNamespace(package="missing", fabric_agent="reviewer", json=True) + + with ( + patch.object(cli, "discover_packages", return_value={"known": Path(".fabric")}), + pytest.raises(SystemExit) as exc_info, + ): + cli.cmd_info(args) + + assert exc_info.value.code == 2 + data = json.loads(capsys.readouterr().out) + assert data["error"] == "Package 'missing' not found" + assert data["available_packages"] == ["known"] + + +def test_cmd_info_text_success_and_errors(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + args = SimpleNamespace(package="pkg", fabric_agent="reviewer", json=False) + config = { + "description": "Reviews code", + "agents": {"reviewer": {"role": "Code Reviewer"}}, + "tasks": {"review": {"description": "Review the change in detail"}}, + "knowledge_paths": [tmp_path / "knowledge"], + } + + with ( + patch.object(cli, "discover_packages", return_value={"pkg": tmp_path / ".fabric"}), + patch.object(cli, "get_fabric_agent_config", return_value=config), + ): + cli.cmd_info(args) + + output = capsys.readouterr().out + assert "FABRIC AGENT: pkg/reviewer" in output + assert "Code Reviewer" in output + assert "Review the change" in output + + with ( + patch.object(cli, "discover_packages", return_value={}), + pytest.raises(SystemExit) as exc_info, + ): + cli.cmd_info(args) + + assert exc_info.value.code == 2 + assert "Package 'pkg' not found" in capsys.readouterr().out + + with ( + patch.object(cli, "discover_packages", return_value={"pkg": tmp_path / ".fabric"}), + patch.object(cli, "get_fabric_agent_config", side_effect=ValueError("missing fabric agent")), + pytest.raises(SystemExit) as exc_info, + ): + cli.cmd_info(args) + + assert exc_info.value.code == 2 + assert "missing fabric agent" in capsys.readouterr().out + + +def test_cmd_list_runners_json_skips_bad_profiles(capsys: pytest.CaptureFixture[str]) -> None: + fake_runner = FakeCliRunner() + + def fake_get_cli_runner(profile: str) -> FakeCliRunner: + if profile == "bad": + raise RuntimeError("broken") + return fake_runner + + with ( + patch("agentic_fabric.core.decomposer.get_available_cli_runners", return_value=["good", "bad"]), + patch("agentic_fabric.core.decomposer.get_cli_runner", side_effect=fake_get_cli_runner), + ): + cli.cmd_list_runners(SimpleNamespace(json=True)) + + data = json.loads(capsys.readouterr().out) + assert data["runners"][0]["name"] == "good" + assert data["runners"][0]["available"] is True + assert data["runners"][0]["required_env"] == ["FAKE_API_KEY"] + + +def test_cmd_list_runners_reports_missing_profiles(capsys: pytest.CaptureFixture[str]) -> None: + with ( + patch("agentic_fabric.core.decomposer.get_available_cli_runners", side_effect=FileNotFoundError("no profiles")), + pytest.raises(SystemExit) as exc_info, + ): + cli.cmd_list_runners(SimpleNamespace(json=True)) + + assert exc_info.value.code == 2 + assert json.loads(capsys.readouterr().out) == {"error": "no profiles"} + + +def test_cmd_list_runners_text_lists_available_unavailable_and_bad_profiles( + capsys: pytest.CaptureFixture[str], +) -> None: + good_runner = FakeCliRunner(available=True) + bad_runner = FakeCliRunner(available=False) + + def fake_get_cli_runner(profile: str) -> FakeCliRunner: + if profile == "broken": + raise RuntimeError("broken profile") + return good_runner if profile == "good" else bad_runner + + with ( + patch("agentic_fabric.core.decomposer.get_available_cli_runners", return_value=["good", "bad", "broken"]), + patch("agentic_fabric.core.decomposer.get_cli_runner", side_effect=fake_get_cli_runner), + ): + cli.cmd_list_runners(SimpleNamespace(json=False)) + + captured = capsys.readouterr() + assert "AVAILABLE SINGLE-AGENT CLI RUNNERS" in captured.out + assert "good: Runs fake tasks" in captured.out + assert "bad: Runs fake tasks" in captured.out + assert "Install: install fake" in captured.out + assert "Requires: FAKE_API_KEY" in captured.out + assert "Warning: Could not load profile 'broken'" in captured.err + + +def test_cmd_list_runners_text_reports_missing_profiles(capsys: pytest.CaptureFixture[str]) -> None: + with ( + patch("agentic_fabric.core.decomposer.get_available_cli_runners", side_effect=FileNotFoundError("no profiles")), + pytest.raises(SystemExit) as exc_info, + ): + cli.cmd_list_runners(SimpleNamespace(json=False)) + + assert exc_info.value.code == 2 + assert "Error: no profiles" in capsys.readouterr().out + + +def test_single_agent_json_success_uses_package_workdir( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + fake_runner = FakeCliRunner(result="fixed") + package_dir = tmp_path / "pkg" / ".fabric" + + args = SimpleNamespace( + json=True, + runner="fake", + input="fix it", + file=None, + package="pkg", + model="small", + auto_approve=False, + ) + + with ( + patch.object(cli, "discover_packages", return_value={"pkg": package_dir}), + patch("agentic_fabric.core.decomposer.get_cli_runner", return_value=fake_runner), + ): + cli._cmd_run_single_agent(args, use_json=True, start_time=0) + + data = json.loads(capsys.readouterr().out) + assert data["success"] is True + assert data["output"] == "fixed" + assert fake_runner.calls == [ + { + "task": "fix it", + "working_dir": str(package_dir.parent), + "auto_approve": False, + } + ] + + +def test_single_agent_json_requires_input(capsys: pytest.CaptureFixture[str]) -> None: + args = SimpleNamespace(json=True, runner="fake", input=None, file=None, package=None) + + with pytest.raises(SystemExit) as exc_info: + cli._cmd_run_single_agent(args, use_json=True, start_time=0) + + assert exc_info.value.code == 2 + assert json.loads(capsys.readouterr().out)["error"] == "No input provided. Use --input or --file" + + +def test_single_agent_text_requires_input(capsys: pytest.CaptureFixture[str]) -> None: + args = SimpleNamespace(json=False, runner="fake", input=None, file=None, package=None) + + with pytest.raises(SystemExit) as exc_info: + cli._cmd_run_single_agent(args, use_json=False, start_time=0) + + assert exc_info.value.code == 2 + assert "No input provided" in capsys.readouterr().out + + +@pytest.mark.parametrize("use_json", [True, False]) +def test_single_agent_reports_file_read_errors(capsys: pytest.CaptureFixture[str], use_json: bool) -> None: + """Single-agent CLI runs should report unreadable input files.""" + args = SimpleNamespace( + json=use_json, + runner="fake", + input=None, + file="/definitely/missing/task.txt", + package=None, + ) + + with pytest.raises(SystemExit) as exc_info: + cli._cmd_run_single_agent(args, use_json=use_json, start_time=0) + + assert exc_info.value.code == 2 + output = capsys.readouterr().out + if use_json: + assert json.loads(output)["success"] is False + else: + assert "Error:" in output + + +def test_single_agent_json_reports_unavailable_runner(capsys: pytest.CaptureFixture[str]) -> None: + args = SimpleNamespace(json=True, runner="fake", input="task", file=None, package=None, model=None) + fake_runner = FakeCliRunner(available=False) + + with ( + patch("agentic_fabric.core.decomposer.get_cli_runner", return_value=fake_runner), + patch("agentic_fabric.core.decomposer.get_available_cli_runners", return_value=["other"]), + pytest.raises(SystemExit) as exc_info, + ): + cli._cmd_run_single_agent(args, use_json=True, start_time=0) + + assert exc_info.value.code == 2 + data = json.loads(capsys.readouterr().out) + assert data["success"] is False + assert data["available_runners"] == ["other"] + + +def test_single_agent_text_success_unavailable_and_error_paths( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + input_file = tmp_path / "task.txt" + input_file.write_text("from file", encoding="utf-8") + package_dir = tmp_path / "pkg" / ".fabric" + fake_runner = FakeCliRunner(result="fixed") + args = SimpleNamespace( + json=False, + runner="fake", + input=None, + file=str(input_file), + package="pkg", + model=None, + auto_approve=True, + ) + + with ( + patch.object(cli, "discover_packages", return_value={"pkg": package_dir}), + patch("agentic_fabric.core.decomposer.get_cli_runner", return_value=fake_runner), + ): + cli._cmd_run_single_agent(args, use_json=False, start_time=0) + + output = capsys.readouterr().out + assert "Running single-agent: fake" in output + assert "Runner: Fake Runner" in output + assert "fixed" in output + assert fake_runner.calls[0]["task"] == "from file" + + unavailable_runner = FakeCliRunner(available=False) + args.input = "task" + args.file = None + with ( + patch("agentic_fabric.core.decomposer.get_cli_runner", return_value=unavailable_runner), + patch("agentic_fabric.core.decomposer.get_available_cli_runners", return_value=["other"]), + pytest.raises(SystemExit) as exc_info, + ): + cli._cmd_run_single_agent(args, use_json=False, start_time=0) + + assert exc_info.value.code == 2 + assert "Runner 'fake' not available" in capsys.readouterr().out + + def broken_run(**kwargs: Any) -> str: + raise FileNotFoundError("missing binary") + + fake_runner.run = broken_run + with ( + patch("agentic_fabric.core.decomposer.get_cli_runner", return_value=fake_runner), + pytest.raises(SystemExit) as exc_info, + ): + cli._cmd_run_single_agent(args, use_json=False, start_time=0) + + assert exc_info.value.code == 1 + assert "missing binary" in capsys.readouterr().out + + +def test_single_agent_json_reports_runner_errors(capsys: pytest.CaptureFixture[str]) -> None: + args = SimpleNamespace(json=True, runner="fake", input="task", file=None, package=None, model=None) + fake_runner = FakeCliRunner() + + def broken_run(**kwargs: Any) -> str: + raise RuntimeError("runner failed") + + fake_runner.run = broken_run + + with ( + patch("agentic_fabric.core.decomposer.get_cli_runner", return_value=fake_runner), + pytest.raises(SystemExit) as exc_info, + ): + cli._cmd_run_single_agent(args, use_json=True, start_time=0) + + assert exc_info.value.code == 1 + assert json.loads(capsys.readouterr().out)["error"] == "runner failed" diff --git a/packages/agentic-fabric/tests/test_cli_smoke.py b/packages/agentic-fabric/tests/test_cli_smoke.py new file mode 100644 index 0000000..0619256 --- /dev/null +++ b/packages/agentic-fabric/tests/test_cli_smoke.py @@ -0,0 +1,131 @@ +"""Smoke tests for CLI entry points.""" + +from __future__ import annotations + +from unittest.mock import patch + +import pytest + +from agentic_fabric.main import main + + +class TestCLIEntryPoint: + """Smoke tests for the agentic-fabric CLI.""" + + def test_no_args_prints_help(self, capsys) -> None: + """Running with no args should print help text.""" + with patch("sys.argv", ["agentic-fabric"]): + main() + captured = capsys.readouterr() + assert "agentic-fabric" in captured.out.lower() or "usage" in captured.out.lower() + + def test_help_flag(self) -> None: + """--help should exit 0.""" + with ( + patch("sys.argv", ["agentic-fabric", "--help"]), + pytest.raises(SystemExit) as exc_info, + ): + main() + assert exc_info.value.code == 0 + + def test_list_command_with_no_packages(self, capsys) -> None: + """'list' should work even with no packages found.""" + with ( + patch("sys.argv", ["agentic-fabric", "list"]), + patch( + "agentic_fabric.main.discover_packages", + return_value={}, + ), + patch( + "agentic_fabric.main.list_fabric_agents", + return_value={}, + ), + ): + main() + + captured = capsys.readouterr() + assert "No packages" in captured.out or "fabric_agents" in captured.out.lower() + + def test_list_command_json_output(self, capsys) -> None: + """'list --json' should produce valid JSON.""" + import json + + with ( + patch("sys.argv", ["agentic-fabric", "list", "--json"]), + patch( + "agentic_fabric.main.list_fabric_agents", + return_value={}, + ), + ): + main() + + captured = capsys.readouterr() + data = json.loads(captured.out) + assert "fabric_agents" in data + assert isinstance(data["fabric_agents"], list) + + def test_run_missing_package_and_fabric_agent_exits_2(self) -> None: + """'run' without package or fabric_agent should exit with code 2.""" + with ( + patch("sys.argv", ["agentic-fabric", "run"]), + pytest.raises(SystemExit) as exc_info, + ): + main() + assert exc_info.value.code == 2 + + def test_run_nonexistent_package_exits_2(self) -> None: + """'run' with a non-existent package should exit with code 2.""" + with ( + patch("sys.argv", ["agentic-fabric", "run", "nonexistent", "some_fabric_agent", "--input", "test"]), + patch("agentic_fabric.main.discover_packages", return_value={}), + pytest.raises(SystemExit) as exc_info, + ): + main() + assert exc_info.value.code == 2 + + def test_info_nonexistent_package_exits_2(self) -> None: + """'info' with non-existent package should exit with code 2.""" + with ( + patch("sys.argv", ["agentic-fabric", "info", "nonexistent", "some_fabric_agent"]), + patch("agentic_fabric.main.discover_packages", return_value={}), + pytest.raises(SystemExit) as exc_info, + ): + main() + assert exc_info.value.code == 2 + + def test_info_missing_fabric_agent_exits_2(self, tmp_path) -> None: + """'info' with non-existent fabric agent should exit with code 2.""" + config_dir = tmp_path / ".fabric" + config_dir.mkdir() + (config_dir / "manifest.yaml").write_text("fabric_agents: {}\n") + + with ( + patch("sys.argv", ["agentic-fabric", "info", "pkg", "missing_fabric_agent"]), + patch( + "agentic_fabric.main.discover_packages", + return_value={"pkg": config_dir}, + ), + pytest.raises(SystemExit) as exc_info, + ): + main() + assert exc_info.value.code == 2 + + +class TestModuleEntryPoint: + """Test running as python -m agentic_fabric.""" + + def test_module_is_importable(self) -> None: + """The __main__.py module should be importable.""" + import agentic_fabric.__main__ # noqa: F401 + + +class TestListRunnersCommand: + """Smoke tests for list-runners command.""" + + def test_list_runners_command(self, capsys) -> None: + """'list-runners' should execute without error.""" + with patch("sys.argv", ["agentic-fabric", "list-runners"]): + main() + captured = capsys.readouterr() + # Should show some output (either runners or header) + assert len(captured.out) > 0 diff --git a/packages/agentic-fabric/tests/test_connector_builder_fabric.py b/packages/agentic-fabric/tests/test_connector_builder_fabric.py new file mode 100644 index 0000000..dc504a6 --- /dev/null +++ b/packages/agentic-fabric/tests/test_connector_builder_fabric.py @@ -0,0 +1,144 @@ +"""Tests for the connector-builder fabric agent.""" + +from __future__ import annotations + +import builtins +import importlib +import sys +import types + +from typing import Any + +import pytest + + +class FakeAgent: + """Capture Agent constructor kwargs.""" + + calls: list[dict[str, Any]] = [] + + def __init__(self, **kwargs: Any) -> None: + self.kwargs = kwargs + self.calls.append(kwargs) + + +class FakeTask: + """Capture Task constructor kwargs.""" + + calls: list[dict[str, Any]] = [] + + def __init__(self, **kwargs: Any) -> None: + self.kwargs = kwargs + self.calls.append(kwargs) + + +class FakeCrew: + """Capture Crew constructor kwargs and kickoff input.""" + + calls: list[dict[str, Any]] = [] + + def __init__(self, **kwargs: Any) -> None: + self.kwargs = kwargs + self.calls.append(kwargs) + self.kickoff_inputs: dict[str, Any] | None = None + + def kickoff(self, inputs: dict[str, Any]) -> Any: + self.kickoff_inputs = inputs + return "Success" + + +def import_connector_builder_with_fake_crewai(monkeypatch: pytest.MonkeyPatch) -> Any: + """Import the connector builder module with fake CrewAI classes installed.""" + FakeAgent.calls.clear() + FakeTask.calls.clear() + FakeCrew.calls.clear() + + fake_crewai = types.ModuleType("crewai") + fake_crewai.Agent = FakeAgent + fake_crewai.Task = FakeTask + fake_crewai.Crew = FakeCrew + monkeypatch.setitem(sys.modules, "crewai", fake_crewai) + sys.modules.pop("agentic_fabric.fabric_agents.connector_builder.connector_builder_fabric", None) + return importlib.import_module("agentic_fabric.fabric_agents.connector_builder.connector_builder_fabric") + + +@pytest.fixture +def connector_builder_module(monkeypatch: pytest.MonkeyPatch): + """Yield a connector-builder module backed by fake CrewAI classes.""" + module = import_connector_builder_with_fake_crewai(monkeypatch) + yield module + sys.modules.pop("agentic_fabric.fabric_agents.connector_builder.connector_builder_fabric", None) + + +def test_connector_builder_fabric_initializes_and_kicks_off( + monkeypatch: pytest.MonkeyPatch, + connector_builder_module: Any, +) -> None: + """ConnectorBuilderFabricAgent should build configured agents/tasks and delegate kickoff.""" + module = connector_builder_module + monkeypatch.setattr(module, "resolve_tools", lambda tools: [f"resolved:{tool}" for tool in tools]) + + fabric_agent_instance = module.ConnectorBuilderFabricAgent(output_dir="test_output") + + assert len(FakeAgent.calls) == 3 + assert len(FakeTask.calls) == 3 + assert isinstance(fabric_agent_instance.fabric_agent, FakeCrew) + assert FakeAgent.calls[0]["tools"] == ["resolved:ScrapeWebsiteTool", "resolved:CrawlWebsiteTool"] + assert FakeAgent.calls[2]["tools"] == ["resolved:FileWriteTool"] + assert "test_output directory" in FakeTask.calls[2]["description"] + assert FakeCrew.calls[0]["agents"] == [ + fabric_agent_instance.doc_scraper, + fabric_agent_instance.api_analyzer, + fabric_agent_instance.code_generator, + ] + assert FakeCrew.calls[0]["tasks"] == [ + fabric_agent_instance.scrape_docs, + fabric_agent_instance.analyze_api, + fabric_agent_instance.generate_code, + ] + + result = fabric_agent_instance.kickoff(inputs={"url": "http://example.com"}) + + assert result == "Success" + assert fabric_agent_instance.fabric_agent.kickoff_inputs == {"url": "http://example.com"} + + +def test_connector_builder_kickoff_returns_raw_result( + monkeypatch: pytest.MonkeyPatch, + connector_builder_module: Any, +) -> None: + """CrewAI raw results should be returned without string conversion.""" + module = connector_builder_module + + class RawCrew(FakeCrew): + def kickoff(self, inputs: dict[str, Any]) -> Any: + self.kickoff_inputs = inputs + return types.SimpleNamespace(raw="raw success") + + monkeypatch.setattr(module, "Crew", RawCrew) + monkeypatch.setattr(module, "resolve_tools", lambda tools: []) + + fabric_agent_instance = module.ConnectorBuilderFabricAgent() + + assert fabric_agent_instance.kickoff(inputs={"url": "http://example.com"}) == "raw success" + + +def test_connector_builder_reports_missing_crewai(monkeypatch: pytest.MonkeyPatch) -> None: + """Connector builder should raise install guidance when CrewAI is absent.""" + module = import_connector_builder_with_fake_crewai(monkeypatch) + monkeypatch.delitem(sys.modules, "crewai", raising=False) + module.Agent = None + module.Crew = None + module.Task = None + + original_import = builtins.__import__ + + def reject_crewai(name: str, *args: Any, **kwargs: Any) -> Any: + if name == "crewai": + raise ImportError("missing crewai") + return original_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", reject_crewai) + + with pytest.raises(RuntimeError, match="pip install crewai"): + module._load_crewai_classes() diff --git a/packages/agentic-fabric/tests/test_core_runner.py b/packages/agentic-fabric/tests/test_core_runner.py new file mode 100644 index 0000000..8b0f0fc --- /dev/null +++ b/packages/agentic-fabric/tests/test_core_runner.py @@ -0,0 +1,92 @@ +"""Tests for the direct fabric-agent runner facade.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import pytest + +from agentic_fabric.core import runner as fabric_runner + + +class FakeRawResult: + """Result object exposing CrewAI's raw output attribute.""" + + raw = "raw output" + + +class FakeStringResult: + """Result object that relies on string conversion.""" + + def __str__(self) -> str: + return "string output" + + +class FakeCrew: + """Minimal CrewAI-like object with kickoff capture.""" + + def __init__(self, result: Any) -> None: + self.result = result + self.inputs: dict[str, Any] | None = None + + def kickoff(self, inputs: dict[str, Any]) -> Any: + self.inputs = inputs + return self.result + + +def test_run_fabric_agent_discovers_package_loads_config_and_returns_raw(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + config_dir = tmp_path / "pkg" / ".fabric" + fake_crew = FakeCrew(FakeRawResult()) + calls: list[tuple[Any, ...]] = [] + + monkeypatch.setattr(fabric_runner, "discover_packages", lambda workspace_root: {"pkg": config_dir}) + + def fake_get_fabric_agent_config(path: Path, fabric_agent_name: str) -> dict[str, Any]: + calls.append(("config", path, fabric_agent_name)) + return {"name": fabric_agent_name} + + def fake_load_fabric_agent_from_config(config: dict[str, Any]) -> FakeCrew: + calls.append(("load", config)) + return fake_crew + + monkeypatch.setattr(fabric_runner, "get_fabric_agent_config", fake_get_fabric_agent_config) + monkeypatch.setattr(fabric_runner, "load_fabric_agent_from_config", fake_load_fabric_agent_from_config) + + result = fabric_runner.run_fabric_agent("pkg", "builder", inputs={"topic": "tests"}, workspace_root=tmp_path) + + assert result == "raw output" + assert fake_crew.inputs == {"topic": "tests"} + assert calls == [("config", config_dir, "builder"), ("load", {"name": "builder"})] + + +def test_run_fabric_agent_reports_available_packages(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(fabric_runner, "discover_packages", lambda workspace_root: {"known": Path(".fabric")}) + + with pytest.raises(ValueError, match=r"Package 'missing' not found. Available: \['known'\]"): + fabric_runner.run_fabric_agent("missing", "builder") + + +def test_run_fabric_agent_defaults_inputs_to_empty_dict(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + fake_crew = FakeCrew(FakeStringResult()) + + monkeypatch.setattr(fabric_runner, "discover_packages", lambda workspace_root: {"pkg": tmp_path}) + monkeypatch.setattr(fabric_runner, "get_fabric_agent_config", lambda path, fabric_agent_name: {"name": fabric_agent_name}) + monkeypatch.setattr(fabric_runner, "load_fabric_agent_from_config", lambda config: fake_crew) + + result = fabric_runner.run_fabric_agent("pkg", "builder") + + assert result == "string output" + assert fake_crew.inputs == {} + + +def test_run_fabric_agent_from_path_loads_direct_config(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + fake_crew = FakeCrew(FakeStringResult()) + + monkeypatch.setattr(fabric_runner, "get_fabric_agent_config", lambda path, fabric_agent_name: {"path": path, "name": fabric_agent_name}) + monkeypatch.setattr(fabric_runner, "load_fabric_agent_from_config", lambda config: fake_crew) + + result = fabric_runner.run_fabric_agent_from_path(tmp_path / ".fabric", "builder", inputs={"x": 1}) + + assert result == "string output" + assert fake_crew.inputs == {"x": 1} diff --git a/packages/agentic-fabric/tests/test_decomposer_edge_cases.py b/packages/agentic-fabric/tests/test_decomposer_edge_cases.py new file mode 100644 index 0000000..fdb3b29 --- /dev/null +++ b/packages/agentic-fabric/tests/test_decomposer_edge_cases.py @@ -0,0 +1,324 @@ +"""Tests for framework auto-detection edge cases in the decomposer module.""" + +from __future__ import annotations + +from unittest.mock import patch + +import pytest + +from agentic_fabric.core.decomposer import ( + _framework_cache, + _get_install_command, + _resolve_required_framework, + compose_fabric_agent, + detect_framework, + get_available_frameworks, + get_framework_info, + get_runner, + is_cli_runner_available, + is_framework_available, + run_fabric_agent_auto, +) +from agentic_fabric.runners.registry import clear_runtime_cache + + +class TestIsFrameworkAvailable: + """Edge cases for is_framework_available.""" + + def setup_method(self): + """Clear the framework cache before each test.""" + _framework_cache.clear() + clear_runtime_cache() + + def test_unsupported_framework_returns_false(self) -> None: + """An unsupported framework name should return False immediately.""" + result = is_framework_available("pytorch") + assert result is False + + def test_unsupported_framework_is_cached_as_false(self) -> None: + """Unsupported framework names should still be cached.""" + is_framework_available("unsupported_thing") + assert "unsupported_thing" in _framework_cache + assert _framework_cache["unsupported_thing"] is False + + def test_import_failure_returns_false(self) -> None: + """When importlib.import_module raises ImportError, return False.""" + with patch("agentic_fabric.core.decomposer.importlib.import_module", side_effect=ImportError("nope")): + result = is_framework_available("crewai") + assert result is False + + def test_successful_import_returns_true_and_caches(self) -> None: + """A successful import should cache True.""" + with patch("agentic_fabric.core.decomposer.importlib.import_module"): + result = is_framework_available("crewai") + assert result is True + assert _framework_cache["crewai"] is True + + def test_cache_hit_does_not_reimport(self) -> None: + """Once cached, no re-import should occur.""" + _framework_cache["strands"] = True + with patch("agentic_fabric.core.decomposer.importlib.import_module") as mock_import: + result = is_framework_available("strands") + assert result is True + mock_import.assert_not_called() + + def test_empty_string_framework_returns_false(self) -> None: + """Empty string framework name should return False.""" + result = is_framework_available("") + assert result is False + + +class TestDetectFramework: + """Edge cases for detect_framework.""" + + def test_preferred_auto_falls_through_to_detection(self) -> None: + """preferred='auto' should be treated as no preference.""" + + def mock_available(framework): + return framework == "strands" + + with patch( + "agentic_fabric.core.decomposer.is_framework_available", + side_effect=mock_available, + ): + result = detect_framework(preferred="auto") + + assert result == "strands" + + def test_preferred_unavailable_falls_back_gracefully(self) -> None: + """When preferred framework is unavailable, fall back to auto-detect.""" + + def mock_available(framework): + return framework == "crewai" + + with patch( + "agentic_fabric.core.decomposer.is_framework_available", + side_effect=mock_available, + ): + result = detect_framework(preferred="strands") + + assert result == "crewai" + + def test_preferred_none_auto_detects(self) -> None: + """preferred=None should auto-detect.""" + + def mock_available(framework): + return framework == "langgraph" + + with patch( + "agentic_fabric.core.decomposer.is_framework_available", + side_effect=mock_available, + ): + result = detect_framework(preferred=None) + + assert result == "langgraph" + + def test_crewai_has_highest_priority(self) -> None: + """When all frameworks available, crewai should win.""" + with patch( + "agentic_fabric.core.decomposer.is_framework_available", + return_value=True, + ): + result = detect_framework() + + assert result == "crewai" + + def test_error_message_lists_install_options(self) -> None: + """RuntimeError should include installation instructions.""" + with ( + patch("agentic_fabric.core.decomposer.is_framework_available", return_value=False), + pytest.raises(RuntimeError, match="pip install crewai"), + ): + detect_framework() + + +class TestGetAvailableFrameworks: + """Edge cases for get_available_frameworks.""" + + def test_returns_only_available(self) -> None: + """Should only include frameworks that are importable.""" + + def mock_available(framework): + return framework == "strands" + + with patch( + "agentic_fabric.core.decomposer.is_framework_available", + side_effect=mock_available, + ): + result = get_available_frameworks() + + assert result == ["strands"] + + def test_preserves_priority_order(self) -> None: + """Returned list should preserve priority order.""" + + def mock_available(framework): + return framework in ["strands", "crewai"] + + with patch( + "agentic_fabric.core.decomposer.is_framework_available", + side_effect=mock_available, + ): + result = get_available_frameworks() + + assert result == ["crewai", "strands"] + + +class TestRunnerDispatch: + """Tests for framework runner dispatch helpers.""" + + def test_resolve_required_framework_preserves_requested_runtime_without_requirement(self) -> None: + """Framework selection should pass through when configs are runtime-agnostic.""" + assert _resolve_required_framework({"name": "reviewer"}, "langgraph") == "langgraph" + + def test_get_runner_auto_detects_and_instantiates_runner(self, monkeypatch: pytest.MonkeyPatch) -> None: + """get_runner should instantiate the registered runner class.""" + + class FakeRunner: + pass + + class FakeSpec: + runner_module = "fake.runner" + runner_class = "FakeRunner" + + fake_module = type("FakeModule", (), {"FakeRunner": FakeRunner}) + monkeypatch.setattr("agentic_fabric.core.decomposer.detect_framework", lambda: "fake") + monkeypatch.setattr("agentic_fabric.core.decomposer.get_runtime_spec", lambda framework: FakeSpec()) + monkeypatch.setattr("agentic_fabric.core.decomposer.importlib.import_module", lambda module_name: fake_module) + + assert isinstance(get_runner(), FakeRunner) + + def test_get_runner_wraps_unknown_framework(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Unknown framework names should include registered options.""" + + def fake_get_runtime_spec(framework: str): + raise ValueError("unknown") + + monkeypatch.setattr("agentic_fabric.core.decomposer.get_runtime_spec", fake_get_runtime_spec) + + with pytest.raises(ValueError, match="Unknown framework: unknown"): + get_runner("unknown") + + def test_is_cli_runner_available_handles_available_and_missing_profiles( + self, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """CLI availability should map runner errors to False.""" + + class FakeCliRunner: + def is_available(self) -> bool: + return True + + monkeypatch.setattr("agentic_fabric.core.decomposer.get_cli_runner", lambda profile: FakeCliRunner()) + assert is_cli_runner_available("ok") is True + + monkeypatch.setattr( + "agentic_fabric.core.decomposer.get_cli_runner", + lambda profile: (_ for _ in ()).throw(ValueError("missing")), + ) + assert is_cli_runner_available("missing") is False + + def test_compose_fabric_agent_enforces_required_runtime_and_builds( + self, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """Required framework configs should build through that runtime.""" + + class FakeRunner: + def build_fabric_agent(self, fabric_agent_config): + return {"built": fabric_agent_config} + + monkeypatch.setattr("agentic_fabric.core.decomposer.is_framework_available", lambda framework: True) + monkeypatch.setattr("agentic_fabric.core.decomposer.get_runner", lambda framework: FakeRunner()) + + fabric_agent_config = {"required_framework": "crewai", "name": "reviewer"} + + assert compose_fabric_agent(fabric_agent_config, framework="auto") == {"built": fabric_agent_config} + + def test_compose_fabric_agent_reports_conflicting_or_missing_required_runtime( + self, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """Required runtime errors should surface clear guidance.""" + fabric_agent_config = {"required_framework": "crewai"} + + with pytest.raises(ValueError, match="Fabric agent requires crewai"): + compose_fabric_agent(fabric_agent_config, framework="strands") + + monkeypatch.setattr("agentic_fabric.core.decomposer.is_framework_available", lambda framework: False) + + with pytest.raises(RuntimeError, match="pip install crewai"): + compose_fabric_agent(fabric_agent_config) + + def test_run_fabric_agent_auto_enforces_required_runtime_and_runs( + self, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """run_fabric_agent_auto should build and run through the selected runner.""" + + class FakeRunner: + def build_fabric_agent(self, fabric_agent_config): + return {"fabric_agent": fabric_agent_config} + + def run(self, fabric_agent, inputs): + return f"{fabric_agent['fabric_agent']['name']}:{inputs['task']}" + + monkeypatch.setattr("agentic_fabric.core.decomposer.is_framework_available", lambda framework: True) + monkeypatch.setattr("agentic_fabric.core.decomposer.get_runner", lambda framework: FakeRunner()) + + result = run_fabric_agent_auto({"name": "reviewer", "required_framework": "langgraph"}, {"task": "go"}) + + assert result == "reviewer:go" + + def test_run_fabric_agent_auto_reports_conflicting_or_missing_required_runtime( + self, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """run_fabric_agent_auto should reject conflicts and unavailable required runtimes.""" + fabric_agent_config = {"required_framework": "langgraph"} + + with pytest.raises(ValueError, match="Fabric agent requires langgraph"): + run_fabric_agent_auto(fabric_agent_config, framework="crewai") + + monkeypatch.setattr("agentic_fabric.core.decomposer.is_framework_available", lambda framework: False) + + with pytest.raises(RuntimeError, match=r"agentic-fabric\[langgraph\]"): + run_fabric_agent_auto(fabric_agent_config) + + +class TestFrameworkInfo: + """Tests for lazy runtime registry metadata.""" + + def setup_method(self): + """Clear the framework cache before each test.""" + _framework_cache.clear() + clear_runtime_cache() + + def test_framework_info_includes_install_guidance(self) -> None: + """Runtime registry metadata should expose extras and availability.""" + with patch("agentic_fabric.runners.registry.importlib.import_module", side_effect=ImportError("missing")): + info = get_framework_info("crewai") + + assert isinstance(info, dict) + assert info["name"] == "crewai" + assert info["extra"] is None + assert info["available"] is False + assert info["install"] == "pip install crewai" + + +class TestGetInstallCommand: + """Tests for _get_install_command.""" + + def test_crewai_is_external_install(self) -> None: + assert _get_install_command("crewai") == "pip install crewai" + + def test_langgraph_includes_langchain(self) -> None: + assert _get_install_command("langgraph") == 'pip install "agentic-fabric[langgraph]"' + + def test_strands_maps_correctly(self) -> None: + assert _get_install_command("strands") == 'pip install "agentic-fabric[strands]"' + + def test_unknown_framework_returns_itself(self) -> None: + """Unknown framework name should return as-is.""" + result = _get_install_command("unknown_framework") + assert result == "pip install unknown_framework" diff --git a/packages/agentic-fabric/tests/test_discovery.py b/packages/agentic-fabric/tests/test_discovery.py new file mode 100644 index 0000000..7d2cc82 --- /dev/null +++ b/packages/agentic-fabric/tests/test_discovery.py @@ -0,0 +1,574 @@ +"""Tests for the discovery module.""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import patch + +import pytest + + +class TestDiscovery: + """Tests for package discovery functionality.""" + + def test_discover_packages_finds_fabric_workspace_directories(self, temp_workspace: Path) -> None: + """Test that discover_packages finds packages with .fabric directories.""" + from agentic_fabric.core.discovery import discover_packages + + packages = discover_packages(workspace_root=temp_workspace) + + assert "sample" in packages + assert packages["sample"].name == ".fabric" + + def test_discover_packages_finds_fabric_directories(self, tmp_path: Path) -> None: + """Test that discover_packages finds framework-agnostic .fabric directories.""" + from agentic_fabric.core.discovery import discover_packages + + # Create packages with .fabric directory + pkg_dir = tmp_path / "packages" / "strata" + fabric_dir = pkg_dir / ".fabric" + fabric_dir.mkdir(parents=True) + (fabric_dir / "manifest.yaml").write_text("name: strata\nfabric_agents: {}") + + packages = discover_packages(workspace_root=tmp_path) + + assert "strata" in packages + assert packages["strata"].name == ".fabric" + + def test_discover_packages_prefers_fabric_over_crewai(self, tmp_path: Path) -> None: + """Test that .fabric takes priority over .crewai when both exist.""" + from agentic_fabric.core.discovery import discover_packages + + # Create package with both .fabric and .crewai + pkg_dir = tmp_path / "packages" / "hybrid" + (pkg_dir / ".fabric").mkdir(parents=True) + (pkg_dir / ".fabric" / "manifest.yaml").write_text("name: hybrid\nfabric_agents: {}") + (pkg_dir / ".crewai").mkdir(parents=True) + (pkg_dir / ".crewai" / "manifest.yaml").write_text("name: hybrid\nfabric_agents: {}") + + packages = discover_packages(workspace_root=tmp_path) + + assert "hybrid" in packages + # .fabric should be preferred (framework-agnostic first) + assert packages["hybrid"].name == ".fabric" + + def test_discover_packages_returns_empty_when_no_packages(self, tmp_path: Path) -> None: + """Test that discover_packages returns empty dict when no config dirs exist.""" + from agentic_fabric.core.discovery import discover_packages + + # Create empty packages directory + packages_dir = tmp_path / "packages" + packages_dir.mkdir() + (packages_dir / "some_package").mkdir() + + packages = discover_packages(workspace_root=tmp_path) + + assert packages == {} + + def test_discover_packages_rejects_unknown_framework(self, tmp_path: Path) -> None: + """Unknown framework filters should not widen discovery to all directories.""" + from agentic_fabric.core.discovery import discover_packages + + with pytest.raises(ValueError, match="Unknown framework: crewaii"): + discover_packages(workspace_root=tmp_path, framework="crewaii") + + def test_discover_all_framework_configs(self, tmp_path: Path) -> None: + """Test discovering all framework configs for a package.""" + from agentic_fabric.core.discovery import discover_all_framework_configs + + # Create package with multiple framework configs + pkg_dir = tmp_path / "packages" / "multi" + (pkg_dir / ".fabric").mkdir(parents=True) + (pkg_dir / ".fabric" / "manifest.yaml").write_text("name: multi\nfabric_agents: {}") + (pkg_dir / ".crewai").mkdir(parents=True) + (pkg_dir / ".crewai" / "manifest.yaml").write_text("name: multi\nfabric_agents: {}") + + configs = discover_all_framework_configs(workspace_root=tmp_path) + + assert "multi" in configs + assert None in configs["multi"] # .fabric -> None (agnostic) + assert "crewai" in configs["multi"] + + def test_discover_all_framework_configs_uses_default_root_and_skips_files( + self, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """All-framework discovery should use the default root and skip non-package files.""" + from agentic_fabric.core.discovery import discover_all_framework_configs + + packages_dir = tmp_path / "packages" + packages_dir.mkdir() + (packages_dir / "README.md").write_text("not a package", encoding="utf-8") + + pkg_dir = packages_dir / "multi" + (pkg_dir / ".fabric").mkdir(parents=True) + (pkg_dir / ".fabric" / "manifest.yaml").write_text("name: multi\nfabric_agents: {}\n", encoding="utf-8") + (pkg_dir / ".strands").mkdir() + (pkg_dir / ".strands" / "manifest.yaml").write_text("name: multi\nfabric_agents: {}\n", encoding="utf-8") + + (tmp_path / ".langgraph").mkdir() + (tmp_path / ".langgraph" / "manifest.yaml").write_text("name: root\nfabric_agents: {}\n", encoding="utf-8") + monkeypatch.setattr("agentic_fabric.core.discovery.get_workspace_root", lambda: tmp_path) + + configs = discover_all_framework_configs() + + assert configs["multi"][None] == pkg_dir / ".fabric" + assert configs["multi"]["strands"] == pkg_dir / ".strands" + assert configs[tmp_path.name]["langgraph"] == tmp_path / ".langgraph" + + def test_list_fabric_agents_returns_entries_from_manifest(self, temp_workspace: Path) -> None: + """Test that list_fabric_agents returns fabric agent definitions from manifest.""" + from agentic_fabric.core.discovery import list_fabric_agents + + with patch( + "agentic_fabric.core.discovery.discover_packages", + return_value={"sample": temp_workspace / "packages" / "sample" / ".fabric"}, + ): + fabric_agents_by_package = list_fabric_agents() + + assert "sample" in fabric_agents_by_package + fabric_agents = fabric_agents_by_package["sample"] + assert len(fabric_agents) == 1 + assert fabric_agents[0]["name"] == "test_fabric_agent" + + def test_list_fabric_agents_filters_by_package_name(self, temp_workspace: Path) -> None: + """Test that list_fabric_agents can filter to a specific package.""" + from agentic_fabric.core.discovery import list_fabric_agents + + with patch( + "agentic_fabric.core.discovery.discover_packages", + return_value={"sample": temp_workspace / "packages" / "sample" / ".fabric"}, + ): + fabric_agents_by_package = list_fabric_agents(package_name="sample") + + assert "sample" in fabric_agents_by_package + assert len(fabric_agents_by_package) == 1 + + def test_list_fabric_agents_returns_empty_for_nonexistent_package(self, temp_workspace: Path) -> None: + """Test that list_fabric_agents returns empty for non-existent package.""" + from agentic_fabric.core.discovery import list_fabric_agents + + with patch( + "agentic_fabric.core.discovery.discover_packages", + return_value={"sample": temp_workspace / "packages" / "sample" / ".fabric"}, + ): + fabric_agents_by_package = list_fabric_agents(package_name="nonexistent") + + assert fabric_agents_by_package == {} + + def test_load_manifest_parses_yaml(self, temp_workspace: Path) -> None: + """Test that load_manifest parses YAML correctly.""" + from agentic_fabric.core.discovery import load_manifest + + fabric_dir = temp_workspace / "packages" / "sample" / ".fabric" + manifest = load_manifest(fabric_dir) + + assert manifest is not None + assert manifest.get("name") == "sample" + assert "fabric_agents" in manifest + + def test_get_workspace_root_finds_root(self) -> None: + """Test that get_workspace_root finds the workspace root.""" + from agentic_fabric.core.discovery import get_workspace_root + + # This should find the actual workspace root + root = get_workspace_root() + + # Verify it looks like a workspace root + assert (root / "packages").exists() or root == Path.cwd() + + def test_get_workspace_root_falls_back_to_cwd(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Workspace root discovery should fall back when no markers are found.""" + from agentic_fabric.core import discovery + + module_path = tmp_path / "installed" / "module.py" + module_path.parent.mkdir() + module_path.write_text("# module\n", encoding="utf-8") + monkeypatch.setattr(discovery, "__file__", str(module_path)) + monkeypatch.chdir(tmp_path) + + assert discovery.get_workspace_root() == tmp_path + + def test_get_framework_from_config_dir(self) -> None: + """Test framework detection from directory name.""" + from agentic_fabric.core.discovery import get_framework_from_config_dir + + assert get_framework_from_config_dir(Path("/some/path/.fabric")) is None + assert get_framework_from_config_dir(Path("/some/path/.crewai")) == "crewai" + assert get_framework_from_config_dir(Path("/some/path/.langgraph")) == "langgraph" + assert get_framework_from_config_dir(Path("/some/path/.strands")) == "strands" + + def test_get_fabric_agent_config_leaves_runtime_open_for_fabric(self, temp_workspace: Path) -> None: + """Framework-agnostic .fabric configs should not require one runtime.""" + from agentic_fabric.core.discovery import get_fabric_agent_config + + fabric_dir = temp_workspace / "packages" / "sample" / ".fabric" + config = get_fabric_agent_config(fabric_dir, "test_fabric_agent") + + assert config["required_framework"] is None + + def test_get_fabric_agent_config_includes_required_framework_for_crewai(self, tmp_path: Path) -> None: + """Framework-specific .crewai configs should require CrewAI.""" + from agentic_fabric.core.discovery import get_fabric_agent_config + + crewai_dir = tmp_path / ".crewai" + config_dir = crewai_dir / "fabric_agents" / "test_fabric_agent" + config_dir.mkdir(parents=True) + (crewai_dir / "manifest.yaml").write_text( + """ +name: crewai-specific +fabric_agents: + test_fabric_agent: + description: CrewAI-specific test fabric agent + agents: fabric_agents/test_fabric_agent/agents.yaml + tasks: fabric_agents/test_fabric_agent/tasks.yaml +""", + encoding="utf-8", + ) + (config_dir / "agents.yaml").write_text( + """ +test_agent: + role: Test Agent + goal: Test goal + backstory: Test backstory +""", + encoding="utf-8", + ) + (config_dir / "tasks.yaml").write_text( + """ +test_task: + description: Test task + expected_output: Test output + agent: test_agent +""", + encoding="utf-8", + ) + + config = get_fabric_agent_config(crewai_dir, "test_fabric_agent") + + assert config["required_framework"] == "crewai" + + def test_get_fabric_agent_config_requires_agents_and_tasks_keys(self, tmp_path: Path) -> None: + """Fabric agent configs should fail before resolving missing YAML paths.""" + from agentic_fabric.core.discovery import get_fabric_agent_config + + fabric_dir = tmp_path / ".fabric" + fabric_dir.mkdir() + (fabric_dir / "manifest.yaml").write_text( + """ +name: missing-files +fabric_agents: + no_agents: + tasks: tasks.yaml + no_tasks: + agents: agents.yaml +""", + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="missing required key: agents"): + get_fabric_agent_config(fabric_dir, "no_agents") + + with pytest.raises(ValueError, match="missing required key: tasks"): + get_fabric_agent_config(fabric_dir, "no_tasks") + + +class TestDecomposer: + """Tests for the decomposer module.""" + + def test_is_framework_available_caches_results(self) -> None: + """Test that framework availability is cached.""" + from agentic_fabric.core.decomposer import _framework_cache, is_framework_available + + # Clear cache first + _framework_cache.clear() + + # Check availability (will cache result) + result1 = is_framework_available("nonexistent_framework") + result2 = is_framework_available("nonexistent_framework") + + assert result1 is False + assert result2 is False + assert "nonexistent_framework" in _framework_cache + + def test_detect_framework_raises_when_none_available(self) -> None: + """Test that detect_framework raises when no frameworks are available.""" + from agentic_fabric.core.decomposer import detect_framework + + with ( + patch("agentic_fabric.core.decomposer.is_framework_available", return_value=False), + pytest.raises(RuntimeError, match="No AI frameworks installed"), + ): + detect_framework() + + def test_detect_framework_respects_priority(self) -> None: + """Test that frameworks are detected in priority order.""" + from agentic_fabric.core.decomposer import detect_framework + + def mock_available(framework): + return framework in ["langgraph", "strands"] + + with patch( + "agentic_fabric.core.decomposer.is_framework_available", + side_effect=mock_available, + ): + result = detect_framework() + + # langgraph should be preferred over strands + assert result == "langgraph" + + def test_detect_framework_with_preferred_returns_preferred(self) -> None: + """Test that detect_framework returns preferred if available.""" + from agentic_fabric.core.decomposer import detect_framework + + def mock_available(framework): + return framework in ["crewai", "langgraph", "strands"] + + with patch( + "agentic_fabric.core.decomposer.is_framework_available", + side_effect=mock_available, + ): + result = detect_framework(preferred="strands") + + assert result == "strands" + + def test_detect_framework_falls_back_when_preferred_unavailable(self) -> None: + """Test that detect_framework falls back when preferred not available.""" + from agentic_fabric.core.decomposer import detect_framework + + def mock_available(framework): + return framework == "langgraph" + + with patch( + "agentic_fabric.core.decomposer.is_framework_available", + side_effect=mock_available, + ): + result = detect_framework(preferred="crewai") + + assert result == "langgraph" + + def test_get_available_frameworks_returns_list(self) -> None: + """Test that get_available_frameworks returns installed frameworks.""" + from agentic_fabric.core.decomposer import get_available_frameworks + + def mock_available(framework): + return framework in ["crewai", "strands"] + + with patch( + "agentic_fabric.core.decomposer.is_framework_available", + side_effect=mock_available, + ): + result = get_available_frameworks() + + assert isinstance(result, list) + assert "crewai" in result + assert "strands" in result + assert "langgraph" not in result + + def test_get_available_frameworks_returns_empty_when_none_installed(self) -> None: + """Test get_available_frameworks returns empty list when none installed.""" + from agentic_fabric.core.decomposer import get_available_frameworks + + with patch( + "agentic_fabric.core.decomposer.is_framework_available", + return_value=False, + ): + result = get_available_frameworks() + + assert result == [] + + def test_get_runner_returns_crewai_runner(self) -> None: + """Test that get_runner returns CrewAIRunner for crewai.""" + from unittest.mock import MagicMock + + from agentic_fabric.core.decomposer import get_runner + + mock_runner = MagicMock() + mock_runner.framework_name = "crewai" + + with ( + patch( + "agentic_fabric.core.decomposer.is_framework_available", + return_value=True, + ), + patch( + "agentic_fabric.runners.crewai_runner.CrewAIRunner", + return_value=mock_runner, + ), + ): + runner = get_runner("crewai") + + assert runner is not None + assert runner.framework_name == "crewai" + + def test_get_runner_returns_langgraph_runner(self) -> None: + """Test that get_runner returns LangGraphRunner for langgraph.""" + from unittest.mock import MagicMock + + from agentic_fabric.core.decomposer import get_runner + + mock_runner = MagicMock() + mock_runner.framework_name = "langgraph" + + with ( + patch( + "agentic_fabric.core.decomposer.is_framework_available", + return_value=True, + ), + patch( + "agentic_fabric.runners.langgraph_runner.LangGraphRunner", + return_value=mock_runner, + ), + ): + runner = get_runner("langgraph") + + assert runner is not None + assert runner.framework_name == "langgraph" + + def test_get_runner_returns_strands_runner(self) -> None: + """Test that get_runner returns StrandsRunner for strands.""" + from unittest.mock import MagicMock + + from agentic_fabric.core.decomposer import get_runner + + mock_runner = MagicMock() + mock_runner.framework_name = "strands" + + with ( + patch( + "agentic_fabric.core.decomposer.is_framework_available", + return_value=True, + ), + patch( + "agentic_fabric.runners.strands_runner.StrandsRunner", + return_value=mock_runner, + ), + ): + runner = get_runner("strands") + + assert runner is not None + assert runner.framework_name == "strands" + + def test_get_runner_auto_detects_framework(self) -> None: + """Test that get_runner with no args auto-detects framework.""" + from unittest.mock import MagicMock + + from agentic_fabric.core.decomposer import get_runner + + mock_runner = MagicMock() + mock_runner.framework_name = "strands" + + def mock_available(framework): + return framework == "strands" + + with ( + patch( + "agentic_fabric.core.decomposer.is_framework_available", + side_effect=mock_available, + ), + patch( + "agentic_fabric.runners.strands_runner.StrandsRunner", + return_value=mock_runner, + ), + ): + runner = get_runner() + + assert runner.framework_name == "strands" + + def test_get_runner_raises_for_unknown_framework(self) -> None: + """Test that get_runner raises for unknown framework.""" + from agentic_fabric.core.decomposer import get_runner + + with pytest.raises(ValueError, match="Unknown framework"): + get_runner("unknown_framework") + + def test_compose_fabric_agent_uses_required_framework(self, tmp_path: Path) -> None: + """Test that compose_fabric_agent respects required_framework from config.""" + from unittest.mock import MagicMock + + from agentic_fabric.core.decomposer import compose_fabric_agent + + fabric_agent_config = { + "name": "test_fabric_agent", + "required_framework": "strands", + "agents": {}, + "tasks": {}, + } + + mock_runner = MagicMock() + mock_runner.framework_name = "strands" + mock_runner.build_fabric_agent.return_value = MagicMock() + + def mock_available(framework): + return framework in ["crewai", "strands"] + + with ( + patch( + "agentic_fabric.core.decomposer.is_framework_available", + side_effect=mock_available, + ), + patch( + "agentic_fabric.runners.strands_runner.StrandsRunner", + return_value=mock_runner, + ), + ): + # compose_fabric_agent returns the result of runner.build_fabric_agent() + compose_fabric_agent(fabric_agent_config) + + # Verify the runner's build_fabric_agent was called + mock_runner.build_fabric_agent.assert_called_once_with(fabric_agent_config) + + def test_compose_fabric_agent_raises_when_required_unavailable(self) -> None: + """Test compose_fabric_agent raises when required framework not available.""" + from agentic_fabric.core.decomposer import compose_fabric_agent + + fabric_agent_config = { + "name": "test_fabric_agent", + "required_framework": "langgraph", + "agents": {}, + "tasks": {}, + } + + with ( + patch( + "agentic_fabric.core.decomposer.is_framework_available", + return_value=False, + ), + pytest.raises(RuntimeError, match=r"requires langgraph.*not installed"), + ): + compose_fabric_agent(fabric_agent_config) + + def test_compose_fabric_agent_validates_framework_conflict(self) -> None: + """Test compose_fabric_agent validates requested vs required conflict.""" + from agentic_fabric.core.decomposer import compose_fabric_agent + + fabric_agent_config = { + "name": "test_fabric_agent", + "required_framework": "crewai", + "agents": {}, + "tasks": {}, + } + + with ( + patch( + "agentic_fabric.core.decomposer.is_framework_available", + return_value=True, + ), + pytest.raises(ValueError, match=r"requires crewai.*langgraph was requested"), + ): + compose_fabric_agent(fabric_agent_config, framework="langgraph") + + def test_get_install_command_returns_pip_install(self) -> None: + """Test that _get_install_command returns correct pip command.""" + from agentic_fabric.core.decomposer import _get_install_command + + result = _get_install_command("crewai") + assert "crewai" in result + + def test_get_install_command_maps_langgraph(self) -> None: + """Test that _get_install_command maps langgraph correctly.""" + from agentic_fabric.core.decomposer import _get_install_command + + result = _get_install_command("langgraph") + assert "langgraph" in result diff --git a/packages/agentic-fabric/tests/test_discovery_nested.py b/packages/agentic-fabric/tests/test_discovery_nested.py new file mode 100644 index 0000000..0c9090d --- /dev/null +++ b/packages/agentic-fabric/tests/test_discovery_nested.py @@ -0,0 +1,195 @@ +"""Tests for fabric_agent discovery in nested directories and edge cases.""" + +from __future__ import annotations + +from pathlib import Path + +from agentic_fabric.core.discovery import ( + DIR_TO_FRAMEWORK, + FRAMEWORK_DIRS, + FRAMEWORK_TO_DIR, + discover_all_framework_configs, + discover_packages, +) + + +class TestDiscoverPackagesNested: + """Test discovery across nested directory structures.""" + + def test_skips_non_directory_entries(self, tmp_path: Path) -> None: + """Files in the packages/ directory should be ignored.""" + packages_dir = tmp_path / "packages" + packages_dir.mkdir() + + # Create a file (not a directory) in packages/ + (packages_dir / "README.md").write_text("# Packages") + + # Create a real package too + pkg_dir = packages_dir / "real_pkg" + fabric_dir = pkg_dir / ".fabric" + fabric_dir.mkdir(parents=True) + (fabric_dir / "manifest.yaml").write_text("name: real\nfabric_agents: {}") + + packages = discover_packages(workspace_root=tmp_path) + assert "real_pkg" in packages + assert "README.md" not in packages + + def test_standalone_project_at_root(self, tmp_path: Path) -> None: + """A .fabric directory at workspace root should be discovered.""" + fabric_dir = tmp_path / ".fabric" + fabric_dir.mkdir() + (fabric_dir / "manifest.yaml").write_text("name: root_project\nfabric_agents: {}") + + packages = discover_packages(workspace_root=tmp_path) + assert tmp_path.name in packages + + def test_standalone_and_packages_coexist(self, tmp_path: Path) -> None: + """Root .fabric and packages/.fabric should both be discovered.""" + # Root level .fabric + root_fabric = tmp_path / ".fabric" + root_fabric.mkdir() + (root_fabric / "manifest.yaml").write_text("name: root\nfabric_agents: {}") + + # Package level .fabric + pkg_dir = tmp_path / "packages" / "sub_pkg" + pkg_fabric = pkg_dir / ".fabric" + pkg_fabric.mkdir(parents=True) + (pkg_fabric / "manifest.yaml").write_text("name: sub\nfabric_agents: {}") + + packages = discover_packages(workspace_root=tmp_path) + assert "sub_pkg" in packages + assert tmp_path.name in packages + + def test_packages_dir_not_present(self, tmp_path: Path) -> None: + """When there is no packages/ directory, only root is checked.""" + packages = discover_packages(workspace_root=tmp_path) + assert packages == {} + + def test_packages_path_as_file_is_ignored(self, tmp_path: Path) -> None: + """A file named packages should not crash package discovery.""" + (tmp_path / "packages").write_text("not a directory") + + packages = discover_packages(workspace_root=tmp_path) + + assert packages == {} + + def test_framework_filter_crewai(self, tmp_path: Path) -> None: + """Filtering by framework='crewai' should only find .crewai dirs.""" + pkg_dir = tmp_path / "packages" / "test_pkg" + + # Create .fabric (agnostic) and .crewai (specific) + fabric_dir = pkg_dir / ".fabric" + fabric_dir.mkdir(parents=True) + (fabric_dir / "manifest.yaml").write_text("name: test\nfabric_agents: {}") + + crewai_dir = pkg_dir / ".crewai" + crewai_dir.mkdir() + (crewai_dir / "manifest.yaml").write_text("name: test\nfabric_agents: {}") + + packages = discover_packages(workspace_root=tmp_path, framework="crewai") + assert "test_pkg" in packages + assert packages["test_pkg"].name == ".crewai" + + def test_framework_filter_no_match(self, tmp_path: Path) -> None: + """Filtering by framework with no matching dirs returns empty.""" + pkg_dir = tmp_path / "packages" / "test_pkg" + crewai_dir = pkg_dir / ".crewai" + crewai_dir.mkdir(parents=True) + (crewai_dir / "manifest.yaml").write_text("name: test\nfabric_agents: {}") + + packages = discover_packages(workspace_root=tmp_path, framework="strands") + assert packages == {} + + def test_missing_manifest_skips_directory(self, tmp_path: Path) -> None: + """A .crewai directory without manifest.yaml should be skipped.""" + pkg_dir = tmp_path / "packages" / "test_pkg" + crewai_dir = pkg_dir / ".crewai" + crewai_dir.mkdir(parents=True) + # No manifest.yaml created + + packages = discover_packages(workspace_root=tmp_path) + assert packages == {} + + def test_file_shaped_config_path_is_skipped(self, tmp_path: Path) -> None: + """A file named like a config directory should not be discovered.""" + pkg_dir = tmp_path / "packages" / "test_pkg" + pkg_dir.mkdir(parents=True) + (pkg_dir / ".fabric").write_text("not a directory", encoding="utf-8") + (tmp_path / ".langgraph").write_text("not a directory", encoding="utf-8") + + packages = discover_packages(workspace_root=tmp_path) + + assert packages == {} + + def test_multiple_packages_discovered(self, tmp_path: Path) -> None: + """Multiple packages should all be discovered.""" + packages_dir = tmp_path / "packages" + for name in ["alpha", "beta", "gamma"]: + fabric_dir = packages_dir / name / ".fabric" + fabric_dir.mkdir(parents=True) + (fabric_dir / "manifest.yaml").write_text(f"name: {name}\nfabric_agents: {{}}") + + packages = discover_packages(workspace_root=tmp_path) + assert set(packages.keys()) == {"alpha", "beta", "gamma"} + + +class TestDiscoverAllFrameworkConfigs: + """Test discover_all_framework_configs.""" + + def test_multiple_frameworks_per_package(self, tmp_path: Path) -> None: + """A package with .fabric, .crewai, and .strands should return all.""" + pkg_dir = tmp_path / "packages" / "multi" + + for dir_name in [".fabric", ".crewai", ".strands"]: + fw_dir = pkg_dir / dir_name + fw_dir.mkdir(parents=True) + (fw_dir / "manifest.yaml").write_text(f"name: multi\nframework_dir: {dir_name}\nfabric_agents: {{}}") + + configs = discover_all_framework_configs(workspace_root=tmp_path) + + assert "multi" in configs + assert None in configs["multi"] # .fabric -> agnostic + assert "crewai" in configs["multi"] + assert "strands" in configs["multi"] + + def test_root_configs_discovered(self, tmp_path: Path) -> None: + """Root-level framework configs should be discovered.""" + fabric_dir = tmp_path / ".langgraph" + fabric_dir.mkdir() + (fabric_dir / "manifest.yaml").write_text("name: root\nfabric_agents: {}") + + configs = discover_all_framework_configs(workspace_root=tmp_path) + assert tmp_path.name in configs + assert "langgraph" in configs[tmp_path.name] + + def test_empty_workspace(self, tmp_path: Path) -> None: + """An empty workspace should return empty dict.""" + configs = discover_all_framework_configs(workspace_root=tmp_path) + assert configs == {} + + def test_packages_path_as_file_is_ignored(self, tmp_path: Path) -> None: + """A file named packages should not crash all-framework discovery.""" + (tmp_path / "packages").write_text("not a directory") + + configs = discover_all_framework_configs(workspace_root=tmp_path) + + assert configs == {} + + +class TestFrameworkConstants: + """Test the framework constant mappings.""" + + def test_dir_to_framework_completeness(self) -> None: + """Every entry in FRAMEWORK_DIRS should have a DIR_TO_FRAMEWORK entry.""" + for dir_name in FRAMEWORK_DIRS: + assert dir_name in DIR_TO_FRAMEWORK + + def test_framework_to_dir_completeness(self) -> None: + """Every framework in DIR_TO_FRAMEWORK should have a FRAMEWORK_TO_DIR entry.""" + for framework in DIR_TO_FRAMEWORK.values(): + assert framework in FRAMEWORK_TO_DIR + + def test_round_trip_mapping(self) -> None: + """DIR_TO_FRAMEWORK -> FRAMEWORK_TO_DIR should be a round trip.""" + for dir_name, framework in DIR_TO_FRAMEWORK.items(): + assert FRAMEWORK_TO_DIR[framework] == dir_name diff --git a/packages/agentic-fabric/tests/test_examples.py b/packages/agentic-fabric/tests/test_examples.py new file mode 100644 index 0000000..9aa5647 --- /dev/null +++ b/packages/agentic-fabric/tests/test_examples.py @@ -0,0 +1,107 @@ +"""Integration checks for shipped examples.""" + +from __future__ import annotations + +import json +import subprocess +import sys + +from pathlib import Path + +from examples.discovery_workflow import DEFAULT_WORKSPACE, summarize_workspace +from examples.mcp_adapters import inspect_mcp_adapters +from examples.runtime_context import inspect_runtime_context +from examples.tool_registry import inspect_registry + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_discovery_workflow_summary_uses_bundled_workspace() -> None: + summary = summarize_workspace(DEFAULT_WORKSPACE) + + assert sorted(summary["packages"]) == ["review"] + review = summary["packages"]["review"] + assert review["config_dir"] == ".fabric" + fabric_agent = review["fabric_agents"]["implementation_review"] + assert fabric_agent["required_framework"] is None + assert fabric_agent["agents"] == ["documenter", "reviewer"] + assert fabric_agent["tasks"] == ["review_code", "review_docs"] + + +def test_tool_registry_example_inspects_core_registry() -> None: + summary = inspect_registry() + + assert summary["framework_dirs"] == [".fabric", ".crewai", ".langgraph", ".strands"] + assert summary["mcp_git_available"] is False + + +def test_runtime_context_example_inspects_agentic_data() -> None: + summary = inspect_runtime_context() + + assert summary["active_runtime"] == "crewai" + assert summary["registered_fabric_agents"] == ["reviewer"] + assert summary["runtime_names"] == ["crewai", "langgraph", "strands"] + + +def test_mcp_adapters_example_inspects_owned_entry_points() -> None: + summary = inspect_mcp_adapters() + + assert summary["extra"] == "mcp" + assert summary["entry_points"] == { + "vendor": "agentic-fabric-vendor-mcp", + "meshy": "agentic-fabric-meshy-mcp", + } + assert "agentic-fabric[mcp]" in summary["install_guidance"]["mcp"] + assert "vendor-fabric" in summary["install_guidance"]["vendor"] + assert "vendor-fabric[meshy]" in summary["install_guidance"]["meshy"] + assert summary["client_config"]["mcpServers"]["vendor-fabric"]["command"] == "agentic-fabric-vendor-mcp" + assert summary["client_config"]["mcpServers"]["meshy"]["command"] == "agentic-fabric-meshy-mcp" + + +def test_discovery_workflow_script_outputs_json() -> None: + result = subprocess.run( + [sys.executable, str(ROOT / "examples" / "discovery_workflow.py")], + check=True, + capture_output=True, + text=True, + ) + + parsed = json.loads(result.stdout) + assert "review" in parsed["packages"] + + +def test_tool_registry_script_outputs_json() -> None: + result = subprocess.run( + [sys.executable, str(ROOT / "examples" / "tool_registry.py")], + check=True, + capture_output=True, + text=True, + ) + + parsed = json.loads(result.stdout) + assert parsed["mcp_git_available"] is False + + +def test_runtime_context_script_outputs_json() -> None: + result = subprocess.run( + [sys.executable, str(ROOT / "examples" / "runtime_context.py")], + check=True, + capture_output=True, + text=True, + ) + + parsed = json.loads(result.stdout) + assert parsed["active_runtime"] == "crewai" + + +def test_mcp_adapters_script_outputs_json() -> None: + result = subprocess.run( + [sys.executable, str(ROOT / "examples" / "mcp_adapters.py")], + check=True, + capture_output=True, + text=True, + ) + + parsed = json.loads(result.stdout) + assert parsed["entry_points"]["vendor"] == "agentic-fabric-vendor-mcp" diff --git a/packages/agentic-fabric/tests/test_fabric_mocker.py b/packages/agentic-fabric/tests/test_fabric_mocker.py new file mode 100644 index 0000000..55d037b --- /dev/null +++ b/packages/agentic-fabric/tests/test_fabric_mocker.py @@ -0,0 +1,73 @@ +"""Tests for local agentic-fabric test support.""" + +from __future__ import annotations + +import sys + +from pathlib import Path +from typing import TYPE_CHECKING + +from pytest_agentic_fabric.mocking import ALL_FRAMEWORK_MODULES, CREWAI_MODULES, LANGGRAPH_MODULES, STRANDS_MODULES + + +if TYPE_CHECKING: + from pytest_agentic_fabric.mocking import FabricMocker + + +class TestFabricMockerBasics: + """Test basic FabricMocker functionality.""" + + def test_fabric_mocker_has_mocker(self, fabric_mocker: FabricMocker) -> None: + assert fabric_mocker.mocker is not None + + def test_magic_mock_property(self, fabric_mocker: FabricMocker) -> None: + assert fabric_mocker.MagicMock() is not None + + def test_mock_module_adds_to_sys_modules(self, fabric_mocker: FabricMocker) -> None: + mock = fabric_mocker.mock_module("test_fake_module_xyz") + assert sys.modules["test_fake_module_xyz"] is mock + + def test_mock_module_returns_same_mock_on_repeat(self, fabric_mocker: FabricMocker) -> None: + assert fabric_mocker.mock_module("test_fake_repeat") is fabric_mocker.mock_module("test_fake_repeat") + + def test_restore_modules_cleans_up(self, fabric_mocker: FabricMocker) -> None: + fabric_mocker.mock_module("test_restore_target") + fabric_mocker.restore_modules() + assert "test_restore_target" not in sys.modules + + +class TestFrameworkMocking: + """Test framework-specific mocking helpers.""" + + def test_mock_crewai_mocks_all_modules(self, fabric_mocker: FabricMocker) -> None: + mocks = fabric_mocker.mock_crewai() + for module in CREWAI_MODULES: + assert module in mocks + assert module in sys.modules + + def test_mock_langgraph_mocks_all_modules(self, fabric_mocker: FabricMocker) -> None: + mocks = fabric_mocker.mock_langgraph() + for module in LANGGRAPH_MODULES: + assert module in mocks + + def test_mock_strands_mocks_all_modules(self, fabric_mocker: FabricMocker) -> None: + mocks = fabric_mocker.mock_strands() + for module in STRANDS_MODULES: + assert module in mocks + + def test_mock_all_frameworks(self, fabric_mocker: FabricMocker) -> None: + mocks = fabric_mocker.mock_all_frameworks() + for module in ALL_FRAMEWORK_MODULES: + assert module in mocks + + def test_patch_get_llm_custom_return(self, fabric_mocker: FabricMocker) -> None: + custom_llm = fabric_mocker.MagicMock(name="custom_llm") + mock = fabric_mocker.patch_get_llm(return_value=custom_llm) + assert mock.return_value is custom_llm + + +class TestLocalFixtures: + """Test reusable local fixtures.""" + + def test_temp_workspace_fixture(self, temp_workspace: Path) -> None: + assert (temp_workspace / "packages" / "sample" / ".fabric" / "manifest.yaml").exists() diff --git a/packages/agentic-fabric/tests/test_file_tool_paths.py b/packages/agentic-fabric/tests/test_file_tool_paths.py new file mode 100644 index 0000000..c20ee1d --- /dev/null +++ b/packages/agentic-fabric/tests/test_file_tool_paths.py @@ -0,0 +1,204 @@ +"""Security tests for file tool path resolution helpers.""" + +from __future__ import annotations + +import importlib +import sys +import types + +from pathlib import Path + +import pytest + + +def import_file_tools_with_fake_crewai(monkeypatch: pytest.MonkeyPatch) -> types.ModuleType: + """Import file_tools without requiring the optional CrewAI package.""" + fake_crewai = types.ModuleType("crewai") + fake_tools = types.ModuleType("crewai.tools") + + class BaseTool: + """Minimal stand-in for crewai.tools.BaseTool.""" + + fake_tools.BaseTool = BaseTool + monkeypatch.setitem(sys.modules, "crewai", fake_crewai) + monkeypatch.setitem(sys.modules, "crewai.tools", fake_tools) + sys.modules.pop("agentic_fabric.tools.file_tools", None) + return importlib.import_module("agentic_fabric.tools.file_tools") + + +def test_resolve_workspace_path_rejects_symlink_escape(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Resolved paths must remain below the workspace root after symlinks.""" + file_tools = import_file_tools_with_fake_crewai(monkeypatch) + workspace_root = tmp_path / "workspace" + workspace_root.mkdir() + outside = tmp_path / "outside" + outside.mkdir() + (workspace_root / "src").mkdir() + (workspace_root / "src" / "ecs").symlink_to(outside, target_is_directory=True) + + monkeypatch.setattr(file_tools, "get_workspace_root", lambda: workspace_root) + + with pytest.raises(ValueError, match="escapes workspace root"): + file_tools._resolve_workspace_path("src/ecs/secret.ts") + + +def test_allowed_write_path_requires_directory_boundary(monkeypatch: pytest.MonkeyPatch) -> None: + """Allowed write dirs match real path segments, not arbitrary prefixes.""" + file_tools = import_file_tools_with_fake_crewai(monkeypatch) + + assert file_tools._is_allowed_write_path("src/ecs/component.ts") + assert not file_tools._is_allowed_write_path("src/ecsnot/component.ts") + + +def test_workspace_root_resolution_uses_workspace_env_and_cwd( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Workspace discovery should fall back from package roots to env vars and cwd.""" + file_tools = import_file_tools_with_fake_crewai(monkeypatch) + workspace_root = tmp_path / "workspace" + target_package = workspace_root / "packages" / "sample" + target_package.mkdir(parents=True) + monkeypatch.setattr(file_tools, "_find_workspace_root", lambda: workspace_root) + + assert file_tools.get_workspace_root() == target_package + + env_root = tmp_path / "env-root" + env_root.mkdir() + monkeypatch.setattr(file_tools, "_find_workspace_root", lambda: None) + monkeypatch.setenv("SAMPLE_ROOT", str(env_root)) + + assert file_tools.get_workspace_root("sample") == env_root + + monkeypatch.delenv("SAMPLE_ROOT") + monkeypatch.chdir(tmp_path) + + assert file_tools.get_workspace_root("sample") == tmp_path + + +def test_find_workspace_root_finds_repository_root(monkeypatch: pytest.MonkeyPatch) -> None: + """The marker-file search should find the checked-out workspace root.""" + file_tools = import_file_tools_with_fake_crewai(monkeypatch) + + root = file_tools._find_workspace_root() + + assert root is not None + assert (root / "pyproject.toml").exists() + assert (root / "packages").is_dir() + + +def test_find_workspace_root_returns_none_without_markers(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """The marker-file search should return None for installed modules outside a workspace.""" + file_tools = import_file_tools_with_fake_crewai(monkeypatch) + module_path = tmp_path / "installed" / "file_tools.py" + module_path.parent.mkdir() + module_path.write_text("# module\n", encoding="utf-8") + monkeypatch.setattr(file_tools, "__file__", str(module_path)) + + assert file_tools._find_workspace_root() is None + + +def test_clean_relative_path_rejects_invalid_values(monkeypatch: pytest.MonkeyPatch) -> None: + """Relative path cleaning should reject empty, absolute, and traversal paths.""" + file_tools = import_file_tools_with_fake_crewai(monkeypatch) + + for value in ("", "/tmp/file.ts", "src/../secret.ts"): + with pytest.raises(ValueError, match="Path traversal not allowed"): + file_tools._clean_relative_path(value) + + +def test_file_tools_write_read_and_list_allowed_workspace(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """The CrewAI file tools should operate inside the resolved workspace root.""" + file_tools = import_file_tools_with_fake_crewai(monkeypatch) + workspace_root = tmp_path / "workspace" + workspace_root.mkdir() + monkeypatch.setattr(file_tools, "get_workspace_root", lambda: workspace_root) + + write_result = file_tools.GameCodeWriterTool()._run("src/ecs/data/species.ts", "export const species = [];") + read_result = file_tools.GameCodeReaderTool()._run("src/ecs/data/species.ts") + list_result = file_tools.DirectoryListTool()._run("src/ecs/data") + + assert "Successfully wrote" in write_result + assert read_result == "export const species = [];" + assert "species.ts" in list_result + + +def test_file_tools_report_common_user_errors(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Runtime tool errors should be returned as agent-readable strings.""" + file_tools = import_file_tools_with_fake_crewai(monkeypatch) + workspace_root = tmp_path / "workspace" + workspace_root.mkdir() + monkeypatch.setattr(file_tools, "get_workspace_root", lambda: workspace_root) + + bad_write_dir = file_tools.GameCodeWriterTool()._run("src/forbidden/file.ts", "") + bad_write_ext = file_tools.GameCodeWriterTool()._run("src/ecs/data/file.py", "") + missing_read = file_tools.GameCodeReaderTool()._run("src/ecs/data/missing.ts") + missing_dir = file_tools.DirectoryListTool()._run("src/ecs/data") + + assert "not in an allowed directory" in bad_write_dir + assert "Extension '.py' not allowed" in bad_write_ext + assert "File not found" in missing_read + assert "Directory not found" in missing_dir + + +def test_file_tools_report_reader_and_lister_edge_cases(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Reader and lister should report directories, large files, files, and empty directories.""" + file_tools = import_file_tools_with_fake_crewai(monkeypatch) + workspace_root = tmp_path / "workspace" + data_dir = workspace_root / "src" / "ecs" / "data" + data_dir.mkdir(parents=True) + monkeypatch.setattr(file_tools, "get_workspace_root", lambda: workspace_root) + + (data_dir / "folder").mkdir() + large_file = data_dir / "large.ts" + large_file.write_text("x" * 100_001, encoding="utf-8") + empty_dir = data_dir / "empty" + empty_dir.mkdir() + (data_dir / ".hidden.ts").write_text("hidden", encoding="utf-8") + (data_dir / "visible.ts").write_text("visible", encoding="utf-8") + + assert "Path is not a file" in file_tools.GameCodeReaderTool()._run("src/ecs/data/folder") + assert "File too large" in file_tools.GameCodeReaderTool()._run("src/ecs/data/large.ts") + assert "Path is not a directory" in file_tools.DirectoryListTool()._run("src/ecs/data/visible.ts") + assert file_tools.DirectoryListTool()._run("src/ecs/data/empty") == "Directory src/ecs/data/empty is empty" + + list_result = file_tools.DirectoryListTool()._run("src/ecs/data") + assert "visible.ts" in list_result + assert ".hidden.ts" not in list_result + + +def test_file_tools_convert_permission_and_unexpected_errors(monkeypatch: pytest.MonkeyPatch) -> None: + """Tool exceptions should be converted to agent-readable strings.""" + file_tools = import_file_tools_with_fake_crewai(monkeypatch) + + def raise_permission(path_value: str) -> tuple[str, Path]: + raise PermissionError + + monkeypatch.setattr(file_tools, "_resolve_workspace_path", raise_permission) + + assert "Permission denied writing" in file_tools.GameCodeWriterTool()._run("src/ecs/data/file.ts", "") + assert "Permission denied reading" in file_tools.GameCodeReaderTool()._run("src/ecs/data/file.ts") + assert "Permission denied accessing" in file_tools.DirectoryListTool()._run("src/ecs/data") + + def raise_unexpected(path_value: str) -> tuple[str, Path]: + raise RuntimeError("boom") + + monkeypatch.setattr(file_tools, "_resolve_workspace_path", raise_unexpected) + + assert "Error writing file: boom" in file_tools.GameCodeWriterTool()._run("src/ecs/data/file.ts", "") + assert "Error reading file: boom" in file_tools.GameCodeReaderTool()._run("src/ecs/data/file.ts") + assert "Error listing directory: boom" in file_tools.DirectoryListTool()._run("src/ecs/data") + + +def test_file_tools_convert_path_validation_errors(monkeypatch: pytest.MonkeyPatch) -> None: + """ValueError path validation failures should be returned as agent-readable errors.""" + file_tools = import_file_tools_with_fake_crewai(monkeypatch) + + def raise_value_error(path_value: str) -> tuple[str, Path]: + raise ValueError("bad path") + + monkeypatch.setattr(file_tools, "_resolve_workspace_path", raise_value_error) + + assert file_tools.GameCodeWriterTool()._run("../file.ts", "") == "Error: bad path" + assert file_tools.GameCodeReaderTool()._run("../file.ts") == "Error: bad path" + assert file_tools.DirectoryListTool()._run("../") == "Error: bad path" diff --git a/packages/agentic-fabric/tests/test_file_tools.py b/packages/agentic-fabric/tests/test_file_tools.py new file mode 100644 index 0000000..00e65f0 --- /dev/null +++ b/packages/agentic-fabric/tests/test_file_tools.py @@ -0,0 +1,247 @@ +"""Tests for framework-neutral file manipulation tools.""" + +from __future__ import annotations + +import builtins +import os +import sys + +from pathlib import Path +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def fresh_file_tools_module(): + """Keep import-branch assertions isolated between tests.""" + sys.modules.pop("agentic_fabric.tools.file_tools", None) + yield + sys.modules.pop("agentic_fabric.tools.file_tools", None) + + +def test_file_tools_import_without_optional_frameworks(monkeypatch: pytest.MonkeyPatch) -> None: + """Built-in tools should not require CrewAI or Pydantic at import time.""" + real_import = builtins.__import__ + + def block_optional_imports(name: str, *args, **kwargs): + if name in {"crewai.tools", "pydantic"}: + raise ImportError(name) + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", block_optional_imports) + + from agentic_fabric.tools.file_tools import GameCodeReaderTool, GameCodeWriterTool + + assert GameCodeReaderTool().args_schema is None + assert GameCodeWriterTool()._run("forbidden/example.ts", "").startswith("Error:") + + +class TestGetWorkspaceRoot: + """Tests for get_workspace_root function.""" + + def test_uses_target_package_env_var(self, temp_workspace: Path) -> None: + """Test that TARGET_PACKAGE env var is respected.""" + from agentic_fabric.tools.file_tools import get_workspace_root + + # Create another package + other_pkg = temp_workspace / "packages" / "other_package" + other_pkg.mkdir(parents=True) + + # Create pyproject.toml at workspace root + (temp_workspace / "pyproject.toml").write_text("[project]\nname = 'test'") + + with ( + patch.dict(os.environ, {"TARGET_PACKAGE": "other_package"}), + patch( + "agentic_fabric.tools.file_tools._find_workspace_root", + return_value=temp_workspace, + ), + ): + root = get_workspace_root() + + assert root == other_pkg + + def test_defaults_to_single_workspace_package(self, tmp_path: Path) -> None: + """A single package workspace should be selected without repo-specific names.""" + from agentic_fabric.tools.file_tools import get_workspace_root + + target_package = tmp_path / "packages" / "sample" + target_package.mkdir(parents=True, exist_ok=True) + + with patch( + "agentic_fabric.tools.file_tools._find_workspace_root", + return_value=tmp_path, + ): + # Remove TARGET_PACKAGE if set + env = os.environ.copy() + env.pop("TARGET_PACKAGE", None) + with patch.dict(os.environ, env, clear=True): + root = get_workspace_root() + + assert root == target_package + + def test_explicit_package_name(self, temp_workspace: Path) -> None: + """Test explicit package name parameter.""" + from agentic_fabric.tools.file_tools import get_workspace_root + + # Create custom package + custom_pkg = temp_workspace / "packages" / "custom" + custom_pkg.mkdir(parents=True) + + with patch( + "agentic_fabric.tools.file_tools._find_workspace_root", + return_value=temp_workspace, + ): + root = get_workspace_root(package_name="custom") + + assert root == custom_pkg + + +class TestGameCodeWriterTool: + """Tests for GameCodeWriterTool.""" + + def test_rejects_path_traversal(self) -> None: + """Test that path traversal is rejected.""" + from agentic_fabric.tools.file_tools import GameCodeWriterTool + + tool = GameCodeWriterTool() + result = tool._run(file_path="../../../etc/passwd", content="malicious") + + assert "Error" in result + assert "Path traversal" in result or "not allowed" in result + + def test_rejects_absolute_paths(self) -> None: + """Test that absolute paths are rejected.""" + from agentic_fabric.tools.file_tools import GameCodeWriterTool + + tool = GameCodeWriterTool() + result = tool._run(file_path="/etc/passwd", content="malicious") + + assert "Error" in result + + def test_rejects_disallowed_directories(self) -> None: + """Test that writes to non-allowed directories are rejected.""" + from agentic_fabric.tools.file_tools import GameCodeWriterTool + + tool = GameCodeWriterTool() + result = tool._run(file_path="node_modules/test.ts", content="test") + + assert "Error" in result + assert "not in an allowed directory" in result + + def test_rejects_disallowed_extensions(self) -> None: + """Test that disallowed file extensions are rejected.""" + from agentic_fabric.tools.file_tools import GameCodeWriterTool + + tool = GameCodeWriterTool() + result = tool._run(file_path="src/ecs/test.exe", content="binary") + + assert "Error" in result + assert "not allowed" in result + + def test_writes_to_allowed_directory(self, temp_workspace: Path) -> None: + """Test that writing to allowed directories works.""" + from agentic_fabric.tools.file_tools import GameCodeWriterTool + + # Create the allowed directory structure + ecs_dir = temp_workspace / "packages" / "sample" / "src" / "ecs" + ecs_dir.mkdir(parents=True) + + tool = GameCodeWriterTool() + + with patch( + "agentic_fabric.tools.file_tools.get_workspace_root", + return_value=temp_workspace / "packages" / "sample", + ): + result = tool._run( + file_path="src/ecs/TestComponent.ts", + content="export const TestComponent = {};", + ) + + assert "Successfully wrote" in result + assert (ecs_dir / "TestComponent.ts").exists() + assert (ecs_dir / "TestComponent.ts").read_text(encoding="utf-8") == "export const TestComponent = {};" + + +class TestGameCodeReaderTool: + """Tests for GameCodeReaderTool.""" + + def test_rejects_path_traversal(self) -> None: + """Test that path traversal is rejected.""" + from agentic_fabric.tools.file_tools import GameCodeReaderTool + + tool = GameCodeReaderTool() + result = tool._run(file_path="../../../etc/passwd") + + assert "Error" in result + assert "not allowed" in result + + def test_reads_existing_file(self, temp_workspace: Path) -> None: + """Test reading an existing file.""" + from agentic_fabric.tools.file_tools import GameCodeReaderTool + + # Create a test file + test_file = temp_workspace / "packages" / "sample" / "src" / "test.ts" + test_file.parent.mkdir(parents=True, exist_ok=True) + test_file.write_text("export const Test = 'hello';") + + tool = GameCodeReaderTool() + + with patch( + "agentic_fabric.tools.file_tools.get_workspace_root", + return_value=temp_workspace / "packages" / "sample", + ): + result = tool._run(file_path="src/test.ts") + + assert "export const Test" in result + + def test_returns_error_for_missing_file(self, temp_workspace: Path) -> None: + """Test that missing files return an error.""" + from agentic_fabric.tools.file_tools import GameCodeReaderTool + + tool = GameCodeReaderTool() + + with patch( + "agentic_fabric.tools.file_tools.get_workspace_root", + return_value=temp_workspace / "packages" / "sample", + ): + result = tool._run(file_path="src/nonexistent.ts") + + assert "Error" in result + assert "not found" in result + + +class TestDirectoryListTool: + """Tests for DirectoryListTool.""" + + def test_lists_directory_contents(self, temp_workspace: Path) -> None: + """Test listing directory contents.""" + from agentic_fabric.tools.file_tools import DirectoryListTool + + # Create some test files + src_dir = temp_workspace / "packages" / "sample" / "src" + src_dir.mkdir(parents=True) + (src_dir / "index.ts").write_text("export {};") + (src_dir / "utils").mkdir() + + tool = DirectoryListTool() + + with patch( + "agentic_fabric.tools.file_tools.get_workspace_root", + return_value=temp_workspace / "packages" / "sample", + ): + result = tool._run(directory="src") + + assert "index.ts" in result + assert "utils" in result + + def test_rejects_path_traversal(self) -> None: + """Test that path traversal is rejected.""" + from agentic_fabric.tools.file_tools import DirectoryListTool + + tool = DirectoryListTool() + result = tool._run(directory="../../../etc") + + assert "Error" in result + assert "not allowed" in result diff --git a/packages/agentic-fabric/tests/test_import_surfaces.py b/packages/agentic-fabric/tests/test_import_surfaces.py new file mode 100644 index 0000000..2188db8 --- /dev/null +++ b/packages/agentic-fabric/tests/test_import_surfaces.py @@ -0,0 +1,134 @@ +"""Tests for small import and module-entry surfaces.""" + +from __future__ import annotations + +import importlib +import importlib.metadata +import runpy +import sys + +from pathlib import Path + +import pytest + + +PACKAGE_ROOT = Path(__file__).resolve().parents[1] +WORKSPACE_ROOT = PACKAGE_ROOT.parents[1] +REPO_SPECIFIC_LEGACY_TOKEN = "otter" + "fall" + + +def test_package_version_falls_back_when_not_installed(monkeypatch: pytest.MonkeyPatch) -> None: + """Source-tree imports should have a stable version fallback.""" + package_file = importlib.import_module("agentic_fabric").__file__ + + def missing_version(name: str) -> str: + if name == "agentic-fabric": + raise importlib.metadata.PackageNotFoundError(name) + return "1.0.0" + + monkeypatch.setattr(importlib.metadata, "version", missing_version) + + namespace = runpy.run_path(package_file, run_name="agentic_fabric_version_fallback") + + assert namespace["__version__"] == "0.0.0" + + +def test_base_exports_file_tools_without_crewai() -> None: + """The base package should re-export framework-neutral file tools.""" + sys.modules.pop("agentic_fabric.tools.file_tools", None) + sys.modules.pop("agentic_fabric.base", None) + + base = importlib.import_module("agentic_fabric.base") + + assert base.__all__ == ["DirectoryListTool", "GameCodeReaderTool", "GameCodeWriterTool"] + assert base.DirectoryListTool.__name__ == "DirectoryListTool" + + +def test_module_entrypoint_delegates_to_main(monkeypatch: pytest.MonkeyPatch) -> None: + """python -m agentic_fabric should call the CLI main function.""" + from agentic_fabric import main as cli_main + + calls: list[str] = [] + monkeypatch.setattr(cli_main, "main", lambda: calls.append("main")) + sys.modules.pop("agentic_fabric.__main__", None) + + runpy.run_module("agentic_fabric.__main__", run_name="__main__") + + assert calls == ["main"] + + +def test_cli_script_entrypoint_prints_help(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + """Executing main.py as a script should use the same CLI entrypoint.""" + from agentic_fabric import main as cli_main + + monkeypatch.setattr(sys, "argv", ["agentic-fabric"]) + + runpy.run_path(str(cli_main.__file__), run_name="__main__") + + assert "agentic-fabric - framework-agnostic fabric agent runner" in capsys.readouterr().out + + +def test_runtime_source_does_not_reown_vendor_or_legacy_surfaces() -> None: + """Runtime source should not drift into vendor ownership or old names.""" + disallowed = { + "secrets_sync": "SecretSync Python access must route through vendor-fabric capabilities", + "secretssync": "SecretSync direct-import spelling is not part of this layer", + REPO_SPECIFIC_LEGACY_TOKEN: "repo-specific package naming is not part of the agnostic package", + "agentic-crew": "fabric agent naming replaced old crew package naming", + "connector_builder_crew": "connector builder code should use fabric naming", + "ConnectorBuilderCrew": "connector builder code should use fabric naming", + } + source_files = sorted((PACKAGE_ROOT / "src" / "agentic_fabric").rglob("*.py")) + + failures: list[str] = [] + for source_file in source_files: + text = source_file.read_text(encoding="utf-8") + for token, reason in disallowed.items(): + if token in text: + failures.append(f"{source_file.relative_to(PACKAGE_ROOT)} contains {token!r}: {reason}") + + assert failures == [] + + +def test_public_surfaces_do_not_advertise_retired_vendor_mcp_entrypoints() -> None: + """MCP ownership should stay with agentic-fabric, not vendor-fabric.""" + retired = { + "vendor_fabric.mcp": "use agentic_fabric.tools.vendor_mcp", + "vendor_fabric.meshy.mcp": "use agentic_fabric.tools.meshy_mcp", + "vendor-fabric-mcp": "use the agentic-fabric-vendor-mcp console script", + "vendor-fabric[mcp]": "MCP transport dependencies live in agentic-fabric[mcp]", + "vendor-fabric[meshy,mcp]": "combine agentic-fabric[mcp] with vendor-fabric[meshy]", + } + roots = [ + PACKAGE_ROOT / "README.md", + PACKAGE_ROOT / "pyproject.toml", + PACKAGE_ROOT / "src", + PACKAGE_ROOT / "examples", + WORKSPACE_ROOT / "docs", + WORKSPACE_ROOT / "AGENTS.md", + WORKSPACE_ROOT / "README.md", + WORKSPACE_ROOT / "pyproject.toml", + WORKSPACE_ROOT / "tox.ini", + ] + + files: list[Path] = [] + for root in roots: + if root.is_file(): + files.append(root) + elif root.is_dir(): + files.extend( + path + for path in root.rglob("*") + if path.suffix in {".md", ".py", ".rst", ".toml", ".yaml", ".yml"} + and "_build" not in path.parts + and "__pycache__" not in path.parts + ) + + failures: list[str] = [] + for source_file in sorted(files): + text = source_file.read_text(encoding="utf-8") + for token, replacement in retired.items(): + if token in text: + failures.append(f"{source_file.relative_to(WORKSPACE_ROOT)} contains {token!r}: {replacement}") + + assert failures == [] diff --git a/packages/agentic-fabric/tests/test_llm_config.py b/packages/agentic-fabric/tests/test_llm_config.py new file mode 100644 index 0000000..4f7a125 --- /dev/null +++ b/packages/agentic-fabric/tests/test_llm_config.py @@ -0,0 +1,159 @@ +"""Tests for LLM provider selection helpers.""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from agentic_fabric.config import llm as llm_config +from agentic_fabric.config.llm import LLMProvider + + +class FakeLLM: + """Small stand-in for CrewAI's LLM wrapper.""" + + def __init__(self, **kwargs: Any) -> None: + self.kwargs = kwargs + + +def clear_llm_env(monkeypatch: pytest.MonkeyPatch) -> None: + """Remove provider credentials installed by the autouse test fixture.""" + monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + + +def enable_fake_llm(monkeypatch: pytest.MonkeyPatch) -> None: + """Install the fake LLM constructor for provider-selection tests.""" + monkeypatch.setattr(llm_config, "LLM", FakeLLM) + + +def test_get_llm_returns_none_when_constructor_unavailable(monkeypatch: pytest.MonkeyPatch) -> None: + clear_llm_env(monkeypatch) + monkeypatch.setattr(llm_config, "LLM", None) + + assert llm_config.get_llm() is None + + +def test_get_llm_returns_none_without_provider_keys(monkeypatch: pytest.MonkeyPatch) -> None: + clear_llm_env(monkeypatch) + enable_fake_llm(monkeypatch) + + assert llm_config.get_llm() is None + + +def test_get_llm_prefers_anthropic_for_direct_models(monkeypatch: pytest.MonkeyPatch) -> None: + enable_fake_llm(monkeypatch) + monkeypatch.setenv("ANTHROPIC_API_KEY", "anthropic-key") + monkeypatch.setenv("OPENROUTER_API_KEY", "openrouter-key") + + result = llm_config.get_llm("claude-sonnet-4-20250514", temperature=0.2) + + assert isinstance(result, FakeLLM) + assert result.kwargs == { + "model": "claude-sonnet-4-20250514", + "api_key": "anthropic-key", + "temperature": 0.2, + } + + +def test_get_llm_can_force_openrouter_and_normalizes_model(monkeypatch: pytest.MonkeyPatch) -> None: + clear_llm_env(monkeypatch) + enable_fake_llm(monkeypatch) + monkeypatch.setenv("OPENROUTER_API_KEY", "openrouter-key") + + result = llm_config.get_llm( + "claude-sonnet-4-20250514", + temperature=0.4, + provider=LLMProvider.OPENROUTER, + ) + + assert isinstance(result, FakeLLM) + assert result.kwargs == { + "model": "openrouter/anthropic/claude-sonnet-4", + "api_key": "openrouter-key", + "base_url": "https://openrouter.ai/api/v1", + "temperature": 0.4, + } + + +def test_get_llm_can_force_anthropic(monkeypatch: pytest.MonkeyPatch) -> None: + clear_llm_env(monkeypatch) + enable_fake_llm(monkeypatch) + monkeypatch.setenv("ANTHROPIC_API_KEY", "anthropic-key") + + result = llm_config.get_llm("claude-haiku-4-5-20251001", temperature=0.1, provider=LLMProvider.ANTHROPIC) + + assert isinstance(result, FakeLLM) + assert result.kwargs == { + "model": "claude-haiku-4-5-20251001", + "api_key": "anthropic-key", + "temperature": 0.1, + } + + +def test_get_llm_preserves_openrouter_model_in_auto_detection(monkeypatch: pytest.MonkeyPatch) -> None: + enable_fake_llm(monkeypatch) + monkeypatch.setenv("ANTHROPIC_API_KEY", "anthropic-key") + monkeypatch.setenv("OPENROUTER_API_KEY", "openrouter-key") + + result = llm_config.get_llm("openrouter/anthropic/claude-haiku-4.5") + + assert isinstance(result, FakeLLM) + assert result.kwargs["model"] == "openrouter/anthropic/claude-haiku-4.5" + assert result.kwargs["api_key"] == "openrouter-key" + assert result.kwargs["base_url"] == "https://openrouter.ai/api/v1" + + +def test_forced_providers_require_matching_key(monkeypatch: pytest.MonkeyPatch) -> None: + clear_llm_env(monkeypatch) + enable_fake_llm(monkeypatch) + monkeypatch.setenv("ANTHROPIC_API_KEY", "anthropic-key") + + assert llm_config.get_llm(provider=LLMProvider.OPENROUTER) is None + + monkeypatch.delenv("ANTHROPIC_API_KEY") + monkeypatch.setenv("OPENROUTER_API_KEY", "openrouter-key") + + assert llm_config.get_llm(provider=LLMProvider.ANTHROPIC) is None + + +def test_get_llm_or_raise_reports_install_guidance(monkeypatch: pytest.MonkeyPatch) -> None: + clear_llm_env(monkeypatch) + enable_fake_llm(monkeypatch) + + with pytest.raises(ValueError, match="ANTHROPIC_API_KEY or OPENROUTER_API_KEY"): + llm_config.get_llm_or_raise() + + +def test_get_llm_or_raise_returns_configured_llm(monkeypatch: pytest.MonkeyPatch) -> None: + clear_llm_env(monkeypatch) + enable_fake_llm(monkeypatch) + monkeypatch.setenv("OPENROUTER_API_KEY", "openrouter-key") + + result = llm_config.get_llm_or_raise(provider=LLMProvider.OPENROUTER) + + assert isinstance(result, FakeLLM) + assert result.kwargs["api_key"] == "openrouter-key" + + +def test_task_helpers_use_declared_configs(monkeypatch: pytest.MonkeyPatch) -> None: + calls: list[tuple[str, float]] = [] + + def fake_get_llm(model: str, temperature: float, provider: LLMProvider | None = None) -> str: + calls.append((model, temperature)) + assert provider is None + return "llm" + + monkeypatch.setattr(llm_config, "get_llm", fake_get_llm) + + assert llm_config.get_llm_for_task("code") == "llm" + assert calls == [(llm_config.LLM_CONFIGS["code"].model, llm_config.LLM_CONFIGS["code"].temperature)] + assert llm_config.get_reasoning_llm() == "llm" + assert llm_config.get_creative_llm() == "llm" + assert llm_config.get_code_llm() == "llm" + + +def test_get_llm_for_task_rejects_unknown_task() -> None: + with pytest.raises(ValueError, match="Unknown task type: unknown"): + llm_config.get_llm_for_task("unknown") diff --git a/packages/agentic-fabric/tests/test_loader.py b/packages/agentic-fabric/tests/test_loader.py new file mode 100644 index 0000000..47f2627 --- /dev/null +++ b/packages/agentic-fabric/tests/test_loader.py @@ -0,0 +1,161 @@ +"""Tests for the loader module. + +Note: These tests require crewai to be installed. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest + + +if TYPE_CHECKING: + from pytest_agentic_fabric.mocking import FabricMocker + + +pytest.importorskip("crewai", reason="crewai not installed") +pytestmark = pytest.mark.crewai + + +class TestCreateAgentFromConfig: + """Tests for create_agent_from_config function.""" + + def test_creates_agent_with_config(self, fabric_mocker: FabricMocker) -> None: + """Test that create_agent_from_config creates an Agent.""" + from agentic_fabric.core.loader import create_agent_from_config + + config = { + "role": "Test Agent Role", + "goal": "Test goal", + "backstory": "Test backstory", + } + + MockAgent = fabric_mocker.patch("crewai.Agent") + fabric_mocker.patch_get_llm() + + create_agent_from_config("test_agent", config) + + MockAgent.assert_called_once() + call_kwargs = MockAgent.call_args[1] + assert call_kwargs["role"] == "Test Agent Role" + assert call_kwargs["goal"] == "Test goal" + assert call_kwargs["backstory"] == "Test backstory" + + def test_uses_agent_name_as_default_role(self, fabric_mocker: FabricMocker) -> None: + """Test that agent name is used as default role.""" + from agentic_fabric.core.loader import create_agent_from_config + + config = {"goal": "Test goal", "backstory": "Test backstory"} + + MockAgent = fabric_mocker.patch("crewai.Agent") + fabric_mocker.patch_get_llm() + + create_agent_from_config("custom_agent_name", config) + + call_kwargs = MockAgent.call_args[1] + assert call_kwargs["role"] == "custom_agent_name" + + +class TestCreateTaskFromConfig: + """Tests for create_task_from_config function.""" + + def test_creates_task_with_config(self, fabric_mocker: FabricMocker) -> None: + """Test that create_task_from_config creates a Task.""" + from agentic_fabric.core.loader import create_task_from_config + + config = { + "description": "Test task description", + "expected_output": "Test output", + } + mock_agent = fabric_mocker.MagicMock() + + MockTask = fabric_mocker.patch("crewai.Task") + + create_task_from_config("test_task", config, mock_agent) + + MockTask.assert_called_once() + call_kwargs = MockTask.call_args[1] + assert call_kwargs["description"] == "Test task description" + assert call_kwargs["expected_output"] == "Test output" + assert call_kwargs["agent"] == mock_agent + + +class TestLoadKnowledgeSources: + """Tests for load_knowledge_sources function.""" + + def test_loads_md_files(self, fabric_mocker: FabricMocker, tmp_path: Path) -> None: + """Test that .md files are loaded as knowledge sources.""" + from agentic_fabric.core.loader import load_knowledge_sources + + # Create test knowledge directory with .md file + knowledge_dir = tmp_path / "knowledge" + knowledge_dir.mkdir() + (knowledge_dir / "test.md").write_text("# Test Knowledge\nSome content") + + MockKnowledgeSource = fabric_mocker.patch_knowledge_source() + + load_knowledge_sources([knowledge_dir]) + + # Verify TextFileKnowledgeSource was called + assert MockKnowledgeSource.called + + def test_skips_nonexistent_paths(self, tmp_path: Path) -> None: + """Test that nonexistent paths are skipped.""" + from agentic_fabric.core.loader import load_knowledge_sources + + sources = load_knowledge_sources([tmp_path / "nonexistent"]) + + assert sources == [] + + +class TestLoadCrewFromConfig: + """Tests for load_fabric_agent_from_config function.""" + + def test_creates_crewai_crew_with_agents_and_tasks(self, fabric_mocker: FabricMocker) -> None: + """Test that load_fabric_agent_from_config creates a complete Crew.""" + from agentic_fabric.core.loader import load_fabric_agent_from_config + + config = { + "name": "test_fabric_agent", + "agents": { + "test_agent": { + "role": "Test Agent", + "goal": "Test goal", + "backstory": "Test backstory", + } + }, + "tasks": { + "test_task": { + "description": "Test description", + "expected_output": "Test output", + "agent": "test_agent", + } + }, + "knowledge_paths": [], + } + + MockCrew = fabric_mocker.patch_crewai_crew() + MockAgent = fabric_mocker.patch_crewai_agent() + MockTask = fabric_mocker.patch_crewai_task() + fabric_mocker.patch_crewai_process() + fabric_mocker.patch_get_llm() + + mock_agent = fabric_mocker.MagicMock() + MockAgent.return_value = mock_agent + + mock_task = fabric_mocker.MagicMock() + MockTask.return_value = mock_task + + load_fabric_agent_from_config(config) + + # Verify Agent was created + MockAgent.assert_called() + # Verify Task was created + MockTask.assert_called() + # Verify Crew was created with agents and tasks + MockCrew.assert_called_once() + call_kwargs = MockCrew.call_args[1] + assert len(call_kwargs["agents"]) == 1 + assert len(call_kwargs["tasks"]) == 1 diff --git a/packages/agentic-fabric/tests/test_loader_unit.py b/packages/agentic-fabric/tests/test_loader_unit.py new file mode 100644 index 0000000..040172c --- /dev/null +++ b/packages/agentic-fabric/tests/test_loader_unit.py @@ -0,0 +1,209 @@ +"""CrewAI loader tests that do not require the optional CrewAI package.""" + +from __future__ import annotations + +import importlib +import sys +import types + +from collections.abc import Generator +from pathlib import Path +from typing import Any + +import pytest + + +class FakeAgent: + """Capture CrewAI Agent constructor kwargs.""" + + def __init__(self, **kwargs: Any) -> None: + self.kwargs = kwargs + + +class FakeTask: + """Capture CrewAI Task constructor kwargs.""" + + def __init__(self, **kwargs: Any) -> None: + self.kwargs = kwargs + + +class FakeCrew: + """Capture CrewAI Crew constructor kwargs.""" + + def __init__(self, **kwargs: Any) -> None: + self.kwargs = kwargs + + +class FakeProcess: + """Small stand-in for CrewAI Process.""" + + sequential = "sequential" + + +class FakeKnowledgeSource: + """Capture knowledge source file paths.""" + + def __init__(self, file_paths: list[str]) -> None: + self.file_paths = file_paths + + +@pytest.fixture +def loader_with_fake_crewai(monkeypatch: pytest.MonkeyPatch) -> Generator[Any, None, None]: + """Import the loader with fake CrewAI modules installed.""" + fake_crewai = types.ModuleType("crewai") + fake_crewai.Agent = FakeAgent + fake_crewai.Task = FakeTask + fake_crewai.Crew = FakeCrew + fake_crewai.Process = FakeProcess + + fake_tools = types.ModuleType("crewai.tools") + + class BaseTool: + """Minimal stand-in for crewai.tools.BaseTool.""" + + fake_tools.BaseTool = BaseTool + + fake_knowledge = types.ModuleType("crewai.knowledge") + fake_knowledge.__path__ = [] + fake_source = types.ModuleType("crewai.knowledge.source") + fake_source.__path__ = [] + fake_text_source = types.ModuleType("crewai.knowledge.source.text_file_knowledge_source") + fake_text_source.TextFileKnowledgeSource = FakeKnowledgeSource + + for module_name, module in { + "crewai": fake_crewai, + "crewai.tools": fake_tools, + "crewai.knowledge": fake_knowledge, + "crewai.knowledge.source": fake_source, + "crewai.knowledge.source.text_file_knowledge_source": fake_text_source, + }.items(): + monkeypatch.setitem(sys.modules, module_name, module) + + sys.modules.pop("agentic_fabric.core.loader", None) + sys.modules.pop("agentic_fabric.tools.file_tools", None) + loader = importlib.import_module("agentic_fabric.core.loader") + + yield loader + + sys.modules.pop("agentic_fabric.core.loader", None) + sys.modules.pop("agentic_fabric.tools.file_tools", None) + + +def test_load_knowledge_sources_reads_supported_nonempty_files( + loader_with_fake_crewai: Any, + tmp_path: Path, +) -> None: + knowledge_dir = tmp_path / "knowledge" + knowledge_dir.mkdir() + note = knowledge_dir / "note.md" + note.write_text("# Useful context", encoding="utf-8") + (knowledge_dir / "empty.py").write_text(" ", encoding="utf-8") + (knowledge_dir / "ignored.txt").write_text("not supported", encoding="utf-8") + + sources = loader_with_fake_crewai.load_knowledge_sources([knowledge_dir, tmp_path / "missing"]) + + assert len(sources) == 1 + assert sources[0].file_paths == [str(note)] + + +def test_load_knowledge_sources_logs_unreadable_files( + loader_with_fake_crewai: Any, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + knowledge_dir = tmp_path / "knowledge" + knowledge_dir.mkdir() + bad_file = knowledge_dir / "bad.md" + bad_file.write_text("content", encoding="utf-8") + + def broken_content_check(path: Path) -> bool: + if path == bad_file: + raise OSError("boom") + return True + + monkeypatch.setattr(loader_with_fake_crewai, "_has_non_whitespace_content", broken_content_check) + + assert loader_with_fake_crewai.load_knowledge_sources([knowledge_dir]) == [] + assert "Could not load knowledge source" in caplog.text + + +def test_create_agent_and_task_from_config(loader_with_fake_crewai: Any, monkeypatch: pytest.MonkeyPatch) -> None: + sentinel_llm = object() + monkeypatch.setattr("agentic_fabric.config.llm.get_llm", lambda: sentinel_llm) + + agent = loader_with_fake_crewai.create_agent_from_config("writer", {"goal": "write"}, tools=["tool"]) + task = loader_with_fake_crewai.create_task_from_config("draft", {"description": "draft"}, agent) + + assert isinstance(agent, FakeAgent) + assert agent.kwargs["role"] == "writer" + assert agent.kwargs["goal"] == "write" + assert agent.kwargs["backstory"] == "" + assert agent.kwargs["llm"] is sentinel_llm + assert agent.kwargs["tools"] == ["tool"] + assert agent.kwargs["allow_delegation"] is False + assert task.kwargs["description"] == "draft" + assert task.kwargs["expected_output"] == "" + assert task.kwargs["agent"] is agent + + +def test_load_fabric_agent_from_config_resolves_tools_and_fallbacks( + loader_with_fake_crewai: Any, + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr("agentic_fabric.config.llm.get_llm", lambda: None) + monkeypatch.setattr(loader_with_fake_crewai, "load_knowledge_sources", lambda paths: ["knowledge"]) + monkeypatch.setattr( + loader_with_fake_crewai, + "resolve_tools", + lambda tool_names: ["resolved-tool"] if tool_names else [], + ) + + fabric_agent = loader_with_fake_crewai.load_fabric_agent_from_config( + { + "agents": { + "tool_user": {"role": "Tool User", "tools": ["registered"]}, + "engineer": {"role": "Engineer"}, + "researcher": {"role": "Researcher"}, + }, + "tasks": { + "tool_task": {"description": "use a tool", "agent": "tool_user"}, + "build_task": {"description": "build", "agent": "engineer"}, + "research_task": {"description": "read", "agent": "researcher"}, + }, + "knowledge_paths": [Path("knowledge")], + } + ) + + assert isinstance(fabric_agent, FakeCrew) + assert fabric_agent.kwargs["process"] == "sequential" + assert fabric_agent.kwargs["planning"] is True + assert fabric_agent.kwargs["memory"] is True + assert fabric_agent.kwargs["knowledge_sources"] == ["knowledge"] + + agents = fabric_agent.kwargs["agents"] + assert [agent.kwargs["role"] for agent in agents] == ["Tool User", "Engineer", "Researcher"] + assert agents[0].kwargs["tools"] == ["resolved-tool"] + assert len(agents[1].kwargs["tools"]) == 3 + assert len(agents[2].kwargs["tools"]) == 2 + assert len(fabric_agent.kwargs["tasks"]) == 3 + + +def test_load_fabric_agent_from_config_requires_task_agent(loader_with_fake_crewai: Any) -> None: + with pytest.raises(ValueError, match="missing an 'agent' assignment"): + loader_with_fake_crewai.load_fabric_agent_from_config( + { + "agents": {"writer": {"role": "Writer"}}, + "tasks": {"draft": {"description": "draft"}}, + } + ) + + +def test_load_fabric_agent_from_config_requires_existing_agent(loader_with_fake_crewai: Any) -> None: + with pytest.raises(ValueError, match="Agent 'missing' for task 'draft' not found"): + loader_with_fake_crewai.load_fabric_agent_from_config( + { + "agents": {"writer": {"role": "Writer"}}, + "tasks": {"draft": {"description": "draft", "agent": "missing"}}, + } + ) diff --git a/packages/agentic-fabric/tests/test_manager.py b/packages/agentic-fabric/tests/test_manager.py new file mode 100644 index 0000000..7c31f79 --- /dev/null +++ b/packages/agentic-fabric/tests/test_manager.py @@ -0,0 +1,408 @@ +"""Tests for the ManagerAgent class.""" + +from __future__ import annotations + +import asyncio + +from pathlib import Path +from unittest.mock import patch + +import pytest + +from agentic_fabric.core.manager import ManagerAgent + + +class TestManagerAgent: + """Tests for ManagerAgent base class.""" + + def test_init_with_fabric_agents(self): + """Test manager initialization with fabric agent mappings.""" + fabric_agents = {"design": "game_design", "qa": "quality_assurance"} + manager = ManagerAgent(fabric_agents=fabric_agents) + + assert manager.fabric_agents == fabric_agents + assert manager.package_name is None + assert manager.workspace_root is None + + def test_init_with_package_name(self): + """Test manager initialization with package name.""" + fabric_agents = {"design": "game_design"} + manager = ManagerAgent(fabric_agents=fabric_agents, package_name="my_package") + + assert manager.package_name == "my_package" + + def test_get_packages_caches_result(self): + """Test that package discovery is cached.""" + manager = ManagerAgent(fabric_agents={"test": "test_fabric_agent"}) + + with patch("agentic_fabric.core.manager.discover_packages") as mock_discover: + mock_packages = {"pkg1": Path("/path/pkg1")} + mock_discover.return_value = mock_packages + + # First call + result1 = manager._get_packages() + assert result1 == mock_packages + assert mock_discover.call_count == 1 + + # Second call should use cache + result2 = manager._get_packages() + assert result2 == mock_packages + assert mock_discover.call_count == 1 # Not called again + + def test_delegate_with_string_input(self): + """Test delegation with string input.""" + manager = ManagerAgent( + fabric_agents={"design": "game_design"}, + package_name="test_pkg", + ) + + mock_packages = {"test_pkg": Path("/test/.fabric")} + mock_config = { + "name": "game_design", + "agents": {}, + "tasks": {}, + } + + with ( + patch("agentic_fabric.core.manager.discover_packages") as mock_discover, + patch("agentic_fabric.core.manager.get_fabric_agent_config") as mock_get_config, + patch("agentic_fabric.core.manager.run_fabric_agent_auto") as mock_run, + ): + mock_discover.return_value = mock_packages + mock_get_config.return_value = mock_config + mock_run.return_value = "Design complete" + + result = manager.delegate("design", "Create a game design") + + assert result == "Design complete" + # Verify string was converted to dict + mock_run.assert_called_once() + call_args = mock_run.call_args + assert call_args[1]["inputs"] == {"task": "Create a game design"} + + def test_delegate_with_dict_input(self): + """Test delegation with dict input.""" + manager = ManagerAgent( + fabric_agents={"design": "game_design"}, + package_name="test_pkg", + ) + + mock_packages = {"test_pkg": Path("/test/.fabric")} + mock_config = {"name": "game_design", "agents": {}, "tasks": {}} + + with ( + patch("agentic_fabric.core.manager.discover_packages") as mock_discover, + patch("agentic_fabric.core.manager.get_fabric_agent_config") as mock_get_config, + patch("agentic_fabric.core.manager.run_fabric_agent_auto") as mock_run, + ): + mock_discover.return_value = mock_packages + mock_get_config.return_value = mock_config + mock_run.return_value = "Design complete" + + inputs = {"task": "Create design", "theme": "fantasy"} + result = manager.delegate("design", inputs) + + assert result == "Design complete" + mock_run.assert_called_once() + call_args = mock_run.call_args + assert call_args[1]["inputs"] == inputs + + def test_delegate_unknown_role_raises_error(self): + """Test that delegating to unknown role raises ValueError.""" + manager = ManagerAgent(fabric_agents={"design": "game_design"}) + + with pytest.raises(ValueError, match="Unknown fabric agent role 'unknown'"): + manager.delegate("unknown", "test task") + + def test_delegate_package_not_found_raises_error(self): + """Test that missing package raises ValueError.""" + manager = ManagerAgent( + fabric_agents={"design": "game_design"}, + package_name="nonexistent", + ) + + with patch("agentic_fabric.core.manager.discover_packages") as mock_discover: + mock_discover.return_value = {"other_pkg": Path("/other")} + + with pytest.raises(ValueError, match="Package 'nonexistent' not found"): + manager.delegate("design", "test task") + + def test_delegate_auto_discovers_fabric_agent(self): + """Test auto-discovery of a fabric agent when package_name is not specified.""" + manager = ManagerAgent(fabric_agents={"design": "game_design"}) + + mock_packages = { + "pkg1": Path("/pkg1/.fabric"), + "pkg2": Path("/pkg2/.fabric"), + } + mock_config = {"name": "game_design", "agents": {}, "tasks": {}} + + with ( + patch("agentic_fabric.core.manager.discover_packages") as mock_discover, + patch("agentic_fabric.core.manager.get_fabric_agent_config") as mock_get_config, + patch("agentic_fabric.core.manager.run_fabric_agent_auto") as mock_run, + ): + mock_discover.return_value = mock_packages + + # First package doesn't have the fabric agent. + def get_config_side_effect(pkg_dir, fabric_agent_name): + if pkg_dir == Path("/pkg1/.fabric"): + raise ValueError("Fabric agent not found") + return mock_config + + mock_get_config.side_effect = get_config_side_effect + mock_run.return_value = "Success" + + result = manager.delegate("design", "test task") + + assert result == "Success" + # Should have tried pkg1 (failed), then found in pkg2 (config is cached) + assert mock_get_config.call_count == 2 + + def test_delegate_uses_cached_fabric_agent_config(self): + """Repeated delegation to the same fabric agent should reuse the cached config.""" + manager = ManagerAgent(fabric_agents={"design": "game_design"}, package_name="test_pkg") + mock_config = {"name": "game_design", "agents": {}, "tasks": {}} + + with ( + patch("agentic_fabric.core.manager.discover_packages", return_value={"test_pkg": Path("/test/.fabric")}), + patch("agentic_fabric.core.manager.get_fabric_agent_config", return_value=mock_config) as mock_get_config, + patch("agentic_fabric.core.manager.run_fabric_agent_auto", side_effect=["first", "second"]) as mock_run, + ): + assert manager.delegate("design", "first task", framework="crewai") == "first" + assert manager.delegate("design", {"task": "second task"}, framework="langgraph") == "second" + + mock_get_config.assert_called_once_with(Path("/test/.fabric"), "game_design") + assert mock_run.call_args_list[1].kwargs == {"inputs": {"task": "second task"}, "framework": "langgraph"} + + def test_delegate_fabric_agent_not_found_raises_error(self): + """Test that fabric agent not found in any package raises ValueError.""" + manager = ManagerAgent(fabric_agents={"design": "missing_fabric_agent"}) + + mock_packages = {"pkg1": Path("/pkg1/.fabric")} + + with ( + patch("agentic_fabric.core.manager.discover_packages") as mock_discover, + patch("agentic_fabric.core.manager.get_fabric_agent_config") as mock_get_config, + ): + mock_discover.return_value = mock_packages + mock_get_config.side_effect = ValueError("Fabric agent not found") + + with pytest.raises(ValueError, match="Fabric agent 'missing_fabric_agent' not found"): + manager.delegate("design", "test task") + + @pytest.mark.asyncio + async def test_delegate_async(self): + """Test async delegation.""" + manager = ManagerAgent( + fabric_agents={"design": "game_design"}, + package_name="test_pkg", + ) + + mock_packages = {"test_pkg": Path("/test/.fabric")} + mock_config = {"name": "game_design", "agents": {}, "tasks": {}} + + with ( + patch("agentic_fabric.core.manager.discover_packages") as mock_discover, + patch("agentic_fabric.core.manager.get_fabric_agent_config") as mock_get_config, + patch("agentic_fabric.core.manager.run_fabric_agent_auto") as mock_run, + ): + mock_discover.return_value = mock_packages + mock_get_config.return_value = mock_config + mock_run.return_value = "Async result" + + result = await manager.delegate_async("design", "test task") + + assert result == "Async result" + + @pytest.mark.asyncio + async def test_delegate_parallel(self): + """Test parallel delegation to multiple fabric agents.""" + manager = ManagerAgent( + fabric_agents={"design": "game_design", "assets": "asset_gen"}, + package_name="test_pkg", + ) + + mock_packages = {"test_pkg": Path("/test/.fabric")} + + with ( + patch("agentic_fabric.core.manager.discover_packages") as mock_discover, + patch("agentic_fabric.core.manager.get_fabric_agent_config") as mock_get_config, + patch("agentic_fabric.core.manager.run_fabric_agent_auto") as mock_run, + ): + mock_discover.return_value = mock_packages + + # Return different configs for different fabric agents to distinguish them. + def get_config_side_effect(pkg_dir, fabric_agent_name): + return {"name": fabric_agent_name, "agents": {}, "tasks": {}} + + mock_get_config.side_effect = get_config_side_effect + + # Mock different results based on fabric agent name in config. + def run_side_effect(config, inputs, framework=None): + fabric_agent_name = config["name"] + if fabric_agent_name == "game_design": + return "Design done" + elif fabric_agent_name == "asset_gen": + return "Assets done" + return f"Unknown fabric agent: {fabric_agent_name}" + + mock_run.side_effect = run_side_effect + + results = await manager.delegate_parallel( + [ + ("design", "Create design"), + ("assets", "Generate assets"), + ] + ) + + # Verify correct results returned (order matches input order) + assert results == ["Design done", "Assets done"] + # Both should have been executed + assert mock_run.call_count == 2 + + def test_delegate_sequential(self): + """Test sequential delegation to multiple fabric agents.""" + manager = ManagerAgent( + fabric_agents={"design": "game_design", "impl": "implementation"}, + package_name="test_pkg", + ) + + mock_packages = {"test_pkg": Path("/test/.fabric")} + mock_config = {"name": "test", "agents": {}, "tasks": {}} + + with ( + patch("agentic_fabric.core.manager.discover_packages") as mock_discover, + patch("agentic_fabric.core.manager.get_fabric_agent_config") as mock_get_config, + patch("agentic_fabric.core.manager.run_fabric_agent_auto") as mock_run, + ): + mock_discover.return_value = mock_packages + mock_get_config.return_value = mock_config + mock_run.side_effect = ["Design result", "Implementation result"] + + results = manager.delegate_sequential( + [ + ("design", "Create design"), + ("impl", "Implement design"), + ] + ) + + assert results == ["Design result", "Implementation result"] + assert mock_run.call_count == 2 + # Verify they were called in order + calls = mock_run.call_args_list + assert calls[0][1]["inputs"]["task"] == "Create design" + assert calls[1][1]["inputs"]["task"] == "Implement design" + + def test_checkpoint_auto_approve(self): + """Test checkpoint with auto_approve=True.""" + manager = ManagerAgent(fabric_agents={"test": "test_fabric_agent"}) + + approved, result = manager.checkpoint( + "Review design", + "Design output", + auto_approve=True, + ) + + assert approved is True + assert result == "Design output" + + def test_checkpoint_base_implementation_auto_approves(self): + """Test that base checkpoint implementation auto-approves.""" + manager = ManagerAgent(fabric_agents={"test": "test_fabric_agent"}) + + approved, result = manager.checkpoint( + "Review design", + "Design output", + auto_approve=False, # Even with False, base impl approves + ) + + assert approved is True + assert result == "Design output" + + def test_execute_workflow_not_implemented(self): + """Test that execute_workflow raises NotImplementedError in base class.""" + manager = ManagerAgent(fabric_agents={"test": "test_fabric_agent"}) + + with pytest.raises(NotImplementedError, match="Subclasses must implement"): + asyncio.run(manager.execute_workflow("test task")) + + +class TestManagerAgentSubclass: + """Tests for ManagerAgent subclass implementation.""" + + @pytest.mark.asyncio + async def test_custom_workflow_implementation(self): + """Test a custom manager implementation.""" + + class TestManager(ManagerAgent): + async def execute_workflow(self, task: str, **kwargs): + # Simple sequential workflow + design = await self.delegate_async("design", task) + return await self.delegate_async("impl", design) + + manager = TestManager( + fabric_agents={"design": "game_design", "impl": "implementation"}, + package_name="test_pkg", + ) + + mock_packages = {"test_pkg": Path("/test/.fabric")} + mock_config = {"name": "test", "agents": {}, "tasks": {}} + + with ( + patch("agentic_fabric.core.manager.discover_packages") as mock_discover, + patch("agentic_fabric.core.manager.get_fabric_agent_config") as mock_get_config, + patch("agentic_fabric.core.manager.run_fabric_agent_auto") as mock_run, + ): + mock_discover.return_value = mock_packages + mock_get_config.return_value = mock_config + mock_run.side_effect = ["Design done", "Implementation done"] + + result = await manager.execute_workflow("Build a game") + + assert result == "Implementation done" + assert mock_run.call_count == 2 + + @pytest.mark.asyncio + async def test_custom_workflow_with_parallel_execution(self): + """Test a custom manager with parallel execution.""" + + class ParallelManager(ManagerAgent): + async def execute_workflow(self, task: str, **kwargs): + # Parallel execution + results = await self.delegate_parallel( + [ + ("design", task), + ("assets", task), + ] + ) + # Then sequential QA + return await self.delegate_async( + "qa", + { + "design": results[0], + "assets": results[1], + }, + ) + + manager = ParallelManager( + fabric_agents={"design": "design", "assets": "assets", "qa": "qa"}, + package_name="test_pkg", + ) + + mock_packages = {"test_pkg": Path("/test/.fabric")} + mock_config = {"name": "test", "agents": {}, "tasks": {}} + + with ( + patch("agentic_fabric.core.manager.discover_packages") as mock_discover, + patch("agentic_fabric.core.manager.get_fabric_agent_config") as mock_get_config, + patch("agentic_fabric.core.manager.run_fabric_agent_auto") as mock_run, + ): + mock_discover.return_value = mock_packages + mock_get_config.return_value = mock_config + mock_run.side_effect = ["Design done", "Assets done", "QA passed"] + + result = await manager.execute_workflow("Build game") + + assert result == "QA passed" + assert mock_run.call_count == 3 diff --git a/packages/agentic-fabric/tests/test_mcp_adapters.py b/packages/agentic-fabric/tests/test_mcp_adapters.py new file mode 100644 index 0000000..82b7b61 --- /dev/null +++ b/packages/agentic-fabric/tests/test_mcp_adapters.py @@ -0,0 +1,508 @@ +"""Tests for MCP transport adapters.""" + +from __future__ import annotations + +import asyncio +import builtins +import sys +import types + +from dataclasses import dataclass +from typing import Any + +import pytest + +from agentic_fabric.tools import meshy_mcp, vendor_mcp + + +@dataclass +class FakeTextContent: + type: str + text: str + + +class FakeTool: + def __init__(self, name: str, description: str, inputSchema: dict[str, Any]) -> None: + self.name = name + self.description = description + self.inputSchema = inputSchema + + +class FakeServer: + """Minimal MCP server stand-in that records registered handlers.""" + + def __init__(self, name: str) -> None: + self.name = name + self.list_tools_handler: Any | None = None + self.call_tool_handler: Any | None = None + + def list_tools(self) -> Any: + def decorator(func: Any) -> Any: + self.list_tools_handler = func + return func + + return decorator + + def call_tool(self) -> Any: + def decorator(func: Any) -> Any: + self.call_tool_handler = func + return func + + return decorator + + +def install_fake_mcp(monkeypatch: pytest.MonkeyPatch) -> None: + """Install fake MCP modules for adapter tests.""" + mcp_module = types.ModuleType("mcp") + server_module = types.ModuleType("mcp.server") + types_module = types.ModuleType("mcp.types") + server_module.Server = FakeServer + types_module.Tool = FakeTool + types_module.TextContent = FakeTextContent + + monkeypatch.setitem(sys.modules, "mcp", mcp_module) + monkeypatch.setitem(sys.modules, "mcp.server", server_module) + monkeypatch.setitem(sys.modules, "mcp.types", types_module) + + +def install_fake_stdio(monkeypatch: pytest.MonkeyPatch) -> list[tuple[Any, Any, Any]]: + """Install a fake MCP stdio server and return recorded run calls.""" + calls: list[tuple[Any, Any, Any]] = [] + + class FakeStdio: + async def __aenter__(self) -> tuple[str, str]: + return ("read", "write") + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + traceback: types.TracebackType | None, + ) -> bool: + return False + + stdio_module = types.ModuleType("mcp.server.stdio") + stdio_module.stdio_server = FakeStdio + monkeypatch.setitem(sys.modules, "mcp.server.stdio", stdio_module) + return calls + + +class RunnableServer: + def __init__(self, calls: list[tuple[Any, Any, Any]]) -> None: + self.calls = calls + + def create_initialization_options(self) -> dict[str, bool]: + return {"ready": True} + + async def run(self, read_stream: Any, write_stream: Any, options: Any) -> None: + self.calls.append((read_stream, write_stream, options)) + + +def install_fake_extended_data(monkeypatch: pytest.MonkeyPatch) -> None: + """Install fake extended-data helpers to cover optional success branches.""" + extended_data_module = types.ModuleType("extended_data") + containers_module = types.ModuleType("extended_data.containers") + redaction_module = types.ModuleType("extended_data.primitives.redaction") + io_module = types.ModuleType("extended_data.io") + + containers_module.to_builtin = lambda value: {"converted": value} + redaction_module.redact_sensitive_data = lambda value: {"redacted": value} + redaction_module.redact_sensitive_text = lambda value, *, values=None: f"redacted:{value}" + io_module.wrap_raw_data_for_export = lambda payload, **kwargs: f"wrapped:{payload}:{sorted(kwargs)}" + + monkeypatch.setitem(sys.modules, "extended_data", extended_data_module) + monkeypatch.setitem(sys.modules, "extended_data.containers", containers_module) + monkeypatch.setitem(sys.modules, "extended_data.primitives.redaction", redaction_module) + monkeypatch.setitem(sys.modules, "extended_data.io", io_module) + + +def reject_imports(monkeypatch: pytest.MonkeyPatch, prefix: str) -> None: + """Reject imports under a package prefix.""" + real_import = builtins.__import__ + + def fake_import(name: str, *args: Any, **kwargs: Any) -> Any: + if name == prefix or name.startswith(f"{prefix}."): + raise ImportError(f"missing {prefix}") + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", fake_import) + + +def test_meshy_mcp_reports_missing_mcp(monkeypatch: pytest.MonkeyPatch) -> None: + reject_imports(monkeypatch, "mcp") + + with pytest.raises(ImportError, match=r"agentic-fabric\[mcp\]"): + meshy_mcp.create_server() + + +def test_meshy_mcp_create_tools_reports_missing_mcp_types(monkeypatch: pytest.MonkeyPatch) -> None: + reject_imports(monkeypatch, "mcp.types") + + with pytest.raises(ImportError, match=r"agentic-fabric\[mcp\]"): + meshy_mcp._create_mcp_tools() + + +def test_meshy_mcp_reports_missing_vendor_fabric(monkeypatch: pytest.MonkeyPatch) -> None: + install_fake_mcp(monkeypatch) + reject_imports(monkeypatch, "vendor_fabric") + + with pytest.raises(ImportError, match=r"vendor-fabric\[meshy\]"): + meshy_mcp.create_server() + + +def test_meshy_mcp_preserves_provider_import_error(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setitem(sys.modules, "vendor_fabric", types.ModuleType("vendor_fabric")) + monkeypatch.setitem(sys.modules, "vendor_fabric.meshy", types.ModuleType("vendor_fabric.meshy")) + + with pytest.raises(ImportError) as exc_info: + meshy_mcp._require_meshy_tool_definitions() + + assert "vendor-fabric[meshy] is required" in str(exc_info.value) + assert "Original import error:" in str(exc_info.value) + + +def test_meshy_mcp_exposes_vendor_tool_definitions(monkeypatch: pytest.MonkeyPatch) -> None: + install_fake_mcp(monkeypatch) + + class FakeSchema: + @staticmethod + def model_json_schema() -> dict[str, Any]: + return {"properties": {"prompt": {"type": "string"}}, "required": ["prompt"]} + + def generate(prompt: str) -> dict[str, str]: + return {"ok": prompt} + + vendor_module = types.ModuleType("vendor_fabric") + meshy_module = types.ModuleType("vendor_fabric.meshy") + tools_module = types.ModuleType("vendor_fabric.meshy.tools") + tools_module.TOOL_DEFINITIONS = [ + { + "name": "text3d_generate", + "description": "Generate a model.", + "schema": FakeSchema, + "func": generate, + } + ] + monkeypatch.setitem(sys.modules, "vendor_fabric", vendor_module) + monkeypatch.setitem(sys.modules, "vendor_fabric.meshy", meshy_module) + monkeypatch.setitem(sys.modules, "vendor_fabric.meshy.tools", tools_module) + + server = meshy_mcp.create_server() + listed = asyncio.run(server.list_tools_handler()) + result = asyncio.run(server.call_tool_handler("text3d_generate", {"prompt": "cube"})) + unknown = asyncio.run(server.call_tool_handler("missing", None)) + + assert server.name == "meshy-ai" + assert [(tool.name, tool.description, tool.inputSchema) for tool in listed] == [ + ( + "text3d_generate", + "Generate a model.", + {"type": "object", "properties": {"prompt": {"type": "string"}}, "required": ["prompt"]}, + ) + ] + assert "cube" in result[0].text + assert "Unknown tool: missing" in unknown[0].text + + +def test_meshy_mcp_handles_tool_errors(monkeypatch: pytest.MonkeyPatch) -> None: + install_fake_mcp(monkeypatch) + + def explode(prompt: str) -> dict[str, str]: + raise RuntimeError(f"failed {prompt}") + + tools_module = types.ModuleType("vendor_fabric.meshy.tools") + tools_module.TOOL_DEFINITIONS = [{"name": "explode", "schema": None, "func": explode}] + monkeypatch.setitem(sys.modules, "vendor_fabric", types.ModuleType("vendor_fabric")) + monkeypatch.setitem(sys.modules, "vendor_fabric.meshy", types.ModuleType("vendor_fabric.meshy")) + monkeypatch.setitem(sys.modules, "vendor_fabric.meshy.tools", tools_module) + + server = meshy_mcp.create_server() + result = asyncio.run(server.call_tool_handler("explode", {"prompt": "cube"})) + + assert "failed cube" in result[0].text + + +def test_meshy_mcp_awaits_async_provider_capabilities(monkeypatch: pytest.MonkeyPatch) -> None: + install_fake_mcp(monkeypatch) + + async def generate(prompt: str) -> dict[str, str]: + return {"async": prompt} + + tools_module = types.ModuleType("vendor_fabric.meshy.tools") + tools_module.TOOL_DEFINITIONS = [{"name": "async_generate", "schema": None, "func": generate}] + monkeypatch.setitem(sys.modules, "vendor_fabric", types.ModuleType("vendor_fabric")) + monkeypatch.setitem(sys.modules, "vendor_fabric.meshy", types.ModuleType("vendor_fabric.meshy")) + monkeypatch.setitem(sys.modules, "vendor_fabric.meshy.tools", tools_module) + + server = meshy_mcp.create_server() + result = asyncio.run(server.call_tool_handler("async_generate", {"prompt": "cube"})) + + assert "cube" in result[0].text + assert "coroutine" not in result[0].text + + +def test_meshy_mcp_helper_fallbacks(monkeypatch: pytest.MonkeyPatch) -> None: + reject_imports(monkeypatch, "extended_data") + + class Dumpable: + def model_dump(self) -> dict[str, bool]: + return {"dumped": True} + + assert meshy_mcp._schema_for_definition({}) == {"type": "object", "properties": {}, "required": []} + assert meshy_mcp._schema_for_definition({"schema": object()}) == { + "type": "object", + "properties": {}, + "required": [], + } + assert str(meshy_mcp._install_error("install it", ImportError())) == "install it" + assert meshy_mcp._to_builtin(Dumpable()) == {"dumped": True} + assert meshy_mcp._to_builtin([Dumpable()]) == [{"dumped": True}] + assert meshy_mcp._jsonable_tool_result(Dumpable()) == {"dumped": True} + assert meshy_mcp._jsonable_tool_result([Dumpable()]) == [{"dumped": True}] + assert sorted(meshy_mcp._jsonable_tool_result({2, 1})) == [1, 2] + + monkeypatch.setattr(meshy_mcp, "_to_builtin", lambda value: {2, 1} if value == "converted-set" else value) + assert sorted(meshy_mcp._jsonable_tool_result("converted-set")) == [1, 2] + + +def test_meshy_mcp_uses_extended_data_helpers(monkeypatch: pytest.MonkeyPatch) -> None: + install_fake_extended_data(monkeypatch) + + assert meshy_mcp._to_builtin({"value": 1}) == {"converted": {"value": 1}} + assert meshy_mcp._redact_sensitive_data({"token": "secret"}) == {"redacted": {"token": "secret"}} + assert meshy_mcp._redact_sensitive_text("secret") == "redacted:secret" + assert meshy_mcp._tool_payload_text({"ok": True}).startswith("wrapped:") + + +def test_meshy_mcp_run_server_and_main(monkeypatch: pytest.MonkeyPatch) -> None: + install_fake_mcp(monkeypatch) + calls = install_fake_stdio(monkeypatch) + server = RunnableServer(calls) + + meshy_mcp.run_server(server) + + assert calls == [("read", "write", {"ready": True})] + + calls.clear() + monkeypatch.setattr(meshy_mcp, "create_server", lambda: RunnableServer(calls)) + + meshy_mcp.run_server() + + assert calls == [("read", "write", {"ready": True})] + + called: list[bool] = [] + monkeypatch.setattr(meshy_mcp, "run_server", lambda: called.append(True)) + + meshy_mcp.main() + + assert called == [True] + + +def test_meshy_mcp_run_server_reports_missing_stdio(monkeypatch: pytest.MonkeyPatch) -> None: + reject_imports(monkeypatch, "mcp.server.stdio") + + with pytest.raises(ImportError, match=r"agentic-fabric\[mcp\]"): + meshy_mcp.run_server(RunnableServer([])) + + +def install_fake_vendor_fabric(monkeypatch: pytest.MonkeyPatch) -> None: + """Install a fake vendor-fabric registry and data surface.""" + + class DemoConnector: + def search(self, query: str, limit: int = 5) -> dict[str, Any]: + """Search demo records. + + query: Search text. + limit: Maximum results. + """ + return {"query": query, "limit": limit} + + def status(self) -> dict[str, str]: + return {"status": "ok"} + + async def async_lookup(self) -> dict[str, bool]: + return {"async": True} + + def fail(self) -> dict[str, bool]: + raise RuntimeError("connector failed") + + async def list_available_connectors() -> list[str]: + return ["demo"] + + def list_connector_categories(*, include_unavailable: bool = True) -> list[str]: + raise RuntimeError("category failed") + + registry = types.SimpleNamespace( + _list_connector_classes=lambda: {"demo": DemoConnector}, + get_connector=lambda name: DemoConnector(), + list_connectors=lambda *, include_unavailable=True: ["demo"], + list_available_connectors=list_available_connectors, + list_connector_info=lambda *, include_unavailable=True: [{"name": "demo", "available": True}], + get_connector_info=lambda name, *, include_unavailable=True: {"name": name, "available": True}, + list_connector_categories=list_connector_categories, + list_connector_capabilities=lambda *, include_unavailable=True: ["search"], + list_connectors_by_category=lambda category, *, include_unavailable=True: [{"name": "demo", "category": category}], + list_connectors_by_capability=lambda capability, *, include_unavailable=True: [ + {"name": "demo", "capability": capability} + ], + ) + + vendor_module = types.ModuleType("vendor_fabric") + vendor_module.registry = registry + surface_module = types.ModuleType("vendor_fabric.surface") + surface_module.connector_data_methods = lambda connector_class: [ + ("close", connector_class.search), + ("search", connector_class.search), + ("status", connector_class.status), + ("async_lookup", connector_class.async_lookup), + ("fail", connector_class.fail), + ] + + monkeypatch.setitem(sys.modules, "vendor_fabric", vendor_module) + monkeypatch.setitem(sys.modules, "vendor_fabric.surface", surface_module) + + +def test_vendor_mcp_reports_missing_mcp(monkeypatch: pytest.MonkeyPatch) -> None: + reject_imports(monkeypatch, "mcp") + + with pytest.raises(ImportError, match=r"agentic-fabric\[mcp\]"): + vendor_mcp.create_server() + + +def test_vendor_mcp_reports_missing_vendor_fabric(monkeypatch: pytest.MonkeyPatch) -> None: + install_fake_mcp(monkeypatch) + reject_imports(monkeypatch, "vendor_fabric") + + with pytest.raises(ImportError, match="vendor-fabric is required"): + vendor_mcp.create_server() + + +def test_vendor_mcp_preserves_provider_import_error(monkeypatch: pytest.MonkeyPatch) -> None: + vendor_module = types.ModuleType("vendor_fabric") + vendor_module.registry = types.SimpleNamespace() + monkeypatch.setitem(sys.modules, "vendor_fabric", vendor_module) + + with pytest.raises(ImportError) as exc_info: + vendor_mcp._require_vendor_fabric() + + assert "vendor-fabric is required" in str(exc_info.value) + assert "Original import error:" in str(exc_info.value) + + +def test_vendor_mcp_exposes_catalog_and_connector_tools(monkeypatch: pytest.MonkeyPatch) -> None: + install_fake_mcp(monkeypatch) + install_fake_vendor_fabric(monkeypatch) + + server = vendor_mcp.create_server() + listed = asyncio.run(server.list_tools_handler()) + names = {tool.name for tool in listed} + catalog_result = asyncio.run(server.call_tool_handler("fabric_vendors_list", None)) + available_result = asyncio.run(server.call_tool_handler("fabric_vendors_list_available", {})) + category_error = asyncio.run(server.call_tool_handler("fabric_vendors_list_categories", {})) + connector_result = asyncio.run(server.call_tool_handler("demo_search", {"query": "alpha", "limit": 2})) + status_result = asyncio.run(server.call_tool_handler("demo_status", {})) + async_result = asyncio.run(server.call_tool_handler("demo_async_lookup", {})) + connector_error = asyncio.run(server.call_tool_handler("demo_fail", {})) + unknown = asyncio.run(server.call_tool_handler("missing", {})) + + assert server.name == "vendor-fabric" + assert "fabric_vendors_list" in names + assert "demo_search" in names + assert "demo" in catalog_result[0].text + assert "demo" in available_result[0].text + assert "category failed" in category_error[0].text + assert "alpha" in connector_result[0].text + assert "ok" in status_result[0].text + assert "async" in async_result[0].text + assert "connector failed" in connector_error[0].text + assert "Unknown tool: missing" in unknown[0].text + + +def test_vendor_mcp_helper_branches(monkeypatch: pytest.MonkeyPatch) -> None: + reject_imports(monkeypatch, "extended_data") + + class Dumpable: + def model_dump(self) -> dict[str, bool]: + return {"dumped": True} + + def typed( + self: object, + count: int, + ratio: float, + enabled: bool, + items: list[str], + payload: dict[str, Any], + optional: str = "value", + ) -> dict[str, Any]: + """Typed method. + + count: Count value. + """ + return {} + + def unresolved(value: MissingType) -> dict[str, Any]: # noqa: F821 + return {} + + schema = vendor_mcp._get_method_schema(typed) + unresolved_schema = vendor_mcp._get_method_schema(unresolved) + + assert schema["properties"]["count"]["type"] == "integer" + assert schema["properties"]["ratio"]["type"] == "number" + assert schema["properties"]["enabled"]["type"] == "boolean" + assert schema["properties"]["items"]["type"] == "array" + assert schema["properties"]["payload"]["type"] == "object" + assert schema["properties"]["optional"]["default"] == "value" + assert schema["properties"]["count"]["description"] == "Count value." + assert unresolved_schema["properties"]["value"]["type"] == "string" + assert vendor_mcp._connector_classes(types.SimpleNamespace()) == {} + assert str(vendor_mcp._install_error("install it", ImportError())) == "install it" + assert vendor_mcp._to_builtin(Dumpable()) == {"dumped": True} + assert vendor_mcp._to_builtin([Dumpable()]) == [{"dumped": True}] + assert vendor_mcp._jsonable_tool_result(Dumpable()) == {"dumped": True} + assert vendor_mcp._jsonable_tool_result([Dumpable()]) == [{"dumped": True}] + assert sorted(vendor_mcp._jsonable_tool_result({2, 1})) == [1, 2] + + monkeypatch.setattr(vendor_mcp, "_to_builtin", lambda value: {2, 1} if value == "converted-set" else value) + assert sorted(vendor_mcp._jsonable_tool_result("converted-set")) == [1, 2] + + +def test_vendor_mcp_uses_extended_data_helpers(monkeypatch: pytest.MonkeyPatch) -> None: + install_fake_extended_data(monkeypatch) + + assert vendor_mcp._to_builtin({"value": 1}) == {"converted": {"value": 1}} + assert vendor_mcp._redact_sensitive_data({"token": "secret"}) == {"redacted": {"token": "secret"}} + assert vendor_mcp._tool_error_text(RuntimeError("secret")) == "Error: RuntimeError: redacted:secret" + assert vendor_mcp._unknown_tool_text("secret") == "Unknown tool: redacted:secret" + assert vendor_mcp._tool_result_text({"ok": True}).startswith("wrapped:") + + +def test_vendor_mcp_run_server_and_main(monkeypatch: pytest.MonkeyPatch) -> None: + install_fake_mcp(monkeypatch) + install_fake_vendor_fabric(monkeypatch) + calls = install_fake_stdio(monkeypatch) + server = RunnableServer(calls) + + vendor_mcp.run_server(server) + + assert calls == [("read", "write", {"ready": True})] + + calls.clear() + monkeypatch.setattr(vendor_mcp, "create_server", lambda: RunnableServer(calls)) + + vendor_mcp.run_server() + + assert calls == [("read", "write", {"ready": True})] + + called: list[bool] = [] + monkeypatch.setattr(vendor_mcp, "run_server", lambda: called.append(True)) + + vendor_mcp.main() + + assert called == [True] + + +def test_vendor_mcp_run_server_reports_missing_stdio(monkeypatch: pytest.MonkeyPatch) -> None: + reject_imports(monkeypatch, "mcp.server.stdio") + + with pytest.raises(ImportError, match=r"agentic-fabric\[mcp\]"): + vendor_mcp.run_server(RunnableServer([])) diff --git a/packages/agentic-fabric/tests/test_runners.py b/packages/agentic-fabric/tests/test_runners.py new file mode 100644 index 0000000..e5ee30f --- /dev/null +++ b/packages/agentic-fabric/tests/test_runners.py @@ -0,0 +1,1003 @@ +"""Tests for runner implementations. + +These tests verify the interface contracts for all runner implementations +without requiring the actual frameworks to be installed. +""" + +from __future__ import annotations + +import builtins +import sys + +from typing import TYPE_CHECKING, Any + + +if TYPE_CHECKING: + from pytest_agentic_fabric.mocking import FabricMocker + + +def reject_imports(*blocked_names: str): + """Return an import hook that raises for selected top-level packages.""" + real_import = builtins.__import__ + + def fake_import(name: str, *args: Any, **kwargs: Any) -> Any: + if name in blocked_names: + raise ImportError(name) + return real_import(name, *args, **kwargs) + + return fake_import + + +class TestCrewAIRunner: + """Tests for CrewAI runner implementation.""" + + def test_init_reports_missing_crewai(self, fabric_mocker: FabricMocker) -> None: + """CrewAI runner construction should explain how to install CrewAI.""" + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + fabric_mocker.patch("builtins.__import__", side_effect=reject_imports("crewai")) + + try: + CrewAIRunner() + except RuntimeError as exc: + assert "pip install crewai" in str(exc) + else: # pragma: no cover - defensive if CrewAI import mocking stops working + raise AssertionError("CrewAIRunner did not report missing CrewAI") + + def test_build_agent_creates_crewai_agent(self, fabric_mocker: FabricMocker) -> None: + """Test that build_agent creates a CrewAI Agent.""" + fabric_mocker.mock_crewai() + + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + MockAgent = fabric_mocker.patch_crewai_agent() + fabric_mocker.patch_get_llm() + + runner = CrewAIRunner() + + agent_config = { + "role": "Test Agent", + "goal": "Test goal", + "backstory": "Test backstory", + "allow_delegation": True, + } + + tools = [fabric_mocker.MagicMock()] + runner.build_agent(agent_config, tools=tools) + + MockAgent.assert_called_once() + call_kwargs = MockAgent.call_args[1] + assert call_kwargs["role"] == "Test Agent" + assert call_kwargs["goal"] == "Test goal" + assert call_kwargs["backstory"] == "Test backstory" + assert call_kwargs["allow_delegation"] is True + assert call_kwargs["tools"] == tools + + def test_build_task_creates_crewai_task(self, fabric_mocker: FabricMocker) -> None: + """Test that build_task creates a CrewAI Task.""" + fabric_mocker.mock_crewai() + + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + MockTask = fabric_mocker.patch_crewai_task() + + runner = CrewAIRunner() + + task_config = { + "description": "Test task description", + "expected_output": "Test output", + } + mock_agent = fabric_mocker.MagicMock() + + runner.build_task(task_config, mock_agent) + + MockTask.assert_called_once() + call_kwargs = MockTask.call_args[1] + assert call_kwargs["description"] == "Test task description" + assert call_kwargs["expected_output"] == "Test output" + assert call_kwargs["agent"] == mock_agent + + def test_build_task_includes_context(self, fabric_mocker: FabricMocker) -> None: + """Test that build_task includes context tasks when provided.""" + fabric_mocker.mock_crewai() + + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + MockTask = fabric_mocker.patch_crewai_task() + + runner = CrewAIRunner() + + task_config = { + "description": "Test task", + "expected_output": "Test output", + } + mock_agent = fabric_mocker.MagicMock() + context_tasks = [fabric_mocker.MagicMock(), fabric_mocker.MagicMock()] + + runner.build_task(task_config, mock_agent, context=context_tasks) + + call_kwargs = MockTask.call_args[1] + assert call_kwargs["context"] == context_tasks + + def test_build_fabric_agent_creates_crewai_crew(self, fabric_mocker: FabricMocker) -> None: + """Test that build_fabric_agent creates a CrewAI Crew.""" + fabric_mocker.mock_crewai() + + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + MockCrew = fabric_mocker.patch_crewai_crew() + MockAgent = fabric_mocker.patch_crewai_agent() + MockTask = fabric_mocker.patch_crewai_task() + fabric_mocker.patch_crewai_process() + fabric_mocker.patch_get_llm() + + runner = CrewAIRunner() + + mock_agent = fabric_mocker.MagicMock() + MockAgent.return_value = mock_agent + + mock_task = fabric_mocker.MagicMock() + MockTask.return_value = mock_task + + fabric_agent_config = { + "name": "test_fabric_agent", + "agents": { + "agent1": { + "role": "Agent 1", + "goal": "Goal 1", + "backstory": "Backstory 1", + } + }, + "tasks": { + "task1": { + "description": "Task 1", + "expected_output": "Output 1", + "agent": "agent1", + } + }, + "knowledge_paths": [], + } + + runner.build_fabric_agent(fabric_agent_config) + + MockCrew.assert_called_once() + call_kwargs = MockCrew.call_args[1] + assert len(call_kwargs["agents"]) == 1 + assert len(call_kwargs["tasks"]) == 1 + assert call_kwargs["planning"] is True + assert call_kwargs["memory"] is True + + def test_run_executes_crewai_crew_and_returns_result(self, fabric_mocker: FabricMocker) -> None: + """Test that run executes fabric_agent and returns string result.""" + fabric_mocker.mock_crewai() + + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + runner = CrewAIRunner() + + mock_crew = fabric_mocker.mock_crewai_crew(result="Test output") + + result = runner.run(mock_crew, {"input": "test input"}) + + mock_crew.kickoff.assert_called_once_with(inputs={"input": "test input"}) + assert result == "Test output" + + def test_run_handles_result_without_raw(self, fabric_mocker: FabricMocker) -> None: + """Test that run handles results without raw attribute.""" + fabric_mocker.mock_crewai() + + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + runner = CrewAIRunner() + + mock_crew = fabric_mocker.MagicMock() + mock_crew.kickoff.return_value = "Direct string result" + + result = runner.run(mock_crew, {}) + + assert result == "Direct string result" + + def test_handles_knowledge_sources(self, fabric_mocker: FabricMocker, tmp_path) -> None: + """Test that knowledge sources are loaded from paths.""" + fabric_mocker.mock_crewai() + + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + MockKnowledge = fabric_mocker.patch_knowledge_source() + + runner = CrewAIRunner() + + # Create test knowledge file + knowledge_dir = tmp_path / "knowledge" + knowledge_dir.mkdir() + (knowledge_dir / "test.md").write_text("# Test Knowledge\nContent here") + + sources = runner._load_knowledge([knowledge_dir]) + + # Verify TextFileKnowledgeSource was called for the .md file + assert MockKnowledge.called + assert len(sources) > 0 + + def test_handles_task_context_dependencies(self, fabric_mocker: FabricMocker) -> None: + """Test that tasks with context dependencies are handled correctly.""" + fabric_mocker.mock_crewai() + + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + fabric_mocker.patch_crewai_crew() + MockAgent = fabric_mocker.patch_crewai_agent() + MockTask = fabric_mocker.patch_crewai_task() + fabric_mocker.patch_crewai_process() + fabric_mocker.patch_get_llm() + + runner = CrewAIRunner() + + mock_agent = fabric_mocker.MagicMock() + MockAgent.return_value = mock_agent + + mock_task1 = fabric_mocker.MagicMock() + mock_task2 = fabric_mocker.MagicMock() + MockTask.side_effect = [mock_task1, mock_task2] + + fabric_agent_config = { + "agents": { + "agent1": { + "role": "Agent", + "goal": "Goal", + "backstory": "Story", + } + }, + "tasks": { + "task1": { + "description": "Task 1", + "expected_output": "Output 1", + "agent": "agent1", + }, + "task2": { + "description": "Task 2", + "expected_output": "Output 2", + "agent": "agent1", + "context": ["task1"], + }, + }, + "knowledge_paths": [], + } + + runner.build_fabric_agent(fabric_agent_config) + + # Verify second task was created with context + assert MockTask.call_count == 2 + second_task_kwargs = MockTask.call_args_list[1][1] + assert "context" in second_task_kwargs + assert second_task_kwargs["context"] == [mock_task1] + + def test_build_fabric_agent_warns_for_unresolved_context_dependencies( + self, + fabric_mocker: FabricMocker, + caplog, + ) -> None: + """Unresolved task context references should be visible to callers.""" + fabric_mocker.mock_crewai() + + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + fabric_mocker.patch_crewai_crew() + fabric_mocker.patch_crewai_agent() + fabric_mocker.patch_crewai_task() + fabric_mocker.patch_crewai_process() + fabric_mocker.patch_get_llm() + + runner = CrewAIRunner() + fabric_agent_config = { + "agents": {"agent1": {"role": "Agent", "goal": "Goal", "backstory": "Story"}}, + "tasks": { + "task1": { + "description": "Task 1", + "expected_output": "Output 1", + "agent": "agent1", + "context": ["later_task"], + } + }, + "knowledge_paths": [], + } + + runner.build_fabric_agent(fabric_agent_config) + + assert "references context tasks that are not yet available: later_task" in caplog.text + + def test_build_fabric_agent_resolves_declared_tools(self, fabric_mocker: FabricMocker) -> None: + """Tool names declared in agent config should be instantiated and attached.""" + fabric_mocker.mock_crewai() + + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + fabric_mocker.patch_crewai_crew() + MockAgent = fabric_mocker.patch_crewai_agent() + fabric_mocker.patch_crewai_task() + fabric_mocker.patch_crewai_process() + fabric_mocker.patch_get_llm() + + resolved_tool = fabric_mocker.MagicMock() + + runner = CrewAIRunner() + runner._resolve_tools = fabric_mocker.MagicMock(return_value=[resolved_tool]) + + fabric_agent_config = { + "agents": { + "agent1": { + "role": "Agent 1", + "goal": "Goal 1", + "backstory": "Backstory 1", + "tools": ["FileWriteTool"], + } + }, + "tasks": { + "task1": { + "description": "Task 1", + "expected_output": "Output 1", + "agent": "agent1", + } + }, + "knowledge_paths": [], + } + + runner.build_fabric_agent(fabric_agent_config) + + runner._resolve_tools.assert_called_once_with(["FileWriteTool"]) + assert MockAgent.call_args[1]["tools"] == [resolved_tool] + + def test_build_fabric_agent_rejects_tasks_with_unknown_agents(self, fabric_mocker: FabricMocker) -> None: + """CrewAI tasks must reference agents declared in the fabric_agent config.""" + fabric_mocker.mock_crewai() + + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + runner = CrewAIRunner() + + fabric_agent_config = { + "agents": {}, + "tasks": {"task1": {"description": "Task", "agent": "missing"}}, + } + + try: + runner.build_fabric_agent(fabric_agent_config) + except ValueError as exc: + assert "invalid agent: missing" in str(exc) + else: # pragma: no cover - defensive + raise AssertionError("CrewAIRunner accepted a task with an unknown agent") + + def test_load_knowledge_skips_non_directories_and_logs_read_errors( + self, + fabric_mocker: FabricMocker, + tmp_path, + ) -> None: + """Knowledge loading should skip bad paths and tolerate unreadable files.""" + fabric_mocker.mock_crewai() + + from agentic_fabric.runners.crewai_runner import CrewAIRunner + + fabric_mocker.patch_knowledge_source() + runner = CrewAIRunner() + knowledge_dir = tmp_path / "knowledge" + knowledge_dir.mkdir() + bad_file = knowledge_dir / "bad.md" + bad_file.write_text("content", encoding="utf-8") + original_read_text = type(bad_file).read_text + + def fake_read_text(self, *args: Any, **kwargs: Any) -> str: + if self == bad_file: + raise OSError("cannot read") + return original_read_text(self, *args, **kwargs) + + fabric_mocker.patch.object(type(bad_file), "read_text", fake_read_text) + + assert runner._load_knowledge([tmp_path / "missing", knowledge_dir]) == [] + + +class TestLangGraphRunner: + """Tests for LangGraph runner implementation.""" + + def test_init_reports_missing_langgraph(self, fabric_mocker: FabricMocker) -> None: + """LangGraph runner construction should explain how to install LangGraph.""" + from agentic_fabric.runners.langgraph_runner import LangGraphRunner + + fabric_mocker.patch("builtins.__import__", side_effect=reject_imports("langgraph")) + + try: + LangGraphRunner() + except RuntimeError as exc: + assert 'pip install "agentic-fabric[langgraph]"' in str(exc) + else: # pragma: no cover - defensive if LangGraph import mocking stops working + raise AssertionError("LangGraphRunner did not report missing LangGraph") + + def test_build_agent_creates_react_agent(self, fabric_mocker: FabricMocker) -> None: + """Test that build_agent creates a LangGraph ReAct agent.""" + fabric_mocker.mock_langgraph() + + from agentic_fabric.runners.langgraph_runner import LangGraphRunner + + mock_create = fabric_mocker.patch_create_react_agent() + MockLLM = fabric_mocker.patch_chat_anthropic() + mock_llm = fabric_mocker.MagicMock() + MockLLM.return_value = mock_llm + + runner = LangGraphRunner() + + agent_config = { + "role": "Test Agent", + "goal": "Test goal", + "llm": "claude-sonnet-4-20250514", + } + tools = [fabric_mocker.MagicMock()] + + runner.build_agent(agent_config, tools=tools) + + mock_create.assert_called_once() + args = mock_create.call_args[0] + assert args[0] == mock_llm + assert args[1] == tools + + def test_build_fabric_agent_creates_graph(self, fabric_mocker: FabricMocker) -> None: + """Test that build_fabric_agent creates a LangGraph workflow.""" + fabric_mocker.mock_langgraph() + + from agentic_fabric.runners.langgraph_runner import LangGraphRunner + + mock_create = fabric_mocker.patch_create_react_agent() + fabric_mocker.patch_chat_anthropic() + + runner = LangGraphRunner() + + fabric_agent_config = { + "llm": {"model": "claude-sonnet-4-20250514"}, + "agents": {}, + "tasks": {}, + } + + runner.build_fabric_agent(fabric_agent_config) + + mock_create.assert_called_once() + + def test_run_invokes_graph(self, fabric_mocker: FabricMocker) -> None: + """Test that run invokes the LangGraph workflow.""" + fabric_mocker.mock_langgraph() + + from agentic_fabric.runners.langgraph_runner import LangGraphRunner + + runner = LangGraphRunner() + + mock_graph = fabric_mocker.mock_langgraph_graph(result="Test response") + + result = runner.run(mock_graph, {"input": "test prompt"}) + + mock_graph.invoke.assert_called_once() + invoke_args = mock_graph.invoke.call_args[0][0] + assert "messages" in invoke_args + assert result == "Test response" + + def test_run_handles_dict_messages(self, fabric_mocker: FabricMocker) -> None: + """Test that run handles dictionary message format.""" + fabric_mocker.mock_langgraph() + + from agentic_fabric.runners.langgraph_runner import LangGraphRunner + + runner = LangGraphRunner() + + mock_graph = fabric_mocker.MagicMock() + mock_graph.invoke.return_value = {"messages": [("assistant", "Test response")]} + + runner.run(mock_graph, {"task": "test task"}) + + mock_graph.invoke.assert_called_once() + invoke_args = mock_graph.invoke.call_args[0][0] + assert "messages" in invoke_args + assert any("test task" in str(msg) for msg in invoke_args["messages"]) + + def test_run_returns_raw_result_when_no_messages(self, fabric_mocker: FabricMocker) -> None: + """LangGraph runner should stringify results that do not contain messages.""" + fabric_mocker.mock_langgraph() + + from agentic_fabric.runners.langgraph_runner import LangGraphRunner + + runner = LangGraphRunner() + mock_graph = fabric_mocker.MagicMock() + mock_graph.invoke.return_value = {"state": "done"} + + assert runner.run(mock_graph, {"other": "value"}) == "{'state': 'done'}" + + def test_get_llm_returns_chat_anthropic(self, fabric_mocker: FabricMocker) -> None: + """Test that get_llm returns ChatAnthropic instance.""" + fabric_mocker.mock_langgraph() + + from agentic_fabric.runners.langgraph_runner import LangGraphRunner + + MockLLM = fabric_mocker.patch_chat_anthropic() + + runner = LangGraphRunner() + runner.get_llm("claude-sonnet-4-20250514") + + MockLLM.assert_called_once_with(model="claude-sonnet-4-20250514") + + def test_get_llm_uses_default_model(self, fabric_mocker: FabricMocker) -> None: + """Test that get_llm uses default model when none specified.""" + fabric_mocker.mock_langgraph() + + from agentic_fabric.runners.langgraph_runner import LangGraphRunner + + MockLLM = fabric_mocker.patch_chat_anthropic() + + runner = LangGraphRunner() + runner.get_llm() + + MockLLM.assert_called_once() + assert MockLLM.call_args[1]["model"] == "claude-haiku-4-5-20251001" + + def test_build_task_returns_dict(self, fabric_mocker: FabricMocker) -> None: + """Test that build_task returns task configuration dict.""" + fabric_mocker.mock_langgraph() + + from agentic_fabric.runners.langgraph_runner import LangGraphRunner + + runner = LangGraphRunner() + + task_config = { + "description": "Test task", + "expected_output": "Test output", + } + mock_agent = fabric_mocker.MagicMock() + + result = runner.build_task(task_config, mock_agent) + + assert isinstance(result, dict) + assert result["description"] == "Test task" + assert result["expected_output"] == "Test output" + assert result["agent"] == mock_agent + + def test_handles_multi_agent_flows(self, fabric_mocker: FabricMocker) -> None: + """Test that multiple agents can be created for complex flows.""" + fabric_mocker.mock_langgraph() + + from agentic_fabric.runners.langgraph_runner import LangGraphRunner + + mock_create = fabric_mocker.patch_create_react_agent() + fabric_mocker.patch_chat_anthropic() + + runner = LangGraphRunner() + + # Create multiple agents + agent1_config = {"role": "Agent 1", "goal": "Goal 1"} + agent2_config = {"role": "Agent 2", "goal": "Goal 2"} + + runner.build_agent(agent1_config) + runner.build_agent(agent2_config) + + # Verify create_react_agent was called twice + assert mock_create.call_count == 2 + + def test_build_fabric_agent_resolves_declared_tools(self, fabric_mocker: FabricMocker) -> None: + """Configured agent tools should be adapted for LangGraph fabric_agents.""" + fabric_mocker.mock_langgraph() + + from agentic_fabric.runners.langgraph_runner import LangGraphRunner + + mock_create = fabric_mocker.patch_create_react_agent() + fabric_mocker.patch_chat_anthropic() + resolved_tool = fabric_mocker.MagicMock() + fabric_mocker.patch("agentic_fabric.runners.langgraph_runner.resolve_langgraph_tools", return_value=[resolved_tool]) + + runner = LangGraphRunner() + fabric_agent_config = { + "llm": {"model": "claude-sonnet-4-20250514"}, + "agents": { + "researcher": { + "tools": ["ScrapeWebsiteTool"], + } + }, + "tasks": {}, + } + + runner.build_fabric_agent(fabric_agent_config) + + mock_create.assert_called_once() + assert mock_create.call_args[0][1] == [resolved_tool] + + +class TestStrandsRunner: + """Tests for Strands runner implementation.""" + + def test_init_reports_missing_strands(self, fabric_mocker: FabricMocker) -> None: + """Strands runner construction should explain how to install Strands.""" + from agentic_fabric.runners.strands_runner import StrandsRunner + + fabric_mocker.patch("builtins.__import__", side_effect=reject_imports("strands")) + + try: + StrandsRunner() + except RuntimeError as exc: + assert 'pip install "agentic-fabric[strands]"' in str(exc) + else: # pragma: no cover - defensive if Strands import mocking stops working + raise AssertionError("StrandsRunner did not report missing Strands") + + def test_build_agent_creates_strands_agent(self, fabric_mocker: FabricMocker) -> None: + """Test that build_agent creates a Strands Agent.""" + fabric_mocker.mock_strands() + + from agentic_fabric.runners.strands_runner import StrandsRunner + + MockAgent = fabric_mocker.patch_strands_agent() + + runner = StrandsRunner() + + agent_config = { + "role": "Test Agent", + "goal": "Test goal", + "backstory": "Test backstory", + } + tools = [fabric_mocker.MagicMock()] + + runner.build_agent(agent_config, tools=tools) + + MockAgent.assert_called_once() + call_kwargs = MockAgent.call_args[1] + assert "Test Agent" in call_kwargs["system_prompt"] + assert "Test goal" in call_kwargs["system_prompt"] + assert "Test backstory" in call_kwargs["system_prompt"] + assert call_kwargs["tools"] == tools + + def test_build_system_prompt_combines_agent_prompts(self, fabric_mocker: FabricMocker) -> None: + """Test that _build_system_prompt combines multiple agent roles.""" + fabric_mocker.mock_strands() + + from agentic_fabric.runners.strands_runner import StrandsRunner + + runner = StrandsRunner() + + fabric_agent_config = { + "description": "Test fabric_agent description", + "agents": { + "agent1": { + "role": "Agent One", + "goal": "Goal one", + }, + "agent2": { + "role": "Agent Two", + "goal": "Goal two", + }, + }, + "tasks": { + "task1": { + "description": "Task one description", + } + }, + } + + prompt = runner._build_system_prompt(fabric_agent_config) + + assert "Test fabric_agent description" in prompt + assert "Agent One" in prompt + assert "Agent Two" in prompt + assert "Goal one" in prompt + assert "Goal two" in prompt + assert "task1" in prompt + + def test_build_system_prompt_truncates_long_descriptions(self, fabric_mocker: FabricMocker) -> None: + """Test that long task descriptions are truncated.""" + fabric_mocker.mock_strands() + + from agentic_fabric.runners.strands_runner import StrandsRunner + + runner = StrandsRunner() + + long_desc = "A" * 250 + fabric_agent_config = { + "tasks": { + "task1": { + "description": long_desc, + } + }, + } + + prompt = runner._build_system_prompt(fabric_agent_config) + + # Should be truncated to 200 chars + ellipsis + assert "..." in prompt + assert prompt.count("A") == 200 + + def test_build_system_prompt_keeps_short_descriptions(self, fabric_mocker: FabricMocker) -> None: + """Test that short task descriptions are not truncated.""" + fabric_mocker.mock_strands() + + from agentic_fabric.runners.strands_runner import StrandsRunner + + runner = StrandsRunner() + + short_desc = "Short description" + fabric_agent_config = { + "tasks": { + "task1": { + "description": short_desc, + } + }, + } + + prompt = runner._build_system_prompt(fabric_agent_config) + + # Should not have ellipsis for short descriptions + assert "..." not in prompt + assert short_desc in prompt + + def test_run_returns_string_result(self, fabric_mocker: FabricMocker) -> None: + """Test that run executes agent and returns string result.""" + fabric_mocker.mock_strands() + + from agentic_fabric.runners.strands_runner import StrandsRunner + + runner = StrandsRunner() + + mock_agent = fabric_mocker.mock_strands_agent(result="Test agent response") + + result = runner.run(mock_agent, {"input": "test prompt"}) + + mock_agent.assert_called_once_with("test prompt") + assert result == "Test agent response" + + def test_run_handles_task_input(self, fabric_mocker: FabricMocker) -> None: + """Test that run handles 'task' key in inputs.""" + fabric_mocker.mock_strands() + + from agentic_fabric.runners.strands_runner import StrandsRunner + + runner = StrandsRunner() + + mock_agent = fabric_mocker.mock_strands_agent(result="Response") + + runner.run(mock_agent, {"task": "do something"}) + + mock_agent.assert_called_once_with("do something") + + def test_get_model_provider_from_string(self, fabric_mocker: FabricMocker) -> None: + """Test extracting model provider from string config.""" + fabric_mocker.mock_strands() + + from agentic_fabric.runners.strands_runner import StrandsRunner + + runner = StrandsRunner() + + result = runner._get_model_provider("claude-3-5-sonnet") + + assert result == "claude-3-5-sonnet" + + def test_get_model_provider_from_dict(self, fabric_mocker: FabricMocker) -> None: + """Test extracting model provider from dict config.""" + fabric_mocker.mock_strands() + + from agentic_fabric.runners.strands_runner import StrandsRunner + + runner = StrandsRunner() + + result = runner._get_model_provider({"model": "claude-3-5-sonnet", "provider": "anthropic"}) + + assert result == "claude-3-5-sonnet" + + def test_get_model_provider_returns_none_for_empty(self, fabric_mocker: FabricMocker) -> None: + """Test that None is returned when no config provided.""" + fabric_mocker.mock_strands() + + from agentic_fabric.runners.strands_runner import StrandsRunner + + runner = StrandsRunner() + + result = runner._get_model_provider(None) + + assert result is None + + def test_build_fabric_agent_creates_strands_agent(self, fabric_mocker: FabricMocker) -> None: + """Test that build_fabric_agent creates a Strands agent.""" + fabric_mocker.mock_strands() + + from agentic_fabric.runners.strands_runner import StrandsRunner + + MockAgent = fabric_mocker.patch_strands_agent() + + runner = StrandsRunner() + + fabric_agent_config = { + "description": "Test fabric_agent", + "llm": {"model": "claude-3-5-sonnet"}, + "agents": {}, + "tasks": {}, + } + + runner.build_fabric_agent(fabric_agent_config) + + MockAgent.assert_called_once() + call_kwargs = MockAgent.call_args[1] + assert "system_prompt" in call_kwargs + assert call_kwargs["model_id"] == "claude-3-5-sonnet" + + def test_build_task_returns_dict(self, fabric_mocker: FabricMocker) -> None: + """Test that build_task returns task configuration dict.""" + fabric_mocker.mock_strands() + + from agentic_fabric.runners.strands_runner import StrandsRunner + + runner = StrandsRunner() + + task_config = { + "description": "Test task", + "expected_output": "Test output", + } + mock_agent = fabric_mocker.MagicMock() + + result = runner.build_task(task_config, mock_agent) + + assert isinstance(result, dict) + assert result["description"] == "Test task" + assert result["expected_output"] == "Test output" + assert result["agent"] == mock_agent + + def test_combines_agent_prompts(self, fabric_mocker: FabricMocker) -> None: + """Test that multiple agent capabilities are combined into system prompt.""" + fabric_mocker.mock_strands() + + from agentic_fabric.runners.strands_runner import StrandsRunner + + MockAgent = fabric_mocker.patch_strands_agent() + + runner = StrandsRunner() + + fabric_agent_config = { + "description": "Multi-capability fabric_agent", + "agents": { + "researcher": {"role": "Researcher", "goal": "Research topics"}, + "writer": {"role": "Writer", "goal": "Write content"}, + }, + "tasks": {}, + } + + runner.build_fabric_agent(fabric_agent_config) + + call_kwargs = MockAgent.call_args[1] + system_prompt = call_kwargs["system_prompt"] + + # Verify both agent capabilities are in the prompt + assert "Researcher" in system_prompt + assert "Writer" in system_prompt + assert "Research topics" in system_prompt + assert "Write content" in system_prompt + + def test_build_fabric_agent_resolves_declared_tools(self, fabric_mocker: FabricMocker) -> None: + """Configured agent tools should be adapted for Strands fabric_agents.""" + fabric_mocker.mock_strands() + + from agentic_fabric.runners.strands_runner import StrandsRunner + + MockAgent = fabric_mocker.patch_strands_agent() + resolved_tool = fabric_mocker.MagicMock() + fabric_mocker.patch("agentic_fabric.runners.strands_runner.resolve_strands_tools", return_value=[resolved_tool]) + + runner = StrandsRunner() + fabric_agent_config = { + "description": "Test fabric_agent", + "agents": { + "writer": { + "tools": ["FileWriteTool"], + } + }, + "tasks": {}, + } + + runner.build_fabric_agent(fabric_agent_config) + + assert MockAgent.call_args[1]["tools"] == [resolved_tool] + + +class TestBaseRunner: + """Tests for base runner interface.""" + + def test_build_and_run_convenience_method(self, fabric_mocker: FabricMocker) -> None: + """Test that build_and_run calls build_fabric_agent and run with correct args.""" + from agentic_fabric.runners.base import BaseRunner + + class TestRunner(BaseRunner): + # Define abstract methods so the class can be instantiated + def build_fabric_agent(self, fabric_agent_config: dict[str, Any]) -> Any: + pass + + def run(self, fabric_agent: Any, inputs: dict[str, Any]) -> str: + pass + + def build_agent(self, agent_config: dict[str, Any], tools: list | None = None) -> Any: + return fabric_mocker.MagicMock() + + def build_task(self, task_config: dict[str, Any], agent: Any) -> Any: + return fabric_mocker.MagicMock() + + runner = TestRunner() + + # Mock the methods to verify they're called correctly + mock_fabric_agent_obj = fabric_mocker.MagicMock() + runner.build_fabric_agent = fabric_mocker.MagicMock(return_value=mock_fabric_agent_obj) + runner.run = fabric_mocker.MagicMock(return_value="test result") + + fabric_agent_config = {"test": "config"} + inputs = {"test": "input"} + + result = runner.build_and_run(fabric_agent_config, inputs) + + assert result == "test result" + runner.build_fabric_agent.assert_called_once_with(fabric_agent_config) + runner.run.assert_called_once_with(mock_fabric_agent_obj, inputs) + + def test_build_and_run_uses_empty_inputs_when_none(self, fabric_mocker: FabricMocker) -> None: + """Test that build_and_run uses empty dict when inputs is None.""" + from agentic_fabric.runners.base import BaseRunner + + class TestRunner(BaseRunner): + def build_fabric_agent(self, fabric_agent_config: dict[str, Any]) -> Any: + return fabric_mocker.MagicMock() + + def run(self, fabric_agent: Any, inputs: dict[str, Any]) -> str: + assert inputs == {} + return "result" + + def build_agent(self, agent_config: dict[str, Any], tools: list | None = None) -> Any: + return fabric_mocker.MagicMock() + + def build_task(self, task_config: dict[str, Any], agent: Any) -> Any: + return fabric_mocker.MagicMock() + + runner = TestRunner() + result = runner.build_and_run({}, None) + + assert result == "result" + + def test_get_llm_default_implementation(self, fabric_mocker: FabricMocker) -> None: + """Test that get_llm has a default implementation.""" + mock_get_llm = fabric_mocker.patch_get_llm() + fabric_mocker.patch("agentic_fabric.config.llm.DEFAULT_MODEL", "claude-sonnet-4-20250514") + + from agentic_fabric.runners.base import BaseRunner + + class TestRunner(BaseRunner): + def build_fabric_agent(self, fabric_agent_config: dict[str, Any]) -> Any: + return fabric_mocker.MagicMock() + + def run(self, fabric_agent: Any, inputs: dict[str, Any]) -> str: + return "result" + + def build_agent(self, agent_config: dict[str, Any], tools: list | None = None) -> Any: + return fabric_mocker.MagicMock() + + def build_task(self, task_config: dict[str, Any], agent: Any) -> Any: + return fabric_mocker.MagicMock() + + runner = TestRunner() + llm = runner.get_llm() + + assert llm == mock_get_llm.return_value + + def test_get_llm_returns_none_when_module_unavailable(self, fabric_mocker: FabricMocker) -> None: + """Test that get_llm returns None when llm module not available.""" + from agentic_fabric.runners.base import BaseRunner + + class TestRunner(BaseRunner): + def build_fabric_agent(self, fabric_agent_config: dict[str, Any]) -> Any: + return fabric_mocker.MagicMock() + + def run(self, fabric_agent: Any, inputs: dict[str, Any]) -> str: + return "result" + + def build_agent(self, agent_config: dict[str, Any], tools: list | None = None) -> Any: + return fabric_mocker.MagicMock() + + def build_task(self, task_config: dict[str, Any], agent: Any) -> Any: + return fabric_mocker.MagicMock() + + runner = TestRunner() + + # Simulate that the agentic_fabric.config.llm module is not available + fabric_mocker.patch.dict(sys.modules, {"agentic_fabric.config.llm": None}) + llm = runner.get_llm() + + assert llm is None diff --git a/packages/agentic-fabric/tests/test_runtime_registry.py b/packages/agentic-fabric/tests/test_runtime_registry.py new file mode 100644 index 0000000..e060075 --- /dev/null +++ b/packages/agentic-fabric/tests/test_runtime_registry.py @@ -0,0 +1,115 @@ +"""Tests for the optional runtime registry.""" + +from __future__ import annotations + +import types + +from typing import Any +from unittest.mock import MagicMock + +import pytest + +from agentic_fabric.runners import registry +from agentic_fabric.runners.registry import RuntimeSpec, RuntimeUnavailableError + + +class FakeRunner: + """Runner class loaded through the registry in tests.""" + + +def install_fake_registry(monkeypatch: pytest.MonkeyPatch, spec: RuntimeSpec) -> None: + """Replace the global runtime registry for one test.""" + monkeypatch.setattr(registry, "_RUNTIME_SPECS", {spec.name: spec}) + monkeypatch.setattr(registry, "_AVAILABILITY_CACHE", {}) + + +def test_runtime_spec_serializes_metadata() -> None: + spec = RuntimeSpec( + name="fake", + import_name="fake_runtime", + runner_module="fake_runner", + runner_class="FakeRunner", + install="install fake", + extra="fake", + description="Fake runtime.", + ) + + assert spec.as_dict(available=True) == { + "name": "fake", + "import_name": "fake_runtime", + "runner": "fake_runner:FakeRunner", + "install": "install fake", + "extra": "fake", + "available": True, + "description": "Fake runtime.", + } + + +def test_register_runtime_replaces_spec_and_clears_cache(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(registry, "_RUNTIME_SPECS", {}) + monkeypatch.setattr(registry, "_AVAILABILITY_CACHE", {"fake": True}) + spec = RuntimeSpec("fake", "fake_runtime", "fake_runner", "FakeRunner", "install fake", "fake") + + registry.register_runtime(spec) + + assert registry.runtime_names() == ["fake"] + assert registry.runtime_specs()["fake"] is spec + assert "fake" not in registry._AVAILABILITY_CACHE + + with pytest.raises(TypeError): + registry.runtime_specs()["other"] = spec # type: ignore[index] + + +def test_unknown_runtime_reports_options(monkeypatch: pytest.MonkeyPatch) -> None: + spec = RuntimeSpec("known", "known_runtime", "known_runner", "KnownRunner", "install known", "known") + install_fake_registry(monkeypatch, spec) + + with pytest.raises(ValueError, match=r"Unknown runtime: missing. Options: \[known\]"): + registry.get_runtime_spec("missing") + + +def test_runtime_availability_caches_import_success_and_unknown(monkeypatch: pytest.MonkeyPatch) -> None: + spec = RuntimeSpec("fake", "fake_runtime", "fake_runner", "FakeRunner", "install fake", "fake") + install_fake_registry(monkeypatch, spec) + mock_import = MagicMock(return_value=types.ModuleType("fake_runtime")) + monkeypatch.setattr(registry.importlib, "import_module", mock_import) + + assert registry.is_runtime_available("unknown") is False + assert registry.is_runtime_available("fake") is True + assert registry.is_runtime_available("fake") is True + mock_import.assert_called_once_with("fake_runtime") + assert registry.available_runtimes() == ["fake"] + + +def test_runtime_info_and_require_runtime_report_unavailable(monkeypatch: pytest.MonkeyPatch) -> None: + spec = RuntimeSpec("fake", "missing_runtime", "fake_runner", "FakeRunner", "install fake", "fake") + install_fake_registry(monkeypatch, spec) + monkeypatch.setattr(registry.importlib, "import_module", MagicMock(side_effect=ImportError("missing"))) + + assert registry.runtime_info("fake")["available"] is False + assert registry.runtime_info() == [spec.as_dict(available=False)] + + with pytest.raises(RuntimeUnavailableError) as exc_info: + registry.require_runtime("fake") + + assert exc_info.value.runtime == "fake" + assert exc_info.value.install == "install fake" + + +def test_load_runner_imports_runner_class(monkeypatch: pytest.MonkeyPatch) -> None: + spec = RuntimeSpec("fake", "fake_runtime", "fake_runner", "FakeRunner", "install fake", "fake") + install_fake_registry(monkeypatch, spec) + fake_runner_module = types.ModuleType("fake_runner") + fake_runner_module.FakeRunner = FakeRunner + + def fake_import_module(name: str) -> Any: + if name == "fake_runtime": + return types.ModuleType("fake_runtime") + if name == "fake_runner": + return fake_runner_module + raise ImportError(name) + + monkeypatch.setattr(registry.importlib, "import_module", fake_import_module) + + assert isinstance(registry.load_runner("fake"), FakeRunner) + assert registry.install_command("fake") == "install fake" diff --git a/packages/agentic-fabric/tests/test_scraping_tool_crawler.py b/packages/agentic-fabric/tests/test_scraping_tool_crawler.py new file mode 100644 index 0000000..f50aa2c --- /dev/null +++ b/packages/agentic-fabric/tests/test_scraping_tool_crawler.py @@ -0,0 +1,161 @@ +"""Crawler behavior tests without optional scraping dependencies.""" + +from __future__ import annotations + +import importlib +import re +import sys +import types + +from unittest.mock import MagicMock + +import pytest + + +def import_scraping_tools(monkeypatch: pytest.MonkeyPatch) -> types.ModuleType: + """Import scraping_tools with lightweight optional dependency stand-ins.""" + fake_requests = types.ModuleType("requests") + fake_bs4 = types.ModuleType("bs4") + + class RequestError(Exception): + """Minimal stand-in for requests.RequestException.""" + + class FakeLink(dict): + """BeautifulSoup-like link node for href access.""" + + class BeautifulSoup: + """Minimal parser for the crawler test fixture.""" + + def __init__(self, content: bytes, parser: str): + self.content = content.decode("utf-8") + + def __call__(self, tags: list[str]) -> list[object]: + return [] + + def find_all(self, tag: str, href: bool = False) -> list[FakeLink]: + if tag != "a" or not href: + return [] + return [FakeLink(href=match) for match in re.findall(r"]+href=['\"]([^'\"]+)['\"]", self.content)] + + @property + def stripped_strings(self) -> list[str]: + return [part.strip() for part in re.sub(r"<[^>]+>", " ", self.content).split() if part.strip()] + + fake_requests.RequestException = RequestError + fake_requests.get = MagicMock() + fake_bs4.BeautifulSoup = BeautifulSoup + + monkeypatch.setitem(sys.modules, "requests", fake_requests) + monkeypatch.setitem(sys.modules, "bs4", fake_bs4) + sys.modules.pop("agentic_fabric.tools.scraping_tools", None) + return importlib.import_module("agentic_fabric.tools.scraping_tools") + + +def test_crawler_marks_failed_url_visited_before_retry(monkeypatch: pytest.MonkeyPatch) -> None: + """Duplicate failed URLs should not be requested repeatedly.""" + scraping_tools = import_scraping_tools(monkeypatch) + root_response = MagicMock() + root_response.content = b"duplicate-failduplicate-fail" + root_response.raise_for_status.return_value = None + + scraping_tools.requests.get.side_effect = [ + root_response, + scraping_tools.requests.RequestException("boom"), + ] + + result = scraping_tools.CrawlWebsiteTool()._run("https://example.test") + + assert "duplicate-fail" in result + assert scraping_tools.requests.get.call_count == 2 + + +@pytest.mark.parametrize("url", ["file:///etc/passwd", "http://127.0.0.1", "http://localhost:8000"]) +def test_crawler_rejects_unsafe_start_urls(monkeypatch: pytest.MonkeyPatch, url: str) -> None: + """Unsafe starting URLs should not be requested.""" + scraping_tools = import_scraping_tools(monkeypatch) + + assert scraping_tools.CrawlWebsiteTool()._run(url) == "" + scraping_tools.requests.get.assert_not_called() + + +def test_crawler_enforces_page_limit(monkeypatch: pytest.MonkeyPatch) -> None: + """Crawl breadth should stay bounded.""" + scraping_tools = import_scraping_tools(monkeypatch) + monkeypatch.setattr(scraping_tools, "MAX_CRAWL_PAGES", 3) + monkeypatch.setattr(scraping_tools, "MAX_CRAWL_DEPTH", 2) + root_response = MagicMock() + root_response.content = b"".join([f"Page {index}".encode() for index in range(10)]) + root_response.raise_for_status.return_value = None + page_response = MagicMock() + page_response.content = b"

child page

" + page_response.raise_for_status.return_value = None + scraping_tools.requests.get.side_effect = [root_response, page_response, page_response] + + result = scraping_tools.CrawlWebsiteTool()._run("https://example.test") + + assert "Page" in result + assert scraping_tools.requests.get.call_count == 3 + + +def test_scrapers_disable_redirects(monkeypatch: pytest.MonkeyPatch) -> None: + """Scraping requests should not follow redirects to unvalidated hosts.""" + scraping_tools = import_scraping_tools(monkeypatch) + response = MagicMock() + response.status_code = 302 + response.content = b"redirect body" + response.raise_for_status.return_value = None + scraping_tools.requests.get.return_value = response + + assert scraping_tools.ScrapeWebsiteTool()._run("https://example.test") == "" + assert scraping_tools.CrawlWebsiteTool()._run("https://example.test") == "" + assert scraping_tools.requests.get.call_args_list[0].kwargs["allow_redirects"] is False + assert scraping_tools.requests.get.call_args_list[1].kwargs["allow_redirects"] is False + + +def test_crawler_enforces_depth_limit(monkeypatch: pytest.MonkeyPatch) -> None: + """Crawl depth should prevent child requests when configured to zero.""" + scraping_tools = import_scraping_tools(monkeypatch) + monkeypatch.setattr(scraping_tools, "MAX_CRAWL_DEPTH", 0) + root_response = MagicMock() + root_response.content = b"Page 1

root

" + root_response.raise_for_status.return_value = None + scraping_tools.requests.get.return_value = root_response + + result = scraping_tools.CrawlWebsiteTool()._run("https://example.test") + + assert "root" in result + assert scraping_tools.requests.get.call_count == 1 + + +def test_scrape_website_tool_success_and_failure(monkeypatch: pytest.MonkeyPatch) -> None: + """The first-party scrape tool should handle safe, unsafe, and failed URLs.""" + scraping_tools = import_scraping_tools(monkeypatch) + response = MagicMock() + response.content = b"

Visible content

" + response.raise_for_status.return_value = None + scraping_tools.requests.get.return_value = response + + assert "Visible content" in scraping_tools.ScrapeWebsiteTool()._run("https://example.test") + + scraping_tools.requests.get.reset_mock() + assert scraping_tools.ScrapeWebsiteTool()._run("http://127.0.0.1") == "" + scraping_tools.requests.get.assert_not_called() + + scraping_tools.requests.get.side_effect = scraping_tools.requests.RequestException("boom") + assert scraping_tools.ScrapeWebsiteTool()._run("https://example.test") == "" + + +def test_scrape_content_removes_script_and_style_tags(monkeypatch: pytest.MonkeyPatch) -> None: + """The scraper should remove script/style nodes before reading text.""" + scraping_tools = import_scraping_tools(monkeypatch) + script = MagicMock() + style = MagicMock() + soup = MagicMock() + soup.return_value = [script, style] + soup.stripped_strings = ["Visible", "content"] + + result = scraping_tools.CrawlWebsiteTool()._scrape_content(soup) + + script.decompose.assert_called_once_with() + style.decompose.assert_called_once_with() + assert result == "Visible content" diff --git a/packages/agentic-fabric/tests/test_scraping_tools.py b/packages/agentic-fabric/tests/test_scraping_tools.py new file mode 100644 index 0000000..a16a4df --- /dev/null +++ b/packages/agentic-fabric/tests/test_scraping_tools.py @@ -0,0 +1,31 @@ +"""Tests for the scraping tools.""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +import pytest + +from tests.test_scraping_tool_crawler import import_scraping_tools + + +def test_crawl_website_tool(monkeypatch: pytest.MonkeyPatch): + """Tests that the CrawlWebsiteTool scrapes content and discovers links.""" + scraping_tools = import_scraping_tools(monkeypatch) + mock_response_page1 = MagicMock() + mock_response_page1.content = b'Page 2

Content 1

' + mock_response_page1.raise_for_status.return_value = None + + mock_response_page2 = MagicMock() + mock_response_page2.content = b"

Content 2

" + mock_response_page2.raise_for_status.return_value = None + + scraping_tools.requests.get.side_effect = [mock_response_page1, mock_response_page2] + + tool = scraping_tools.CrawlWebsiteTool() + result = tool._run("http://example.com") + + assert "Content 1" in result + assert "Page 2" in result + assert "Content 2" in result + assert scraping_tools.requests.get.call_count == 2 diff --git a/packages/agentic-fabric/tests/test_single_agent_runners.py b/packages/agentic-fabric/tests/test_single_agent_runners.py new file mode 100644 index 0000000..06bd790 --- /dev/null +++ b/packages/agentic-fabric/tests/test_single_agent_runners.py @@ -0,0 +1,650 @@ +"""Tests for single-agent CLI runners.""" + +from __future__ import annotations + +import os +import subprocess + +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from agentic_fabric.runners.local_cli_runner import LocalCLIConfig, LocalCLIRunner +from agentic_fabric.runners.single_agent_runner import SingleAgentRunner + + +class TestSingleAgentRunner: + """Test the SingleAgentRunner base class.""" + + def test_is_abstract(self): + """SingleAgentRunner should be abstract.""" + with pytest.raises(TypeError): + SingleAgentRunner() + + def test_default_is_available(self): + """Default is_available should return True.""" + + class TestRunner(SingleAgentRunner): + def run(self, task: str, working_dir: str | None = None, **kwargs): + return "test" + + runner = TestRunner() + assert runner.is_available() is True + + def test_default_get_required_env_vars(self): + """Default get_required_env_vars should return empty list.""" + + class TestRunner(SingleAgentRunner): + def run(self, task: str, working_dir: str | None = None, **kwargs): + return "test" + + runner = TestRunner() + assert runner.get_required_env_vars() == [] + + +class TestLocalCLIConfig: + """Test the LocalCLIConfig dataclass.""" + + def test_minimal_config(self): + """Should create config with minimal required fields.""" + config = LocalCLIConfig( + command="test-tool", + task_flag="--task", + ) + + assert config.command == "test-tool" + assert config.task_flag == "--task" + assert config.auth_env == [] + assert config.timeout == 300 + + def test_full_config(self): + """Should create config with all fields.""" + config = LocalCLIConfig( + command="aider", + task_flag="--message", + subcommand=None, + auth_env=["OPENAI_API_KEY"], + auto_approve="--yes-always", + structured_output="--json", + model_flag="--model", + default_model="gpt-4o", + working_dir_flag="--cwd", + additional_flags=["--no-git"], + timeout=600, + name="Aider", + description="AI pair programming", + install_cmd="pipx install aider-chat", + ) + + assert config.command == "aider" + assert config.auth_env == ["OPENAI_API_KEY"] + assert config.auto_approve == "--yes-always" + assert config.timeout == 600 + + +class TestLocalCLIRunner: + """Test the LocalCLIRunner implementation.""" + + @pytest.fixture + def mock_profiles_file(self, tmp_path: Path): + """Create a temporary profiles YAML file.""" + profiles_content = """ +profiles: + test-tool: + name: "Test Tool" + description: "A test tool" + command: "test-tool" + task_flag: "--task" + auth_env: + - "TEST_API_KEY" + auto_approve: "--yes" + timeout: 120 + install_cmd: "pip install test-tool" + + aider: + name: "Aider" + description: "AI pair programming" + command: "aider" + task_flag: "--message" + auth_env: + - "OPENAI_API_KEY" + auto_approve: "--yes-always" + model_flag: "--model" + additional_flags: + - "--no-git" + timeout: 300 + install_cmd: "pipx install aider-chat" +""" + profiles_file = tmp_path / "local_cli_profiles.yaml" + profiles_file.write_text(profiles_content) + return profiles_file + + def test_load_profiles(self, mock_profiles_file: Path): + """Should load profiles from YAML file.""" + with patch("agentic_fabric.runners.local_cli_runner.Path") as mock_path: + mock_path.return_value.parent = mock_profiles_file.parent + mock_path.return_value.__truediv__.return_value = mock_profiles_file + + # Clear cache + LocalCLIRunner._profiles_cache = None + + profiles = LocalCLIRunner._load_profiles() + + assert "test-tool" in profiles + assert "aider" in profiles + assert profiles["test-tool"].command == "test-tool" + assert profiles["aider"].auth_env == ["OPENAI_API_KEY"] + + @pytest.mark.skipif(os.name != "posix", reason="POSIX file mode check") + def test_rejects_group_writable_profiles_file(self, mock_profiles_file: Path): + """Bundled local CLI profiles should not be group or world writable.""" + mock_profiles_file.chmod(0o664) + with patch("agentic_fabric.runners.local_cli_runner.Path") as mock_path: + mock_path.return_value.parent = mock_profiles_file.parent + mock_path.return_value.__truediv__.return_value = mock_profiles_file + + LocalCLIRunner._profiles_cache = None + + with pytest.raises(PermissionError, match="writable by group or other"): + LocalCLIRunner._load_profiles() + + def test_profiles_permission_check_is_noop_on_non_posix(self, mock_profiles_file: Path): + """Non-POSIX platforms should skip POSIX mode checks.""" + from agentic_fabric.runners import local_cli_runner + + with patch.object(local_cli_runner.os, "name", "nt"): + local_cli_runner._validate_profiles_file_permissions(mock_profiles_file) + + def test_init_with_profile_name(self, mock_profiles_file: Path): + """Should initialize with a profile name.""" + with patch("agentic_fabric.runners.local_cli_runner.Path") as mock_path: + mock_path.return_value.parent = mock_profiles_file.parent + mock_path.return_value.__truediv__.return_value = mock_profiles_file + + LocalCLIRunner._profiles_cache = None + + runner = LocalCLIRunner("aider") + + assert runner.config.command == "aider" + assert runner.config.task_flag == "--message" + + def test_named_profile_receives_isolated_config_copy(self, mock_profiles_file: Path): + """Mutating one named runner should not mutate the cached profile.""" + with patch("agentic_fabric.runners.local_cli_runner.Path") as mock_path: + mock_path.return_value.parent = mock_profiles_file.parent + mock_path.return_value.__truediv__.return_value = mock_profiles_file + + LocalCLIRunner._profiles_cache = None + runner = LocalCLIRunner("aider") + + runner.config.auth_env.append("MUTATED") + runner.config.additional_flags.append("--mutated") + + cached = LocalCLIRunner._load_profiles()["aider"] + assert cached.auth_env == ["OPENAI_API_KEY"] + assert cached.additional_flags == ["--no-git"] + + def test_init_with_unknown_profile(self, mock_profiles_file: Path): + """Should raise ValueError for unknown profile.""" + with patch("agentic_fabric.runners.local_cli_runner.Path") as mock_path: + mock_path.return_value.parent = mock_profiles_file.parent + mock_path.return_value.__truediv__.return_value = mock_profiles_file + + LocalCLIRunner._profiles_cache = None + + with pytest.raises(ValueError, match="Unknown profile"): + LocalCLIRunner("unknown-tool") + + def test_init_with_config_dict(self): + """Should initialize with a config dict.""" + config_dict = { + "command": "my-tool", + "task_flag": "--prompt", + "auto_approve": "--yes", + } + + runner = LocalCLIRunner(config_dict) + + assert runner.config.command == "my-tool" + assert runner.config.task_flag == "--prompt" + assert runner.config.auto_approve == "--yes" + + def test_rejects_unknown_config_dict_fields(self): + """Custom config dicts should reject unsupported fields.""" + with pytest.raises(ValueError, match="unsupported fields"): + LocalCLIRunner({"command": "my-tool", "task_flag": "--prompt", "shell": True}) + + @pytest.mark.parametrize( + "config,error_type,match", + [ + ({"command": "", "task_flag": "--prompt"}, ValueError, "non-empty command"), + ({"command": "my-tool", "task_flag": None}, TypeError, "task_flag as a string"), + ], + ) + def test_rejects_invalid_required_scalar_fields(self, config, error_type, match): + """Required scalar profile fields should have strict types.""" + with pytest.raises(error_type, match=match): + LocalCLIRunner(config) + + def test_rejects_shell_operator_in_command(self): + """Profile commands should be direct executable invocations.""" + with pytest.raises(ValueError, match="direct executable"): + LocalCLIRunner({"command": "my-tool && rm -rf /", "task_flag": "--prompt"}) + + @pytest.mark.parametrize( + "field_name,value", + [ + ("auto_approve", " "), + ("structured_output", '--json "unterminated'), + ("task_flag", "--prompt; rm -rf /"), + ("subcommand", "run && rm -rf /"), + ("auto_approve", "--yes | sh"), + ("structured_output", "--json > out"), + ("model_flag", "--model $(touch bad)"), + ("default_model", "model;touch-bad"), + ("working_dir_flag", "--cwd\n--bad"), + ], + ) + def test_rejects_shell_control_in_config_cli_fields(self, field_name, value): + """Config-controlled CLI fragments should stay single safe arguments.""" + config = {"command": "my-tool", "task_flag": "--prompt", field_name: value} + + with pytest.raises(ValueError, match=field_name): + LocalCLIRunner(config) + + def test_rejects_invalid_additional_flags(self): + """List fields should contain only non-empty strings.""" + with pytest.raises(ValueError, match="additional_flags"): + LocalCLIRunner({"command": "my-tool", "task_flag": "--prompt", "additional_flags": ["--ok", ""]}) + + with pytest.raises(ValueError, match="additional_flags"): + LocalCLIRunner({"command": "my-tool", "task_flag": "--prompt", "additional_flags": ["--ok", "--bad;"]}) + + def test_rejects_invalid_auth_env_names(self): + """Auth env names should be valid environment variable identifiers.""" + with pytest.raises(ValueError, match="auth_env"): + LocalCLIRunner({"command": "my-tool", "task_flag": "--prompt", "auth_env": ["OK", "BAD-NAME"]}) + + def test_rejects_invalid_optional_scalar_field(self): + """Optional string fields should reject non-string values.""" + with pytest.raises(ValueError, match="working_dir_flag"): + LocalCLIRunner({"command": "my-tool", "task_flag": "--prompt", "working_dir_flag": 123}) + + def test_rejects_invalid_timeout(self): + """Timeouts should stay within the bounded execution range.""" + with pytest.raises(ValueError, match="timeout"): + LocalCLIRunner({"command": "my-tool", "task_flag": "--prompt", "timeout": 0}) + + def test_load_profiles_reports_missing_profiles_file(self, tmp_path: Path): + """Missing bundled profiles should raise clear guidance.""" + missing_profiles_file = tmp_path / "local_cli_profiles.yaml" + with patch("agentic_fabric.runners.local_cli_runner.Path") as mock_path: + mock_path.return_value.parent = tmp_path + mock_path.return_value.__truediv__.return_value = missing_profiles_file + + LocalCLIRunner._profiles_cache = None + + with pytest.raises(FileNotFoundError, match=r"Expected local_cli_profiles\.yaml"): + LocalCLIRunner._load_profiles() + + def test_load_profiles_rejects_non_mapping_yaml(self, tmp_path: Path): + """Profiles YAML must have a mapping root and mapping profiles section.""" + profiles_file = tmp_path / "local_cli_profiles.yaml" + profiles_file.write_text("- not\n- a mapping\n", encoding="utf-8") + with patch("agentic_fabric.runners.local_cli_runner.Path") as mock_path: + mock_path.return_value.parent = tmp_path + mock_path.return_value.__truediv__.return_value = profiles_file + + LocalCLIRunner._profiles_cache = None + + with pytest.raises(TypeError, match="must contain a mapping"): + LocalCLIRunner._load_profiles() + + def test_load_profiles_rejects_non_mapping_profiles_section(self, tmp_path: Path): + """Profiles YAML must define profiles as a mapping.""" + profiles_file = tmp_path / "local_cli_profiles.yaml" + profiles_file.write_text("profiles:\n - not-a-mapping\n", encoding="utf-8") + with patch("agentic_fabric.runners.local_cli_runner.Path") as mock_path: + mock_path.return_value.parent = tmp_path + mock_path.return_value.__truediv__.return_value = profiles_file + + LocalCLIRunner._profiles_cache = None + + with pytest.raises(TypeError, match="profiles"): + LocalCLIRunner._load_profiles() + + def test_load_profiles_rejects_symlink_profiles_file(self, tmp_path: Path): + """Bundled profiles should be read from a regular file.""" + target_file = tmp_path / "target.yaml" + target_file.write_text("profiles: {}\n", encoding="utf-8") + profiles_file = tmp_path / "local_cli_profiles.yaml" + profiles_file.symlink_to(target_file) + + with patch("agentic_fabric.runners.local_cli_runner.Path") as mock_path: + mock_path.return_value.parent = tmp_path + mock_path.return_value.__truediv__.return_value = profiles_file + + LocalCLIRunner._profiles_cache = None + + with pytest.raises(FileNotFoundError, match="regular file"): + LocalCLIRunner._load_profiles() + + def test_profiles_source_rejects_unexpected_file_name(self, tmp_path: Path): + """The bundled profiles source should have the expected file name.""" + from agentic_fabric.runners import local_cli_runner + + unexpected_file = tmp_path / "profiles.yaml" + unexpected_file.write_text("profiles: {}\n", encoding="utf-8") + + with pytest.raises(ValueError, match="Unexpected profiles file name"): + local_cli_runner._validate_profiles_file_source(unexpected_file) + + def test_load_profiles_rejects_invalid_profile_entries(self, tmp_path: Path): + """Profiles YAML should contain string names mapped to config mappings.""" + profiles_file = tmp_path / "local_cli_profiles.yaml" + profiles_file.write_text("profiles:\n bad: not-a-mapping\n", encoding="utf-8") + + with patch("agentic_fabric.runners.local_cli_runner.Path") as mock_path: + mock_path.return_value.parent = tmp_path + mock_path.return_value.__truediv__.return_value = profiles_file + + LocalCLIRunner._profiles_cache = None + + with pytest.raises(TypeError, match="invalid profile entry"): + LocalCLIRunner._load_profiles() + + def test_init_with_config_object(self): + """Should initialize with a LocalCLIConfig object.""" + config = LocalCLIConfig( + command="custom-tool", + task_flag="--task", + ) + + runner = LocalCLIRunner(config) + + assert runner.config.command == "custom-tool" + assert runner.config.task_flag == "--task" + + def test_init_with_config_object_uses_profile_validation(self): + """LocalCLIConfig objects should not bypass fragment validation.""" + config = LocalCLIConfig( + command="custom-tool", + task_flag="--task; rm -rf /", + ) + + with pytest.raises(ValueError, match="task_flag"): + LocalCLIRunner(config) + + def test_init_rejects_unsupported_profile_input(self): + """Unsupported profile inputs should fail with a clear type error.""" + with pytest.raises(TypeError, match="profile must be"): + LocalCLIRunner(["not", "supported"]) + + def test_is_available_tool_exists(self): + """Should return True if tool is in PATH.""" + config = LocalCLIConfig(command="python", task_flag="--task") + runner = LocalCLIRunner(config) + + assert runner.is_available() is True + + def test_is_available_tool_missing(self): + """Should return False if tool is not in PATH.""" + config = LocalCLIConfig(command="nonexistent-tool-xyz", task_flag="--task") + runner = LocalCLIRunner(config) + + assert runner.is_available() is False + + def test_get_required_env_vars(self): + """Should return required env vars from config.""" + config = LocalCLIConfig( + command="test", + task_flag="--task", + auth_env=["API_KEY", "SECRET"], + ) + runner = LocalCLIRunner(config) + + assert runner.get_required_env_vars() == ["API_KEY", "SECRET"] + + def test_run_missing_env_vars(self): + """Should raise RuntimeError if required env vars missing.""" + config = LocalCLIConfig( + command="test", + task_flag="--task", + auth_env=["MISSING_ENV_VAR"], + ) + runner = LocalCLIRunner(config) + + with pytest.raises(RuntimeError, match="Missing required environment variables"): + runner.run("test task") + + def test_run_rejects_nul_task_argument(self): + """User arguments should be valid subprocess arguments.""" + config = LocalCLIConfig(command="test-tool", task_flag="--task") + runner = LocalCLIRunner(config) + + with pytest.raises(ValueError, match="NUL byte"): + runner.run("safe\x00unsafe") + + def test_run_rejects_non_string_runtime_arguments(self): + """Runtime arguments should be strings before reaching subprocess.""" + config = LocalCLIConfig(command="test-tool", task_flag="--task") + runner = LocalCLIRunner(config) + + with pytest.raises(TypeError, match="task must be a string"): + runner.run(123) # type: ignore[arg-type] + + @patch.dict("os.environ", {"TEST_API_KEY": "test-key"}) + @patch("subprocess.run") + def test_run_success(self, mock_run: MagicMock): + """Should execute command successfully.""" + config = LocalCLIConfig( + command="test-tool", + task_flag="--task", + auth_env=["TEST_API_KEY"], + ) + runner = LocalCLIRunner(config) + + # Mock successful execution + mock_run.return_value = MagicMock( + returncode=0, + stdout="Test output", + stderr="", + ) + + result = runner.run("Fix the bug") + + assert result == "Test output" + mock_run.assert_called_once() + + # Check command was built correctly + call_args = mock_run.call_args + assert call_args[0][0] == ["test-tool", "--task", "Fix the bug"] + assert call_args.kwargs["shell"] is False + + @patch.dict("os.environ", {"TEST_API_KEY": "test-key"}) + @patch("subprocess.run") + def test_run_with_auto_approve(self, mock_run: MagicMock): + """Should include auto-approve flag when enabled.""" + config = LocalCLIConfig( + command="test-tool", + task_flag="--task", + auth_env=["TEST_API_KEY"], + auto_approve="--yes", + ) + runner = LocalCLIRunner(config) + + mock_run.return_value = MagicMock(returncode=0, stdout="Output", stderr="") + + runner.run("Test task", auto_approve=True) + + call_args = mock_run.call_args + assert "--yes" in call_args[0][0] + + @patch.dict("os.environ", {"TEST_API_KEY": "test-key"}) + @patch("subprocess.run") + def test_run_with_structured_output(self, mock_run: MagicMock): + """Should include structured output flag when enabled.""" + config = LocalCLIConfig( + command="test-tool", + task_flag="--task", + auth_env=["TEST_API_KEY"], + structured_output="--output-format json", + ) + runner = LocalCLIRunner(config) + + mock_run.return_value = MagicMock(returncode=0, stdout="Output", stderr="") + + runner.run("Test task", structured_output=True) + + call_args = mock_run.call_args + assert call_args[0][0] == ["test-tool", "--task", "Test task", "--output-format", "json"] + + @patch.dict("os.environ", {"TEST_API_KEY": "test-key"}) + @patch("subprocess.run") + def test_run_with_model(self, mock_run: MagicMock): + """Should include model flag when model specified.""" + config = LocalCLIConfig( + command="test-tool", + task_flag="--task", + auth_env=["TEST_API_KEY"], + model_flag="--model", + ) + runner = LocalCLIRunner(config) + + mock_run.return_value = MagicMock(returncode=0, stdout="Output", stderr="") + + runner.run("Test task", model="gpt-4o") + + call_args = mock_run.call_args + cmd = call_args[0][0] + assert "--model" in cmd + assert "gpt-4o" in cmd + + @patch.dict("os.environ", {"TEST_API_KEY": "test-key"}) + @patch("subprocess.run") + def test_run_with_additional_flags(self, mock_run: MagicMock): + """Should include additional flags from config.""" + config = LocalCLIConfig( + command="test-tool", + task_flag="--task", + auth_env=["TEST_API_KEY"], + additional_flags=["--no-cache", "--log-level debug"], + ) + runner = LocalCLIRunner(config) + + mock_run.return_value = MagicMock(returncode=0, stdout="Output", stderr="") + + runner.run("Test task") + + call_args = mock_run.call_args + cmd = call_args[0][0] + assert "--no-cache" in cmd + assert "--log-level" in cmd + assert "debug" in cmd + + @patch.dict("os.environ", {"TEST_API_KEY": "test-key"}) + @patch("subprocess.run") + def test_run_failure(self, mock_run: MagicMock): + """Should raise RuntimeError on command failure.""" + config = LocalCLIConfig( + command="test-tool", + task_flag="--task", + auth_env=["TEST_API_KEY"], + ) + runner = LocalCLIRunner(config) + + # Mock failed execution with CalledProcessError (raised when check=True) + mock_run.side_effect = subprocess.CalledProcessError( + returncode=1, + cmd=["test-tool", "--task", "Test task"], + output="", + stderr="Error occurred", + ) + + with pytest.raises(RuntimeError, match="Command failed with exit code 1"): + runner.run("Test task") + + @patch.dict("os.environ", {"TEST_API_KEY": "test-key"}) + @patch("subprocess.run") + def test_run_timeout(self, mock_run: MagicMock): + """Should raise RuntimeError on timeout.""" + config = LocalCLIConfig( + command="test-tool", + task_flag="--task", + auth_env=["TEST_API_KEY"], + timeout=1, + ) + runner = LocalCLIRunner(config) + + # Mock timeout + mock_run.side_effect = subprocess.TimeoutExpired("test-tool", 1) + + with pytest.raises(RuntimeError, match="Command timed out"): + runner.run("Test task") + + @patch.dict("os.environ", {"TEST_API_KEY": "test-key"}) + @patch("subprocess.run") + def test_build_command_positional_task(self, mock_run: MagicMock): + """Should handle positional task argument (no flag).""" + # Note: command can contain spaces and will be split with shlex + # But using subcommand/default_model is more explicit (see next test) + config = LocalCLIConfig( + command="test-tool", + task_flag="", # Positional + auth_env=["TEST_API_KEY"], + ) + runner = LocalCLIRunner(config) + + mock_run.return_value = MagicMock(returncode=0, stdout="Output", stderr="") + + runner.run("Fix the bug") + + call_args = mock_run.call_args + cmd = call_args[0][0] + # Should be: ["test-tool", "Fix the bug"] + assert cmd == ["test-tool", "Fix the bug"] + assert "--task" not in cmd + + @patch.dict("os.environ", {"TEST_API_KEY": "test-key"}) + @patch("subprocess.run") + def test_build_command_with_subcommand(self, mock_run: MagicMock): + """Should include subcommand if specified.""" + config = LocalCLIConfig( + command="ollama", + subcommand="run", + task_flag="", + auth_env=["TEST_API_KEY"], + default_model="codellama", + ) + runner = LocalCLIRunner(config) + + mock_run.return_value = MagicMock(returncode=0, stdout="Output", stderr="") + + runner.run("Test task") + + call_args = mock_run.call_args + cmd = call_args[0][0] + assert cmd[0] == "ollama" + assert cmd[1] == "run" + assert "codellama" in cmd + + @patch.dict("os.environ", {"TEST_API_KEY": "test-key"}) + @patch("subprocess.run") + def test_build_command_with_positional_model_and_working_dir_flag(self, mock_run: MagicMock): + """Positional models and supported working-dir flags should be included.""" + config = LocalCLIConfig( + command="ollama", + subcommand="run", + task_flag="", + auth_env=["TEST_API_KEY"], + working_dir_flag="--cwd", + ) + runner = LocalCLIRunner(config, model="deepseek-coder") + + mock_run.return_value = MagicMock(returncode=0, stdout="Output", stderr="") + + runner.run("Test task", working_dir="/tmp/work") + + cmd = mock_run.call_args[0][0] + assert cmd == ["ollama", "run", "deepseek-coder", "Test task", "--cwd", "/tmp/work"] diff --git a/packages/agentic-fabric/tests/test_tool_adapters.py b/packages/agentic-fabric/tests/test_tool_adapters.py new file mode 100644 index 0000000..9d46b85 --- /dev/null +++ b/packages/agentic-fabric/tests/test_tool_adapters.py @@ -0,0 +1,267 @@ +"""Tests for framework-specific configured tool adapters.""" + +from __future__ import annotations + +import sys + +from types import ModuleType, SimpleNamespace +from unittest.mock import MagicMock, patch + +import pytest + +from agentic_fabric.tools.adapters import ( + _build_runner, + _invoke_tool, + _tool_name, + resolve_langgraph_tools, + resolve_strands_tools, +) + + +class WriteFileArgs: + """Schema used to verify adapter schema forwarding.""" + + @classmethod + def model_json_schema(cls) -> dict[str, str]: + """Return a minimal schema compatible with adapter expectations.""" + return {"title": "WriteFileArgs"} + + +class DummyTool: + """Simple tool object with a CrewAI-style `_run` entrypoint.""" + + name = "Write Game Code File" + description = "Write code into the workspace." + args_schema = WriteFileArgs + + def __init__(self) -> None: + self.calls: list[dict[str, str]] = [] + + def _run(self, **kwargs: str) -> str: + self.calls.append(kwargs) + return f"wrote:{kwargs['file_path']}" + + +class CallableTool: + """Callable tool object without a CrewAI _run method.""" + + __name__ = "1 callable tool" + + def __call__(self, **kwargs: str) -> str: + return f"called:{kwargs['value']}" + + +class SignatureTool: + """Tool without args_schema whose _run signature should be preserved.""" + + name = "signature_tool" + description = "Uses signature inference." + + def _run(self, file_path: str, mode: str = "r") -> str: + return f"{mode}:{file_path}" + + +class FakeStructuredTool: + """Minimal LangChain-style tool wrapper used for adapter tests.""" + + def __init__( + self, + *, + func, + name: str, + description: str, + args_schema: type[WriteFileArgs] | None, + infer_schema: bool, + ) -> None: + self.func = func + self.name = name + self.description = description + self.args_schema = args_schema + self.infer_schema = infer_schema + + @classmethod + def from_function( + cls, + *, + func, + name: str, + description: str, + args_schema: type[WriteFileArgs] | None, + infer_schema: bool, + ) -> FakeStructuredTool: + return cls( + func=func, + name=name, + description=description, + args_schema=args_schema, + infer_schema=infer_schema, + ) + + def invoke(self, kwargs: dict[str, str]) -> str: + return self.func(**kwargs) + + +def fake_strands_tool(*, name: str, description: str, inputSchema: dict | None): + """Minimal Strands decorator used for adapter tests.""" + + def decorator(func): + def wrapped(**kwargs: str) -> str: + return func(**kwargs) + + wrapped.__name__ = func.__name__ + wrapped.__doc__ = func.__doc__ + wrapped.TOOL_SPEC = { + "name": name, + "description": description, + "inputSchema": inputSchema, + } + return wrapped + + return decorator + + +class TestLangGraphToolAdapters: + """Tests for LangGraph configured-tool wrapping.""" + + def test_empty_and_unresolved_tools_return_empty(self) -> None: + """Empty declarations and unresolved names should not require LangChain.""" + assert resolve_langgraph_tools([]) == [] + + with patch("agentic_fabric.tools.adapters.resolve_tools", return_value=[]): + assert resolve_langgraph_tools(["missing"]) == [] + + @patch("agentic_fabric.tools.adapters.resolve_tools") + def test_missing_langchain_dependency_skips_tools(self, mock_resolve_tools: MagicMock) -> None: + """Missing LangChain should be reported as unavailable adaptation.""" + mock_resolve_tools.return_value = [DummyTool()] + + with patch.dict(sys.modules, {"langchain_core.tools": None}): + assert resolve_langgraph_tools(["FileWriteTool"]) == [] + + @patch("agentic_fabric.tools.adapters.resolve_tools") + def test_wraps_resolved_tools_with_structured_tool(self, mock_resolve_tools: MagicMock) -> None: + """Resolved tools should be adapted into LangChain-compatible wrappers.""" + tool = DummyTool() + mock_resolve_tools.return_value = [tool] + + fake_tools_module = ModuleType("langchain_core.tools") + fake_tools_module.StructuredTool = FakeStructuredTool + + with patch.dict(sys.modules, {"langchain_core.tools": fake_tools_module}): + adapted = resolve_langgraph_tools(["FileWriteTool"]) + + assert len(adapted) == 1 + wrapped = adapted[0] + assert wrapped.name == "Write_Game_Code_File" + assert wrapped.description == "Write code into the workspace." + assert wrapped.args_schema is WriteFileArgs + assert wrapped.infer_schema is False + assert wrapped.invoke({"file_path": "src/game.ts"}) == "wrote:src/game.ts" + assert tool.calls == [{"file_path": "src/game.ts"}] + + @patch("agentic_fabric.tools.adapters.resolve_tools") + def test_preserves_existing_langgraph_tool_objects(self, mock_resolve_tools: MagicMock) -> None: + """Existing invoke-capable tools should pass through unchanged.""" + existing_tool = SimpleNamespace(name="get_magic_number", invoke=MagicMock(return_value=42)) + mock_resolve_tools.return_value = [existing_tool] + + fake_tools_module = ModuleType("langchain_core.tools") + fake_tools_module.StructuredTool = FakeStructuredTool + + with patch.dict(sys.modules, {"langchain_core.tools": fake_tools_module}): + adapted = resolve_langgraph_tools(["get_magic_number"]) + + assert adapted == [existing_tool] + + @patch("agentic_fabric.tools.adapters.resolve_tools") + def test_preserves_runner_signature_for_schema_inference(self, mock_resolve_tools: MagicMock) -> None: + """LangChain schema inference should see the wrapped tool's real arguments.""" + mock_resolve_tools.return_value = [SignatureTool()] + + fake_tools_module = ModuleType("langchain_core.tools") + fake_tools_module.StructuredTool = FakeStructuredTool + + with patch.dict(sys.modules, {"langchain_core.tools": fake_tools_module}): + adapted = resolve_langgraph_tools(["SignatureTool"]) + + wrapped = adapted[0] + assert wrapped.infer_schema is True + assert tuple(wrapped.func.__signature__.parameters) == ("file_path", "mode") + + +class TestStrandsToolAdapters: + """Tests for Strands configured-tool wrapping.""" + + def test_empty_and_unresolved_tools_return_empty(self) -> None: + """Empty declarations and unresolved names should not require Strands.""" + assert resolve_strands_tools([]) == [] + + with patch("agentic_fabric.tools.adapters.resolve_tools", return_value=[]): + assert resolve_strands_tools(["missing"]) == [] + + @patch("agentic_fabric.tools.adapters.resolve_tools") + def test_missing_strands_dependency_skips_tools(self, mock_resolve_tools: MagicMock) -> None: + """Missing Strands should be reported as unavailable adaptation.""" + mock_resolve_tools.return_value = [DummyTool()] + + with patch.dict(sys.modules, {"strands": None}): + assert resolve_strands_tools(["FileWriteTool"]) == [] + + @patch("agentic_fabric.tools.adapters.resolve_tools") + def test_wraps_resolved_tools_with_strands_decorator(self, mock_resolve_tools: MagicMock) -> None: + """Resolved tools should be adapted into callable Strands tool wrappers.""" + tool = DummyTool() + mock_resolve_tools.return_value = [tool] + + fake_strands_module = ModuleType("strands") + fake_strands_module.tool = fake_strands_tool + + with patch.dict(sys.modules, {"strands": fake_strands_module}): + adapted = resolve_strands_tools(["FileWriteTool"]) + + assert len(adapted) == 1 + wrapped = adapted[0] + assert wrapped(file_path="src/game.ts") == "wrote:src/game.ts" + assert tool.calls == [{"file_path": "src/game.ts"}] + assert wrapped.TOOL_SPEC["name"] == "Write_Game_Code_File" + assert wrapped.TOOL_SPEC["description"] == "Write code into the workspace." + assert wrapped.TOOL_SPEC["inputSchema"] == WriteFileArgs.model_json_schema() + + @patch("agentic_fabric.tools.adapters.resolve_tools") + def test_preserves_existing_strands_tool_objects(self, mock_resolve_tools: MagicMock) -> None: + """Existing Strands tool objects should pass through unchanged.""" + existing_tool = SimpleNamespace(TOOL_SPEC={"name": "get_secret_number"}) + mock_resolve_tools.return_value = [existing_tool] + + fake_strands_module = ModuleType("strands") + fake_strands_module.tool = fake_strands_tool + + with patch.dict(sys.modules, {"strands": fake_strands_module}): + adapted = resolve_strands_tools(["get_secret_number"]) + + assert adapted == [existing_tool] + + +class TestToolAdapterHelpers: + """Tests for plain adapter helper functions.""" + + def test_tool_names_are_framework_safe(self) -> None: + assert _tool_name(CallableTool()) == "tool_1_callable_tool" + assert _tool_name(object()) == "object" + + def test_build_runner_invokes_callable_tools(self) -> None: + runner = _build_runner(CallableTool(), "callable_tool", "Call a tool.") + + assert runner(value="x") == "called:x" + assert runner.__name__ == "callable_tool" + assert runner.__doc__ == "Call a tool." + + def test_build_runner_preserves_tool_signature(self) -> None: + runner = _build_runner(SignatureTool(), "signature_tool", "Uses signature inference.") + + assert runner(file_path="README.md", mode="rb") == "rb:README.md" + assert tuple(runner.__signature__.parameters) == ("file_path", "mode") + + def test_invoke_tool_rejects_noncallable_objects(self) -> None: + with pytest.raises(TypeError, match="not callable"): + _invoke_tool(object(), {}) diff --git a/packages/agentic-fabric/tests/test_tool_registry.py b/packages/agentic-fabric/tests/test_tool_registry.py new file mode 100644 index 0000000..0860ee9 --- /dev/null +++ b/packages/agentic-fabric/tests/test_tool_registry.py @@ -0,0 +1,259 @@ +"""Tests for configured tool resolution.""" + +from __future__ import annotations + +import os + +from unittest.mock import MagicMock, patch + +import pytest + +from agentic_fabric.tools.registry import register_tool_factory, resolve_tool, resolve_tools +from agentic_fabric.tools.vendor import VendorCapabilityTool, vendor_capability_tools + + +class TestResolveTool: + """Tests for resolving configured tool names.""" + + @patch("agentic_fabric.tools.registry.importlib.import_module") + def test_resolves_builtin_alias(self, mock_import_module: MagicMock) -> None: + """FileWriteTool should map to the game code writer implementation.""" + mock_tool_class = MagicMock() + mock_tool_instance = MagicMock() + mock_tool_class.return_value = mock_tool_instance + + mock_module = MagicMock() + mock_module.GameCodeWriterTool = mock_tool_class + mock_import_module.return_value = mock_module + + result = resolve_tool("FileWriteTool") + + mock_import_module.assert_called_once_with("agentic_fabric.tools.file_tools") + mock_tool_class.assert_called_once_with() + assert result is mock_tool_instance + + @patch("agentic_fabric.tools.registry.importlib.import_module", side_effect=ImportError("missing crewai")) + def test_builtin_alias_skips_missing_optional_dependency(self, mock_import_module: MagicMock) -> None: + """Built-in aliases should skip cleanly when their optional imports fail.""" + assert resolve_tool("FileWriteTool") is None + mock_import_module.assert_called_once_with("agentic_fabric.tools.file_tools") + + @patch("agentic_fabric.tools.registry.importlib.import_module") + def test_resolves_fully_qualified_reference(self, mock_import_module: MagicMock) -> None: + """Allowed module:attribute references should be resolved dynamically.""" + mock_tool_class = MagicMock() + mock_tool_instance = MagicMock() + mock_tool_class.return_value = mock_tool_instance + + mock_module = MagicMock() + mock_module.CustomTool = mock_tool_class + mock_import_module.return_value = mock_module + + with patch.dict(os.environ, {"AGENTIC_FABRIC_TOOL_IMPORT_ALLOWLIST": "custom.module"}): + result = resolve_tool("custom.module:CustomTool") + + mock_import_module.assert_called_once_with("custom.module") + mock_tool_class.assert_called_once_with() + assert result is mock_tool_instance + + @patch("agentic_fabric.tools.registry.importlib.import_module") + def test_skips_unallowed_fully_qualified_reference(self, mock_import_module: MagicMock) -> None: + """Fully qualified references should require an explicit allowlist.""" + result = resolve_tool("custom.module:CustomTool") + + assert result is None + mock_import_module.assert_not_called() + + @patch("agentic_fabric.tools.registry.importlib.import_module") + def test_resolves_allowed_dotted_reference(self, mock_import_module: MagicMock) -> None: + """Allowed dotted references should resolve module and attribute names.""" + tool = object() + mock_tool_class = MagicMock(return_value=tool) + mock_module = MagicMock() + mock_module.CustomTool = mock_tool_class + mock_import_module.return_value = mock_module + + result = resolve_tool("agentic_fabric.custom.CustomTool") + + mock_import_module.assert_called_once_with("agentic_fabric.custom") + assert result is tool + + @patch("agentic_fabric.tools.registry.importlib.import_module") + def test_skips_unallowed_dotted_reference(self, mock_import_module: MagicMock) -> None: + """Unallowlisted dotted references should not be imported.""" + result = resolve_tool("external_package.tools.CustomTool") + + assert result is None + mock_import_module.assert_not_called() + + @pytest.mark.parametrize("side_effect", [ImportError("missing"), AttributeError("missing attr")]) + @patch("agentic_fabric.tools.registry.importlib.import_module") + def test_returns_none_when_dynamic_import_fails(self, mock_import_module: MagicMock, side_effect: Exception) -> None: + """Failed dynamic imports should be skipped cleanly.""" + mock_import_module.side_effect = side_effect + + assert resolve_tool("agentic_fabric.missing:Tool") is None + + def test_resolves_registered_factory_alias(self) -> None: + """Application code can register safe tool factories directly.""" + tool = object() + factory = MagicMock(return_value=tool) + + register_tool_factory("UnitTestTool", factory, aliases=("unit-test-tool",)) + + assert resolve_tool("unit-test-tool") is tool + factory.assert_called_once_with() + + def test_returns_none_for_unresolved_mcp_tool(self) -> None: + """Unsupported MCP tool references should be skipped cleanly.""" + assert resolve_tool("mcp://git/execute_command") is None + + def test_resolves_vendor_uri_tool(self) -> None: + """Vendor tool URIs should create lazy vendor capability wrappers.""" + tool = resolve_tool("vendor://github/get_file") + + assert isinstance(tool, VendorCapabilityTool) + assert tool.provider == "github" + assert tool.operation == "get_file" + + def test_resolves_vendor_colon_tool(self) -> None: + """Colon-style vendor tool references should also be supported.""" + tool = resolve_tool("vendor:slack:list_messages") + + assert isinstance(tool, VendorCapabilityTool) + assert tool.provider == "slack" + assert tool.operation == "list_messages" + + @pytest.mark.parametrize("tool_name", ["vendor://github", "vendor:github"]) + def test_skips_malformed_vendor_tool_references(self, tool_name: str) -> None: + """Malformed vendor references should behave like unresolved tools.""" + assert resolve_tool(tool_name) is None + + def test_skips_malformed_dotted_reference(self) -> None: + """Malformed dotted references should resolve to None without importing.""" + assert resolve_tool(".Tool") is None + + @patch("agentic_fabric.tools.registry.importlib.import_module") + def test_resolve_tools_deduplicates_aliases(self, mock_import_module: MagicMock) -> None: + """Aliases pointing to the same tool should not instantiate duplicates.""" + mock_tool_class = MagicMock() + mock_tool_class.return_value = MagicMock() + + mock_module = MagicMock() + mock_module.GameCodeReaderTool = mock_tool_class + mock_import_module.return_value = mock_module + + tools = resolve_tools(["FileReadTool", "mcp://filesystem/read_file", "FileReadTool"]) + + assert len(tools) == 1 + mock_tool_class.assert_called_once_with() + + def test_resolve_tools_skips_unresolved_unique_names(self) -> None: + """Unresolved tools should not block later unique tools.""" + tool = object() + factory = MagicMock(return_value=tool) + register_tool_factory("UnitTestResolvedTool", factory) + + assert resolve_tools(["mcp://missing/tool", "UnitTestResolvedTool"]) == [tool] + + +class TestVendorCapabilityTool: + """Tests for the generic vendor-backed tool wrapper.""" + + def test_builds_tool_from_vendor_capability_metadata(self) -> None: + """VendorData capability metadata should become a lazy tool wrapper.""" + metadata = { + "provider": "github", + "operation": "get_file", + "description": "Read a repository file.", + } + + tool = VendorCapabilityTool.from_metadata(metadata) + + assert tool.provider == "github" + assert tool.operation == "get_file" + assert tool.description == "Read a repository file." + assert tool.metadata is metadata + + def test_runs_operation_through_supplied_data(self) -> None: + """Supplied data facade should receive provider and kwargs.""" + data = MagicMock() + data.call.return_value = {"ok": True} + tool = VendorCapabilityTool("github", "get_file", data=data) + + result = tool(path="README.md") + + assert result == {"ok": True} + data.call.assert_called_once_with("get_file", "github", path="README.md") + + def test_runs_operation_through_default_agentic_data(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Without supplied data, the wrapper should create AgenticData lazily.""" + fake_data = MagicMock() + + class FakeAgenticData: + def __new__(cls) -> MagicMock: + return fake_data + + monkeypatch.setattr("agentic_fabric.agentic_data.AgenticData", FakeAgenticData) + + VendorCapabilityTool("github", "get_file")(path="README.md") + + fake_data.call.assert_called_once_with("get_file", "github", path="README.md") + + def test_vendor_capability_tools_reads_vendor_data_catalog(self) -> None: + """VendorData capabilities should be converted into agent tool wrappers.""" + data = MagicMock() + data.capabilities.return_value = [ + {"provider": "github", "operation": "get_file", "description": "Read files"}, + {"provider": "", "operation": "broken"}, + ] + + tools = vendor_capability_tools(data, provider="github", include_unavailable=False) + + data.capabilities.assert_called_once_with("github", include_unavailable=False) + assert len(tools) == 1 + assert tools[0].provider == "github" + assert tools[0].operation == "get_file" + assert tools[0].data is data + + def test_vendor_capability_tools_returns_empty_without_vendor_catalog(self) -> None: + """The import shim should not invent provider behavior.""" + assert vendor_capability_tools(object()) == [] + + def test_vendor_capability_tools_creates_default_agentic_data(self, monkeypatch: pytest.MonkeyPatch) -> None: + """The vendor catalog helper should create AgenticData when none is supplied.""" + fake_data = MagicMock() + fake_data.capabilities.return_value = [{"provider": "slack", "operation": "list_messages"}] + + class FakeAgenticData: + def __new__(cls) -> MagicMock: + return fake_data + + monkeypatch.setattr("agentic_fabric.agentic_data.AgenticData", FakeAgenticData) + + tools = vendor_capability_tools(provider="slack") + + fake_data.capabilities.assert_called_once_with("slack", include_unavailable=True) + assert tools[0].provider == "slack" + + def test_builds_tool_from_getter_style_metadata(self) -> None: + """Capability metadata can expose values through a get method.""" + + class GetterMetadata: + def get(self, key: str, default: str) -> str: + values = {"provider": "aws", "operation": "get_secret"} + return values.get(key, default) + + tool = VendorCapabilityTool.from_metadata(GetterMetadata()) + + assert tool.provider == "aws" + assert tool.operation == "get_secret" + + def test_builds_tool_from_attribute_metadata(self) -> None: + """Capability metadata can expose values as attributes.""" + metadata = type("Metadata", (), {"provider": "github", "operation": "list_repos"})() + + tool = VendorCapabilityTool.from_metadata(metadata) + + assert tool.provider == "github" + assert tool.operation == "list_repos" diff --git a/packages/agentic-fabric/tests/test_tools_init.py b/packages/agentic-fabric/tests/test_tools_init.py new file mode 100644 index 0000000..895e44a --- /dev/null +++ b/packages/agentic-fabric/tests/test_tools_init.py @@ -0,0 +1,92 @@ +"""Tests for lazy tool package exports.""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from agentic_fabric import tools + + +class FakeTool: + """Constructible fake tool used by lazy loader tests.""" + + +def test_getattr_lazily_loads_declared_tool(monkeypatch: pytest.MonkeyPatch) -> None: + calls: list[tuple[str, str]] = [] + + def fake_load_attr(module_name: str, attr_name: str) -> type[FakeTool]: + calls.append((module_name, attr_name)) + return FakeTool + + monkeypatch.setattr(tools, "_load_attr", fake_load_attr) + + assert tools.__getattr__("GameCodeReaderTool") is FakeTool + assert tools.__getattr__("ScrapeWebsiteTool") is FakeTool + assert tools.__getattr__("VendorCapabilityTool") is FakeTool + assert calls == [ + ("agentic_fabric.tools.file_tools", "GameCodeReaderTool"), + ("agentic_fabric.tools.scraping_tools", "ScrapeWebsiteTool"), + ("agentic_fabric.tools.vendor", "VendorCapabilityTool"), + ] + + +def test_load_attr_imports_module_attribute(monkeypatch: pytest.MonkeyPatch) -> None: + imported: list[str] = [] + + class FakeModule: + Tool = FakeTool + + def fake_import_module(module_name: str) -> type[FakeModule]: + imported.append(module_name) + return FakeModule + + monkeypatch.setattr(tools, "import_module", fake_import_module) + + assert tools._load_attr("example.tools", "Tool") is FakeTool + assert imported == ["example.tools"] + + +def test_getattr_rejects_unknown_name() -> None: + with pytest.raises(AttributeError, match="does-not-exist"): + tools.__getattr__("does-not-exist") + + +def test_tool_collection_helpers_instantiate_lazy_tools(monkeypatch: pytest.MonkeyPatch) -> None: + def fake_load_attr(module_name: str, attr_name: str) -> type[FakeTool]: + assert module_name in {"agentic_fabric.tools.file_tools", "agentic_fabric.tools.scraping_tools"} + assert attr_name.endswith("Tool") + return FakeTool + + monkeypatch.setattr(tools, "_load_attr", fake_load_attr) + + file_tools = tools.get_file_tools() + scraping_tools = tools.get_scraping_tools() + + assert len(file_tools) == 3 + assert all(isinstance(tool, FakeTool) for tool in file_tools) + assert len(scraping_tools) == 2 + assert all(isinstance(tool, FakeTool) for tool in scraping_tools) + + +def test_get_all_tools_suppresses_unavailable_optional_tools(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(tools, "get_file_tools", lambda: ["file"]) + monkeypatch.setattr(tools, "get_scraping_tools", lambda: (_ for _ in ()).throw(ImportError("missing scraping"))) + monkeypatch.setattr(tools, "_load_attr", lambda *_: (_ for _ in ()).throw(RuntimeError("missing"))) + + assert tools.get_all_tools() == ["file"] + + +def test_get_all_tools_includes_vendor_capability_tools_when_available(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(tools, "get_file_tools", lambda: ["file"]) + monkeypatch.setattr(tools, "get_scraping_tools", lambda: ["scrape"]) + + def fake_load_attr(module_name: str, attr_name: str) -> Any: + assert module_name == "agentic_fabric.tools.vendor" + assert attr_name == "vendor_capability_tools" + return lambda **_: ["vendor"] + + monkeypatch.setattr(tools, "_load_attr", fake_load_attr) + + assert tools.get_all_tools() == ["file", "scrape", "vendor"] diff --git a/packages/agentic-fabric/tests/test_utils_files.py b/packages/agentic-fabric/tests/test_utils_files.py new file mode 100644 index 0000000..54bf594 --- /dev/null +++ b/packages/agentic-fabric/tests/test_utils_files.py @@ -0,0 +1,26 @@ +"""Tests for utility file helpers.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from agentic_fabric.utils.files import load_config + + +def test_load_config_reads_utf8_yaml(tmp_path: Path) -> None: + """YAML files should be decoded as UTF-8 across platforms.""" + config_file = tmp_path / "config.yaml" + config_file.write_text("description: café\n", encoding="utf-8") + + assert load_config(config_file) == {"description": "café"} + + +def test_load_config_rejects_non_mapping_yaml(tmp_path: Path) -> None: + """Top-level YAML sequences should fail with a clear error.""" + config_file = tmp_path / "config.yaml" + config_file.write_text("- one\n- two\n", encoding="utf-8") + + with pytest.raises(TypeError, match="Expected mapping"): + load_config(config_file) diff --git a/packages/agentic-fabric/tests/test_workflow_contracts.py b/packages/agentic-fabric/tests/test_workflow_contracts.py new file mode 100644 index 0000000..07d669d --- /dev/null +++ b/packages/agentic-fabric/tests/test_workflow_contracts.py @@ -0,0 +1,50 @@ +"""Tests for repository workflow contracts.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import yaml + + +WORKSPACE_ROOT = Path(__file__).resolve().parents[3] + + +def load_workflow(name: str) -> dict[str, Any]: + """Load a GitHub Actions workflow without YAML 1.1 boolean coercion issues.""" + return yaml.safe_load((WORKSPACE_ROOT / ".github" / "workflows" / name).read_text(encoding="utf-8")) + + +def test_automerge_uses_base_context_without_checkout_or_secrets() -> None: + """Automerge should match the split-package base-context workflow contract.""" + workflow = load_workflow("automerge.yml") + automerge = workflow["jobs"]["automerge"] + steps = automerge["steps"] + + assert workflow[True] == { + "pull_request_target": {"types": ["opened", "reopened", "synchronize", "ready_for_review"]} + } + assert workflow["permissions"] == {"contents": "write", "pull-requests": "write"} + assert "github-actions[bot]" in automerge["if"] + assert all("uses" not in step or "actions/checkout" not in step["uses"] for step in steps) + assert steps == [ + { + "name": "Enable auto-merge (squash)", + "env": { + "GH_TOKEN": "${{ github.token }}", + "PR_URL": "${{ github.event.pull_request.html_url }}", + }, + "run": 'gh pr merge --auto --squash "$PR_URL"', + } + ] + + +def test_ci_and_cd_quality_run_security_audit_and_examples() -> None: + """CI and CD quality gates should include auditing and shipped examples.""" + for workflow_name in ("ci.yml", "cd.yml"): + workflow = load_workflow(workflow_name) + quality_steps = workflow["jobs"]["quality"]["steps"] + tox_commands = [step["run"] for step in quality_steps if step.get("run", "").startswith("tox -e ")] + + assert tox_commands == ["tox -e lint,typecheck,audit,examples,coverage,plugin,docs,build"] diff --git a/packages/agentic-fabric/tests/test_yaml_config_errors.py b/packages/agentic-fabric/tests/test_yaml_config_errors.py new file mode 100644 index 0000000..b460ae1 --- /dev/null +++ b/packages/agentic-fabric/tests/test_yaml_config_errors.py @@ -0,0 +1,398 @@ +"""Tests for YAML configuration parsing error handling.""" + +from __future__ import annotations + +import logging + +from pathlib import Path + +import pytest +import yaml + +from agentic_fabric.core.discovery import ( + get_fabric_agent_config, + get_framework_from_config_dir, + load_manifest, +) + + +def write_empty_agent_task_files(config_dir: Path) -> None: + """Write minimal valid agents/tasks YAML files for config-loading tests.""" + (config_dir / "agents.yaml").write_text("{}\n", encoding="utf-8") + (config_dir / "tasks.yaml").write_text("{}\n", encoding="utf-8") + + +class TestLoadManifestErrors: + """Test error handling in load_manifest.""" + + def test_missing_manifest_file_raises(self, tmp_path: Path) -> None: + """Should raise FileNotFoundError for missing manifest.""" + with pytest.raises(FileNotFoundError): + load_manifest(tmp_path / "nonexistent") + + def test_empty_manifest_returns_empty_dict(self, tmp_path: Path) -> None: + """Empty YAML file should return empty dict, not None.""" + manifest_file = tmp_path / "manifest.yaml" + manifest_file.write_text("") + result = load_manifest(tmp_path) + assert result == {} + + def test_manifest_with_only_comments_returns_empty_dict(self, tmp_path: Path) -> None: + """YAML with only comments should return empty dict.""" + manifest_file = tmp_path / "manifest.yaml" + manifest_file.write_text("# just a comment\n# another comment\n") + result = load_manifest(tmp_path) + assert result == {} + + def test_invalid_yaml_raises_error(self, tmp_path: Path) -> None: + """Badly formed YAML should raise a yaml error.""" + manifest_file = tmp_path / "manifest.yaml" + manifest_file.write_text(":\n bad:\n - ][invalid yaml") + with pytest.raises(yaml.YAMLError): + load_manifest(tmp_path) + + def test_non_mapping_manifest_raises_type_error(self, tmp_path: Path) -> None: + """Manifest root must be a mapping.""" + manifest_file = tmp_path / "manifest.yaml" + manifest_file.write_text("- not\n- a mapping\n", encoding="utf-8") + + with pytest.raises(TypeError, match="must contain a mapping"): + load_manifest(tmp_path) + + def test_manifest_with_no_fabric_agents_key(self, tmp_path: Path) -> None: + """Manifest without 'fabric_agents' key should parse without error.""" + manifest_file = tmp_path / "manifest.yaml" + manifest_file.write_text("name: test_package\ndescription: A test\n") + result = load_manifest(tmp_path) + assert result.get("name") == "test_package" + assert result.get("fabric_agents") is None + + +class TestGetFabricAgentConfigErrors: + """Test error handling in get_fabric_agent_config.""" + + def test_fabric_agent_not_in_manifest_raises_value_error(self, tmp_path: Path) -> None: + """Requesting a non-existent fabric_agent should raise ValueError.""" + manifest_file = tmp_path / "manifest.yaml" + manifest_file.write_text( + "fabric_agents:\n" + " existing_fabric_agent:\n" + " description: An existing fabric_agent\n" + " agents: fabric_agents/existing/agents.yaml\n" + " tasks: fabric_agents/existing/tasks.yaml\n" + ) + with pytest.raises(ValueError, match="Fabric agent 'nonexistent' not found"): + get_fabric_agent_config(tmp_path, "nonexistent") + + def test_error_lists_available_fabric_agents(self, tmp_path: Path) -> None: + """ValueError message should list available fabric_agent names.""" + manifest_file = tmp_path / "manifest.yaml" + manifest_file.write_text( + "fabric_agents:\n" + " alpha_fabric_agent:\n" + " agents: a.yaml\n" + " tasks: t.yaml\n" + " beta_fabric_agent:\n" + " agents: a.yaml\n" + " tasks: t.yaml\n" + ) + with pytest.raises(ValueError, match="alpha_fabric_agent") as exc_info: + get_fabric_agent_config(tmp_path, "missing") + assert "beta_fabric_agent" in str(exc_info.value) + + def test_missing_agents_yaml_raises_file_not_found(self, tmp_path: Path) -> None: + """Missing agents YAML should fail as a manifest contract error.""" + manifest_file = tmp_path / "manifest.yaml" + manifest_file.write_text( + "fabric_agents:\n" + " test_fabric_agent:\n" + " description: Test\n" + " agents: nonexistent_agents.yaml\n" + " tasks: tasks.yaml\n" + ) + (tmp_path / "tasks.yaml").write_text("{}\n", encoding="utf-8") + + with pytest.raises(FileNotFoundError, match="Agents file not found"): + get_fabric_agent_config(tmp_path, "test_fabric_agent") + + def test_missing_tasks_yaml_raises_file_not_found(self, tmp_path: Path) -> None: + """Missing tasks YAML should fail as a manifest contract error.""" + manifest_file = tmp_path / "manifest.yaml" + manifest_file.write_text( + "fabric_agents:\n" + " test_fabric_agent:\n" + " description: Test\n" + " agents: agents.yaml\n" + " tasks: nonexistent_tasks.yaml\n" + ) + (tmp_path / "agents.yaml").write_text("{}\n", encoding="utf-8") + + with pytest.raises(FileNotFoundError, match="Tasks file not found"): + get_fabric_agent_config(tmp_path, "test_fabric_agent") + + def test_non_mapping_agents_or_tasks_yaml_raises_type_error(self, tmp_path: Path) -> None: + """Agents and tasks YAML roots must be mappings.""" + manifest_file = tmp_path / "manifest.yaml" + manifest_file.write_text( + "fabric_agents:\n" + " test_fabric_agent:\n" + " agents: agents.yaml\n" + " tasks: tasks.yaml\n", + encoding="utf-8", + ) + (tmp_path / "agents.yaml").write_text("- not\n- a mapping\n", encoding="utf-8") + (tmp_path / "tasks.yaml").write_text("{}\n", encoding="utf-8") + + with pytest.raises(TypeError, match="mapping of agents"): + get_fabric_agent_config(tmp_path, "test_fabric_agent") + + (tmp_path / "agents.yaml").write_text("{}\n", encoding="utf-8") + (tmp_path / "tasks.yaml").write_text("- not\n- a mapping\n", encoding="utf-8") + + with pytest.raises(TypeError, match="mapping of tasks"): + get_fabric_agent_config(tmp_path, "test_fabric_agent") + + def test_empty_agents_and_tasks_yaml_return_empty_mappings(self, tmp_path: Path) -> None: + """Empty but present agents/tasks files should load as empty mappings.""" + manifest_file = tmp_path / "manifest.yaml" + manifest_file.write_text( + "fabric_agents:\n" + " test_fabric_agent:\n" + " agents: agents.yaml\n" + " tasks: tasks.yaml\n", + encoding="utf-8", + ) + (tmp_path / "agents.yaml").write_text("", encoding="utf-8") + (tmp_path / "tasks.yaml").write_text("", encoding="utf-8") + + config = get_fabric_agent_config(tmp_path, "test_fabric_agent") + + assert config["agents"] == {} + assert config["tasks"] == {} + + def test_agents_and_tasks_yaml_are_read_as_utf8(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Agents and tasks YAML should use explicit UTF-8 decoding.""" + manifest_file = tmp_path / "manifest.yaml" + manifest_file.write_text( + "fabric_agents:\n" + " test_fabric_agent:\n" + " agents: agents.yaml\n" + " tasks: tasks.yaml\n", + encoding="utf-8", + ) + (tmp_path / "agents.yaml").write_text("agent:\n role: Café reviewer\n", encoding="utf-8") + (tmp_path / "tasks.yaml").write_text("task:\n description: Résumé review\n", encoding="utf-8") + original_read_text = Path.read_text + encodings: list[str | None] = [] + + def tracking_read_text(self: Path, *args, **kwargs) -> str: + if self.name in {"agents.yaml", "tasks.yaml"}: + encodings.append(kwargs.get("encoding")) + return original_read_text(self, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", tracking_read_text) + + config = get_fabric_agent_config(tmp_path, "test_fabric_agent") + + assert config["agents"]["agent"]["role"] == "Café reviewer" + assert config["tasks"]["task"]["description"] == "Résumé review" + assert encodings == ["utf-8", "utf-8"] + + def test_empty_fabric_agents_section_raises_for_any_fabric_agent(self, tmp_path: Path) -> None: + """Empty fabric_agents section should raise ValueError.""" + manifest_file = tmp_path / "manifest.yaml" + manifest_file.write_text("fabric_agents: {}\n") + with pytest.raises(ValueError, match="not found"): + get_fabric_agent_config(tmp_path, "anything") + + def test_knowledge_paths_resolves_relative(self, tmp_path: Path) -> None: + """Knowledge paths should resolve relative to config dir.""" + # Create the knowledge directory + knowledge_dir = tmp_path / "knowledge" + knowledge_dir.mkdir() + (knowledge_dir / "docs.md").write_text("# Docs") + + manifest_file = tmp_path / "manifest.yaml" + manifest_file.write_text( + "fabric_agents:\n test_fabric_agent:\n agents: agents.yaml\n tasks: tasks.yaml\n knowledge:\n - knowledge\n" + ) + write_empty_agent_task_files(tmp_path) + config = get_fabric_agent_config(tmp_path, "test_fabric_agent") + assert len(config["knowledge_paths"]) == 1 + assert config["knowledge_paths"][0] == knowledge_dir + + def test_nonexistent_knowledge_paths_excluded(self, tmp_path: Path) -> None: + """Non-existent knowledge paths should be silently excluded.""" + manifest_file = tmp_path / "manifest.yaml" + manifest_file.write_text( + "fabric_agents:\n" + " test_fabric_agent:\n" + " agents: agents.yaml\n" + " tasks: tasks.yaml\n" + " knowledge:\n" + " - missing_dir\n" + ) + write_empty_agent_task_files(tmp_path) + config = get_fabric_agent_config(tmp_path, "test_fabric_agent") + assert config["knowledge_paths"] == [] + + def test_knowledge_file_paths_excluded(self, tmp_path: Path) -> None: + """Knowledge paths must be directories, not plain files.""" + (tmp_path / "knowledge.md").write_text("# Not a directory") + manifest_file = tmp_path / "manifest.yaml" + manifest_file.write_text( + "fabric_agents:\n" + " test_fabric_agent:\n" + " agents: agents.yaml\n" + " tasks: tasks.yaml\n" + " knowledge:\n" + " - knowledge.md\n" + ) + write_empty_agent_task_files(tmp_path) + + config = get_fabric_agent_config(tmp_path, "test_fabric_agent") + + assert config["knowledge_paths"] == [] + + def test_agents_path_cannot_escape_config_dir(self, tmp_path: Path) -> None: + """Manifest agents paths must stay inside the config directory.""" + manifest_file = tmp_path / "manifest.yaml" + manifest_file.write_text( + "fabric_agents:\n" + " test_fabric_agent:\n" + " agents: ../outside_agents.yaml\n" + " tasks: tasks.yaml\n" + ) + + with pytest.raises(ValueError, match="Manifest path must be relative"): + get_fabric_agent_config(tmp_path, "test_fabric_agent") + + def test_agents_symlink_path_cannot_escape_config_dir(self, tmp_path: Path) -> None: + """Resolved symlinks must not escape the config directory.""" + config_dir = tmp_path / ".fabric" + config_dir.mkdir() + outside = tmp_path / "outside_agents.yaml" + outside.write_text("agent: {}\n", encoding="utf-8") + link_path = config_dir / "agents-link.yaml" + try: + link_path.symlink_to(outside) + except (OSError, NotImplementedError) as exc: + pytest.skip(f"symlinks unavailable: {exc}") + + manifest_file = config_dir / "manifest.yaml" + manifest_file.write_text( + "fabric_agents:\n" + " test_fabric_agent:\n" + " agents: agents-link.yaml\n" + " tasks: tasks.yaml\n", + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="Manifest path escapes config directory"): + get_fabric_agent_config(config_dir, "test_fabric_agent") + + def test_knowledge_path_cannot_escape_config_dir(self, tmp_path: Path) -> None: + """Manifest knowledge paths must stay inside the config directory.""" + manifest_file = tmp_path / "manifest.yaml" + manifest_file.write_text( + "fabric_agents:\n" + " test_fabric_agent:\n" + " agents: agents.yaml\n" + " tasks: tasks.yaml\n" + " knowledge:\n" + " - ../knowledge\n" + ) + write_empty_agent_task_files(tmp_path) + + with pytest.raises(ValueError, match="Manifest path must be relative"): + get_fabric_agent_config(tmp_path, "test_fabric_agent") + + +class TestGetFrameworkFromConfigDir: + """Test edge cases for get_framework_from_config_dir.""" + + def test_unknown_directory_name_returns_none(self) -> None: + """Unknown directory name should return None.""" + result = get_framework_from_config_dir(Path("/path/.unknown")) + assert result is None + + def test_non_hidden_directory_returns_none(self) -> None: + """Non-hidden directory name should return None.""" + result = get_framework_from_config_dir(Path("/path/crewai")) + assert result is None + + def test_all_known_dirs_return_expected_framework(self) -> None: + """All known framework directories should map correctly.""" + expected = { + ".fabric": None, + ".crewai": "crewai", + ".langgraph": "langgraph", + ".strands": "strands", + } + for dir_name, framework in expected.items(): + result = get_framework_from_config_dir(Path(f"/any/path/{dir_name}")) + assert result == framework, f"Expected {framework} for {dir_name}, got {result}" + + +class TestCrewConfigFrameworkConflict: + """Test framework conflict warnings in get_fabric_agent_config.""" + + def test_framework_mismatch_logs_warning(self, tmp_path: Path, caplog) -> None: + """When manifest preferred_framework differs from directory, warn.""" + crewai_dir = tmp_path / ".crewai" + crewai_dir.mkdir() + + manifest_file = crewai_dir / "manifest.yaml" + manifest_file.write_text( + "fabric_agents:\n" + " test_fabric_agent:\n" + " description: Test\n" + " agents: agents.yaml\n" + " tasks: tasks.yaml\n" + " preferred_framework: strands\n" + ) + write_empty_agent_task_files(crewai_dir) + with caplog.at_level(logging.WARNING): + config = get_fabric_agent_config(crewai_dir, "test_fabric_agent") + assert "preferred_framework=strands" in caplog.text + assert "requires crewai" in caplog.text + # required_framework should still be crewai (directory wins) + assert config["required_framework"] == "crewai" + + def test_matching_preferred_framework_no_warning(self, tmp_path: Path, caplog) -> None: + """When preferred_framework matches directory, no warning.""" + crewai_dir = tmp_path / ".crewai" + crewai_dir.mkdir() + + manifest_file = crewai_dir / "manifest.yaml" + manifest_file.write_text( + "fabric_agents:\n" + " test_fabric_agent:\n" + " description: Test\n" + " agents: agents.yaml\n" + " tasks: tasks.yaml\n" + " preferred_framework: crewai\n" + ) + write_empty_agent_task_files(crewai_dir) + with caplog.at_level(logging.WARNING): + get_fabric_agent_config(crewai_dir, "test_fabric_agent") + assert "preferred_framework" not in caplog.text + + def test_preferred_auto_no_warning(self, tmp_path: Path, caplog) -> None: + """preferred_framework='auto' should not trigger warning.""" + crewai_dir = tmp_path / ".crewai" + crewai_dir.mkdir() + + manifest_file = crewai_dir / "manifest.yaml" + manifest_file.write_text( + "fabric_agents:\n" + " test_fabric_agent:\n" + " description: Test\n" + " agents: agents.yaml\n" + " tasks: tasks.yaml\n" + " preferred_framework: auto\n" + ) + write_empty_agent_task_files(crewai_dir) + with caplog.at_level(logging.WARNING): + get_fabric_agent_config(crewai_dir, "test_fabric_agent") + assert "preferred_framework" not in caplog.text diff --git a/packages/pytest-agentic-fabric/CHANGELOG.md b/packages/pytest-agentic-fabric/CHANGELOG.md new file mode 100644 index 0000000..8b7d992 --- /dev/null +++ b/packages/pytest-agentic-fabric/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +All notable changes to this project are documented by release-please. diff --git a/packages/pytest-agentic-fabric/README.md b/packages/pytest-agentic-fabric/README.md new file mode 100644 index 0000000..c4173b5 --- /dev/null +++ b/packages/pytest-agentic-fabric/README.md @@ -0,0 +1,38 @@ +# pytest-agentic-fabric + +Pytest fixtures for projects built on `agentic-fabric`. + +Fixtures: + +- `agentic_runtime_available`: importability predicate for optional runtimes. +- `agentic_runtime_registry`: isolated mutable registry for tests. +- `agentic_runtime_modules`: known runtime import module names. +- `agentic_mock_runtime`: installs fake runtime modules into `sys.modules`. +- `agentic_fabric_mocker` / `fabric_mocker`: `FabricMocker` helper for + mocking CrewAI, LangGraph, Strands, and agentic-fabric internals. +- `mock_agentic_frameworks`: installs fake modules for all optional runtimes. +- `mock_crewai`, `mock_langgraph`, `mock_strands`: installs fake modules for + one optional runtime path. +- `agentic_fabric_agent_config`: minimal framework-neutral fabric agent config. +- `simple_agent_config`, `simple_task_config`: reusable agent and task config + mappings. +- `simple_fabric_agent_config`, `multi_agent_fabric_agent_config`, + `fabric_agent_with_knowledge`: reusable fabric agent config mappings. +- `temp_fabric_dir`: temporary framework-agnostic `.fabric` directory. +- `agentic_workspace`: temporary workspace with a discoverable `.fabric` package. + +`agentic_workspace` serializes the current `agentic_fabric_agent_config` +fixture into `packages/sample/.fabric/manifest.yaml` plus matching +`agents.yaml` and `tasks.yaml` files. Override `agentic_fabric_agent_config` +in a test module or `conftest.py` to create a custom workspace while keeping +the discovery layout consistent. + +Tests marked `@pytest.mark.agentic_e2e` or +`@pytest.mark.agentic_runtime("crewai")` are skipped unless +`--agentic-e2e` is passed. + +`FabricMocker` exposes `mock_crewai()`, `mock_langgraph()`, `mock_strands()`, +`mock_all_frameworks()`, framework-specific patch helpers, and +`patch_get_llm()`, `patch_discover_packages()`, +`patch_get_fabric_agent_config()`, and `patch_run_fabric_agent_auto()` for +tests that need to isolate fabric orchestration from optional runtime imports. diff --git a/packages/pytest-agentic-fabric/pyproject.toml b/packages/pytest-agentic-fabric/pyproject.toml new file mode 100644 index 0000000..7a0e197 --- /dev/null +++ b/packages/pytest-agentic-fabric/pyproject.toml @@ -0,0 +1,61 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "pytest-agentic-fabric" +version = "0.1.0" +description = "Pytest fixtures for projects built on agentic-fabric" +requires-python = ">=3.11" +license = { text = "MIT" } +readme = "README.md" +authors = [{ name = "Jon Bogaty", email = "jon@jonbogaty.com" }] +maintainers = [{ name = "Jon Bogaty", email = "jon@jonbogaty.com" }] +classifiers = [ + "Framework :: Pytest", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Typing :: Typed", +] +dependencies = [ + "pytest-mock>=3.15.1", + "pytest>=9.1.1", + "pyyaml>=6.0.3", +] + +[project.optional-dependencies] +tests = [ + "pytest-asyncio>=1.4.0", + "pytest-mock>=3.15.1", + "pytest>=9.1.1", +] +typing = [ + "mypy>=2.1.0", +] +dev = [ + "pytest-agentic-fabric[tests,typing]", + "ruff>=0.15.20", +] + +[project.entry-points.pytest11] +agentic_fabric = "pytest_agentic_fabric.plugin" + +[project.urls] +Documentation = "https://jonbogaty.com/agentic-fabric/" +Issues = "https://github.com/jbcom/agentic-fabric/issues" +Source = "https://github.com/jbcom/agentic-fabric" +Changelog = "https://github.com/jbcom/agentic-fabric/blob/main/packages/pytest-agentic-fabric/CHANGELOG.md" + +[tool.hatch.build.targets.wheel] +packages = ["src/pytest_agentic_fabric"] + +[tool.pytest.ini_options] +addopts = ["-ra", "--strict-markers", "--strict-config"] +testpaths = ["tests"] +xfail_strict = true +asyncio_default_fixture_loop_scope = "function" diff --git a/packages/pytest-agentic-fabric/src/pytest_agentic_fabric/__init__.py b/packages/pytest-agentic-fabric/src/pytest_agentic_fabric/__init__.py new file mode 100644 index 0000000..290e0d3 --- /dev/null +++ b/packages/pytest-agentic-fabric/src/pytest_agentic_fabric/__init__.py @@ -0,0 +1,29 @@ +"""Pytest support for agentic-fabric.""" + +from __future__ import annotations + +from importlib.metadata import PackageNotFoundError, version + +from pytest_agentic_fabric.mocking import ( + ALL_FRAMEWORK_MODULES, + CREWAI_MODULES, + LANGGRAPH_MODULES, + RUNTIME_MODULES, + STRANDS_MODULES, + FabricMocker, +) + +__all__ = [ + "ALL_FRAMEWORK_MODULES", + "CREWAI_MODULES", + "LANGGRAPH_MODULES", + "RUNTIME_MODULES", + "STRANDS_MODULES", + "FabricMocker", + "__version__", +] + +try: + __version__ = version("pytest-agentic-fabric") +except PackageNotFoundError: + __version__ = "0.0.0" diff --git a/packages/pytest-agentic-fabric/src/pytest_agentic_fabric/mocking.py b/packages/pytest-agentic-fabric/src/pytest_agentic_fabric/mocking.py new file mode 100644 index 0000000..5551b43 --- /dev/null +++ b/packages/pytest-agentic-fabric/src/pytest_agentic_fabric/mocking.py @@ -0,0 +1,343 @@ +"""Mocking helpers for tests that exercise agentic-fabric integrations.""" + +from __future__ import annotations + +import sys + +from collections.abc import Generator, Iterable +from dataclasses import dataclass, field +from types import ModuleType +from typing import TYPE_CHECKING, Any + +import pytest + + +if TYPE_CHECKING: + from pytest_mock import MockerFixture + + +CREWAI_MODULES: tuple[str, ...] = ( + "crewai", + "crewai.knowledge", + "crewai.knowledge.source", + "crewai.knowledge.source.text_file_knowledge_source", +) +LANGGRAPH_MODULES: tuple[str, ...] = ("langgraph", "langgraph.prebuilt", "langchain_anthropic") +STRANDS_MODULES: tuple[str, ...] = ("strands",) +RUNTIME_MODULES: dict[str, tuple[str, ...]] = { + "crewai": CREWAI_MODULES, + "langgraph": LANGGRAPH_MODULES, + "strands": STRANDS_MODULES, +} +ALL_FRAMEWORK_MODULES: tuple[str, ...] = (*CREWAI_MODULES, *LANGGRAPH_MODULES, *STRANDS_MODULES) + + +@dataclass +class FabricMocker: + """Convenience wrapper around pytest-mock for optional agent runtime tests.""" + + mocker: MockerFixture + mocked_modules: dict[str, ModuleType] = field(default_factory=dict) + _original_modules: dict[str, ModuleType] = field(default_factory=dict) + _original_parent_attrs: dict[tuple[str, str], Any] = field(default_factory=dict) + _missing_parent_attrs: set[tuple[str, str]] = field(default_factory=set) + + @property + def MagicMock(self) -> Any: + """Return ``mocker.MagicMock`` for direct test customization.""" + return self.mocker.MagicMock + + @property + def Mock(self) -> Any: + """Return ``mocker.Mock`` for direct test customization.""" + return self.mocker.Mock + + @property + def patch(self) -> Any: + """Return ``mocker.patch`` for direct test customization.""" + return self.mocker.patch + + @property + def spy(self) -> Any: + """Return ``mocker.spy`` for direct test customization.""" + return self.mocker.spy + + @property + def stub(self) -> Any: + """Return ``mocker.stub`` for direct test customization.""" + return self.mocker.stub + + def mock_module(self, module_name: str) -> ModuleType: + """Install one fake import module and return it.""" + if module_name in self.mocked_modules: + return self.mocked_modules[module_name] + + module = ModuleType(module_name) + if any(name.startswith(f"{module_name}.") for name in ALL_FRAMEWORK_MODULES): + module.__path__ = [] # type: ignore[attr-defined] + + original = sys.modules.get(module_name) + if isinstance(original, ModuleType): + self._original_modules[module_name] = original + sys.modules[module_name] = module + self.mocked_modules[module_name] = module + self._attach_to_parent(module_name, module) + return module + + def mock_modules(self, module_names: Iterable[str]) -> dict[str, ModuleType]: + """Install multiple fake import modules.""" + return {name: self.mock_module(name) for name in module_names} + + def restore_modules(self) -> None: + """Restore fake modules and parent module attributes.""" + for module_name in list(self.mocked_modules): + if module_name in self._original_modules: + sys.modules[module_name] = self._original_modules[module_name] + else: + sys.modules.pop(module_name, None) + + for parent_name, child_name in self._missing_parent_attrs: + parent = sys.modules.get(parent_name) + if parent is not None and hasattr(parent, child_name): + delattr(parent, child_name) + + for (parent_name, child_name), value in self._original_parent_attrs.items(): + parent = sys.modules.get(parent_name) + if parent is not None: + setattr(parent, child_name, value) + + self.mocked_modules.clear() + self._original_modules.clear() + self._original_parent_attrs.clear() + self._missing_parent_attrs.clear() + + def mock_crewai(self) -> dict[str, ModuleType]: + """Install fake CrewAI modules with common entry points.""" + modules = self.mock_modules(CREWAI_MODULES) + setattr(modules["crewai"], "Agent", self.mocker.MagicMock()) + setattr(modules["crewai"], "Crew", self.mocker.MagicMock()) + setattr(modules["crewai"], "Task", self.mocker.MagicMock()) + setattr(modules["crewai"], "Process", self.mocker.MagicMock()) + setattr( + modules["crewai.knowledge.source.text_file_knowledge_source"], + "TextFileKnowledgeSource", + self.mocker.MagicMock(), + ) + return modules + + def mock_crewai_agent(self, **kwargs: Any) -> Any: + """Create a standalone CrewAI agent mock.""" + return self._mock_with_attrs(**kwargs) + + def mock_crewai_task(self, **kwargs: Any) -> Any: + """Create a standalone CrewAI task mock.""" + return self._mock_with_attrs(**kwargs) + + def mock_crewai_crew(self, result: str = "Test result", **kwargs: Any) -> Any: + """Create a CrewAI crew mock whose ``kickoff`` returns a raw result.""" + mock_crew = self._mock_with_attrs(**kwargs) + mock_result = self.mocker.MagicMock() + mock_result.raw = result + mock_crew.kickoff.return_value = mock_result + return mock_crew + + def patch_crewai_agent(self) -> Any: + """Patch ``crewai.Agent`` and return the patch mock.""" + self._ensure_crewai() + return self.mocker.patch("crewai.Agent", create=True) + + def patch_crewai_task(self) -> Any: + """Patch ``crewai.Task`` and return the patch mock.""" + self._ensure_crewai() + return self.mocker.patch("crewai.Task", create=True) + + def patch_crewai_crew(self) -> Any: + """Patch ``crewai.Crew`` and return the patch mock.""" + self._ensure_crewai() + return self.mocker.patch("crewai.Crew", create=True) + + def patch_crewai_process(self) -> Any: + """Patch ``crewai.Process`` and return the patch mock.""" + self._ensure_crewai() + return self.mocker.patch("crewai.Process", create=True) + + def patch_knowledge_source(self) -> Any: + """Patch or return the CrewAI text-file knowledge source mock.""" + self._ensure_crewai() + module = self.mocked_modules["crewai.knowledge.source.text_file_knowledge_source"] + return getattr(module, "TextFileKnowledgeSource") + + def mock_langgraph(self) -> dict[str, ModuleType]: + """Install fake LangGraph modules with common entry points.""" + modules = self.mock_modules(LANGGRAPH_MODULES) + setattr(modules["langgraph.prebuilt"], "create_react_agent", self.mocker.MagicMock()) + setattr(modules["langchain_anthropic"], "ChatAnthropic", self.mocker.MagicMock()) + return modules + + def patch_create_react_agent(self) -> Any: + """Patch or return ``langgraph.prebuilt.create_react_agent``.""" + self._ensure_langgraph() + return getattr(self.mocked_modules["langgraph.prebuilt"], "create_react_agent") + + def patch_chat_anthropic(self) -> Any: + """Patch or return ``langchain_anthropic.ChatAnthropic``.""" + self._ensure_langgraph() + return getattr(self.mocked_modules["langchain_anthropic"], "ChatAnthropic") + + def mock_langgraph_graph(self, result: str = "Test response") -> Any: + """Create a LangGraph graph mock whose ``invoke`` returns one message.""" + mock_graph = self.mocker.MagicMock() + mock_message = self.mocker.MagicMock() + mock_message.content = result + mock_graph.invoke.return_value = {"messages": [mock_message]} + return mock_graph + + def mock_strands(self) -> dict[str, ModuleType]: + """Install fake Strands modules with common entry points.""" + modules = self.mock_modules(STRANDS_MODULES) + setattr(modules["strands"], "Agent", self.mocker.MagicMock()) + return modules + + def patch_strands_agent(self) -> Any: + """Patch or return ``strands.Agent``.""" + self._ensure_strands() + return getattr(self.mocked_modules["strands"], "Agent") + + def mock_strands_agent(self, result: str = "Test response") -> Any: + """Create a Strands agent mock whose call returns text.""" + mock_agent = self.mocker.MagicMock() + mock_agent.return_value = result + return mock_agent + + def mock_all_frameworks(self) -> dict[str, ModuleType]: + """Install all supported optional runtime modules.""" + return {**self.mock_crewai(), **self.mock_langgraph(), **self.mock_strands()} + + def patch_get_llm(self, return_value: Any = None) -> Any: + """Patch ``agentic_fabric.config.llm.get_llm``.""" + mock = self.mocker.patch("agentic_fabric.config.llm.get_llm") + mock.return_value = self.mocker.MagicMock() if return_value is None else return_value + return mock + + def patch_discover_packages(self, packages: dict[str, Any] | None = None) -> Any: + """Patch ``agentic_fabric.core.discovery.discover_packages``.""" + mock = self.mocker.patch("agentic_fabric.core.discovery.discover_packages") + mock.return_value = {} if packages is None else packages + return mock + + def patch_get_fabric_agent_config(self, config: dict[str, Any] | None = None) -> Any: + """Patch ``agentic_fabric.core.discovery.get_fabric_agent_config``.""" + mock = self.mocker.patch("agentic_fabric.core.discovery.get_fabric_agent_config") + mock.return_value = {"name": "test", "agents": {}, "tasks": {}} if config is None else config + return mock + + def patch_run_fabric_agent_auto(self, result: str = "Test result") -> Any: + """Patch ``agentic_fabric.core.decomposer.run_fabric_agent_auto``.""" + mock = self.mocker.patch("agentic_fabric.core.decomposer.run_fabric_agent_auto") + mock.return_value = result + return mock + + def _attach_to_parent(self, module_name: str, module: ModuleType) -> None: + """Attach a child module to an already mocked parent module.""" + parent_name, separator, child_name = module_name.rpartition(".") + if not separator or parent_name not in sys.modules: + return + + parent = sys.modules[parent_name] + key = (parent_name, child_name) + if key not in self._original_parent_attrs and key not in self._missing_parent_attrs: + if hasattr(parent, child_name): + self._original_parent_attrs[key] = getattr(parent, child_name) + else: + self._missing_parent_attrs.add(key) + setattr(parent, child_name, module) + + def _ensure_crewai(self) -> None: + """Ensure CrewAI modules are mocked.""" + if "crewai" not in self.mocked_modules: + self.mock_crewai() + + def _ensure_langgraph(self) -> None: + """Ensure LangGraph modules are mocked.""" + if "langgraph" not in self.mocked_modules: + self.mock_langgraph() + + def _ensure_strands(self) -> None: + """Ensure Strands modules are mocked.""" + if "strands" not in self.mocked_modules: + self.mock_strands() + + def _mock_with_attrs(self, **kwargs: Any) -> Any: + """Create a mock object with supplied attributes.""" + mock = self.mocker.MagicMock() + for key, value in kwargs.items(): + setattr(mock, key, value) + return mock + + +@pytest.fixture +def agentic_fabric_mocker(mocker: MockerFixture) -> Generator[FabricMocker, None, None]: + """Provide ``FabricMocker`` for agentic-fabric tests.""" + fabric_mocker = FabricMocker(mocker=mocker) + yield fabric_mocker + fabric_mocker.restore_modules() + + +@pytest.fixture +def fabric_mocker(mocker: MockerFixture) -> Generator[FabricMocker, None, None]: + """Provide the concise ``FabricMocker`` fixture name.""" + fabric_mocker = FabricMocker(mocker=mocker) + yield fabric_mocker + fabric_mocker.restore_modules() + + +@pytest.fixture +def mock_agentic_frameworks(mocker: MockerFixture) -> Generator[dict[str, ModuleType], None, None]: + """Mock every optional agent runtime framework.""" + fabric_mocker = FabricMocker(mocker=mocker) + modules = fabric_mocker.mock_all_frameworks() + yield modules + fabric_mocker.restore_modules() + + +@pytest.fixture +def mock_crewai(mocker: MockerFixture) -> Generator[dict[str, ModuleType], None, None]: + """Mock CrewAI modules only.""" + fabric_mocker = FabricMocker(mocker=mocker) + modules = fabric_mocker.mock_crewai() + yield modules + fabric_mocker.restore_modules() + + +@pytest.fixture +def mock_langgraph(mocker: MockerFixture) -> Generator[dict[str, ModuleType], None, None]: + """Mock LangGraph modules only.""" + fabric_mocker = FabricMocker(mocker=mocker) + modules = fabric_mocker.mock_langgraph() + yield modules + fabric_mocker.restore_modules() + + +@pytest.fixture +def mock_strands(mocker: MockerFixture) -> Generator[dict[str, ModuleType], None, None]: + """Mock Strands modules only.""" + fabric_mocker = FabricMocker(mocker=mocker) + modules = fabric_mocker.mock_strands() + yield modules + fabric_mocker.restore_modules() + + +__all__ = [ + "ALL_FRAMEWORK_MODULES", + "CREWAI_MODULES", + "LANGGRAPH_MODULES", + "RUNTIME_MODULES", + "STRANDS_MODULES", + "FabricMocker", + "agentic_fabric_mocker", + "fabric_mocker", + "mock_agentic_frameworks", + "mock_crewai", + "mock_langgraph", + "mock_strands", +] diff --git a/packages/pytest-agentic-fabric/src/pytest_agentic_fabric/plugin.py b/packages/pytest-agentic-fabric/src/pytest_agentic_fabric/plugin.py new file mode 100644 index 0000000..1714196 --- /dev/null +++ b/packages/pytest-agentic-fabric/src/pytest_agentic_fabric/plugin.py @@ -0,0 +1,299 @@ +"""Pytest plugin fixtures for agentic-fabric projects.""" + +from __future__ import annotations + +import os + +from collections.abc import Callable +from importlib.util import find_spec +from pathlib import Path +from types import ModuleType +from typing import Any + +import pytest +import yaml + +from pytest_agentic_fabric.mocking import RUNTIME_MODULES +from pytest_agentic_fabric.mocking import ( # noqa: F401 + agentic_fabric_mocker, + fabric_mocker, + mock_agentic_frameworks, + mock_crewai, + mock_langgraph, + mock_strands, +) + + +def pytest_addoption(parser: pytest.Parser) -> None: + """Register agentic-fabric test options.""" + parser.addoption( + "--agentic-e2e", + action="store_true", + default=False, + help="Run agentic-fabric tests that require external runtime dependencies.", + ) + + +def pytest_configure(config: pytest.Config) -> None: + """Register agentic-fabric markers.""" + config.addinivalue_line( + "markers", + "agentic_e2e: tests that require real agent runtimes, credentials, or external services", + ) + config.addinivalue_line( + "markers", + "agentic_runtime(runtime): tests that require one optional agent runtime", + ) + + +def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None: + """Skip live runtime tests unless explicitly enabled.""" + if config.getoption("--agentic-e2e"): + return + + skip_runtime = pytest.mark.skip(reason="agentic runtime test disabled; pass --agentic-e2e to run") + for item in items: + if "agentic_e2e" in item.keywords or "agentic_runtime" in item.keywords: + item.add_marker(skip_runtime) + + +@pytest.fixture +def agentic_runtime_available() -> Callable[[str], bool]: + """Return a predicate that checks whether an optional runtime can import.""" + + def check(module_name: str) -> bool: + return find_spec(module_name) is not None + + return check + + +@pytest.fixture +def agentic_runtime_registry() -> dict[str, Any]: + """Return a mutable runtime registry for tests.""" + return {} + + +@pytest.fixture +def agentic_runtime_modules() -> dict[str, tuple[str, ...]]: + """Return known import modules for optional runtimes.""" + return dict(RUNTIME_MODULES) + + +@pytest.fixture +def agentic_mock_runtime(monkeypatch: pytest.MonkeyPatch) -> Callable[[str], dict[str, ModuleType]]: + """Return a helper that installs fake runtime modules into ``sys.modules``.""" + + def install(runtime: str) -> dict[str, ModuleType]: + import sys + + modules: dict[str, ModuleType] = {} + for module_name in RUNTIME_MODULES[runtime]: + module = ModuleType(module_name) + modules[module_name] = module + monkeypatch.setitem(sys.modules, module_name, module) + parent_name, separator, child_name = module_name.rpartition(".") + if separator and parent_name in sys.modules: + setattr(sys.modules[parent_name], child_name, module) + + if "crewai" in modules: + modules["crewai"].__dict__.update({"Agent": object, "Crew": object, "Task": object}) + if "langgraph.prebuilt" in modules: + modules["langgraph.prebuilt"].__dict__["create_react_agent"] = ( + lambda *args, **kwargs: {"args": args, "kwargs": kwargs} + ) + if "strands" in modules: + modules["strands"].__dict__["Agent"] = object + return modules + + return install + + +@pytest.fixture +def agentic_fabric_agent_config() -> dict[str, Any]: + """Return a minimal framework-neutral fabric agent config.""" + return { + "name": "test_fabric_agent", + "description": "A test fabric agent", + "agents": { + "tester": { + "role": "Tester", + "goal": "Verify behavior", + "backstory": "Careful runtime test agent.", + } + }, + "tasks": { + "verify": { + "description": "Verify the requested behavior.", + "expected_output": "A concise result", + "agent": "tester", + } + }, + "knowledge_paths": [], + } + + +@pytest.fixture +def check_api_key() -> None: + """Skip a live test unless ``ANTHROPIC_API_KEY`` is configured.""" + if not os.environ.get("ANTHROPIC_API_KEY"): + pytest.skip("ANTHROPIC_API_KEY not set") + + +@pytest.fixture +def check_aws_credentials() -> None: + """Skip a live test unless AWS credentials or a profile are configured.""" + has_key = os.environ.get("AWS_ACCESS_KEY_ID") + has_profile = os.environ.get("AWS_PROFILE") + if not (has_key or has_profile): + pytest.skip("AWS credentials not configured; set AWS_ACCESS_KEY_ID or AWS_PROFILE") + + +@pytest.fixture +def temp_fabric_dir(tmp_path: Path) -> Path: + """Create a temporary framework-agnostic ``.fabric`` directory.""" + fabric_dir = tmp_path / "test_package" / ".fabric" + (fabric_dir / "fabric_agents").mkdir(parents=True) + return fabric_dir + + +@pytest.fixture +def simple_agent_config() -> dict[str, Any]: + """Return a minimal individual agent configuration.""" + return { + "role": "Test Agent", + "goal": "Answer simple questions accurately", + "backstory": "You are a helpful assistant focused on providing clear, concise answers.", + } + + +@pytest.fixture +def simple_task_config() -> dict[str, Any]: + """Return a minimal task configuration.""" + return { + "description": "Answer the question: What is 2 + 2?", + "expected_output": "The answer to the mathematical question", + } + + +@pytest.fixture +def simple_fabric_agent_config( + simple_agent_config: dict[str, Any], + simple_task_config: dict[str, Any], +) -> dict[str, Any]: + """Return a minimal framework-neutral fabric agent configuration.""" + return { + "name": "test_fabric_agent", + "description": "A simple test fabric agent", + "agents": {"test_agent": simple_agent_config}, + "tasks": { + "test_task": { + **simple_task_config, + "agent": "test_agent", + } + }, + "knowledge_paths": [], + } + + +@pytest.fixture +def multi_agent_fabric_agent_config() -> dict[str, Any]: + """Return a fabric agent configuration with multiple collaborating agents.""" + return { + "name": "multi_agent_fabric_agent", + "description": "A fabric agent with multiple collaborating agents", + "agents": { + "researcher": { + "role": "Researcher", + "goal": "Gather and analyze information", + "backstory": "You are an expert researcher.", + }, + "writer": { + "role": "Writer", + "goal": "Write clear summaries", + "backstory": "You are a skilled technical writer.", + }, + }, + "tasks": { + "research_task": { + "description": "Research the topic: Python programming", + "expected_output": "Key facts about Python", + "agent": "researcher", + }, + "writing_task": { + "description": "Write a brief summary based on the research", + "expected_output": "A concise summary", + "agent": "writer", + "context": ["research_task"], + }, + }, + "knowledge_paths": [], + } + + +@pytest.fixture +def fabric_agent_with_knowledge(tmp_path: Path) -> dict[str, Any]: + """Return a fabric agent configuration with a local knowledge source.""" + knowledge_dir = tmp_path / "knowledge" + knowledge_dir.mkdir() + (knowledge_dir / "test_info.md").write_text( + """# Test Knowledge + +This is test knowledge about the color blue. +Blue is a primary color. +It is often associated with calmness and stability. +""", + encoding="utf-8", + ) + + return { + "name": "knowledge_fabric_agent", + "description": "A fabric agent with knowledge sources", + "agents": { + "knowledgeable_agent": { + "role": "Knowledge Expert", + "goal": "Answer questions using provided knowledge", + "backstory": "You have access to specialized knowledge.", + }, + }, + "tasks": { + "knowledge_task": { + "description": "What color is mentioned in the knowledge base?", + "expected_output": "The color mentioned", + "agent": "knowledgeable_agent", + }, + }, + "knowledge_paths": [knowledge_dir], + } + + +@pytest.fixture +def agentic_workspace(tmp_path: Path, agentic_fabric_agent_config: dict[str, Any]) -> Path: + """Create a minimal workspace with a ``.fabric`` package.""" + workspace = tmp_path / "workspace" + fabric_dir = workspace / "packages" / "sample" / ".fabric" + fabric_agent_name = str(agentic_fabric_agent_config.get("name", "test_fabric_agent")) + config_dir = fabric_dir / "fabric_agents" / fabric_agent_name + config_dir.mkdir(parents=True) + manifest_agent_config: dict[str, Any] = { + "description": agentic_fabric_agent_config.get("description", ""), + "agents": f"fabric_agents/{fabric_agent_name}/agents.yaml", + "tasks": f"fabric_agents/{fabric_agent_name}/tasks.yaml", + } + if "preferred_framework" in agentic_fabric_agent_config: + manifest_agent_config["preferred_framework"] = agentic_fabric_agent_config["preferred_framework"] + + manifest = { + "name": "sample", + "description": "Test package", + "fabric_agents": {fabric_agent_name: manifest_agent_config}, + } + fabric_dir.joinpath("manifest.yaml").write_text(yaml.safe_dump(manifest, sort_keys=False), encoding="utf-8") + config_dir.joinpath("agents.yaml").write_text( + yaml.safe_dump(agentic_fabric_agent_config.get("agents", {}), sort_keys=False), + encoding="utf-8", + ) + config_dir.joinpath("tasks.yaml").write_text( + yaml.safe_dump(agentic_fabric_agent_config.get("tasks", {}), sort_keys=False), + encoding="utf-8", + ) + return workspace diff --git a/packages/pytest-agentic-fabric/src/pytest_agentic_fabric/py.typed b/packages/pytest-agentic-fabric/src/pytest_agentic_fabric/py.typed new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/pytest-agentic-fabric/src/pytest_agentic_fabric/py.typed @@ -0,0 +1 @@ + diff --git a/packages/pytest-agentic-fabric/tests/test_plugin.py b/packages/pytest-agentic-fabric/tests/test_plugin.py new file mode 100644 index 0000000..e76fc53 --- /dev/null +++ b/packages/pytest-agentic-fabric/tests/test_plugin.py @@ -0,0 +1,391 @@ +"""Tests for pytest-agentic-fabric.""" + +from __future__ import annotations + +import importlib +import importlib.metadata +import runpy +import sys + +from collections.abc import Callable +from pathlib import Path +from types import ModuleType +from typing import Any + +import pytest +import yaml + +from pytest_agentic_fabric.mocking import FabricMocker + + +pytest_plugins = ("pytester",) + + +def test_package_version_falls_back_when_not_installed(monkeypatch: pytest.MonkeyPatch) -> None: + """Source-tree imports should have a stable version fallback.""" + package_file = importlib.import_module("pytest_agentic_fabric").__file__ + + def missing_version(name: str) -> str: + if name == "pytest-agentic-fabric": + raise importlib.metadata.PackageNotFoundError(name) + return "1.0.0" + + monkeypatch.setattr(importlib.metadata, "version", missing_version) + + namespace = runpy.run_path(package_file, run_name="pytest_agentic_fabric_version_fallback") + + assert namespace["__version__"] == "0.0.0" + + +def test_agentic_runtime_available_fixture(agentic_runtime_available: Callable[[str], bool]) -> None: + """The runtime availability fixture should detect installed modules.""" + assert agentic_runtime_available("sys") is True + assert agentic_runtime_available("module_that_should_not_exist_for_agentic_fabric_tests") is False + + +def test_agentic_runtime_registry_fixture(agentic_runtime_registry: dict[str, Any]) -> None: + """The runtime registry fixture should be isolated and mutable.""" + agentic_runtime_registry["local"] = {"available": True} + + assert agentic_runtime_registry == {"local": {"available": True}} + + +def test_agentic_runtime_modules_fixture(agentic_runtime_modules: dict[str, tuple[str, ...]]) -> None: + """Known runtime modules should be exposed for consumer tests.""" + assert agentic_runtime_modules["crewai"][0] == "crewai" + assert "langgraph.prebuilt" in agentic_runtime_modules["langgraph"] + assert agentic_runtime_modules["strands"] == ("strands",) + + +def test_agentic_mock_runtime_fixture(agentic_mock_runtime: Callable[[str], dict[str, ModuleType]]) -> None: + """Runtime mocking fixture should install importable module objects.""" + modules = agentic_mock_runtime("langgraph") + + import langgraph.prebuilt + + assert modules["langgraph.prebuilt"] is langgraph.prebuilt + assert langgraph.prebuilt.create_react_agent("llm", [])["args"] == ("llm", []) + + +@pytest.mark.parametrize("runtime,expected_attr", [("crewai", "Agent"), ("strands", "Agent")]) +def test_agentic_mock_runtime_sets_runtime_entrypoints( + agentic_mock_runtime: Callable[[str], dict[str, ModuleType]], + runtime: str, + expected_attr: str, +) -> None: + """Runtime mocking fixture should install common runtime entrypoints.""" + modules = agentic_mock_runtime(runtime) + + assert hasattr(modules[runtime], expected_attr) + + +def test_fabric_mocker_properties(fabric_mocker: FabricMocker, mocker: Any) -> None: + """The published mocker should expose pytest-mock helpers.""" + target = ModuleType("spy_target") + target.func = lambda: "ok" + + assert fabric_mocker.MagicMock() is not None + assert fabric_mocker.Mock() is not None + patched: dict[str, bool] = {} + fabric_mocker.patch.dict(patched, {"patched": True}) + assert patched["patched"] is True + spy = fabric_mocker.spy(target, "func") + assert target.func() == "ok" + spy.assert_called_once_with() + assert fabric_mocker.stub(name="stub") is not None + assert fabric_mocker.mocker is mocker + + +def test_fabric_mocker_module_restore_handles_original_parent_attr( + fabric_mocker: FabricMocker, +) -> None: + """Mocked nested modules should restore previous parent attributes.""" + parent = ModuleType("parent_fixture") + original_child = object() + parent.child = original_child + sys.modules["parent_fixture"] = parent + + try: + child = fabric_mocker.mock_module("parent_fixture.child") + + assert parent.child is child + + fabric_mocker.restore_modules() + + assert parent.child is original_child + assert "parent_fixture.child" not in sys.modules + finally: + sys.modules.pop("parent_fixture", None) + + +def test_fabric_mocker_restore_removes_new_parent_attr(fabric_mocker: FabricMocker) -> None: + """Mocked nested modules should remove parent attributes they created.""" + parent = ModuleType("new_parent_fixture") + sys.modules["new_parent_fixture"] = parent + + try: + child = fabric_mocker.mock_module("new_parent_fixture.child") + + assert parent.child is child + + fabric_mocker.restore_modules() + + assert not hasattr(parent, "child") + finally: + sys.modules.pop("new_parent_fixture", None) + + +def test_fabric_mocker_returns_same_module_on_repeat(fabric_mocker: FabricMocker) -> None: + """Repeated module mocks should reuse the same fake module.""" + assert fabric_mocker.mock_module("repeat_fixture") is fabric_mocker.mock_module("repeat_fixture") + + +def test_fabric_mocker_restores_original_module(fabric_mocker: FabricMocker) -> None: + """Existing sys.modules entries should be restored.""" + original = ModuleType("existing_fixture") + sys.modules["existing_fixture"] = original + + try: + mocked = fabric_mocker.mock_module("existing_fixture") + + assert mocked is not original + + fabric_mocker.restore_modules() + + assert sys.modules["existing_fixture"] is original + finally: + sys.modules.pop("existing_fixture", None) + + +def test_fabric_mocker_crewai_helpers(fabric_mocker: FabricMocker) -> None: + """CrewAI helpers should mock modules and common objects.""" + modules = fabric_mocker.mock_crewai() + + patched_agent = fabric_mocker.patch_crewai_agent() + assert modules["crewai"].Agent is patched_agent + assert fabric_mocker.patch_crewai_task() is not None + assert fabric_mocker.patch_crewai_crew() is not None + assert fabric_mocker.patch_crewai_process() is not None + assert fabric_mocker.patch_knowledge_source() is modules[ + "crewai.knowledge.source.text_file_knowledge_source" + ].TextFileKnowledgeSource + assert fabric_mocker.mock_crewai_agent(role="Tester").role == "Tester" + assert fabric_mocker.mock_crewai_task(description="Task").description == "Task" + assert fabric_mocker.mock_crewai_crew(result="done").kickoff().raw == "done" + + +def test_fabric_mocker_langgraph_helpers(fabric_mocker: FabricMocker) -> None: + """LangGraph helpers should mock modules and common objects.""" + modules = fabric_mocker.mock_langgraph() + + assert fabric_mocker.patch_create_react_agent() is modules["langgraph.prebuilt"].create_react_agent + assert fabric_mocker.patch_chat_anthropic() is modules["langchain_anthropic"].ChatAnthropic + assert fabric_mocker.mock_langgraph_graph(result="done").invoke()["messages"][0].content == "done" + + +def test_fabric_mocker_strands_helpers(fabric_mocker: FabricMocker) -> None: + """Strands helpers should mock modules and common objects.""" + modules = fabric_mocker.mock_strands() + + assert fabric_mocker.patch_strands_agent() is modules["strands"].Agent + assert fabric_mocker.mock_strands_agent(result="done")() == "done" + + +def test_fabric_mocker_patch_helpers_install_missing_frameworks(agentic_fabric_mocker: FabricMocker) -> None: + """Patch helpers should lazily install framework modules when needed.""" + crewai_agent = agentic_fabric_mocker.patch_crewai_agent() + langgraph_agent = agentic_fabric_mocker.patch_create_react_agent() + strands_agent = agentic_fabric_mocker.patch_strands_agent() + + assert sys.modules["crewai"].Agent is crewai_agent + assert sys.modules["langgraph"].prebuilt.create_react_agent is langgraph_agent + assert sys.modules["strands"].Agent is strands_agent + + +def test_fabric_mocker_all_frameworks(mock_agentic_frameworks: dict[str, ModuleType]) -> None: + """The all-framework fixture should install each supported runtime module.""" + assert "crewai" in mock_agentic_frameworks + assert "langgraph.prebuilt" in mock_agentic_frameworks + assert "strands" in mock_agentic_frameworks + + +def test_framework_specific_mock_fixtures( + mock_crewai: dict[str, ModuleType], + mock_langgraph: dict[str, ModuleType], + mock_strands: dict[str, ModuleType], +) -> None: + """Framework-specific fixtures should install only their runtime modules.""" + assert "crewai" in mock_crewai + assert "langgraph.prebuilt" in mock_langgraph + assert "strands" in mock_strands + + +def test_agentic_fabric_mocker_internal_patch_helpers(agentic_fabric_mocker: FabricMocker) -> None: + """Agentic-fabric patch helpers should target fabric names.""" + llm = object() + + assert agentic_fabric_mocker.patch_get_llm(return_value=llm).return_value is llm + assert agentic_fabric_mocker.patch_discover_packages({"pkg": Path(".fabric")}).return_value == { + "pkg": Path(".fabric"), + } + assert agentic_fabric_mocker.patch_get_fabric_agent_config({"name": "custom"}).return_value == { + "name": "custom", + } + assert agentic_fabric_mocker.patch_run_fabric_agent_auto("done").return_value == "done" + + +def test_agentic_fabric_agent_config_fixture(agentic_fabric_agent_config: dict[str, Any]) -> None: + """Minimal fabric agent config should be usable by runtime tests.""" + assert agentic_fabric_agent_config["agents"]["tester"]["role"] == "Tester" + assert agentic_fabric_agent_config["tasks"]["verify"]["agent"] == "tester" + + +def test_shared_config_fixtures( + simple_agent_config: dict[str, Any], + simple_task_config: dict[str, Any], + simple_fabric_agent_config: dict[str, Any], + multi_agent_fabric_agent_config: dict[str, Any], + fabric_agent_with_knowledge: dict[str, Any], + temp_fabric_dir: Path, +) -> None: + """Shared config fixtures should be framework-neutral and discoverable.""" + assert simple_agent_config["role"] == "Test Agent" + assert simple_task_config["description"].startswith("Answer the question") + assert simple_fabric_agent_config["tasks"]["test_task"]["agent"] == "test_agent" + assert multi_agent_fabric_agent_config["tasks"]["writing_task"]["context"] == ["research_task"] + assert fabric_agent_with_knowledge["knowledge_paths"][0].joinpath("test_info.md").exists() + assert temp_fabric_dir.name == ".fabric" + assert temp_fabric_dir.joinpath("fabric_agents").is_dir() + + +def test_agentic_workspace_fixture(agentic_workspace: Path) -> None: + """Workspace fixture should create a discoverable .fabric package.""" + manifest = agentic_workspace / "packages" / "sample" / ".fabric" / "manifest.yaml" + agents = agentic_workspace / "packages" / "sample" / ".fabric" / "fabric_agents" / "test_fabric_agent" / "agents.yaml" + tasks = agentic_workspace / "packages" / "sample" / ".fabric" / "fabric_agents" / "test_fabric_agent" / "tasks.yaml" + + assert manifest.exists() + assert yaml.safe_load(manifest.read_text(encoding="utf-8"))["fabric_agents"]["test_fabric_agent"][ + "description" + ] == "A test fabric agent" + assert yaml.safe_load(agents.read_text(encoding="utf-8"))["tester"]["role"] == "Tester" + assert yaml.safe_load(tasks.read_text(encoding="utf-8"))["verify"]["agent"] == "tester" + + +def test_agentic_workspace_fixture_uses_overridden_config(pytester: pytest.Pytester) -> None: + """Workspace fixture should serialize overridden fabric agent configs.""" + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function\n") + pytester.makeconftest( + """ + import pytest + + @pytest.fixture + def agentic_fabric_agent_config(): + return { + "name": "custom_agent", + "description": "Custom description", + "preferred_framework": "langgraph", + "agents": { + "custom": { + "role": "Custom", + "goal": "Check override", + "backstory": "Fixture override", + }, + }, + "tasks": { + "custom_task": { + "description": "Use override", + "expected_output": "Override result", + "agent": "custom", + }, + }, + } + """ + ) + pytester.makepyfile( + """ + import yaml + + def test_workspace_uses_override(agentic_workspace): + fabric_dir = agentic_workspace / "packages" / "sample" / ".fabric" + manifest = yaml.safe_load((fabric_dir / "manifest.yaml").read_text(encoding="utf-8")) + agent_config = manifest["fabric_agents"]["custom_agent"] + agents = yaml.safe_load((fabric_dir / "fabric_agents" / "custom_agent" / "agents.yaml").read_text(encoding="utf-8")) + tasks = yaml.safe_load((fabric_dir / "fabric_agents" / "custom_agent" / "tasks.yaml").read_text(encoding="utf-8")) + + assert agent_config["description"] == "Custom description" + assert agent_config["preferred_framework"] == "langgraph" + assert agents["custom"]["role"] == "Custom" + assert tasks["custom_task"]["agent"] == "custom" + """ + ) + + result = pytester.runpytest("-q") + + result.assert_outcomes(passed=1) + + +def test_check_api_key_fixture_skips_without_key(pytester: pytest.Pytester) -> None: + """Credential fixtures should skip live tests when credentials are absent.""" + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function\n") + pytester.makepyfile( + """ + def test_requires_api_key(check_api_key): + assert True + """ + ) + + result = pytester.runpytest("-q") + + result.assert_outcomes(skipped=1) + + +def test_check_aws_credentials_fixture_skips_without_credentials(pytester: pytest.Pytester) -> None: + """AWS credential fixture should skip live tests when credentials are absent.""" + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function\n") + pytester.makepyfile( + """ + def test_requires_aws(check_aws_credentials): + assert True + """ + ) + + result = pytester.runpytest("-q") + + result.assert_outcomes(skipped=1) + + +def test_agentic_e2e_marker_skips_by_default(pytester: pytest.Pytester) -> None: + """Runtime-dependent tests should skip unless --agentic-e2e is passed.""" + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function\n") + pytester.makepyfile( + """ + import pytest + + @pytest.mark.agentic_e2e + def test_live_runtime(): + assert False + """ + ) + + result = pytester.runpytest("-q") + + result.assert_outcomes(skipped=1) + + +def test_agentic_e2e_marker_runs_when_enabled(pytester: pytest.Pytester) -> None: + """Runtime-dependent tests should run when --agentic-e2e is passed.""" + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function\n") + pytester.makepyfile( + """ + import pytest + + @pytest.mark.agentic_e2e + def test_live_runtime(): + assert True + """ + ) + + result = pytester.runpytest("-q", "--agentic-e2e") + + result.assert_outcomes(passed=1) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f7d8328 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,43 @@ +[project] +name = "agentic-fabric-workspace" +version = "0.0.0" +requires-python = ">=3.11" +dependencies = [] + +[tool.uv] +package = false + +[tool.uv.workspace] +members = [ + "packages/agentic-fabric", + "packages/pytest-agentic-fabric", +] + +[tool.uv.sources] +agentic-fabric = { workspace = true } +pytest-agentic-fabric = { workspace = true } + +[dependency-groups] +dev = [ + "mypy>=2.1.0", + "ruff>=0.15.20", + "tox>=4.56.1", + "tox-gh>=1.7.1", + "tox-uv>=1.35.2", +] + +[tool.pytest.ini_options] +addopts = ["-ra", "--strict-markers", "--strict-config"] +testpaths = [ + "packages/agentic-fabric/tests", + "packages/pytest-agentic-fabric/tests", +] +xfail_strict = true +asyncio_default_fixture_loop_scope = "function" +markers = [ + "crewai: tests that require CrewAI runtime dependencies", + "e2e: end-to-end tests that require external runtime dependencies", + "langgraph: tests that require LangGraph runtime dependencies", + "strands: tests that require Strands runtime dependencies", + "vendor_fabric: tests that require vendor-fabric runtime dependencies", +] diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..f073705 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "python", + "bootstrap-sha": "91e85f3850ead128275d3ed15c8cab9dfe9e2b02", + "packages": { + "packages/agentic-fabric": { + "package-name": "agentic-fabric", + "release-type": "python", + "changelog-path": "CHANGELOG.md", + "component": "agentic-fabric", + "include-component-in-tag": true + }, + "packages/pytest-agentic-fabric": { + "package-name": "pytest-agentic-fabric", + "release-type": "python", + "changelog-path": "CHANGELOG.md", + "component": "pytest-agentic-fabric", + "include-component-in-tag": true + } + }, + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "deps", "section": "Dependencies", "hidden": true }, + { "type": "refactor", "section": "Code Refactoring", "hidden": true }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "chore", "section": "Miscellaneous Chores", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true } + ] +} diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..73f7ad4 --- /dev/null +++ b/tox.ini @@ -0,0 +1,119 @@ +[tox] +requires = + tox>=4.56.1 + tox-uv>=1.35.2 + tox-gh>=1.7.1 +env_list = + lint + typecheck + audit + py311 + py312 + py313 + py314 + coverage + plugin + examples + docs + build +min_version = 4.56.1 +skip_missing_interpreters = false + +[gh] +python = + 3.11 = py311 + 3.12 = py312 + 3.13 = py313 + 3.14 = py314 + +[testenv] +description = Run agentic-fabric package tests +runner = uv-venv-runner +package = skip +deps = -e packages/agentic-fabric[tests] + -e packages/pytest-agentic-fabric[tests] +commands = pytest packages/agentic-fabric/tests {posargs} + +[testenv:py{311,312,313,314}] +description = Run agentic-fabric tests on Python {base_python} +commands = {[testenv]commands} + +[testenv:lint] +description = Lint source, tests, examples, docs, and workflow docs +runner = uv-venv-runner +package = skip +deps = ruff>=0.15.20 +commands = ruff check packages/agentic-fabric/src packages/agentic-fabric/tests packages/agentic-fabric/examples packages/pytest-agentic-fabric/src packages/pytest-agentic-fabric/tests docs AGENTS.md README.md + +[testenv:typecheck] +description = Type-check public package code +runner = uv-venv-runner +package = skip +deps = + -e packages/agentic-fabric[typing] + -e packages/pytest-agentic-fabric[typing] +commands = mypy --config-file packages/agentic-fabric/pyproject.toml packages/agentic-fabric/src packages/pytest-agentic-fabric/src + +[testenv:plugin] +description = Run pytest-agentic-fabric tests +runner = uv-venv-runner +package = skip +deps = + -e packages/agentic-fabric + -e packages/pytest-agentic-fabric[tests] + pytest-mock>=3.15.1 +commands = pytest packages/pytest-agentic-fabric/tests {posargs} + +[testenv:examples] +description = Run shipped examples as standalone scripts +runner = uv-venv-runner +package = skip +deps = -e packages/agentic-fabric +commands = + python packages/agentic-fabric/examples/discovery_workflow.py + python packages/agentic-fabric/examples/tool_registry.py + python packages/agentic-fabric/examples/runtime_context.py + python packages/agentic-fabric/examples/mcp_adapters.py + +[testenv:audit] +description = Audit resolved optional, test, and typing dependencies +runner = uv-venv-runner +package = skip +deps = + -e packages/agentic-fabric[langgraph,strands,mcp,scraping,tests,typing] + -e packages/pytest-agentic-fabric[tests,typing] + pip-audit==2.10.1 +commands = pip-audit --skip-editable + +[testenv:coverage] +description = Enforce full coverage for both publishable packages +runner = uv-venv-runner +package = skip +deps = + -e packages/agentic-fabric[tests] + -e packages/pytest-agentic-fabric[tests] +commands = + coverage erase + coverage run --source=agentic_fabric -m pytest packages/agentic-fabric/tests + coverage report --show-missing --skip-covered --fail-under=100 + coverage erase + coverage run --source=pytest_agentic_fabric -m pytest packages/pytest-agentic-fabric/tests + coverage report --show-missing --skip-covered --fail-under=100 + +[testenv:docs] +description = Build Sphinx docs and generated autodoc2 API reference +runner = uv-venv-runner +package = skip +deps = -e packages/agentic-fabric[docs] +commands_pre = python -c "import shutil; shutil.rmtree('docs/_build/html', ignore_errors=True)" +commands = sphinx-build -W -E -b html docs docs/_build/html + +[testenv:build] +description = Build source and wheel distributions for both publishable packages +runner = uv-venv-runner +package = skip +deps = +allowlist_externals = uv +commands = + uv build --package agentic-fabric --out-dir dist/agentic-fabric --clear + uv build --package pytest-agentic-fabric --out-dir dist/pytest-agentic-fabric --clear diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..59d4835 --- /dev/null +++ b/uv.lock @@ -0,0 +1,3113 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform != 'win32'", +] + +[manifest] +members = [ + "agentic-fabric", + "agentic-fabric-workspace", + "pytest-agentic-fabric", +] + +[[package]] +name = "accessible-pygments" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } +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 = "agentic-fabric" +version = "1.0.0" +source = { editable = "packages/agentic-fabric" } +dependencies = [ + { name = "pyyaml" }, +] + +[package.optional-dependencies] +dev = [ + { name = "coverage", extra = ["toml"] }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-agentic-fabric", extra = ["tests"] }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pytest-timeout" }, + { name = "pytest-xdist" }, + { name = "ruff" }, + { name = "types-pyyaml" }, + { name = "types-requests" }, +] +docs = [ + { name = "furo" }, + { name = "myst-parser" }, + { name = "sphinx" }, + { name = "sphinx-autodoc2" }, + { name = "sphinx-copybutton" }, +] +langgraph = [ + { name = "langchain-anthropic" }, + { name = "langgraph" }, +] +mcp = [ + { name = "mcp" }, +] +scraping = [ + { name = "beautifulsoup4" }, + { name = "requests" }, +] +strands = [ + { name = "strands-agents" }, +] +tests = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, + { name = "pytest-agentic-fabric", extra = ["tests"] }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pytest-timeout" }, + { name = "pytest-xdist" }, +] +typing = [ + { name = "mypy" }, + { name = "types-pyyaml" }, + { name = "types-requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "agentic-fabric", extras = ["tests", "typing"], marker = "extra == 'dev'", editable = "packages/agentic-fabric" }, + { name = "beautifulsoup4", marker = "extra == 'scraping'", specifier = ">=4.12.0" }, + { name = "coverage", extras = ["toml"], marker = "extra == 'tests'", specifier = ">=7.14.3" }, + { name = "furo", marker = "extra == 'docs'", specifier = ">=2025.12.19" }, + { name = "langchain-anthropic", marker = "extra == 'langgraph'", specifier = ">=0.3.0" }, + { name = "langgraph", marker = "extra == 'langgraph'", specifier = ">=0.2.0" }, + { name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.0.0" }, + { name = "mypy", marker = "extra == 'typing'", specifier = ">=2.1.0" }, + { name = "myst-parser", marker = "extra == 'docs'", specifier = ">=5.1.0,<6.0.0" }, + { name = "pytest", marker = "extra == 'tests'", specifier = ">=9.1.1" }, + { name = "pytest-agentic-fabric", extras = ["tests"], marker = "extra == 'tests'", editable = "packages/pytest-agentic-fabric" }, + { name = "pytest-asyncio", marker = "extra == 'tests'", specifier = ">=1.4.0" }, + { name = "pytest-cov", marker = "extra == 'tests'", specifier = ">=7.1.0" }, + { name = "pytest-mock", marker = "extra == 'tests'", specifier = ">=3.15.1" }, + { name = "pytest-timeout", marker = "extra == 'tests'", specifier = ">=2.4.0" }, + { name = "pytest-xdist", marker = "extra == 'tests'", specifier = ">=3.8.0" }, + { name = "pyyaml", specifier = ">=6.0.3" }, + { name = "requests", marker = "extra == 'scraping'", specifier = ">=2.31.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.20" }, + { name = "sphinx", marker = "extra == 'docs'", specifier = ">=8.2.3,<9.0.0" }, + { name = "sphinx-autodoc2", marker = "extra == 'docs'", specifier = ">=0.5.0" }, + { name = "sphinx-copybutton", marker = "extra == 'docs'", specifier = ">=0.5.2" }, + { name = "strands-agents", marker = "extra == 'strands'", specifier = ">=0.1.0" }, + { name = "types-pyyaml", marker = "extra == 'typing'", specifier = ">=6.0.12.20260518" }, + { name = "types-requests", marker = "extra == 'typing'", specifier = ">=2.33.0.20260518" }, +] +provides-extras = ["langgraph", "strands", "mcp", "scraping", "tests", "typing", "dev", "docs"] + +[[package]] +name = "agentic-fabric-workspace" +version = "0.0.0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "ruff" }, + { name = "tox" }, + { name = "tox-gh" }, + { name = "tox-uv" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=2.1.0" }, + { name = "ruff", specifier = ">=0.15.20" }, + { name = "tox", specifier = ">=4.56.1" }, + { name = "tox-gh", specifier = ">=1.7.1" }, + { name = "tox-uv", specifier = ">=1.35.2" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +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.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.112.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/dd/808c144d4a883fcfd12fe0d7689b1d86bbbea6666c1cc957ad19f1017c22/anthropic-0.112.0.tar.gz", hash = "sha256:e180cd91aa5b9b32e4007fe69892ab128d8a86b9f90825103b1903fbc977d0af", size = 937460, upload-time = "2026-06-24T18:45:56.844Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/26/ea71185027956325be1903d4fcaf7461d5ef40ca8f0e64f992e24ea9db0e/anthropic-0.112.0-py3-none-any.whl", hash = "sha256:bcc6268612c716dbb77133dd60fc41d26016d1b81dee9a52314d210193638751", size = 931954, upload-time = "2026-06-24T18:45:58.205Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, +] + +[[package]] +name = "ast-serialize" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157, upload-time = "2026-05-17T17:48:29.429Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520, upload-time = "2026-05-17T17:47:30.831Z" }, + { url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779, upload-time = "2026-05-17T17:47:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750, upload-time = "2026-05-17T17:47:34.731Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942, upload-time = "2026-05-17T17:47:36.287Z" }, + { url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517, upload-time = "2026-05-17T17:47:38.17Z" }, + { url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081, upload-time = "2026-05-17T17:47:39.826Z" }, + { url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910, upload-time = "2026-05-17T17:47:41.369Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678, upload-time = "2026-05-17T17:47:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603, upload-time = "2026-05-17T17:47:46.256Z" }, + { url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332, upload-time = "2026-05-17T17:47:47.899Z" }, + { url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979, upload-time = "2026-05-17T17:47:50.942Z" }, + { url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002, upload-time = "2026-05-17T17:47:54.093Z" }, + { url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231, upload-time = "2026-05-17T17:47:56.311Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668, upload-time = "2026-05-17T17:47:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075, upload-time = "2026-05-17T17:48:00.35Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347, upload-time = "2026-05-17T17:48:01.753Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380, upload-time = "2026-05-17T17:48:03.738Z" }, + { url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879, upload-time = "2026-05-17T17:48:05.463Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529, upload-time = "2026-05-17T17:48:07.008Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560, upload-time = "2026-05-17T17:48:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172, upload-time = "2026-05-17T17:48:09.922Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072, upload-time = "2026-05-17T17:48:11.469Z" }, + { url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488, upload-time = "2026-05-17T17:48:13.575Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702, upload-time = "2026-05-17T17:48:15.141Z" }, + { url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182, upload-time = "2026-05-17T17:48:16.779Z" }, + { url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410, upload-time = "2026-05-17T17:48:18.527Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587, upload-time = "2026-05-17T17:48:20.133Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171, upload-time = "2026-05-17T17:48:21.921Z" }, + { url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668, upload-time = "2026-05-17T17:48:23.544Z" }, + { url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311, upload-time = "2026-05-17T17:48:25.027Z" }, + { url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931, upload-time = "2026-05-17T17:48:26.591Z" }, + { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" }, +] + +[[package]] +name = "astroid" +version = "3.3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/74/dfb75f9ccd592bbedb175d4a32fc643cf569d7c218508bfbd6ea7ef9c091/astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce", size = 400439, upload-time = "2025-07-13T18:04:23.177Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/0f/3b8fdc946b4d9cc8cc1e8af42c4e409468c84441b933d037e101b3d72d86/astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec", size = 275612, upload-time = "2025-07-13T18:04:21.07Z" }, +] + +[[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 = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } +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 = "boto3" +version = "1.43.36" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/9f/897287e955db0f50b12fd69ef45956e4fd2c7ddb48c736872f7ea2314443/boto3-1.43.36.tar.gz", hash = "sha256:587d7ee92a12e440ad12b0e7f11f3358f0c4d65b19f64726efc94aaf194aff28", size = 112690, upload-time = "2026-06-23T02:47:14.561Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/f1/274303f52483ecf199eae6f8d9b6f5951670397ee4d72c06cfd4eb644612/boto3-1.43.36-py3-none-any.whl", hash = "sha256:42942dde254673abcbc9e6e60017c88341a4f49d99d24e1f2e290fb38138c26f", size = 140031, upload-time = "2026-06-23T02:47:13.178Z" }, +] + +[[package]] +name = "botocore" +version = "1.43.36" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/37/da9e7f6ca73ac73afd7f0bb7f238aa5daba35c081e98d7f48a7c399599c0/botocore-1.43.36.tar.gz", hash = "sha256:4cae47d1b2d426316b85a0087d9e69e048f13bc003b5177d74639fe9dfd28205", size = 15625488, upload-time = "2026-06-23T02:47:03.192Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/19/934f81592527a3f7f9b943c893e334c721a4644948642bc33885d584e9ec/botocore-1.43.36-py3-none-any.whl", hash = "sha256:3c65fdc39ed01d8dfde1e961b34038aed03c459f8ddf80717a12ac006475e49d", size = 15313630, upload-time = "2026-06-23T02:46:59.327Z" }, +] + +[[package]] +name = "cachetools" +version = "7.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/8b/0d3945a13955303b81272f759a0331e54c5c793da455e6f5706b89d2639c/cachetools-7.1.4.tar.gz", hash = "sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6", size = 40085, upload-time = "2026-05-21T22:40:43.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/7b/1fc1c09cc0756cf25861a3be10565915953876da48bb228fb9a672b20a42/cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54", size = 16761, upload-time = "2026-05-21T22:40:41.845Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/91/0a7c28934e50d8ac9a7b117712d176f2953c3170bccced5eaacfa3e96175/coverage-7.14.3.tar.gz", hash = "sha256:1a7563a443f3d53fdeb040ec8c9f7466aed7ca3dc5891aa09d3ca3625fa4387f", size = 924398, upload-time = "2026-06-22T23:10:25.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/24/efb17eb94018dd3415d0e8a76a4786a866e8964aa9c50f033399d23939c2/coverage-7.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e574801e1d643561594aa021206c46d80b257e9853087090ba97bed8b0a509d3", size = 220501, upload-time = "2026-06-22T23:08:02.182Z" }, + { url = "https://files.pythonhosted.org/packages/76/93/32f1bfca6cdd34259c8af42820a034b7a28dfb44969a13ed38c17e0ba5b0/coverage-7.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f82b6bb7d75a2613e85d07cefa3a8c973d0544a8993337f6e2728e4a1e94c305", size = 221008, upload-time = "2026-06-22T23:08:03.701Z" }, + { url = "https://files.pythonhosted.org/packages/eb/88/0d0f974855ff905d15a64f7873d00bdc4182e2736267486c6634f4af293c/coverage-7.14.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a2335ea5fed26af2e831094964fa3f8fae60b45f7e37fcc2d3b615b2add3ad87", size = 251420, upload-time = "2026-06-22T23:08:05.211Z" }, + { url = "https://files.pythonhosted.org/packages/39/7f/117dd2ec65e4140576f8ef991d88220f9b806769f7a8c20e0550c0f924e2/coverage-7.14.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fbb8c3a98e779013786ae01d229662aeacbc77100efbd3f2f245219ace5af700", size = 253331, upload-time = "2026-06-22T23:08:06.672Z" }, + { url = "https://files.pythonhosted.org/packages/87/55/f0bd6d6538e3f16829fb8a44b6c0d2fe9da638bbfdd6a20f8b5da8f4fa81/coverage-7.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac082660de8f429ba0ea363595abb838998570b9a7546777c60f413ab902bbde", size = 255441, upload-time = "2026-06-22T23:08:08.208Z" }, + { url = "https://files.pythonhosted.org/packages/1e/98/aa71f7879019c846a8a9662579ea4484b0202cf1e252ffeed647075e7eca/coverage-7.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac012839ff7e396030f1e94e10553a431d14e4de2ab65cb3acb72bbd5628ca2", size = 257398, upload-time = "2026-06-22T23:08:09.749Z" }, + { url = "https://files.pythonhosted.org/packages/f3/4f/5fd367e59844190f5965015d7bee899e67a89d13eb2760118479bf836f2f/coverage-7.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5952f8c1bda2a5347154450379316e6dfa4d934d62ca35f6784451e6f55074fb", size = 251558, upload-time = "2026-06-22T23:08:11.37Z" }, + { url = "https://files.pythonhosted.org/packages/8f/de/5383a6ee5a6376701fe07d980fa8e4a66c0c377fead16712720340d701a3/coverage-7.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8cf0f2509acb4619e2471a1951089054dd58ebea7a912066d2ea56dd4c24ca4a", size = 253134, upload-time = "2026-06-22T23:08:13.04Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/09542b1a99f788e3daec7f0fadc288821e71aca9ea298d51bfa1ba79fed5/coverage-7.14.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2e41fd3aab806770008279a93879b0924b16247e09ab537c043d08bbca53b4ab", size = 251195, upload-time = "2026-06-22T23:08:14.606Z" }, + { url = "https://files.pythonhosted.org/packages/02/9d/722fe8c13f0fbb064491b9e8656e56a606286792e5068c47ca1042e773e8/coverage-7.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f0a47095963cfe054e0df178daca95aec21e680d6076da807c3add28dfe920f7", size = 254959, upload-time = "2026-06-22T23:08:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/fb/58/943627179ff1d82da9e54d0a5b0bb907bb19cf19515599ccd921de50b469/coverage-7.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a090cbf9521e78ffdb2fcf448b72902afe9f5923ff6a12d5c0d0120200348af9", size = 250914, upload-time = "2026-06-22T23:08:18.03Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d4/803efcbf9ae5567454a0c71e983589529448e2704ee0da2dc0163d482f18/coverage-7.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d310baf69a4fbe8a098ce727e4808a34866ac718a6f759ae659cbd3221358bc", size = 251824, upload-time = "2026-06-22T23:08:19.704Z" }, + { url = "https://files.pythonhosted.org/packages/32/79/3f78ea9563132746eed5cecb75d2e576f9d8fec45a47242b5ae0950b82a3/coverage-7.14.3-cp311-cp311-win32.whl", hash = "sha256:74fdd718d88fe144f4579b8747873a07ec3f04cb837d5faec5a25d9e22fa31a8", size = 222594, upload-time = "2026-06-22T23:08:21.311Z" }, + { url = "https://files.pythonhosted.org/packages/85/22/9ebbc5a2ab42ac5d0eea1f48648629e1de9bbe41ec243ed6b93d55a5a53f/coverage-7.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:cc96aa922e21d4bc5d5ed3c915cef27dfcbc13686f47d5e378d647fbfba655a2", size = 223073, upload-time = "2026-06-22T23:08:23.318Z" }, + { url = "https://files.pythonhosted.org/packages/71/af/69d5fcc16cb555153f99cec5467922f226be0369f7335a9506856d2a7bd0/coverage-7.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:c66f9f9d4f1e9712eb9b1de5310f881d4e2188cfcba5065e1a8490f38687f2c4", size = 222617, upload-time = "2026-06-22T23:08:25.054Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b0/8a911f6ffe6974dac4df95b468ab9a2899d0e59f0f99a489afeec39f00bc/coverage-7.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d74ff26299c4879ce3a4d826f9d3d4d556fd285fde7bbce3c0ef5a8ab1cec24", size = 220672, upload-time = "2026-06-22T23:08:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/0fc0cb52538783dbbae0934b834f5a58fd5354380ee6cad4a07b15dc845d/coverage-7.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:96150a9cf3468ea20f0bc5d0e21b3df8972c31480ef90fa7614b773cc6429665", size = 221035, upload-time = "2026-06-22T23:08:28.372Z" }, + { url = "https://files.pythonhosted.org/packages/77/e2/421ccfbb48335ac49e93301478cf5d623b0c2bf1c0cadd8e2b2fc6c0c710/coverage-7.14.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:27d07a46500ba23515b838dbcf52512026af04090755cf6cc64166d88c9b9a1a", size = 252540, upload-time = "2026-06-22T23:08:30.226Z" }, + { url = "https://files.pythonhosted.org/packages/06/c2/05b8c890097c61a7f4406b35396b997a635200ded0339eda83dfbe526c5f/coverage-7.14.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:621e13c6108234d7960aaf5762ab5c3c00f33c30c15af06dcbff0c73bf112727", size = 255274, upload-time = "2026-06-22T23:08:31.876Z" }, + { url = "https://files.pythonhosted.org/packages/dc/be/b6d9efe447f8ba3c3c854195f326bd64c54b907d936cd2fdebf8767ec72e/coverage-7.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b60ca6d8af70473491a15a343cbabab2e8f9ea66a4376e81c7aa24876a6f977", size = 256389, upload-time = "2026-06-22T23:08:33.843Z" }, + { url = "https://files.pythonhosted.org/packages/d4/3c/f26e50acc429e608bc534ac06f0a3c169019c798178ec5e9de3dbc0df9c9/coverage-7.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c90a7cdd5e380e1ce02f19792e2ac2fbfbf177e35a27e69fd3e873b30d895c0c", size = 258648, upload-time = "2026-06-22T23:08:35.481Z" }, + { url = "https://files.pythonhosted.org/packages/9e/a2/01c1fabf816c8e1dae197e258edf878a3d3ddc86fbda34b76e5794277d8f/coverage-7.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d788e5fd55347eef06ca0732c77d04a264de67e8ff24631270cdff3767a60cf", size = 252949, upload-time = "2026-06-22T23:08:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/89/c6/941166dd79c31fd44a13063780ae8d552eee0089a0a0930b9bdb7df554ed/coverage-7.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62c7f79db2851c95ef020e5d28b97afde3daf9f7febcd35b53e05638f729063f", size = 254310, upload-time = "2026-06-22T23:08:39.174Z" }, + { url = "https://files.pythonhosted.org/packages/10/31/80b1fd028201a961033ce95be3cd1e39e521b3762e6b4a1ac1616cb291e7/coverage-7.14.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:90f7608aeb5d9b60b523b9fb2a4ee1973867cc4865a3f26fe6c7577073b70205", size = 252453, upload-time = "2026-06-22T23:08:40.84Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/c3d9addd94c4b524f3f4af0232075f5fe7170ce99a1386edff803e5934db/coverage-7.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1e3b91f9c4740aeb571ecf82e5e8d8e4ab62d34fcb5a5d4e5baa38c6f7d2857c", size = 256522, upload-time = "2026-06-22T23:08:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/14/e5a0575f73795af3a7a9ae13dadf812e17d32422896839987dc3f86947e1/coverage-7.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c946099774a7699de03cbd0ff0a64e21aed4525eed9d959adde4afe6d15758ef", size = 252023, upload-time = "2026-06-22T23:08:44.243Z" }, + { url = "https://files.pythonhosted.org/packages/38/9b/9652ee531937ce3b8a63a8896885b2b4a2d56adc30e53c9540c666286d88/coverage-7.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16b206e521feb8b7133a45754643dead0538489cf8b783b90cf5f4e3299625fd", size = 253893, upload-time = "2026-06-22T23:08:46.113Z" }, + { url = "https://files.pythonhosted.org/packages/b1/05/42678841c8c38e4b08bdfc48269f5a16dfbf5806000fe6a89b4cece3c691/coverage-7.14.3-cp312-cp312-win32.whl", hash = "sha256:ea3169c7116eb6cdf7608c6c7da9ecfcb3da40688e3a510fac2d1d2bafd6dc35", size = 222734, upload-time = "2026-06-22T23:08:47.858Z" }, + { url = "https://files.pythonhosted.org/packages/df/87/07a4fcee55177a25f1b52331a8e92cf4f2c53b1a9c75ce2981fd59c684ad/coverage-7.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:7ea52fc08f007bcc494d4bb3df3851e95843d881860ba38fe2c64dc100db5e7d", size = 223266, upload-time = "2026-06-22T23:08:49.494Z" }, + { url = "https://files.pythonhosted.org/packages/aa/34/2b8b66a989282ea7b370beb49f50bab29470dc30bb0b03935b6b802782f7/coverage-7.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:8cec0ad652ec57790970d817490105bd917d783c2f7b38d6b58a0ca312e1a336", size = 222655, upload-time = "2026-06-22T23:08:51.766Z" }, + { url = "https://files.pythonhosted.org/packages/a9/83/7fefbf5df23ed2b7f489907564a7b34b9b07098128e12e0fdfa92626e456/coverage-7.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47968988b367990ae4ab17523790c38cd125e02c6bfd379b6022be2d40bdc38c", size = 220699, upload-time = "2026-06-22T23:08:53.522Z" }, + { url = "https://files.pythonhosted.org/packages/31/e6/38c3653ff6d56d704b29241362387ca824e38e15b76fdcb7096538195790/coverage-7.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0ee68f5c34812780f3a7063382c0a9fcbb99985b7ddcdcaa626e4f3fb2e0783a", size = 221068, upload-time = "2026-06-22T23:08:55.571Z" }, + { url = "https://files.pythonhosted.org/packages/20/86/4f5c45d51c5cd10a128933f0fd235393c9146abbfd2ce2dfa68b3267ead3/coverage-7.14.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa9e5c6857a7e80fa22ace5cf3550ae392bbfc322f1d8dd2d2d5a8be38cec027", size = 252060, upload-time = "2026-06-22T23:08:57.464Z" }, + { url = "https://files.pythonhosted.org/packages/82/50/dfce42eff2cecabcd5a9bbad5489449c87db3415f408d23ffee417ce01f6/coverage-7.14.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98a0859b0e98e43e1178a9402e19c8127766b14f7109a374d976e5a62c0e5c73", size = 254657, upload-time = "2026-06-22T23:08:59.453Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d2/639ceb1bc8038fd0d66768278d5dc22df3391918b8278c2a21aa2602a531/coverage-7.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69918344541ed9c8368566c2adc03c0e33d4550d7faa87d1b35e49b6a3286ea9", size = 255892, upload-time = "2026-06-22T23:09:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/8b/96/002094a10e113512500dc1e10430a449417e17b0f90f7d496bcb820208b7/coverage-7.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f300ac92cd4b570724c8ffbbd0c130fee298d2447f41d5a3abf58976fae1de", size = 258026, upload-time = "2026-06-22T23:09:03.017Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ec/286a5d2fad9c4bee59bd724feeb7d5bf8303c6c9200b51d1dd945a9c72b0/coverage-7.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11a7ec9f97ab950f4c5af62229befc7faf208fdbc0116d3902d7e306cf2c5abd", size = 252285, upload-time = "2026-06-22T23:09:04.773Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7d/a17753a0b12dd48d0d50f5fab079ad99d3be1eac790494d89f3a417ca0b9/coverage-7.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a571bd889cd36c5922ce8e42e059f9d37d02301531d11374afa4c87a578625d5", size = 254023, upload-time = "2026-06-22T23:09:06.513Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/a76c6ceba6a2c313f905310abf2701d534cada22d372db11731831e9e209/coverage-7.14.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de76caefc8deabb0dd1678b6a980be97d14c8d87e213ac194dbf8b09e96d63fb", size = 251989, upload-time = "2026-06-22T23:09:08.382Z" }, + { url = "https://files.pythonhosted.org/packages/d9/39/353013a75fec0fb49f7553519f9d52b4441e902e5178c93f38eb6c07cedb/coverage-7.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d20a15c622194234161535459affa8f7905830391c9ccfa060d495dbfe3a1c7f", size = 256144, upload-time = "2026-06-22T23:09:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/29/0e/613878555d734def11c5b20a2701a15cb3781b9e9ea749da27c5f436e928/coverage-7.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b488bd4b23397db62e7a9459129d01ff06a846582a732efd24834b24a6ada498", size = 251808, upload-time = "2026-06-22T23:09:12.057Z" }, + { url = "https://files.pythonhosted.org/packages/af/76/359c058c9cfdcf1e8b107663881225b03b364a320017eda24a2a66e55102/coverage-7.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a3693b4153394d265f44fb855fdc80e72403024d4d6f91c4871b334d028e4e0", size = 253579, upload-time = "2026-06-22T23:09:13.858Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d9/4ba2f060933a30ebe363cef9f67a365b0a317e580c0d5d9169d56a73ef1c/coverage-7.14.3-cp313-cp313-win32.whl", hash = "sha256:338b19131ab1a6b767b462bfcbaa692e7ae22f24463e39d49b02a83410ff6b37", size = 222741, upload-time = "2026-06-22T23:09:15.636Z" }, + { url = "https://files.pythonhosted.org/packages/76/e8/196ebc25d8f34c06d43a6e9c8513c9266ef8dbf3b5672beb1a00cf5e29fa/coverage-7.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:b3d77f7f196abdef7e01415de1bce09f216189e83e58159cfeef2b92d0464994", size = 223283, upload-time = "2026-06-22T23:09:17.478Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/51d2aac6417523a286f10fb25f09eb9518a84df9f1151e93ff6871f34849/coverage-7.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:e6230e688c7c3e65cedd41a774eb4ec221adc6bfee13768231015b702d5e4150", size = 222678, upload-time = "2026-06-22T23:09:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/61/56/14e3b97facbfa1304dd19e676e26599ad359f04714bed32f7f1c5a88efdc/coverage-7.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:605ab2b566a22bd94834529d66d295c364aba84afd3e5498285c7a524017b1fc", size = 220741, upload-time = "2026-06-22T23:09:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/12/1d/db378b5cca433b90b893f26dab728b280ddd89f272a1fdfed4aeaa05c686/coverage-7.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3c2134809e80fac091bfed18a6991b5a5eb5df5ae32b17ac4f4f99864b73dd7", size = 221068, upload-time = "2026-06-22T23:09:23.452Z" }, + { url = "https://files.pythonhosted.org/packages/47/f0/3f8421b20d9c4fcd39be9a8ca3c3fda8bc204b44efbd09fede153afd3e2f/coverage-7.14.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c02efd507227bde9969cab0db8f48890eb3b5dcad6afac57a4792df4133543ce", size = 252117, upload-time = "2026-06-22T23:09:25.458Z" }, + { url = "https://files.pythonhosted.org/packages/27/ca/59ea35fb99743549ec8b37eff141ece4431fea590c89e536ed8032ef45cf/coverage-7.14.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1bb93c2aa61d2a5b38f1526546d95cf4132cb681e541a337bf8dfd092be816e5", size = 254622, upload-time = "2026-06-22T23:09:27.523Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/ec6de51ae7493b92a1cf74d1b763121c29636759167e2a593ba4db5881e4/coverage-7.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f502e948e03e866538048bba081c075caaa62e5bda6ea5b7432e45f587eb462a", size = 255968, upload-time = "2026-06-22T23:09:29.43Z" }, + { url = "https://files.pythonhosted.org/packages/5d/05/c8bfc77823f42b4664fb25842f13b567022f6f84a4c83c8ecbb16734b7cb/coverage-7.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9973ef2463f8e6cfb61a6324126bb3e17d67a85f22f58d856e583ea2e3ca6501", size = 258284, upload-time = "2026-06-22T23:09:31.397Z" }, + { url = "https://files.pythonhosted.org/packages/f6/15/1d1b242027124a32b26ef01f82018b8c4ef34ef174aa6aeba7b1eeef48e8/coverage-7.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9be4e7d4c5ca0427889f8f9d614bd630c2be741b1de7699bca3b2b6c0e41003e", size = 252143, upload-time = "2026-06-22T23:09:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/74/b6/d2a9842fd2a5d7d27f1ac851c043a734a494ad75402c5331db3da79ed691/coverage-7.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a574912f3bde4b0619f6e97d01aa590b70998859244793769eb3a6df78ee56d3", size = 253976, upload-time = "2026-06-22T23:09:35.351Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/e1600ddf7e226db5558bb5323d2186fff00f505c4b764643ec89ce5d8175/coverage-7.14.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e343fb086c9cd780b38622fea7c369acd64c1a0724312149b5d769c387a2b1f5", size = 251942, upload-time = "2026-06-22T23:09:37.313Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2c/9159de64f9dd648e324328d588a44cfab1e331eb5259ce1141afe2a92dfb/coverage-7.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3c68df8e61f1e09633fefc7538297145623957a048534368c9d212782aa5e845", size = 256220, upload-time = "2026-06-22T23:09:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/91/67/b7f536cc2c124f48e91b22fbb741d2261f4e3d310faf6f76007f47566e5d/coverage-7.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3e5b550a128419373c2f6cec28a244207013ef15f5cbcff6a5ca09d1dfaaf027", size = 251756, upload-time = "2026-06-22T23:09:41.056Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ec/f3718038e2d4860c715a55428377ca7f6c75872caf98cabd982e1d76967d/coverage-7.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2bfc4dd0a912329eccc7484a7d0b2a38032b38c40663b1e1ac595f10c457954b", size = 253413, upload-time = "2026-06-22T23:09:43.306Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a5/91f11efeef89b3cc9b30461128db15b0511ef813ab889a7b7ab636b3a497/coverage-7.14.3-cp314-cp314-win32.whl", hash = "sha256:0423d64c013057a06e70f070f073cec4b0cbc7d2b27f3c7007292f2ff1d52965", size = 222946, upload-time = "2026-06-22T23:09:45.261Z" }, + { url = "https://files.pythonhosted.org/packages/58/fd/98ac9f524d9ec378de831c034dbdeb544ca7ef7d2d9c9996daf232a037fd/coverage-7.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:92c22e19ce64ca3f2ad751f16f14df1468b4c231bd6af97185063a9c292a0cb3", size = 223436, upload-time = "2026-06-22T23:09:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a0/7cd612d650a772a0ae80144443406bf61981c896c3d57c9e6e79fb2cdbd1/coverage-7.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:41de778bd41780586e2b04912079c73089ab5d839624e28db3bdb26de638da92", size = 222861, upload-time = "2026-06-22T23:09:49.384Z" }, + { url = "https://files.pythonhosted.org/packages/55/57/017353fab573779c0d00448e47d102edd36c792f7b6f233a4d89a7a08384/coverage-7.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8427f370ca67db4c975d2a26acfc0e5783ca0b52444dbc50278ace0f35445949", size = 221474, upload-time = "2026-06-22T23:09:51.417Z" }, + { url = "https://files.pythonhosted.org/packages/69/92/90cf1f1a5c468a9c1b7ba2716e0e205293ad9b02f5f573a6de4318b15ba1/coverage-7.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8e88f335544a47e22ae2e45b344772925ec65166555c958720d5ed971880891", size = 221738, upload-time = "2026-06-22T23:09:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c0/4df964fa539f8399fd7679c09c472d73744de334686fd3f01e3a2465ce4e/coverage-7.14.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:beaab199b9e5ceaf5a225e16a9d4df136f2a1eae0a5c20de1e277c8a5225f388", size = 263101, upload-time = "2026-06-22T23:09:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/06/76/e5d33b2576ae3bf2be2058cd1cae57774b61e400f2c3c58f3783dc2ffb4a/coverage-7.14.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3ff255799f5a1676c71c1c32ec01fd043aa09d57b3d95764b24992757184784", size = 265225, upload-time = "2026-06-22T23:09:57.904Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/e52419afe391a39ba27fdefaf0737d8e34bf03faef6ab3b3006545bbd0d0/coverage-7.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:878832eaac515b62decfa76965aed558775f86bf1fc8cca76993c0c84ae31aed", size = 267643, upload-time = "2026-06-22T23:09:59.938Z" }, + { url = "https://files.pythonhosted.org/packages/58/7a/f2625d8d5006b6b20fba5afaef00b24a763fe96476ea798a3076cbc1f84e/coverage-7.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:611e62cb9386096d81b63e0a05330750268617231e7bd598e1fe77482a2c58a5", size = 268762, upload-time = "2026-06-22T23:10:01.943Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bf/908024006bba57127354d74e938954b9c3cd765cc2e0412dc9c37b415cda/coverage-7.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:02c41de2a88011b893050fc9830267d927a50a215f7ad5ec17349db7090ccf26", size = 262208, upload-time = "2026-06-22T23:10:03.954Z" }, + { url = "https://files.pythonhosted.org/packages/34/a0/d4f9296441b909817442fdb26bd77a698f08272ec683a7394b00eb2e47a0/coverage-7.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:526ce9721116af23b1065089f0b75046fe521e7772ab94b641cd66b7a0421889", size = 265096, upload-time = "2026-06-22T23:10:05.936Z" }, + { url = "https://files.pythonhosted.org/packages/e8/da/4ae4f3f4e477b56a4ce1e5c48a35eff38a94b50130ce5bdc897024741cfc/coverage-7.14.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e4ed44705ca4bead6fc977a8b741f2145608289b33c8a9b42a95d0f15aedbf4d", size = 262699, upload-time = "2026-06-22T23:10:07.973Z" }, + { url = "https://files.pythonhosted.org/packages/d8/7a/6927148073ff32856d78baa77b4ddc07a9be7e90020f9db0661c4ca523a1/coverage-7.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2415902f385a23dcc4ccd26e0ba803249a169af6a930c003a4c715eeb9a5444e", size = 266433, upload-time = "2026-06-22T23:10:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a7/774f658dbe9c4c3f5daa86a87e0459ac3832e4e3cc67affe078547f727b9/coverage-7.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b75ee850fc2d7c831e883220c445b035f2224de2ba6103f1e56dbd237ab913f7", size = 261547, upload-time = "2026-06-22T23:10:12.191Z" }, + { url = "https://files.pythonhosted.org/packages/3d/14/a0c18c0376c43cbf973f43ef6ca20019c950597180e6396232f7b6a27102/coverage-7.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dc9b4e35e7c3920e925ba7f14886fd5fbe481232754624e832ddba66c7535635", size = 263859, upload-time = "2026-06-22T23:10:14.492Z" }, + { url = "https://files.pythonhosted.org/packages/10/ac/43a3d0f460af524b131a6191805bc5d18b806ab4e828fbf82e8c8c3af446/coverage-7.14.3-cp314-cp314t-win32.whl", hash = "sha256:7b27c822a8161afbe48e99f1adfb098d270ae7e0f7d7b0555ce110529bdb69cc", size = 223250, upload-time = "2026-06-22T23:10:16.758Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5f/d5e5c56b0712e96ce8f69fe7dbf229ff938b437bc50862743c8a0d2cea84/coverage-7.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:39e1dbbb6ff2c338e0196a482558a792a1de3aa64261196f5cdb3da016ad9cda", size = 224082, upload-time = "2026-06-22T23:10:19.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/35/947cbd5be1d3bcbbdc43d6791de8a56c6501903311d42915ae06a82815f0/coverage-7.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:68520c90babfa2d560eca6d497921ed3a4f469623bd709733124491b2aa8ef3f", size = 223400, upload-time = "2026-06-22T23:10:21.24Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e3/a0aa32bfa3a081951f60a23bc0e7b512891ef0eecda1153cf1d8ba36c6b1/coverage-7.14.3-py3-none-any.whl", hash = "sha256:fb7e18afb6e903c1a92401a2f0501ac277dca527bb9ca6fe1f691a8a0026a0e8", size = 212469, upload-time = "2026-06-22T23:10:23.405Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" }, + { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" }, + { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, + { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[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" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +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 = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" }, +] + +[[package]] +name = "furo" +version = "2025.12.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accessible-pygments" }, + { name = "beautifulsoup4" }, + { name = "pygments" }, + { name = "sphinx" }, + { name = "sphinx-basic-ng" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/20/5f5ad4da6a5a27c80f2ed2ee9aee3f9e36c66e56e21c00fde467b2f8f88f/furo-2025.12.19.tar.gz", hash = "sha256:188d1f942037d8b37cd3985b955839fea62baa1730087dc29d157677c857e2a7", size = 1661473, upload-time = "2025-12-19T17:34:40.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/b2/50e9b292b5cac13e9e81272c7171301abc753a60460d21505b606e15cf21/furo-2025.12.19-py3-none-any.whl", hash = "sha256:bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f", size = 339262, upload-time = "2025-12-19T17:34:38.905Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +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 = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +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 = "jiter" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/b5/55f06bb281d92fb3cc86d14e1def2bd908bb77693183e7cb1f5a3c388b0c/jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76", size = 166640, upload-time = "2026-05-19T10:09:48.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/13/daa722f5765c393576f466378f9dfd29d77c9bed939e0688f96afa3601ea/jiter-0.15.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0f862193b8696249d22ec433e85fd2ab0ad9596bc3e45e6c0bc55e8aeba97be2", size = 310899, upload-time = "2026-05-19T10:07:12.89Z" }, + { url = "https://files.pythonhosted.org/packages/7f/82/2d2551829b082f4b6d82b9f939b031fb808a10aab1ec0664f82e150bb9a2/jiter-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1303d4d68a9b051ea90502402063ecf3807da00ad2affa19ca1ae3b90b3c5f67", size = 314963, upload-time = "2026-05-19T10:07:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0a/8b1a51466f7fe9f31dbe4bc7e0ca848674f9825e0f737b929b97e8c60aa7/jiter-0.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392b8ab019e5502d08aff85c6272209c24bc2cbe706ea82a56368f524236614a", size = 341730, upload-time = "2026-05-19T10:07:15.869Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2a/e71dea19822e2e404e83992a08c1d6b9b617bb944f28c9c2fbd85d02c91e/jiter-0.15.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:773b6eb282ce11ee19f05f6b2d4404fa308e5bbd353b0b80a0262caad6db2cd7", size = 366214, upload-time = "2026-05-19T10:07:17.259Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/97e1fa539d124a509a00ab7f669289d1c1d236ecabf12948a18f16c91082/jiter-0.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2c0c44d569ce0f2850f5c926f8caeb5f245fbc84475aeb36efccc2103e6dbd", size = 459527, upload-time = "2026-05-19T10:07:18.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7a/4a68d331aef8cf2e2393c14a3aacb635c62aa86071b0229899fb5baaa907/jiter-0.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:032396229564bca02440396bd327710719f724f5e7b7e9f7a8eb3faa4a2c2281", size = 375451, upload-time = "2026-05-19T10:07:20.208Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/1c445c2b6f0e30a274dc8082e0c3c7825411cce80d726bccd697c98cc8d3/jiter-0.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d37768fce7f88dd2a8c6091f2325dea27d30d30d5c6e7a1c0f0af77723b708", size = 349428, upload-time = "2026-05-19T10:07:22.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/94/e20d38984fc17a636371bffd2ae0f698124fdc8e75ef969cd2da6ba7cea7/jiter-0.15.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2c9cb907439d20bd0c7d7565ca01ee52234203208433749bae5b516907526928", size = 355405, upload-time = "2026-05-19T10:07:23.916Z" }, + { url = "https://files.pythonhosted.org/packages/94/fa/4d09f814779d0ea80a28ed8e4c6662ec9a4a8ecef0ac52190ebac6262d14/jiter-0.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9100ddbec09741cc66feb0fc6773f8bdbd0e3c345689368f260082ff85dcc0cd", size = 393688, upload-time = "2026-05-19T10:07:25.854Z" }, + { url = "https://files.pythonhosted.org/packages/54/9d/8eb5d4fb8bf7e93a75964a5da71a75c67c864baf7fa3f98598187b3c7e57/jiter-0.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae1b0d82ac2d987f9ea512b1c9adfcc71a28de3dea3a6039b54d76cffda9901e", size = 520853, upload-time = "2026-05-19T10:07:27.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/5e07874e59e623a943a0acf1552a80d05b70f31b402287a8fc6d7ec634c7/jiter-0.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8020c99ec13a7db2b6f96cbe82ef4721c88b426a4892f27478044af0284615ef", size = 551016, upload-time = "2026-05-19T10:07:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/d2d34422143474cadc15b60d482b1c35683dbc5c63c24346ddd0df09bcaf/jiter-0.15.0-cp311-cp311-win32.whl", hash = "sha256:42bfb257930800cf43e7c62c832402c704ab60797c992faf88d20e903eac8f32", size = 209518, upload-time = "2026-05-19T10:07:30.431Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7d/52778b930e5cc3e52a37d950b1c10494244308b4329b25a0ff0d88303a81/jiter-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:860a74063284a2ae9bfedd694f299cc2c68e2696c5f3d440cc9d18bb81b9dd04", size = 200565, upload-time = "2026-05-19T10:07:32.125Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4f/d9b4067feb69b3fa6eb0488e1b59e2ad5b463fe39f59e527eab2aca00bb0/jiter-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:37a10c377ce3a4a85f4a67f28b7afe093154cde77eaf248a72e856aa08b4d865", size = 195488, upload-time = "2026-05-19T10:07:33.846Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/4f6bddbcde3c71e56d0aa1337ec95950f3d27dd4153e25aadf0feac71751/jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d", size = 308793, upload-time = "2026-05-19T10:07:35.25Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/c01099b59a285a1ebba64ae93f62bfa036675340fd1b0045ae65890a0442/jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0", size = 309570, upload-time = "2026-05-19T10:07:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/8fb7f9d45bb98190355454cd04dad8d8f27223d6bd52f83af07f637168a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138", size = 336783, upload-time = "2026-05-19T10:07:38.694Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b6/f5739011d009b3a30f6a53c5240979030ba29ae46a8c67e3a15759f7c37d/jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61", size = 363555, upload-time = "2026-05-19T10:07:40.832Z" }, + { url = "https://files.pythonhosted.org/packages/e5/12/98a9d9f766665e8a3b6252454e17cb0c464606a28cf2fa09399b003345fa/jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687", size = 452255, upload-time = "2026-05-19T10:07:42.62Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d5/60f972840f79c5e7544fce567c56f1e4e50468f996baba3e78d823dd62a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879", size = 373559, upload-time = "2026-05-19T10:07:44.201Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cf/d46ef1234ba335aabc2f013210db8e0821a22f5e644a2e9449df199ecc23/jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d", size = 346055, upload-time = "2026-05-19T10:07:46.005Z" }, + { url = "https://files.pythonhosted.org/packages/f0/63/4d2749d8d54d230bad9b3a6b0d00cc28c6ff6b2fdffc26a8ccf76cc5a974/jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb", size = 351406, upload-time = "2026-05-19T10:07:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b9/9965b990035d8773328e0a8c8b457a87bf2b19f6c4126d9d99296be5d16a/jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871", size = 389357, upload-time = "2026-05-19T10:07:49.665Z" }, + { url = "https://files.pythonhosted.org/packages/2d/55/9ddf903deda1413e87fed792f416b7123daee5b8efbad6a202a7421c36a5/jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77", size = 517263, upload-time = "2026-05-19T10:07:51.537Z" }, + { url = "https://files.pythonhosted.org/packages/e8/76/a0c40ad064d3a20a4fde231e35d56e9a01ce82164278180e82d5daf85469/jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d", size = 548646, upload-time = "2026-05-19T10:07:53.196Z" }, + { url = "https://files.pythonhosted.org/packages/23/4f/eca9b954942916ba2f453891b8593ab444cd872396fe66a3936616f236f3/jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d", size = 206427, upload-time = "2026-05-19T10:07:55.307Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/8ead82a87495149542748e828d153fd232a512a22c83b02c4815c1a9c7d8/jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7", size = 197300, upload-time = "2026-05-19T10:07:56.651Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e4/9b8a78fb2d894471bc344e37f1949bdd784bd914d031dba0ba3a40c71dd7/jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b", size = 192702, upload-time = "2026-05-19T10:07:58.307Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f4/f708c900ecee41b2025ef8413d5351e5649eb2125c506f6720cc69b06f5c/jiter-0.15.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c11465f97e2abf45a014b83b730222f8f1c5335e802c7055a67d50de6f1f4e3", size = 307829, upload-time = "2026-05-19T10:07:59.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/59/db537c0949e83668c38481d426b9f2fd5ab758c4ee53a811dd0a510626a0/jiter-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e7b1776f0797956c509e123d0952d10d293a9492dea9f288ab9570ec01d1a5", size = 308445, upload-time = "2026-05-19T10:08:01.184Z" }, + { url = "https://files.pythonhosted.org/packages/37/38/ea0e13b18c30ef951da0d47d39e7fa9edb82a93a62990ffbd7cea9b622d4/jiter-0.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351a341c2105aa430b7047e30f1bf7975f6313b00165d3fc07be2edaf741f279", size = 336181, upload-time = "2026-05-19T10:08:02.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/fc/2303901b16c4ba05865588990a420c0b4156270b44379c20931544a1d962/jiter-0.15.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ab395feec8d249ec4044e228e98a7033f043426a265df439dc3698823f0a4e4", size = 362985, upload-time = "2026-05-19T10:08:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6f/11bace093c52e7d4d26c8e606ccd7ae8c972189622469ec0d9e28161e28b/jiter-0.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2a438005b6f22d0273413484d6094d7c2c5d10ec1b3a3bf128e0d1d3ba53258", size = 453292, upload-time = "2026-05-19T10:08:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/22/db/987f2f086ca4d7a6582eb4ccd513f9b26b42d9e4243a087609a3137a8fc7/jiter-0.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f18f85e4218d1b40f000f42a92239a7a61a902cd42c65e6c360dbd17dcb20894", size = 373501, upload-time = "2026-05-19T10:08:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/89fbcabb2739b7a5b8dc959a1b6c5761f6484f5fed3486854b3c789bb1de/jiter-0.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1aa62e277fc1cbd80e6deacae6f4d983b41b3d7728e0645c5d741a6149bba45", size = 344683, upload-time = "2026-05-19T10:08:09.431Z" }, + { url = "https://files.pythonhosted.org/packages/30/6f/6cca7692e7dddfec6d8d76c54dc97f2af2a41df4ac0674b999df1f09a5f3/jiter-0.15.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:6550fa135c7deb8ead6af49ed7ff648532ea8334a1447fe34a36315ef79c5c29", size = 350892, upload-time = "2026-05-19T10:08:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/39/14/0338d6190cb8e6d22e677ab1d4eabd4117f67cca70c54cd04b82ff64e068/jiter-0.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:066f8f33f18b2419cd8213b2436fa7fbc9c499f315971cfa3ce1f9820c001b1b", size = 388723, upload-time = "2026-05-19T10:08:12.912Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/cc19f4a1bdb6afb09ce6a2f2615aa8d44d994eba0d8e6105ed1af920e736/jiter-0.15.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:75e8a04e91432dde9f1838373cf93d23726c79d3e908d319acf0e796f85592e7", size = 516648, upload-time = "2026-05-19T10:08:14.808Z" }, + { url = "https://files.pythonhosted.org/packages/49/9f/833c541512cd091b63c10c0381973dfe11bc7a503a818c16384417e0c81e/jiter-0.15.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a97261f1fccb8e50ecd2890a96e46efdc3f57c80a197324c6777827231eca712", size = 547382, upload-time = "2026-05-19T10:08:16.927Z" }, + { url = "https://files.pythonhosted.org/packages/d2/11/e7b70e91f90bc4477e8eee9e8a5f7cf3cb41b4525d6394dc98a714eb8f7f/jiter-0.15.0-cp313-cp313-win32.whl", hash = "sha256:c77496cb10bd7549690fbbab3e5ec05857b83e49276f4a9423a766ddd2afcd4c", size = 205845, upload-time = "2026-05-19T10:08:18.401Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/5c20d9ad6f02c493e4023e5d2d09e1c1f15fe2753c9102c544aff068a88e/jiter-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b15741f501469009ae0ae90b7147958a664a7dede40aa7ff174a8a4645f546d0", size = 196842, upload-time = "2026-05-19T10:08:20.131Z" }, + { url = "https://files.pythonhosted.org/packages/6b/11/1eb400ef248e8c925fd883fbe325daf5e42cd1b0d308539dd332bd4f7ffc/jiter-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d6a60072b44c3c2b797a7ddcbcbbf2b34ea3cfd4721580fbfd2a09d9d9b84ba", size = 192212, upload-time = "2026-05-19T10:08:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/8a/60/2fd8d7c79da8acf9b7b277c7616847773779356b92acfc9bb158452174da/jiter-0.15.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ef1fd24d9413f6209e00d3d5a453e67acfe004a25cc6c8e8484faed4311ab9e8", size = 315065, upload-time = "2026-05-19T10:08:23.218Z" }, + { url = "https://files.pythonhosted.org/packages/46/f4/008fb7d65e8ac2abf00811651a661e025c4ba80bbc6f378450384ddd3aed/jiter-0.15.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144f8e72cb53dab146347b91cceac01f5481237f2b93b4a339a1ee8f8878b67c", size = 339444, upload-time = "2026-05-19T10:08:24.701Z" }, + { url = "https://files.pythonhosted.org/packages/00/55/90b0c7b9c6896c0f2a591dd36d36b71d22e09674bfef178fa03ba3f81499/jiter-0.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553fcac2ef2cb990877f9fc0833b8b629a3e6a5670b6b5fd58219b41a653ddc4", size = 347779, upload-time = "2026-05-19T10:08:26.408Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/69666cec5000fd57734c118437394516c749ae8dbeea9fb66d6fef9c4775/jiter-0.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:774f93f65031856bf14ad9f59bdcab8b8cad501e5ceabd51ba3525f76937a25b", size = 200395, upload-time = "2026-05-19T10:08:28.055Z" }, + { url = "https://files.pythonhosted.org/packages/39/04/a6aa62cd27e8149b0d28df5561f10f6cceaf7935a9ccf3f1c5a05f9a0cd8/jiter-0.15.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f1e1754960f38ec40613a07e5e372df67acb3b890fb383b6fb3de3e49ddbf3c7", size = 190516, upload-time = "2026-05-19T10:08:29.35Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/079f350ebf7859d081de30aa890f9e3be68516f754f3ba32366ffff4dcee/jiter-0.15.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ac0d9ddea4350974be7a221fc25895f251a8fee748c889bdced2141c0fec1a49", size = 308884, upload-time = "2026-05-19T10:08:31.667Z" }, + { url = "https://files.pythonhosted.org/packages/04/4e/a2c30a7f69b48c03b20935d647479106fe932f6e63f75faf53937197e05d/jiter-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01a8222cf05ab1128e239421156c207949808acaaea2bdfd33130ae666786e86", size = 310028, upload-time = "2026-05-19T10:08:33.304Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/2e7cdfd3cf8ca967be38c48f5cf474d79f089efaf559a40f15984a77ae69/jiter-0.15.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:182226cbc930c9fab81bc2e41a4da672f89539906dadb05e75670ac07b94f71f", size = 337485, upload-time = "2026-05-19T10:08:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/9b/11/15a1aa28b120b8ee5b4f1fb894c125046225f09847738bd64233d3b84883/jiter-0.15.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:71683c38c825452999b5717fcae07ea708e8c93003e808be4319c1b02e3d176e", size = 364223, upload-time = "2026-05-19T10:08:36.694Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/f442e8af5f3d0dcf47b39e83a0efd9ee45ea946aa6d04625dc3181eae3b6/jiter-0.15.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f2218e6a9e5c18bc10fe6d41ac189c442c88eacf11bad9f28ef95a9bef00e6", size = 456387, upload-time = "2026-05-19T10:08:38.143Z" }, + { url = "https://files.pythonhosted.org/packages/da/f4/37f2d2c9f64f49af7da652ed7532bb5a2372e588e6927c3fdd76f911db65/jiter-0.15.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5157de9f76eb4bc5ea74a1219366a25f945ad305641d74e04f59c54087091aa9", size = 374461, upload-time = "2026-05-19T10:08:39.869Z" }, + { url = "https://files.pythonhosted.org/packages/60/28/edcfbbbf0cb15436f36664a8908a0df47ab9006298d4cd937dc08ea932d6/jiter-0.15.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c5db5527c221249a876160663ab891ace358c17f7b9c93ec1478b7f0550e5c", size = 345924, upload-time = "2026-05-19T10:08:41.668Z" }, + { url = "https://files.pythonhosted.org/packages/47/13/89fba6398dab7f202b7278c4b4aac122399d2c0183971c4a57a3b7088df5/jiter-0.15.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:3e4540b8e74e4268811ac05db226a6a128ff572e7e0ce3f1163b693cadb184cd", size = 352283, upload-time = "2026-05-19T10:08:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/1b/da/0f6af8cef2c565a1ab44d970f268c43ccaa72707386ea6388e6fe2b6cd26/jiter-0.15.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62ebd14e47e9aed9df4472afcb2663668ce4d74891cd54f86bf6e44029d6dc89", size = 389985, upload-time = "2026-05-19T10:08:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ec/b9cb7d6d29e24ee14910266157d2a279d7a8f60ee0df7fa840882976ba64/jiter-0.15.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0be6f5ad41a809f303f416d17cec92a7a725902fb9b4f3de3d19362ac0ef8554", size = 517695, upload-time = "2026-05-19T10:08:46.486Z" }, + { url = "https://files.pythonhosted.org/packages/64/5e/6d1bda880723aae0ad86b4b763f044362448efe31e3e819635d41cb03451/jiter-0.15.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:813dfbb17d65328bf86e5f0905dd277ba2265d3ca20556e86c0c7035b7182e5a", size = 548868, upload-time = "2026-05-19T10:08:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/0c/72/7de501cf38dcacaf35098796f3a50e0f2e338baba18a58946c618544b809/jiter-0.15.0-cp314-cp314-win32.whl", hash = "sha256:50e51156192722a9c58db112837d3f8ef96fb3c5ecc14e95f409134b08b158ec", size = 206380, upload-time = "2026-05-19T10:08:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a9/e19addf4b0c1bdce52c6da12351e6bc42c340c45e7c09e2158e46d293ccc/jiter-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:30ce1a5d16b5641dc935d50ef775af6a0871e3d14ab05d6fc54dff371b78e558", size = 197687, upload-time = "2026-05-19T10:08:51.088Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c9/776b1db01db25fc6c1d58d1979a37b0a9fe787e5f5b1d062d2eaacb77923/jiter-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:510c8b3c17a0ed9ac69850c0438dada3c9b82d9c4d589fcb62002a5a9cf3a866", size = 192571, upload-time = "2026-05-19T10:08:52.451Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f6/45bb4670bacf300fd2c7abadbfb3af376e5f1b6ae75fd9bc069891d15870/jiter-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7553333dd0930c104a5a0db8df72bf7219fe663d731383b576bb6ed6351c984d", size = 317151, upload-time = "2026-05-19T10:08:53.867Z" }, + { url = "https://files.pythonhosted.org/packages/d7/68/ed635ad5acd7b73e454283083bbb7c8205ad10e88b0d9d7d793b09fe8226/jiter-0.15.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2143ab06181d2b029eedcb6af3cebe95f11bbac62441781860f98ee9330a6a6", size = 341243, upload-time = "2026-05-19T10:08:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/5d/db/3ff4176b817b8ea33879e71e13d8bc2b0d481a7ed3fe9e080f333d415c16/jiter-0.15.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eac374c5c975709b69c10f09afd199df74150172156ad10c8d4fd785b7da995", size = 363629, upload-time = "2026-05-19T10:08:56.928Z" }, + { url = "https://files.pythonhosted.org/packages/ab/24/5f8270e0ba9c883582f96f722f8a0b58015c7ce1f8c6d4571cf394e99b6b/jiter-0.15.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3b3b775e33d3bfaec9899edc526ae97b0da0bf9d071a46124ba419149a414f8", size = 456198, upload-time = "2026-05-19T10:08:58.618Z" }, + { url = "https://files.pythonhosted.org/packages/45/5b/76fc02b0b5c54c3d18c60653156e2f76fde1816f9b4722db68d6ee2c897e/jiter-0.15.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3071db3346334beae1360b46da4606da57bf3528c167b3c38533afaf9f2c5", size = 373710, upload-time = "2026-05-19T10:09:00.151Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/4310821b0ea9277994d3e1f49fc6a4b34e4800caebacb2c0af81da59a454/jiter-0.15.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6694a173ecabc12eb60efbc0b474464ead1951ff65cd8b1e72100715c64512b", size = 349901, upload-time = "2026-05-19T10:09:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/93/fe/67648c35b3594fba8854ac64cc8a826d8bcd18324bbdb53d77697c60b6ef/jiter-0.15.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a254e10b593624d230c365b6d616b22ca0ad65e63a16e6631c2b3466022e6ba8", size = 352438, upload-time = "2026-05-19T10:09:03.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/28/0a1879d07ad6b3e025a2750027363452ced93c2d16d1c9d4b153ffd51c91/jiter-0.15.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8d2955167274e15d79a7a020afdd9b39c990eb80b2d89fca695d92dcfdd38ec", size = 388152, upload-time = "2026-05-19T10:09:04.741Z" }, + { url = "https://files.pythonhosted.org/packages/c1/78/46c6f6b56ba85c90021f4afd72ed42f691f8f84daacb5fe27277070e3858/jiter-0.15.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:acf4ee4d1fc55917239fe72972fb292dd773055d05eb040d36f4326e02cc2c0e", size = 517707, upload-time = "2026-05-19T10:09:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/ca/cb/720662d4c88fcad606e826fef5424365527ba43ce4868a479aed8f8c507e/jiter-0.15.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e7196e56f1cd69af1dbb07dff02dcfb260a50b45a82d409d92a06fedb32473b5", size = 548241, upload-time = "2026-05-19T10:09:08.093Z" }, + { url = "https://files.pythonhosted.org/packages/60/e3/935b8034fd143f21125c87d51404a9e0e1449186a494405721ff5d1d695e/jiter-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:7f6163c0f10b055245f814dcc59f4818da60dfe72f3e72ab89fc24b6bd5e9c52", size = 207950, upload-time = "2026-05-19T10:09:09.616Z" }, + { url = "https://files.pythonhosted.org/packages/93/59/984fd9ece895953dad3e0880a650e766f5a2da2c5514f0eafdaaabbeb5f9/jiter-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c256edb05b78a111b99c4de3b1d32e31634b867fd1fc2cf726e7b7bba9854", size = 200055, upload-time = "2026-05-19T10:09:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/cf8d779feb133a27a2e3bc833bccb9e13aa332cdf820497ebf72c10ce8c3/jiter-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:66b1880df2d01e206e8339769d1c7c1753bcb653efd6289e203f6f24ebada0c0", size = 191244, upload-time = "2026-05-19T10:09:12.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/43/1fc62172aa98b50a7de9a25554060db510f85c89cfbed0dfe13e1907a139/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:411fa4dfa5a7ae3d11491027ffb9beadec3996010a986862db70d91abba1c750", size = 305585, upload-time = "2026-05-19T10:09:35.995Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c4/dd58fcd9e2df83666e5c1c1347bef58ce919cd8efc3ffa38aeea62ce493b/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:2b0074e2f56eb2dacca1689760fd2852a068f85a0547a157b82cb4cafeb6768b", size = 306936, upload-time = "2026-05-19T10:09:37.435Z" }, + { url = "https://files.pythonhosted.org/packages/39/86/b695e16f1180c07f43ea98e73ecd21cf63fa2e1b0c1103739013784d11ae/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:913d02d29c9606643418d9ccfc3b72492ab25a6bf7889934e09a3490f8d3438b", size = 342453, upload-time = "2026-05-19T10:09:39.294Z" }, + { url = "https://files.pythonhosted.org/packages/34/56/55d76614af37fe3f22a3347d1e410d2a15da581997cb2da499a625000bb5/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b15d3ec9b0449c40e85319bdb4caa8b77ab526e74f5532ed94bec15e2f66822c", size = 345606, upload-time = "2026-05-19T10:09:40.727Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/505941b2b092fd5bbbd60a52a880db1173f1690ae6751bed3af1c9ddcb4e/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0", size = 303769, upload-time = "2026-05-19T10:09:42.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/a06692b29e77473f286e1ec1f426d3ca44d7b5843be8ad21d7a5f3fcdcc0/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45", size = 305128, upload-time = "2026-05-19T10:09:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/7270d7ad41d6061a25b950c6bf91d638bd9aacb113200a8c8d57a055fd67/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c", size = 340459, upload-time = "2026-05-19T10:09:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a", size = 344469, upload-time = "2026-05-19T10:09:46.864Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "langchain-anthropic" +version = "1.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anthropic" }, + { name = "langchain-core" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/22/40ab129b08329ca295b391aa1d48267692b42594757084c6918e22b655ac/langchain_anthropic-1.4.8.tar.gz", hash = "sha256:c76891b2044d56105ff13c106ed12650637b53bd598a4bdf15b4796eefa2a4ec", size = 708524, upload-time = "2026-06-26T21:28:46.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/14/746235c4da89d9bc6a608c5f489f628e03feb8f697195c146e452c8f23c8/langchain_anthropic-1.4.8-py3-none-any.whl", hash = "sha256:778e9301b6fd517824f76ec1776975ce8add97a1f6a36c50ae3c2f4b03a66f7f", size = 52366, upload-time = "2026-06-26T21:28:45.535Z" }, +] + +[[package]] +name = "langchain-core" +version = "1.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch" }, + { name = "langchain-protocol" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/e3/bea6d0080acf183332f24dcd74c208aee5857cf8f783c3fb0bd86027d8fb/langchain_core-1.4.8.tar.gz", hash = "sha256:5bf1f8411077c904182ad8f975943d36adcbf579c4e017b3a118b719229ebf9a", size = 957974, upload-time = "2026-06-18T19:39:23.636Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/d6/bdf6f0481cc57ef300d6b1eb48cf1400c0409be715d6eb3cabadd1142a09/langchain_core-1.4.8-py3-none-any.whl", hash = "sha256:d84c28b05e3ba8d4271d0827aad5b592ccdaaf986e76768c23503f0a2045e8aa", size = 557416, upload-time = "2026-06-18T19:39:21.902Z" }, +] + +[[package]] +name = "langchain-protocol" +version = "0.0.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/59/b5959aea96faa9146e2e49a7a22882b3528c62efafe9a6a95beab30c2305/langchain_protocol-0.0.18.tar.gz", hash = "sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6", size = 6150, upload-time = "2026-06-18T17:08:26.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/2e/d82db9eec13ad0f72e7aaad5c4bc730ab111934fdc83c85523206eb9b0a0/langchain_protocol-0.0.18-py3-none-any.whl", hash = "sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a", size = 7221, upload-time = "2026-06-18T17:08:25.996Z" }, +] + +[[package]] +name = "langgraph" +version = "1.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, + { name = "langgraph-prebuilt" }, + { name = "langgraph-sdk" }, + { name = "pydantic" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/7a/ea09b05bb0cbddfa43bd34fc581357e87fc3f21a751cc0d419688c3106da/langgraph-1.2.6.tar.gz", hash = "sha256:f9b45a34f13930c94d96cdb76277447ad2cc70ec2d18cd2764d7fdadb36cdc1b", size = 714400, upload-time = "2026-06-18T20:58:21.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/32/772db1b00a9fe42f50320d1aa20caefb76e621eff1f7218b9918093d631d/langgraph-1.2.6-py3-none-any.whl", hash = "sha256:1cf94d3ca124f84f77ce408fa1b06c3dee680a8aafffe364a8fd5d7d03eb8695", size = 246132, upload-time = "2026-06-18T20:58:20.335Z" }, +] + +[[package]] +name = "langgraph-checkpoint" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "ormsgpack" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/47/886af6f886f0bff2273164a45f008694e48a96ff3cd25ff0228f2aa9480e/langgraph_checkpoint-4.1.1.tar.gz", hash = "sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25", size = 184020, upload-time = "2026-05-22T16:57:38.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/b4/71425e3e38be92611300b9cc5e46a5bf98ab23f5ea8a75b73d02a2f1413c/langgraph_checkpoint-4.1.1-py3-none-any.whl", hash = "sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e", size = 56212, upload-time = "2026-05-22T16:57:37.203Z" }, +] + +[[package]] +name = "langgraph-prebuilt" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833, upload-time = "2026-05-12T03:37:49.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl", hash = "sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9", size = 41043, upload-time = "2026-05-12T03:37:48.007Z" }, +] + +[[package]] +name = "langgraph-sdk" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "langchain-core" }, + { name = "langchain-protocol" }, + { name = "orjson" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/2b/bd8ac26d4e97f6df88ef05ce5b6a38945a3903e1025d926f4752aa88aa97/langgraph_sdk-0.4.2.tar.gz", hash = "sha256:b88f0f5f6328ac0680d6790614a905b2bcfa257f2276dba4e38f0e86db0aa738", size = 348327, upload-time = "2026-06-01T17:51:19.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/05/aac507337cceae773c2cc9ab91eb6301963af7aeeb55b4217a00e15aff17/langgraph_sdk-0.4.2-py3-none-any.whl", hash = "sha256:75fa5096c1177ce39c847096a8fe3745ffd480ddb412995f836e9f5f884c43dd", size = 160521, upload-time = "2026-06-01T17:51:18.849Z" }, +] + +[[package]] +name = "langsmith" +version = "0.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "sniffio" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, + { name = "websockets" }, + { name = "xxhash" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/b7/43f80940a758ceb48714ccc10c7acb9564be4f0d4786e8ed5d46deb0be39/langsmith-0.9.3.tar.gz", hash = "sha256:868a007b3ecda002914b21212a953c77fcb1d71a8a09a94562c3b57941d3df3a", size = 4577380, upload-time = "2026-06-26T15:56:30.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/fb/dc9f70b5581371fba7b01ba0704fe859bfcd12c4dad82b42ec12bd4160b1/langsmith-0.9.3-py3-none-any.whl", hash = "sha256:aac85686868a7e61d77858b880230e510583c61ea48ac0a197e6ceff64ffb22b", size = 619927, upload-time = "2026-06-26T15:56:28.301Z" }, +] + +[[package]] +name = "librt" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29", size = 141092, upload-time = "2026-05-10T18:15:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9", size = 142035, upload-time = "2026-05-10T18:15:36.242Z" }, + { url = "https://files.pythonhosted.org/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5", size = 475022, upload-time = "2026-05-10T18:15:37.56Z" }, + { url = "https://files.pythonhosted.org/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b", size = 467273, upload-time = "2026-05-10T18:15:39.182Z" }, + { url = "https://files.pythonhosted.org/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89", size = 497083, upload-time = "2026-05-10T18:15:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc", size = 489139, upload-time = "2026-05-10T18:15:41.934Z" }, + { url = "https://files.pythonhosted.org/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5", size = 508442, upload-time = "2026-05-10T18:15:43.206Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7", size = 514230, upload-time = "2026-05-10T18:15:44.761Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d", size = 494231, upload-time = "2026-05-10T18:15:46.308Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412", size = 537585, upload-time = "2026-05-10T18:15:47.629Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d", size = 100509, upload-time = "2026-05-10T18:15:49.157Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73", size = 118628, upload-time = "2026-05-10T18:15:50.345Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c", size = 103122, upload-time = "2026-05-10T18:15:52.068Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, + { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, + { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, + { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, + { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, + { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, + { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, + { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { 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 = "1.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { 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/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +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 = "mypy" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/a1/639f3024794a2a15899cb90707fe02e044c4412794c39c5769fd3df2e2ef/mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41", size = 14691685, upload-time = "2026-05-11T18:33:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/3b/08/9a585dea4325f20d8b80dc78623fa50d1fd2173b710f6237afd6ba6ab39b/mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca", size = 13555165, upload-time = "2026-05-11T18:32:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/81/dc/7c42cc9c6cb01e8eb09961f1f738741d3e9c7e9d5c5b30ec69222625cd5f/mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538", size = 13994376, upload-time = "2026-05-11T18:32:39.256Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/285946c33bce716e082c11dfeee9ee196eaf1f5042efb3581a31f9f205e4/mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398", size = 14864618, upload-time = "2026-05-11T18:34:49.765Z" }, + { url = "https://files.pythonhosted.org/packages/2b/83/82397f48af6c27e295d57979ded8490c9829040152cf7571b2f026aeb9a0/mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563", size = 15102063, upload-time = "2026-05-11T18:34:05.855Z" }, + { url = "https://files.pythonhosted.org/packages/40/68/b02dec39057b88eb03dc0aa854732e26e8361f34f9d0e20c7614967d1eba/mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389", size = 11060564, upload-time = "2026-05-11T18:35:36.494Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a8/ea3dcbef31f99b634f2ee23bb0321cbc8c1b388b76a861eb849f13c347dc/mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666", size = 9966983, upload-time = "2026-05-11T18:37:14.139Z" }, + { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" }, + { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" }, + { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" }, + { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, + { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, + { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, + { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "myst-parser" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02", size = 101182, upload-time = "2026-05-13T09:38:19.361Z" } +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 = "opentelemetry-api" +version = "1.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/cc/e4c9584181f86494df0f6bdec1a4f3280c50db44704dc2a407e994fc87bb/opentelemetry_api-1.43.0.tar.gz", hash = "sha256:107d0d03857ea8fc7c5fcbbbd83f800c281f0d560553d61c1d675fccfd1761c1", size = 73476, upload-time = "2026-06-24T15:19:55.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/83/6dba32b85f31868400440dc7ad2ca1eab94cbbf3a7b0459ed39f8311a9e2/opentelemetry_api-1.43.0-py3-none-any.whl", hash = "sha256:20acf45e9b21851926835292e4045d290acade1edd2ff3de86d2f069687ba1fd", size = 61912, upload-time = "2026-06-24T15:19:35.434Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.64b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/97/02fe6e1c8b1ffac42d0b429c18080edb24e0e0d18c86612edf72b5752382/opentelemetry_instrumentation-0.64b0.tar.gz", hash = "sha256:b47d528dead6271d7743114417eb67fc915bd9258111c48dbf9a4951d2efa88d", size = 41935, upload-time = "2026-06-24T15:19:12.951Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/0c/cb9fe342de5299c7af24582eb7d788661cc53a1c4b904da92309caaa9417/opentelemetry_instrumentation-0.64b0-py3-none-any.whl", hash = "sha256:133ab7ffca796557aec059bf6be3190a34b6dea987f25be3d9409e230cbdad8b", size = 35880, upload-time = "2026-06-24T15:18:17.277Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-threading" +version = "0.64b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/ef/d4a16eb4a0f0971e351ec2ff95846b501b66a619d07d90822942d70a3adc/opentelemetry_instrumentation_threading-0.64b0.tar.gz", hash = "sha256:0a07d7329f69dfae5036a7cb184f502c8b91cb0538012f5304bd32ffe9ade451", size = 9080, upload-time = "2026-06-24T15:19:43.22Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/b3/8838d5f3db35b3854516ccfbf8ec26138d40388d828407182c890c3aa9d0/opentelemetry_instrumentation_threading-0.64b0-py3-none-any.whl", hash = "sha256:a285ffa750a958f7d368e947f5679a0214d588242cbffba0f5934cb02e9a17f4", size = 8487, upload-time = "2026-06-24T15:19:00.813Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/eb/5041074274ac0956b03637cc039d434569112468e875eddfcc9a0674ce06/opentelemetry_sdk-1.43.0.tar.gz", hash = "sha256:d8187c81c162df9913e4003dd6485f7390d9a24fc17026ec7387b8b8218b08e9", size = 254744, upload-time = "2026-06-24T15:20:08.467Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/e3/b17be23af124201c9f52eececd4cc8ddfed1597d37b4ee771895d325805c/opentelemetry_sdk-1.43.0-py3-none-any.whl", hash = "sha256:d1323a547c1ce69d6a069a17a44b7da82bb8b332051ecb074041f87642c86823", size = 178852, upload-time = "2026-06-24T15:19:52.169Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.64b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/30/5f26df29509eccd86b99b481ac9ffa39da49ba9577cc69071c552ae30447/opentelemetry_semantic_conventions-0.64b0.tar.gz", hash = "sha256:72f76fb2d1582d9d033dd1fcd84532e961e6ff3d90d24ba6fabc72975a83864c", size = 148340, upload-time = "2026-06-24T15:20:09.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/ca/23ba87a221b574a7c5a99d48849d80bfe8b047624681357e2b002e566187/opentelemetry_semantic_conventions-0.64b0-py3-none-any.whl", hash = "sha256:ea77e85e354b8f604ddbe5f3d9135216f982fa4d77e5859ac30f6d8a50505aa6", size = 203713, upload-time = "2026-06-24T15:19:53.339Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/51/3fb9e65ae76ee97bd611869a503fa3fc0a6e81dd8b737cf3003f682df7ff/orjson-3.11.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f", size = 228522, upload-time = "2026-05-06T15:09:35.362Z" }, + { url = "https://files.pythonhosted.org/packages/16/fa/9d54b07cb3f3b0bfd57841478e42d7a0ece4a9f49f9907eecf5a45461687/orjson-3.11.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c", size = 128463, upload-time = "2026-05-06T15:09:37.063Z" }, + { url = "https://files.pythonhosted.org/packages/88/b1/6ceafc2eefd0a553e3be77ce6c49d107e772485d9568629376171c50e634/orjson-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5", size = 132306, upload-time = "2026-05-06T15:09:38.299Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/f11311285324a40aab1e3031385c50b635a7cd0734fdaf60c7e89a696f60/orjson-3.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c", size = 127988, upload-time = "2026-05-06T15:09:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/9e/85/0ef63bcf1337f44031ce9b91b1919563f62a37527b3ea4368bb15a22e5d7/orjson-3.11.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd", size = 135188, upload-time = "2026-05-06T15:09:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/05/94/b0d27090ea8a2095db3c2bd1b1c96f96f19bbb494d7fef33130e846e613d/orjson-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62", size = 145937, upload-time = "2026-05-06T15:09:42.249Z" }, + { url = "https://files.pythonhosted.org/packages/09/eb/75d50c29c05b8054013e221e598820a365c8e64065312e75e202ed880709/orjson-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877", size = 132758, upload-time = "2026-05-06T15:09:43.945Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/360686f39348aa88827cb6fbf7dc606fd41c831a35235e1abf1db8e3a9e6/orjson-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1", size = 133971, upload-time = "2026-05-06T15:09:45.239Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/3178eb16f3221aeef068b6f1f1ebe05f656ea5c6dffe9f6c917329fe17a3/orjson-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd", size = 141685, upload-time = "2026-05-06T15:09:46.858Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f1/ff2f19ed0225f9680fafa42febca3570dd59444ebf190980738d376214c2/orjson-3.11.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff", size = 415167, upload-time = "2026-05-06T15:09:48.312Z" }, + { url = "https://files.pythonhosted.org/packages/9b/61/863bddf0da6e9e586765414debd54b4e58db05f560902b6d00658cb88636/orjson-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980", size = 147913, upload-time = "2026-05-06T15:09:49.733Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4081492586d75b073d60c5271a8d0f05a0955cabf1e34c8473f6fcd84235/orjson-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2", size = 136959, upload-time = "2026-05-06T15:09:51.311Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bd/70b6ab193594d7abb875320c0a7c8335e846f28968c432c31042409c3c8d/orjson-3.11.9-cp311-cp311-win32.whl", hash = "sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180", size = 131533, upload-time = "2026-05-06T15:09:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/3f/17/1a1a228183d62d1b77e2c30d210f47dd4768b310ebe1607c63e3c0e3a71e/orjson-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02", size = 127106, upload-time = "2026-05-06T15:09:54.204Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/285de5fa296d09681ee9c546cd4a8aeb773b701cf343dc125994f4d52953/orjson-3.11.9-cp311-cp311-win_arm64.whl", hash = "sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697", size = 126848, upload-time = "2026-05-06T15:09:55.551Z" }, + { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, + { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, + { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" }, + { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" }, + { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" }, + { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" }, + { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" }, + { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" }, + { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" }, + { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" }, + { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" }, + { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" }, + { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" }, + { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" }, +] + +[[package]] +name = "ormsgpack" +version = "1.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031, upload-time = "2026-01-18T20:55:28.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/08/8b68f24b18e69d92238aa8f258218e6dfeacf4381d9d07ab8df303f524a9/ormsgpack-1.12.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9", size = 378266, upload-time = "2026-01-18T20:55:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/29fc13044ecb7c153523ae0a1972269fcd613650d1fa1a9cec1044c6b666/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a", size = 203035, upload-time = "2026-01-18T20:55:30.59Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c2/00169fb25dd8f9213f5e8a549dfb73e4d592009ebc85fbbcd3e1dcac575b/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5", size = 210539, upload-time = "2026-01-18T20:55:48.569Z" }, + { url = "https://files.pythonhosted.org/packages/1b/33/543627f323ff3c73091f51d6a20db28a1a33531af30873ea90c5ac95a9b5/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181", size = 212401, upload-time = "2026-01-18T20:56:10.101Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5d/f70e2c3da414f46186659d24745483757bcc9adccb481a6eb93e2b729301/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b", size = 387082, upload-time = "2026-01-18T20:56:12.047Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d6/06e8dc920c7903e051f30934d874d4afccc9bb1c09dcaf0bc03a7de4b343/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92", size = 482346, upload-time = "2026-01-18T20:56:05.152Z" }, + { url = "https://files.pythonhosted.org/packages/66/c4/f337ac0905eed9c393ef990c54565cd33644918e0a8031fe48c098c71dbf/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a", size = 425181, upload-time = "2026-01-18T20:55:37.83Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/6d5758fabef3babdf4bbbc453738cc7de9cd3334e4c38dd5737e27b85653/ormsgpack-1.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:73670ed0375ecc303858e3613f407628dd1fca18fe6ac57b7b7ce66cc7bb006c", size = 117182, upload-time = "2026-01-18T20:55:31.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/17a15549233c37e7fd054c48fe9207492e06b026dbd872b826a0b5f833b6/ormsgpack-1.12.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2be829954434e33601ae5da328cccce3266b098927ca7a30246a0baec2ce7bd", size = 111464, upload-time = "2026-01-18T20:55:38.811Z" }, + { url = "https://files.pythonhosted.org/packages/4c/36/16c4b1921c308a92cef3bf6663226ae283395aa0ff6e154f925c32e91ff5/ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7", size = 378618, upload-time = "2026-01-18T20:55:50.835Z" }, + { url = "https://files.pythonhosted.org/packages/c0/68/468de634079615abf66ed13bb5c34ff71da237213f29294363beeeca5306/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d", size = 203186, upload-time = "2026-01-18T20:56:11.163Z" }, + { url = "https://files.pythonhosted.org/packages/73/a9/d756e01961442688b7939bacd87ce13bfad7d26ce24f910f6028178b2cc8/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e", size = 210738, upload-time = "2026-01-18T20:56:09.181Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ba/795b1036888542c9113269a3f5690ab53dd2258c6fb17676ac4bd44fcf94/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc", size = 212569, upload-time = "2026-01-18T20:56:06.135Z" }, + { url = "https://files.pythonhosted.org/packages/6c/aa/bff73c57497b9e0cba8837c7e4bcab584b1a6dbc91a5dd5526784a5030c8/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e", size = 387166, upload-time = "2026-01-18T20:55:36.738Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cf/f8283cba44bcb7b14f97b6274d449db276b3a86589bdb363169b51bc12de/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6", size = 482498, upload-time = "2026-01-18T20:55:29.626Z" }, + { url = "https://files.pythonhosted.org/packages/05/be/71e37b852d723dfcbe952ad04178c030df60d6b78eba26bfd14c9a40575e/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd", size = 425518, upload-time = "2026-01-18T20:55:49.556Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0c/9803aa883d18c7ef197213cd2cbf73ba76472a11fe100fb7dab2884edf48/ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4", size = 117462, upload-time = "2026-01-18T20:55:47.726Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9e/029e898298b2cc662f10d7a15652a53e3b525b1e7f07e21fef8536a09bb8/ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6", size = 111559, upload-time = "2026-01-18T20:55:54.273Z" }, + { url = "https://files.pythonhosted.org/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355", size = 378661, upload-time = "2026-01-18T20:55:57.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1", size = 203194, upload-time = "2026-01-18T20:56:08.252Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172", size = 210778, upload-time = "2026-01-18T20:55:17.694Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d", size = 212592, upload-time = "2026-01-18T20:55:32.747Z" }, + { url = "https://files.pythonhosted.org/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7", size = 387164, upload-time = "2026-01-18T20:55:40.853Z" }, + { url = "https://files.pythonhosted.org/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685", size = 482516, upload-time = "2026-01-18T20:55:42.033Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258", size = 425539, upload-time = "2026-01-18T20:55:24.727Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9", size = 117459, upload-time = "2026-01-18T20:55:56.876Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709", size = 111577, upload-time = "2026-01-18T20:55:43.605Z" }, + { url = "https://files.pythonhosted.org/packages/94/16/24d18851334be09c25e87f74307c84950f18c324a4d3c0b41dabdbf19c29/ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c", size = 378717, upload-time = "2026-01-18T20:55:26.164Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a2/88b9b56f83adae8032ac6a6fa7f080c65b3baf9b6b64fd3d37bd202991d4/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553", size = 203183, upload-time = "2026-01-18T20:55:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/43e4555963bf602e5bdc79cbc8debd8b6d5456c00d2504df9775e74b450b/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13", size = 210814, upload-time = "2026-01-18T20:55:33.973Z" }, + { url = "https://files.pythonhosted.org/packages/78/e1/7cfbf28de8bca6efe7e525b329c31277d1b64ce08dcba723971c241a9d60/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d", size = 212634, upload-time = "2026-01-18T20:55:28.634Z" }, + { url = "https://files.pythonhosted.org/packages/95/f8/30ae5716e88d792a4e879debee195653c26ddd3964c968594ddef0a3cc7e/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede", size = 387139, upload-time = "2026-01-18T20:56:02.013Z" }, + { url = "https://files.pythonhosted.org/packages/dc/81/aee5b18a3e3a0e52f718b37ab4b8af6fae0d9d6a65103036a90c2a8ffb5d/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e", size = 482578, upload-time = "2026-01-18T20:55:35.117Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/71c9ba472d5d45f7546317f467a5fc941929cd68fb32796ca3d13dcbaec2/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285", size = 425539, upload-time = "2026-01-18T20:56:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a6/ac99cd7fe77e822fed5250ff4b86fa66dd4238937dd178d2299f10b69816/ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f", size = 117493, upload-time = "2026-01-18T20:56:07.343Z" }, + { url = "https://files.pythonhosted.org/packages/3a/67/339872846a1ae4592535385a1c1f93614138566d7af094200c9c3b45d1e5/ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c", size = 111579, upload-time = "2026-01-18T20:55:21.161Z" }, + { url = "https://files.pythonhosted.org/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8", size = 378721, upload-time = "2026-01-18T20:55:52.12Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9a/900a6b9b413e0f8a471cf07830f9cf65939af039a362204b36bd5b581d8b/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033", size = 203170, upload-time = "2026-01-18T20:55:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/87/4c/27a95466354606b256f24fad464d7c97ab62bce6cc529dd4673e1179b8fb/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d", size = 212816, upload-time = "2026-01-18T20:55:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/73/cd/29cee6007bddf7a834e6cd6f536754c0535fcb939d384f0f37a38b1cddb8/ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2", size = 117232, upload-time = "2026-01-18T20:55:45.448Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +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 = "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.4" +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/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +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 = "pyproject-api" +version = "1.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/62/0fe346fe380b1aafaf819c8cb195d3241bb4f355f908e6339814131a830b/pyproject_api-1.10.1.tar.gz", hash = "sha256:c2b2726bd7aa9217b6c50b621fef5b2ae5def4d55b779c9e0694c15e0a8517ba", size = 23477, upload-time = "2026-05-28T14:22:14.049Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/d7/29e1e5e882f79133631f7bcace42d23db493f616463c157a1ab614bf69dd/pyproject_api-1.10.1-py3-none-any.whl", hash = "sha256:fa9e6f66c35b5017e909825d8f2b5d5482ea699d7be809d21c03bd1f7317f36a", size = 12992, upload-time = "2026-05-28T14:22:12.711Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-agentic-fabric" +version = "0.1.0" +source = { editable = "packages/pytest-agentic-fabric" } +dependencies = [ + { name = "pytest" }, + { name = "pytest-mock" }, + { name = "pyyaml" }, +] + +[package.optional-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-mock" }, + { name = "ruff" }, +] +tests = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-mock" }, +] +typing = [ + { name = "mypy" }, +] + +[package.metadata] +requires-dist = [ + { name = "mypy", marker = "extra == 'typing'", specifier = ">=2.1.0" }, + { name = "pytest", specifier = ">=9.1.1" }, + { name = "pytest", marker = "extra == 'tests'", specifier = ">=9.1.1" }, + { name = "pytest-agentic-fabric", extras = ["tests", "typing"], marker = "extra == 'dev'", editable = "packages/pytest-agentic-fabric" }, + { name = "pytest-asyncio", marker = "extra == 'tests'", specifier = ">=1.4.0" }, + { name = "pytest-mock", specifier = ">=3.15.1" }, + { name = "pytest-mock", marker = "extra == 'tests'", specifier = ">=3.15.1" }, + { name = "pyyaml", specifier = ">=6.0.3" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.20" }, +] +provides-extras = ["tests", "typing", "dev"] + +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/1a/cbbaf13b730abb0a16b964d984e19f2fe520c21a4dc664051359a3f5a9e7/python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690", size = 70277, upload-time = "2026-06-11T16:10:42.383Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", size = 33886, upload-time = "2026-06-11T16:10:41.192Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[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 = "pywin32" +version = "312" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" }, + { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" }, + { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, + { 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 = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { 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" }, + { 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" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +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 = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "roman-numerals-py" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "roman-numerals" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/b5/de96fca640f4f656eb79bbee0e79aeec52e3e0e359f8a3e6a0d366378b64/roman_numerals_py-4.1.0.tar.gz", hash = "sha256:f5d7b2b4ca52dd855ef7ab8eb3590f428c0b1ea480736ce32b01fef2a5f8daf9", size = 4274, upload-time = "2025-12-17T18:25:41.153Z" } +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 = "2026.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/a0/acf8b6fc20bfdcd3a45bd3f57680fb198e157b7e997b9123b10763798bd2/rpds_py-2026.5.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3397a5ed7174dc2786bb214030232fc36fe8e5584fec43a9952cc542b1a12036", size = 355609, upload-time = "2026-05-28T11:58:50.78Z" }, + { url = "https://files.pythonhosted.org/packages/b6/95/f8203fd997484b1690a6869cd0e503b6c3c6be55b0ecc36d1a491fe742f0/rpds_py-2026.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:99ab6ba7bfa2cb0f96a04e3652355bf04e3f51aceb1e943b8541dab7ba4828cc", size = 348460, upload-time = "2026-05-28T11:58:52.374Z" }, + { url = "https://files.pythonhosted.org/packages/33/8c/b47326ad2f0be545a5e5c1a55937a12afaea7d392ba2837bb9680f57e6c9/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0efbe45632665e53e3db8fe1e5692db58fc5cb9bab4459d570b83efefe11164", size = 381031, upload-time = "2026-05-28T11:58:53.775Z" }, + { url = "https://files.pythonhosted.org/packages/22/0b/e83bbd97ffac6f6389b605cd4e1c8ac5761dc7e977769c9255d8c5adb7bd/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:01d17b29c0c23d82b1f4751147ec49cf451f1fc2554eb9ef5f957e55d2656ead", size = 387121, upload-time = "2026-05-28T11:58:55.243Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0e/d285d1bc8864245919c61e1ca82263e4a66d337759c3a4cef72766ff9afc/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7559f72b94ae52659086c595dfa017cde03155f7832071d30959049052cb3ece", size = 501026, upload-time = "2026-05-28T11:58:56.788Z" }, + { url = "https://files.pythonhosted.org/packages/86/06/ccb2109a1e543437b5e43816f2b43b9554cc6783145528a4e3711e05c011/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e25b7088f9ccbfc0dfcaa52bf969300ca229e10ecf758974ebcbb080a4b37bb", size = 391865, upload-time = "2026-05-28T11:58:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/3d/33/237173db1cfef10105b3839a24de00eb8d2a523711add4632447cdf0aedd/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613fc4ee9eaef26dc5840666214dd6fbcebcf32f46e76f4abc473059f4e13dda", size = 378012, upload-time = "2026-05-28T11:58:59.589Z" }, + { url = "https://files.pythonhosted.org/packages/97/64/1eae54e34d5161f9969295e80bd6b62a55f2b6ac5f2a5b60d02c2140e758/rpds_py-2026.5.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:85264a90ff4c05c1568dd65f5921c837614b67c60358fb4c17df3b7f2e90690a", size = 391111, upload-time = "2026-05-28T11:59:01.104Z" }, + { url = "https://files.pythonhosted.org/packages/d8/34/5bb334a5a0f65d77869217c4654f34c78a7d11b93938a3c076a2edeafc52/rpds_py-2026.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe71bca7d547acb17027c7fd1624ff8aae623499c498d3e7011182c4de5c25e0", size = 409225, upload-time = "2026-05-28T11:59:02.433Z" }, + { url = "https://files.pythonhosted.org/packages/16/0f/007ec21283b5b040b4ec3bd95e0402591e22bfa7d5c93dfe01c465c2d2d7/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05fa4f41f37ec97c9c260441a940450a192f78d774d2b097eee1379f1e1246a", size = 556487, upload-time = "2026-05-28T11:59:04.012Z" }, + { url = "https://files.pythonhosted.org/packages/ff/10/5437c94508169b6b22d8418fef7a66e9ffb5f3b9e9c94460f2eedafe06ff/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df1d2a1996755b24b9ecee92cb4d36c28f86f464a6a173349c26bab41e94b8c2", size = 620798, upload-time = "2026-05-28T11:59:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d5/9937dce4d6bda74157b954e7d1460db05a22f5929dccfeeba1ed27a93df0/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8895840ac4809e5f60c88fd07617cd71326e73d6e5a8aa783c5c0f7c24985de2", size = 584053, upload-time = "2026-05-28T11:59:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/6c/31/750617dd0ae1752471bf43f9e41d263398fae7cde7849d23b8574a70e617/rpds_py-2026.5.1-cp311-cp311-win32.whl", hash = "sha256:3684a59b158a7683aaeb8e25352e9a9dd2122cec78f2d8530266e4f91b4c7b3f", size = 214390, upload-time = "2026-05-28T11:59:08.402Z" }, + { url = "https://files.pythonhosted.org/packages/3c/bb/3dcab0e1d9516303f2eb672a5d6f62eca5a69e2886301e9c8c54b520c39b/rpds_py-2026.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:7bd530e6a530bb3ea892f194fafa455f3516ac25ecf7143fd33c09be62b0470a", size = 231097, upload-time = "2026-05-28T11:59:09.786Z" }, + { url = "https://files.pythonhosted.org/packages/49/d6/c6bbf5cb1cf12b9732df8074b57f6ef8341ba884c95d40632ae8bddb44e4/rpds_py-2026.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:0a5ae4dbe43c1076983b72616496919872ae7bbe7a1e21cc48336bc3154d130b", size = 226361, upload-time = "2026-05-28T11:59:11.079Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e7/a78582dc57caa592dcc7d4fb69b61390561e908eb3d2f5df5928a8e354c0/rpds_py-2026.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3abe24a66e57adcfa645d718063a5fa5103ecc71ddbf26d78af8f9368018ff1d", size = 353040, upload-time = "2026-05-28T11:59:12.531Z" }, + { url = "https://files.pythonhosted.org/packages/a3/43/35e3f136343aef451e545ce8c38d36c2f93c0ed88703db8b64ba2b205c68/rpds_py-2026.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c", size = 345775, upload-time = "2026-05-28T11:59:13.827Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/0f2160c5982d3157734d5cb3ed63d8b2d583a73c9864f77b666449f32cf8/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08", size = 376329, upload-time = "2026-05-28T11:59:15.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/11/ee0ba42aff83bf4effdbc576673c6be64c5e173978c3f6d537e94482f77d/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb", size = 383539, upload-time = "2026-05-28T11:59:16.665Z" }, + { url = "https://files.pythonhosted.org/packages/11/df/d94aa6a499d4ac40afe2d7620f2c597fd3c0f182e854ad7cf3f596a81cb6/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1", size = 494674, upload-time = "2026-05-28T11:59:17.991Z" }, + { url = "https://files.pythonhosted.org/packages/1f/75/33d30f43bb2f458de11979486a591b1bf6e5651765ed1704c6197c2dc773/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5", size = 389268, upload-time = "2026-05-28T11:59:19.434Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1e/2c9096fc19d5fd084b0184ca2b651e659aa0a37e6fdbecf6ece47f147fe1/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644", size = 376280, upload-time = "2026-05-28T11:59:21Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e5/61ec9f8be8211ea7f48448195549e4aaf02004083475493b0e137702ecb2/rpds_py-2026.5.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4", size = 387233, upload-time = "2026-05-28T11:59:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/bcec1005c4f4a234f92a29078631fee49206c7265ccae966f18fd332e80e/rpds_py-2026.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0f920015df2a504bebaba6d4c31ccf3fcf942f92655c086da30b671aad19aa6", size = 405009, upload-time = "2026-05-28T11:59:23.845Z" }, + { url = "https://files.pythonhosted.org/packages/72/e6/4d5718c5cf26c522dc7c9999e238da1e77380b81d0c5d1df11e271ddfeb1/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4", size = 553113, upload-time = "2026-05-28T11:59:25.184Z" }, + { url = "https://files.pythonhosted.org/packages/d4/25/2ee807bdb3e1f0b7eddf7782acd5665a8b5205a331a7d7244a52c4812fd9/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cea68bcd53467561ae2f96a6bdad1544299ba97b5b0ddcd5ac3d376e5c781c24", size = 618838, upload-time = "2026-05-28T11:59:26.749Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c1/7d4c26f167f8c41501cc073d30ee22082b16ce358cf5b00ec97cbc7804ea/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732", size = 582436, upload-time = "2026-05-28T11:59:28.11Z" }, + { url = "https://files.pythonhosted.org/packages/04/1d/9d12b0a337bab46f4769f8857f4007e3b2d639e14f9a44a0efe157696e64/rpds_py-2026.5.1-cp312-cp312-win32.whl", hash = "sha256:6736718bd4fc49cbcb538ba30516fdbef161522acefb739657d48b97bd864fed", size = 212734, upload-time = "2026-05-28T11:59:29.689Z" }, + { url = "https://files.pythonhosted.org/packages/c5/93/e4116f2de7f56bc7406a76033dc501811ddeb22b7f056b92d632871ebb0c/rpds_py-2026.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0a7d1eec967df0e9b22614a5e177622e0c89611d03727fa0cb48e45028907870", size = 229045, upload-time = "2026-05-28T11:59:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/53/6c3419d85eb2ec5938a37627c585b42d76a63bb731d6e42ed4b079ebf486/rpds_py-2026.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1841d067089e117142d79b98aa0df2f08b52f2ecc1819dd2700636c0db74a473", size = 223967, upload-time = "2026-05-28T11:59:32.318Z" }, + { url = "https://files.pythonhosted.org/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d", size = 352787, upload-time = "2026-05-28T11:59:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" }, + { url = "https://files.pythonhosted.org/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" }, + { url = "https://files.pythonhosted.org/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" }, + { url = "https://files.pythonhosted.org/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" }, + { url = "https://files.pythonhosted.org/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46", size = 405059, upload-time = "2026-05-28T11:59:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f", size = 618975, upload-time = "2026-05-28T11:59:50.314Z" }, + { url = "https://files.pythonhosted.org/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" }, + { url = "https://files.pythonhosted.org/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842", size = 212481, upload-time = "2026-05-28T11:59:53.148Z" }, + { url = "https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf", size = 228519, upload-time = "2026-05-28T11:59:54.505Z" }, + { url = "https://files.pythonhosted.org/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd", size = 223446, upload-time = "2026-05-28T11:59:56.081Z" }, + { url = "https://files.pythonhosted.org/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600", size = 355287, upload-time = "2026-05-28T11:59:57.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" }, + { url = "https://files.pythonhosted.org/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" }, + { url = "https://files.pythonhosted.org/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2", size = 408578, upload-time = "2026-05-28T12:00:10.779Z" }, + { url = "https://files.pythonhosted.org/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" }, + { url = "https://files.pythonhosted.org/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1", size = 620861, upload-time = "2026-05-28T12:00:14.09Z" }, + { url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" }, + { url = "https://files.pythonhosted.org/packages/d4/6f/19c1918a4b590d8de87e712e4abe4b3875771eff60216fb6153cf6665c68/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9", size = 349756, upload-time = "2026-05-28T12:00:20.217Z" }, + { url = "https://files.pythonhosted.org/packages/e5/60/a06fe7da34eca79dacbf958a2ba0c6eea85bc2b29de20080bf40f72f66fa/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78", size = 343831, upload-time = "2026-05-28T12:00:21.711Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/b2333b97b90e2a6ef6ca8ad386ee284968e74bcfe113b3f1a8d9036429a9/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63", size = 375127, upload-time = "2026-05-28T12:00:23.326Z" }, + { url = "https://files.pythonhosted.org/packages/14/7f/e00aae54067f2b488c4637961d5f58204d470795fc791085fa3f15060d2e/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a", size = 379034, upload-time = "2026-05-28T12:00:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/be/cc/423999bbb8ae8dc93c77fc1d5e984ade5eb89d237d3bb884ccfa72ae2890/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195", size = 490823, upload-time = "2026-05-28T12:00:26.676Z" }, + { url = "https://files.pythonhosted.org/packages/0f/aa/c671bf660f12e68d3c52ff86c7066ed1372df5a0f4f2ff584e419b8207e7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee", size = 388144, upload-time = "2026-05-28T12:00:28.577Z" }, + { url = "https://files.pythonhosted.org/packages/19/c8/d63bb75b68afe77b229e3021c6031bcaf01da5db5b0e69d0d10f9ba679a7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba", size = 371959, upload-time = "2026-05-28T12:00:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/82/35/c51122014d8274ff37dc606d60049c3db7d83da02b5b282511e5a906a9a6/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec", size = 383558, upload-time = "2026-05-28T12:00:31.764Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f9/2790cb99c136a5363acdeacf5c27c56f3de0d4118a1f48fca83404c99c89/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d", size = 402789, upload-time = "2026-05-28T12:00:33.247Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1b/e4fb584f8c75d35c38150ff6a332cda949e6f97acba1f4fd123b14ab56fe/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d", size = 551405, upload-time = "2026-05-28T12:00:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f7/a6731b4216cb3793ea1af5391da240f5683dacc0d13e034fe5fc3503f240/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02", size = 616975, upload-time = "2026-05-28T12:00:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/2e051a81d95d8e63f4b35a1c463a87e8766bc3d083c067c5dfb6bf220747/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0", size = 578701, upload-time = "2026-05-28T12:00:37.82Z" }, + { url = "https://files.pythonhosted.org/packages/65/56/b5f6fdb2083e32bca8a8993d89e70db114b4756c9e2c38421328126689d2/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7", size = 209806, upload-time = "2026-05-28T12:00:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/fb/80/65a5aa96c155e611d1ed844e4e1f57f3e36b021f396d9f8585d756e6b90d/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838", size = 225985, upload-time = "2026-05-28T12:00:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/27/7c/ad185212e87b05f196daef92bc5f3caf07298eb47c295b5585c3dd3093ac/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8", size = 221219, upload-time = "2026-05-28T12:00:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/23/58/e14ae18759020334646b031e708ab4158d653a938822bfb7b95ef2e93aa3/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad", size = 352148, upload-time = "2026-05-28T12:00:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/31/9b/5f4a1e2f960bca3ac5d052b139dd31eed97b259f9d909173821760d542e8/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3", size = 345196, upload-time = "2026-05-28T12:00:46.14Z" }, + { url = "https://files.pythonhosted.org/packages/1a/71/1d9574d6a2fa20ab60eaa55c7467f5aa20cbc770f341a05f09c0876f59e2/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081", size = 374981, upload-time = "2026-05-28T12:00:47.531Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/37e99f4915a80aa71670263c1267f7ae0af95f53a3f61e6c3bdc016d4515/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6", size = 379961, upload-time = "2026-05-28T12:00:49.216Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ff/6e73f74b89d2e0715e0fc86b7dde893f9a61ae2f9b256ff3bdfe41ac4e94/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5", size = 495965, upload-time = "2026-05-28T12:00:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e0/425faba25f59d74d4638b267f7c7a80e8649d2ef4db10a19b0c4a71e6e6f/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b", size = 389526, upload-time = "2026-05-28T12:00:52.77Z" }, + { url = "https://files.pythonhosted.org/packages/c6/76/7a41960e3fddae47fab43a28684d5da981401dffd88253de0944148654cb/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964", size = 376190, upload-time = "2026-05-28T12:00:54.215Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/5f38dc70824fc6951b51d35377e577a3a3a4c81a6769cc5a2de25ebe0ad1/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131", size = 383921, upload-time = "2026-05-28T12:00:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/1a/d60a38caa1505f4b9483c3fbbde12c94e1079154f4f401a6da96f7e77621/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81", size = 404766, upload-time = "2026-05-28T12:00:57.518Z" }, + { url = "https://files.pythonhosted.org/packages/87/ff/602fd3f174d6425f0bce05ad0dfbec0e96b38d0f7d08a79af5aa20083885/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47", size = 551343, upload-time = "2026-05-28T12:00:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c1/1be13327acdbead3eca1fde03b6a34dbb011f1e864e217f0d32cc1779a7f/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a", size = 618502, upload-time = "2026-05-28T12:01:00.656Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca", size = 581916, upload-time = "2026-05-28T12:01:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a", size = 207855, upload-time = "2026-05-28T12:01:03.821Z" }, + { url = "https://files.pythonhosted.org/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6", size = 225422, upload-time = "2026-05-28T12:01:05.194Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb", size = 349576, upload-time = "2026-05-28T12:01:06.722Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291", size = 343640, upload-time = "2026-05-28T12:01:08.441Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1", size = 375322, upload-time = "2026-05-28T12:01:09.934Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8", size = 379066, upload-time = "2026-05-28T12:01:11.48Z" }, + { url = "https://files.pythonhosted.org/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2", size = 494586, upload-time = "2026-05-28T12:01:13.051Z" }, + { url = "https://files.pythonhosted.org/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038", size = 388415, upload-time = "2026-05-28T12:01:14.631Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26", size = 372427, upload-time = "2026-05-28T12:01:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd", size = 383615, upload-time = "2026-05-28T12:01:17.809Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9", size = 402786, upload-time = "2026-05-28T12:01:19.419Z" }, + { url = "https://files.pythonhosted.org/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14", size = 551583, upload-time = "2026-05-28T12:01:21.013Z" }, + { url = "https://files.pythonhosted.org/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01", size = 616941, upload-time = "2026-05-28T12:01:22.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d", size = 578349, upload-time = "2026-05-28T12:01:24.118Z" }, + { url = "https://files.pythonhosted.org/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa", size = 209922, upload-time = "2026-05-28T12:01:25.522Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325", size = 226003, upload-time = "2026-05-28T12:01:27.062Z" }, + { url = "https://files.pythonhosted.org/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16", size = 221245, upload-time = "2026-05-28T12:01:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723", size = 352015, upload-time = "2026-05-28T12:01:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41", size = 345016, upload-time = "2026-05-28T12:01:31.656Z" }, + { url = "https://files.pythonhosted.org/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a", size = 374775, upload-time = "2026-05-28T12:01:33.136Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358", size = 380270, upload-time = "2026-05-28T12:01:34.574Z" }, + { url = "https://files.pythonhosted.org/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb", size = 495285, upload-time = "2026-05-28T12:01:36.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b", size = 389581, upload-time = "2026-05-28T12:01:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc", size = 376041, upload-time = "2026-05-28T12:01:39.307Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015", size = 383946, upload-time = "2026-05-28T12:01:41.043Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa", size = 405526, upload-time = "2026-05-28T12:01:43.032Z" }, + { url = "https://files.pythonhosted.org/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972", size = 551165, upload-time = "2026-05-28T12:01:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66", size = 618778, upload-time = "2026-05-28T12:01:46.337Z" }, + { url = "https://files.pythonhosted.org/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb", size = 581839, upload-time = "2026-05-28T12:01:48.109Z" }, + { url = "https://files.pythonhosted.org/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df", size = 207866, upload-time = "2026-05-28T12:01:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" }, + { url = "https://files.pythonhosted.org/packages/42/56/3fe0fb34820ff667be791b3a3c22b85e8bcba54e9c832f47438c191fa7be/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:edf2765d84e42447f112ad877af8fe1db0089aaec5b28e88d6eab45e7fe99cea", size = 357151, upload-time = "2026-05-28T12:01:53.43Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/3eb9ccdb9f143b8c9b003978898cb497f942a324c077401e6b8834238e63/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad3773236e95f7f33991eb125224b7da66f206504d032a253a02da7e134519fb", size = 350195, upload-time = "2026-05-28T12:01:54.901Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/dbda232bc4f3ed732120692ab0d2c8402cb020516556d8bee622dcef2413/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04df86b3f0fade39ec8fd0e0aab089b1da9fbd2b48df778a57ef96f5e7d38df", size = 381850, upload-time = "2026-05-28T12:01:56.601Z" }, + { url = "https://files.pythonhosted.org/packages/40/30/32e769839a358f78810c234f160f2cc21d1e4e47e1c0e0e0d535be5a0219/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6142dbd80c4df62a5d899f0d616d417f84e0bc8d32526c8e5589019d75d028a7", size = 387899, upload-time = "2026-05-28T12:01:58.212Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/ec84d243aadb3b34b71dd26a010d0930b2d284ff5fc9a69fec53810ee6fd/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b35217adefe87f2fe4db7e9766cabe84744bfe9616d9667be18988928c7f2dc", size = 501618, upload-time = "2026-05-28T12:01:59.888Z" }, + { url = "https://files.pythonhosted.org/packages/74/25/b60e52686bbff777a64f9e4f4d3dd57980dc846913777177a2c92e4937aa/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b95d5e11fc712b752081183a55a244c03cd00570489edd7014d8899f8ceb8162", size = 394003, upload-time = "2026-05-28T12:02:01.482Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c7/b3a6a588cc2219510ef3f42e207483a93950bedd1e3a0fd4015c95cff9e5/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141c9498daf2ace9eda35d2b0e376f9ea8b058d84f2aef4f96fccfd449a2f251", size = 379778, upload-time = "2026-05-28T12:02:03.197Z" }, + { url = "https://files.pythonhosted.org/packages/31/00/c7dba3fc8a3da8cb3f6db1eb3386be4d79c2e97c6890d20eb9ac66ae8c43/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:6f249f8b860a200ad35193af961183ebe9132710484e6f6ce0cf89fd83c63a9a", size = 392359, upload-time = "2026-05-28T12:02:04.817Z" }, + { url = "https://files.pythonhosted.org/packages/93/dd/472ba494c70753f93745992c99855bee0636daf74e6984e5e003f150316f/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e4abbf391a70be864920858bf360f4fb380577c9a0f732438a1996726e2c195b", size = 412820, upload-time = "2026-05-28T12:02:06.401Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6f/93831a3bfe789542ed0c1d0d74b78b440f055d6dc3ea4640eba2d95e6e23/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c74005a7bb87752acf351c93897ec63ad77a07a0da7ecad9c050e32e7286ba34", size = 557243, upload-time = "2026-05-28T12:02:08.013Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ff/0b3d604614ffc77522c6b288fdbce68957eb583da1002aa65ba38ac0ee40/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:8213afbe8a3a906fb9acb2014423fe3359ee783d0bf90995f70623a3217bfa6c", size = 623541, upload-time = "2026-05-28T12:02:09.661Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ea/e7b0251441da9adfeaebcf29601d10f2a1455fcf0772fae9e7e19032bd96/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8c43a8a973270fd173bf48cdf80bbe66312421cba68d40845034f174f2389049", size = 586326, upload-time = "2026-05-28T12:02:11.47Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, + { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, + { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/94/dcdaeb1713cab9c84def276cfac7388b17c7d9855bbcfe88d77e4dbafd44/s3transfer-0.19.0.tar.gz", hash = "sha256:ce436931687addc4c1712d52d40b32f53e88315723f107ffa20ba82b05a0f685", size = 165171, upload-time = "2026-06-16T19:44:51.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/5f/4c174edad94f82de888ac00a5ddd8d07b35609b6c94f0bdf4d74af57703e/s3transfer-0.19.0-py3-none-any.whl", hash = "sha256:777cc2415536f1debadb5c2ef7779275d0fc0fe0e042411cdd6caebeb2685262", size = 90101, upload-time = "2026-06-16T19:44:50.439Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, +] + +[[package]] +name = "sphinx" +version = "8.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals-py" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" }, +] + +[[package]] +name = "sphinx-autodoc2" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/5f/5350046d1aa1a56b063ae08b9ad871025335c9d55fe2372896ea48711da9/sphinx_autodoc2-0.5.0.tar.gz", hash = "sha256:7d76044aa81d6af74447080182b6868c7eb066874edc835e8ddf810735b6565a", size = 115077, upload-time = "2023-11-27T07:27:51.407Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/e6/48d47961bbdae755ba9c17dfc65d89356312c67668dcb36c87cfadfa1964/sphinx_autodoc2-0.5.0-py3-none-any.whl", hash = "sha256:e867013b1512f9d6d7e6f6799f8b537d6884462acd118ef361f3f619a60b5c9e", size = 43385, upload-time = "2023-11-27T07:27:49.929Z" }, +] + +[[package]] +name = "sphinx-basic-ng" +version = "1.0.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736, upload-time = "2023-07-08T18:40:54.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496, upload-time = "2023-07-08T18:40:52.659Z" }, +] + +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039, upload-time = "2023-04-14T08:10:22.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343, upload-time = "2023-04-14T08:10:20.844Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/1b/bc9e3e7a72dcdad7dc7888758f5d00f56f8909ed5cfdff822bd72bb4c520/sse_starlette-3.4.5.tar.gz", hash = "sha256:83072538bc211a2f68b7b0422226c4af3e9b62e106e07034664b832ca019842a", size = 35249, upload-time = "2026-06-20T17:36:58.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/75/c88d3f5dafd59c791da1ce27650d30bf5b70cbf1cbf01cd00e5f9e360915/sse_starlette-3.4.5-py3-none-any.whl", hash = "sha256:e71bad53323f65573c3864a6c3bd0c1eb6e5f092b2e48082b0c35927d19ca296", size = 16518, upload-time = "2026-06-20T17:36:56.729Z" }, +] + +[[package]] +name = "starlette" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, +] + +[[package]] +name = "strands-agents" +version = "1.45.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boto3" }, + { name = "botocore" }, + { name = "docstring-parser" }, + { name = "jsonschema" }, + { name = "mcp" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation-threading" }, + { name = "opentelemetry-sdk" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "typing-extensions" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/ca/42bedf73ca6d47939e20ad58e90f649e78d53a38f40b5d31ca8078458a61/strands_agents-1.45.0.tar.gz", hash = "sha256:c7e65978d319a5de74b52e7511099951f473c9ccc10368c1ebe13d87f8625d06", size = 1101805, upload-time = "2026-06-25T20:40:23.06Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/81/b189a0944d95465de4b97f8b3fb351df458cfe594572a6dfe7068363a515/strands_agents-1.45.0-py3-none-any.whl", hash = "sha256:9a38363a255f9fd89dbdbd766fdf240524ee4af403865412ed139dc904fbb5ad", size = 579326, upload-time = "2026-06-25T20:40:21.121Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + +[[package]] +name = "tox" +version = "4.56.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "colorama" }, + { name = "filelock" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "pluggy" }, + { name = "pyproject-api" }, + { name = "python-discovery" }, + { name = "tomli-w" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6a/d20d405fc6661902ff803a9fa048d8aae27597c3b5dc750369ded82d08f7/tox-4.56.1.tar.gz", hash = "sha256:db1c2610802553189cf40de251661d066a635ee0ed9bf2a60093b5f1a7f36ef8", size = 283155, upload-time = "2026-06-25T06:18:36.802Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/97/560a5dfde154619d9643b1e208119dddc29bbb35a38a4ce4d095c16cf8f0/tox-4.56.1-py3-none-any.whl", hash = "sha256:4d06b925c4dd67872099b39c5a46fba79a2169c5f6e32060f95a8b1181f0ef55", size = 216469, upload-time = "2026-06-25T06:18:35.229Z" }, +] + +[[package]] +name = "tox-gh" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tox" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/abaf6c1a524547b0b871129e1dad73e51bfa66e682ce8be353c8c3b41ac2/tox_gh-1.7.1.tar.gz", hash = "sha256:4f4871ee4091b41464f2df9ab1fcedb14a48accdecbb60d53297301a9d8984c6", size = 11084, upload-time = "2026-02-23T20:45:52.496Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/62/b5affbf88da680c5c063f3a0f42aaa028bffd66bc8f15c4436e543ef6d94/tox_gh-1.7.1-py3-none-any.whl", hash = "sha256:a82285b16a597516f9a24f20326b124ddc7924a8b70cb05a9192e1c53d7f701d", size = 9915, upload-time = "2026-02-23T20:45:51.271Z" }, +] + +[[package]] +name = "tox-uv" +version = "1.35.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tox-uv-bare" }, + { name = "uv" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/dc/6e9994c799bdbb309f829dd6b8d98764dd0757302f3433c380438a3a127b/tox_uv-1.35.2-py3-none-any.whl", hash = "sha256:2d99b0e3c782ba49e7cbe521c8d344758595961b17a3633738d67096641c1bde", size = 6565, upload-time = "2026-05-05T01:34:16.07Z" }, +] + +[[package]] +name = "tox-uv-bare" +version = "1.35.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "tox" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/cb/168dc1ccf24e4065a9a0a33df55709ed2b5eb73bd2b13ddd53187e5dffb8/tox_uv_bare-1.35.2.tar.gz", hash = "sha256:49e28a804c97f23ea17e25859960c0fa78f35bccb7e14344cfd840e89a9aade9", size = 32333, upload-time = "2026-05-05T01:34:18.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/4a33dc81da39db7b31e5622333df361e8fe055b7ec636bd5fea762c9182d/tox_uv_bare-1.35.2-py3-none-any.whl", hash = "sha256:c0d590a41d1054a1ad0874e9e5943ff52402786e3d4599d8f8d37a65b566ef53", size = 22307, upload-time = "2026-05-05T01:34:17.681Z" }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20260518" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/83/4a1afc3fbfcf5b8d46fc390cd95ed6b0dc9010a265f4e9f46314efffa37a/types_pyyaml-6.0.12.20260518.tar.gz", hash = "sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466", size = 17850, upload-time = "2026-05-18T06:01:58.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/a2/c01db32be2ae7d6a1689972f3c492b149ee4e164b12fdfd9f64b50888215/types_pyyaml-6.0.12.20260518-py3-none-any.whl", hash = "sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd", size = 20312, upload-time = "2026-05-18T06:01:57.368Z" }, +] + +[[package]] +name = "types-requests" +version = "2.33.0.20260518" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/01/c5a19253fe1ac159159ddf9a3a07cec8bb5e486ec4d9002ad2821da0e5d2/types_requests-2.33.0.20260518.tar.gz", hash = "sha256:df7bd3bfe0ca8402dfb841e7d9be714bb5578203283d66d7dc4ef69343449a5e", size = 24752, upload-time = "2026-05-18T06:07:37.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl", hash = "sha256:626d697d1adaaff76e2044dc8c5c051d8f21abc157bdfe204a75558076fe0bf0", size = 21391, upload-time = "2026-05-18T06:07:37.044Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uuid-utils" +version = "0.16.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/5a/5da7ae85b38e3eddba0be3e8e4328f90882fe92989728e6fb552963d4c42/uuid_utils-0.16.2.tar.gz", hash = "sha256:fa637e4f314ad5b59ff6d8e809d506443d68bef30bfaecdfcfe02cce689abb2f", size = 42962, upload-time = "2026-06-18T13:36:48.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/96/4023966d42fd9bbf9e2a8ce2b25930113688128b569f68bc4697cb18181d/uuid_utils-0.16.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fadd23eee409237fb8637a35796a6e108873c28b40f7de89a36685f18ca055ad", size = 567776, upload-time = "2026-06-18T13:35:02.902Z" }, + { url = "https://files.pythonhosted.org/packages/fc/30/764d2a76e8e7688abd5577e6024787c13692095eb1230fd1936f27205cd9/uuid_utils-0.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:79c5a3bd4301257b9a524efd16baf61ea65cd0d1b60b47d80f20b151fd65a09f", size = 288938, upload-time = "2026-06-18T13:35:04.285Z" }, + { url = "https://files.pythonhosted.org/packages/45/ef/58077250fe04eda4a3f9fba8c35be8d0937b7d3e02302ac1a6d942b77dae/uuid_utils-0.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90903ab7fcdfb0300390c15f5a68cb91f15139d9a1a93f134c783d7a973fa269", size = 327387, upload-time = "2026-06-18T13:35:05.406Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fd/a9172970fa07ce0b9148ccc679a99540375c7bb32f8fbd72cf1e6cca43ef/uuid_utils-0.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7a44f8250ec178c0af703c3f1b6e81865a771272ae735ca403f27c95c62f132", size = 334212, upload-time = "2026-06-18T13:35:06.611Z" }, + { url = "https://files.pythonhosted.org/packages/ca/58/d8fb393b44ad0b719d96a5b7809d0ee727f7e266d9e88a4da235cbfbf9f8/uuid_utils-0.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e97ab941660f781a8e45f15aba9ee01b40dbb96adb5c43617c1671a4604b25b", size = 450379, upload-time = "2026-06-18T13:35:07.97Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/b3cf708e8942e6494742404a66f1586195a20c8fd235bdc79f385db383f1/uuid_utils-0.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a30b6a5790acb854e4b65fae7875e5d3c6f8076fa9c91dac43ff9e28380bc52", size = 327231, upload-time = "2026-06-18T13:35:09.327Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9c/4c5b16e752a2402259a3a9d1371227025e5b85182024c82a446cbe3ed6ea/uuid_utils-0.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5dfc3e9e75139a84898771d31958ece6cdee8e8f127700aa8aa26a4f1a348d57", size = 353455, upload-time = "2026-06-18T13:35:10.67Z" }, + { url = "https://files.pythonhosted.org/packages/0c/0f/3b14c47fab1544bcfb92d28bc468160a4fc6ff342d0e6defa8ff40d5e4bd/uuid_utils-0.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0529b1ef0788f663e1211d221b59a38ec67f9b084f1ea5342ba84358b3d87e98", size = 504028, upload-time = "2026-06-18T13:35:12.006Z" }, + { url = "https://files.pythonhosted.org/packages/39/79/1a133214626eb0e18c51ef196946b1263d65b578ffee432ad1b7afffa5d3/uuid_utils-0.16.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:cae08df8695f4b01fce2a8ab50e9e310971276d85dfc7103e977bed52d365094", size = 609803, upload-time = "2026-06-18T13:35:13.286Z" }, + { url = "https://files.pythonhosted.org/packages/87/49/22bff932af63764b4cad9c01299ed64c60d101962988efc13964b4165345/uuid_utils-0.16.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f69658c42411540cf58be958a47e317fd2302cc0b613ea5cff1e60d87be2846d", size = 569512, upload-time = "2026-06-18T13:35:14.661Z" }, + { url = "https://files.pythonhosted.org/packages/3f/98/371cc1f332f7463b9cfac0a66f984af00f4e3ada4a196b20879e35404e8b/uuid_utils-0.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:503f020acc7dbeb39c47fa33cf2971cf5960fa11f8394513fac461762a90c556", size = 532855, upload-time = "2026-06-18T13:35:15.99Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/f7ef7f6dabf68021eb6e961c09d16d517ed7587cedfff18969ba7f61798c/uuid_utils-0.16.2-cp311-cp311-win32.whl", hash = "sha256:aab7cdf28a3e2859ca4f40a3e3bf53eb35895039c80d4d8d8c5e15b90346c55c", size = 169971, upload-time = "2026-06-18T13:35:17.294Z" }, + { url = "https://files.pythonhosted.org/packages/75/8b/1e4b51c075eaadd23828b708249374db0bc40146f7b673027942d3383f45/uuid_utils-0.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:71192a59d473f3f638e2a238905046e2942006ad90ac5ec10d578e58ff9a08ce", size = 176464, upload-time = "2026-06-18T13:35:18.459Z" }, + { url = "https://files.pythonhosted.org/packages/d4/71/18a43b6e632adf3cb3cf5db777ea03f9d3b2b259de65de5e41419004c2a1/uuid_utils-0.16.2-cp311-cp311-win_arm64.whl", hash = "sha256:ea175649789f1e93edbf1a0440cab18c9838977703917221777691d8d988d7bf", size = 176056, upload-time = "2026-06-18T13:35:19.826Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/294b72a572218bf6e92355203b832b3356c58a7e1e0b92a034497d15bef9/uuid_utils-0.16.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6f064dc54c6abecb09eb104d953bfb079f3c395e0d6b18899979f852d1083549", size = 560726, upload-time = "2026-06-18T13:35:21.053Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3c/1095b6ab574a7fa69136d47bab5a43f320a8f00a0ecb96059fd49b1747b2/uuid_utils-0.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:dd7aa18db5cc826d482d876a826fee445839701f81f78567e7c74b4458d57a84", size = 288065, upload-time = "2026-06-18T13:35:22.547Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9d/6404d48fe71def0733c9568d96043b2e1945e2e4205c4eb525db3da42ba3/uuid_utils-0.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc25ad320c9b44c2d3ed33aff4f85b0b277bef4ff79b12c01ee58b52ea44be1d", size = 322946, upload-time = "2026-06-18T13:35:23.648Z" }, + { url = "https://files.pythonhosted.org/packages/74/00/8a009762015a134aa04b5451400e0ec9832ccd598ed4845f9aecb0be6299/uuid_utils-0.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0ca752d51d1004caff65fccffd44b32a26cb099b546e0512cfa09facb683d6c", size = 330186, upload-time = "2026-06-18T13:35:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b0/1613bb98ac11234145aa5bc1de618be536818fef05dec595efb3e2b37097/uuid_utils-0.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8323136bb02355c1b973492ab98b0722206dfdedfb148e4115c35fcdf3889bad", size = 444583, upload-time = "2026-06-18T13:35:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/93/66/83e62c7a152bbbb8b30ac58eaad81f3860ba2fba91a334c50f223f9ce878/uuid_utils-0.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bf8bfdffb22f620635580b17fd178272f30a9841b824b19b935c8db64bf09b6", size = 323064, upload-time = "2026-06-18T13:35:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/c1b2faaf3a9d7952f321a9fee3ad74e05b25878bd9b7cd6b0398fe77f279/uuid_utils-0.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:61454f2139424a6cff14eca7849c28b3350f261453b74075aa20fe99592dbb16", size = 347967, upload-time = "2026-06-18T13:35:28.538Z" }, + { url = "https://files.pythonhosted.org/packages/24/d8/cdf79b242e41ae47b7cd617ac5d48f15ce44e81da8000379c757091ae5f8/uuid_utils-0.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:725110434a1d482a639a9ac467a24f1cb531d84ab52e454a13fe145b10b42cae", size = 499187, upload-time = "2026-06-18T13:35:30.042Z" }, + { url = "https://files.pythonhosted.org/packages/be/10/978d5ad82bc0fe7ff02d5be6f1eb83b090849f0a95bf8438593565273b7a/uuid_utils-0.16.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8197870739a3094990743a80f075fa0b17beafd6c187e5f360e021d90a12a6d1", size = 605696, upload-time = "2026-06-18T13:35:31.289Z" }, + { url = "https://files.pythonhosted.org/packages/3a/28/e382ee44a592e35b80397b493bf3fbbdb8e30a64eaaefc7dabc246aeb253/uuid_utils-0.16.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e10a02b3a31ed44c7c9a96abde335f5fa222735e73f3081d693414377eb3b016", size = 564975, upload-time = "2026-06-18T13:35:32.419Z" }, + { url = "https://files.pythonhosted.org/packages/a3/d0/f6011dbe4e5d751a8494715e014019cb5b242d8cd6dbec1cfec3d3fb2e81/uuid_utils-0.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd32dbca0792b9683160151dc07fad11b915020eed7c82b43faf0862c2ff06a0", size = 528462, upload-time = "2026-06-18T13:35:33.685Z" }, + { url = "https://files.pythonhosted.org/packages/42/7f/279e6159c37f43feb9dd70218b49a26696cefddaef1db7f4b79895eaf5d5/uuid_utils-0.16.2-cp312-cp312-win32.whl", hash = "sha256:dcdfcab60562d12dd43c1a6f495b1d089e41f0e10fac37d94db285d72b678c23", size = 167047, upload-time = "2026-06-18T13:35:34.862Z" }, + { url = "https://files.pythonhosted.org/packages/47/38/f72f7bed062601448ec2db47351e6c1faccd78fd693bbc6e067299d1fa11/uuid_utils-0.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:97ee6f5e803ea571f5f6da42efc97d8c5a13f121043680177f8470529b94e855", size = 173821, upload-time = "2026-06-18T13:35:36.117Z" }, + { url = "https://files.pythonhosted.org/packages/37/61/8a025284a31c85b7c0c5319e96868c2c09dea3fc5f676c979a4cd4baf2e7/uuid_utils-0.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:72cfd9ff1e8a7c371a044687e77eb873721c4a9f4814e453439bfba595b84303", size = 172206, upload-time = "2026-06-18T13:35:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/3b48859953ee74fc26628ca5d9e5f848209655a0a8c934032fc596035976/uuid_utils-0.16.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c19b7d595d12923da682ed13d313c2333b9ebf214e65a47a24927a8a3a81b191", size = 560753, upload-time = "2026-06-18T13:35:38.531Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1c/77635489de5454f2a25411030f78d31931dbdc0c86114da00adb9b91f120/uuid_utils-0.16.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:909e26fa2451c8db31b9ed1d3c8e4ecf513b6d1619db4205997fe99eb6b4ef4f", size = 288056, upload-time = "2026-06-18T13:35:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0e/8e799537ea458abaefb0f5c3b3b05304d3faf413feb0997605a3f8ae2484/uuid_utils-0.16.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27271b37fbc6812bb1542c4b8e22ee00223a6bf7f62b1f38d3bcf8e92f6d9acd", size = 323196, upload-time = "2026-06-18T13:35:41.534Z" }, + { url = "https://files.pythonhosted.org/packages/e8/92/4e5b412d4710617fb83ed77b361f5fa6247b99bde2fa6ee07ddf851b59d1/uuid_utils-0.16.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dc4b9d96a2c689d664cf3fc7f7db46b82d2821fb2ce8a4f0798fc0a92c1569f8", size = 330858, upload-time = "2026-06-18T13:35:42.709Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e3/8173202b7cfcfeb4a588c5f8b85d3e2b44973384eb33167ee25c5c78867f/uuid_utils-0.16.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3bf41b696b0fe808df1b4091c70273a52ea033b0fe97341cd67ecd76d22bb3a", size = 444813, upload-time = "2026-06-18T13:35:43.917Z" }, + { url = "https://files.pythonhosted.org/packages/37/0d/c3918356932ce467b11e954d0c93697fb4652cf664957e3d9521f7ece22f/uuid_utils-0.16.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcc329be41bb6534ecb03e50596179ab76c7643ced33d13c66967d5ae1869663", size = 322828, upload-time = "2026-06-18T13:35:45.134Z" }, + { url = "https://files.pythonhosted.org/packages/f0/80/4020556682441b62a25b7d07798812115fca97d417a3498d5af6dce36504/uuid_utils-0.16.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4125bf6ed3ae443c05e140f8585d174b9d647295b12034d5ec94ae2ae38edefa", size = 347909, upload-time = "2026-06-18T13:35:46.364Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/a1e87e268df98f6740af81abf225532c173a971c64df0258c84b630e35a7/uuid_utils-0.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:840b21e609a9b203eee06bdc73e18397154447a9814a8e78d9b68e5104d9802f", size = 499469, upload-time = "2026-06-18T13:35:47.584Z" }, + { url = "https://files.pythonhosted.org/packages/25/75/5a1f297a09556c27d9617c44ab0510de5f3a70120df236f66b9d0fdd1976/uuid_utils-0.16.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5119bec75f56bd028d97472f72b1ed723a0d60b09a48017dc70a3cb1892ed081", size = 606160, upload-time = "2026-06-18T13:35:48.963Z" }, + { url = "https://files.pythonhosted.org/packages/7c/de/140f1d2a161320d1ac9073a03b9eb31fe35ae70f56f8971ec1fb45c14a44/uuid_utils-0.16.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9fe600ab7d3d4eb56986e814042c917e728ac92cd8a41f099a6b59b84d8bf9e6", size = 564856, upload-time = "2026-06-18T13:35:50.244Z" }, + { url = "https://files.pythonhosted.org/packages/01/3b/9a5fe6691f8f6d72899cdc2713ffbd845b8c6981eeeab66d98a71b721116/uuid_utils-0.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e44020a4532229ccfbba353138539774686350dda71cf4368e257973dd8ba403", size = 528376, upload-time = "2026-06-18T13:35:51.825Z" }, + { url = "https://files.pythonhosted.org/packages/87/ad/47c93dcabd00f6749803a00be361c75d7079c78ad5e67077dee63d30b687/uuid_utils-0.16.2-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:280d4f1f22dd2e79c1cc31ffc7fc26dc3534ffc114dedcdd29cc8489c5ce9c98", size = 98033, upload-time = "2026-06-18T13:35:53.385Z" }, + { url = "https://files.pythonhosted.org/packages/c0/fd/8de85eeb8dd59354ad46e897ab0d0f0fe6bc48702239a6c9f2613f961c8e/uuid_utils-0.16.2-cp313-cp313-win32.whl", hash = "sha256:4942b26ad12c5187bac52b7fb4685040139ff0df9a19cde33e5025326f6180fc", size = 167054, upload-time = "2026-06-18T13:35:54.495Z" }, + { url = "https://files.pythonhosted.org/packages/86/b3/b5ba393fbe5142eb9d5db23d4b9b16dde2a4e1aee6f2fcb7fadef97e419a/uuid_utils-0.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:01f81c71cf2185de0707e9d2f248e17025ba50af0acd3cbf51cd8aea96c2e0be", size = 173481, upload-time = "2026-06-18T13:35:55.684Z" }, + { url = "https://files.pythonhosted.org/packages/b2/79/4e5d63d605b13201ae9af6fcc36ec77949cccc99486c430c016d8f8ed274/uuid_utils-0.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:c1dbe65ce6d46c5f645356d64bfb2de7564e2426ca8c9b1a0a401d6f7ae5cc22", size = 172197, upload-time = "2026-06-18T13:35:56.817Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/0e5a0c1e1e3243cf5f12efd2b88a33e63c38b6a79483d3c84b2f5e7265cf/uuid_utils-0.16.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:617955f4b3f649617c0388127d8a257202189d5cc3c720313f8b207df1cdb2a4", size = 566227, upload-time = "2026-06-18T13:35:57.925Z" }, + { url = "https://files.pythonhosted.org/packages/28/b3/2b6f9d6832e939aaf2b2ba89ff70b3994cfa3ae9b14daac3329eb9202ef8/uuid_utils-0.16.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:0aa2569908bdb21ccb216cd6bd06cb934351ee65ea7cd5e351e19f633a99b577", size = 290301, upload-time = "2026-06-18T13:35:59.467Z" }, + { url = "https://files.pythonhosted.org/packages/f5/27/8bb31429884b9f340f964ed70b68bfd81cec61f6e6877633f6a014358e78/uuid_utils-0.16.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4af7673e84e1ec6029f18d3a0408095c471c4e2691b6e46b4e1f0a2051734ba", size = 325409, upload-time = "2026-06-18T13:36:00.786Z" }, + { url = "https://files.pythonhosted.org/packages/1f/87/3b59aa97e788ca4fa46e2a3856ef567b51e03fd7fbf27d39ce36e46478b6/uuid_utils-0.16.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ecadf55ed6b8fb72e7966b52fd02919e7d7bb8e7bffeaf285803b82e774debfb", size = 332071, upload-time = "2026-06-18T13:36:02.043Z" }, + { url = "https://files.pythonhosted.org/packages/1c/21/8c21bf6cf3ce9447b73cee6a38ca63c9bb2f3145259422646bae8e8ddc21/uuid_utils-0.16.2-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:026b96b2f1e6b004579e030692d2f6568ccd0b29d40687213c31694abf570c78", size = 447075, upload-time = "2026-06-18T13:36:03.305Z" }, + { url = "https://files.pythonhosted.org/packages/95/43/77e83019effe1a5ab7169a2d4bf1bd654bebd850b81c8a937b96bd6b5c9c/uuid_utils-0.16.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:273679723e88544dd2de0564ab7f2fddfa2270faf05cabfdf63c275be67ec2a1", size = 325061, upload-time = "2026-06-18T13:36:04.972Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a6/7bf6e0165dc191c09bc4e8c011de5463d64c5a651ed38ad6698bfc552a52/uuid_utils-0.16.2-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec5b1a338b92d1eb121e9eaf06ae3db1b9a5cd794ce318a475f6dc6f9e89c3a8", size = 350302, upload-time = "2026-06-18T13:36:06.172Z" }, + { url = "https://files.pythonhosted.org/packages/45/66/260836aaef14b8254bc449b3163fedec06ef0a0bba0d6a999c918479b2f9/uuid_utils-0.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e75f9429d4533ce275c98bc68bf47fb237ae7b32c954266dabc5edab0c7d682e", size = 501834, upload-time = "2026-06-18T13:36:07.469Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0b/84c1542bf8c465b456f742318ad83eace63551e7f603b06c817b726670af/uuid_utils-0.16.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f3cca9ca5e2c2dfd7b885f0d34c10b993a070d3593f3cdfef785195da36fb0f", size = 607406, upload-time = "2026-06-18T13:36:08.913Z" }, + { url = "https://files.pythonhosted.org/packages/48/7f/1024c22657a0c0572c4fd5189fad3127cb46731fb26fad3be1e8a4a64972/uuid_utils-0.16.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1ef8c561fdf88fec205e3d54037824cfe2addce16b509a8d2ecb69daa904cbb7", size = 567623, upload-time = "2026-06-18T13:36:10.14Z" }, + { url = "https://files.pythonhosted.org/packages/15/0e/ad7424a6444e3e108a22781c2e164e82752da5db23ccc5cba8b4470c3164/uuid_utils-0.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e3acb5e1451232381daea01645a98c69de4bb9ad88d77a1f7c1df4d83d54e62", size = 530659, upload-time = "2026-06-18T13:36:11.649Z" }, + { url = "https://files.pythonhosted.org/packages/69/60/cf1666d0dbd6fa869b6de3b85a17254ff0ab10ed286fd59366148bf08e89/uuid_utils-0.16.2-cp314-cp314-win32.whl", hash = "sha256:b5f8e7d0bb2c6e6180176237f92d2e949626e04fcf701c49d73f128e1f64e1d1", size = 169272, upload-time = "2026-06-18T13:36:12.846Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5e/111908bdc7287b2589e9a9f10be8e0358844fb4a0554677cbbe0ade49766/uuid_utils-0.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:bf922bad7df257336b594d316a1657df569860bb5389602919001fa6fb17f06e", size = 175435, upload-time = "2026-06-18T13:36:14.114Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5d/b3bd7415622060dd17d587545e3c037f83dc0dffb8880ac798ca7936f630/uuid_utils-0.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:fad82e6482129c58ba9b00da6c247ab6e767645ab17981599229cce19d7b2ce9", size = 173553, upload-time = "2026-06-18T13:36:15.561Z" }, + { url = "https://files.pythonhosted.org/packages/92/43/401acf6fc0e0665dd11a095a28f6d22708c6f8f148c326cfc5b0b1ae9882/uuid_utils-0.16.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e0609e7e906c08386b7f33141254df05dcab24f1c4884150988dc7a287516aca", size = 567548, upload-time = "2026-06-18T13:36:16.848Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2c/cc2bb8273d414d651acafccc3705a8843c130a541fcce65fbeaac22266ba/uuid_utils-0.16.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:9ad2adeb941292fe02e1e5c70b80a5746c45b1b77594506c2a1421455d8384f9", size = 291348, upload-time = "2026-06-18T13:36:18.145Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a8/fdadd7ada0de53dbc03f719da0948cc275abd24d8013a26e42e50d3665c1/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d906c00f965d5c5f4812d0086dc49bf813285ea84c97e8816405200e146f805b", size = 325495, upload-time = "2026-06-18T13:36:19.417Z" }, + { url = "https://files.pythonhosted.org/packages/16/42/e397a1eda06b20dd3a206e3a55b346ff2caad23906586801a87359530864/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a59205fc15463dd0f978f14df14307737e3d4e8ef4aefa29a9d0fa766d84d16b", size = 332301, upload-time = "2026-06-18T13:36:20.747Z" }, + { url = "https://files.pythonhosted.org/packages/46/be/12d3df7bd824e3ce71630c022184a5aecfea92b0a7fa70459542b237777a/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac82500329ffaf2788dac36cf133e1e4e23b6d5e1118274ea6749c3b512f4f1", size = 446760, upload-time = "2026-06-18T13:36:22.198Z" }, + { url = "https://files.pythonhosted.org/packages/f7/10/0c5d1dd6874fa35e2cb66a8499ce303eb8678bef226951182603bd30017d/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d8257329f26905f009aed694bd3b17f334f43748b03134dc7bc99d6c5b4e371", size = 325781, upload-time = "2026-06-18T13:36:23.566Z" }, + { url = "https://files.pythonhosted.org/packages/04/e2/9ebb8414875e5c14737fa7145a023458c9b15754f1d129cefe7824197256/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e04b5c10c6fcf9d9801084d1e86c9d7ada7eb48fe07ee4ae5e7fe5b1a852db8a", size = 351189, upload-time = "2026-06-18T13:36:25.09Z" }, + { url = "https://files.pythonhosted.org/packages/1b/5c/168d1f4d30b33c08365debfe4176c2f713a0940f1f11a64128a186d050c6/uuid_utils-0.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3d4805c4739dd06d539f8f4fa94f5aaf26eca4b3ece1ef134d4ff904c6b08dcf", size = 501866, upload-time = "2026-06-18T13:36:26.31Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8d/003865d5ed5bf82ece80bd61edb2692985f7548051749fd10f34edb16705/uuid_utils-0.16.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:76632d2e16e26de777851ec07961ceaea14e65167d0603a0b17fb169fa9ca37b", size = 607632, upload-time = "2026-06-18T13:36:27.704Z" }, + { url = "https://files.pythonhosted.org/packages/ea/52/6102f21f28323b27122a6aa3d4cea183b4fc401868c5c40767e1b9f53beb/uuid_utils-0.16.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6c02f85f49c9c2abbf247a8622458c30232332a28711755aa191da5f38015af6", size = 568216, upload-time = "2026-06-18T13:36:29.377Z" }, + { url = "https://files.pythonhosted.org/packages/68/50/644e4e55f47048d12bc20665fac85bc1fecbed9c892acfb91626abf8ad8d/uuid_utils-0.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f668035ea9faa763e8f1ea42040e8439db88cf2517056d47c348a62a257a1d02", size = 531370, upload-time = "2026-06-18T13:36:30.804Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5d/d98d99f601d70cc00287dce5aadef9c199912f0d64343962542f35e7db59/uuid_utils-0.16.2-cp314-cp314t-win32.whl", hash = "sha256:62b8841895eff1c0afbaf5f0050411667231160478c8ff9f411742abffd3b619", size = 169424, upload-time = "2026-06-18T13:36:32.246Z" }, + { url = "https://files.pythonhosted.org/packages/a6/af/c0d482bdd637a8a742d3274cec462b770919f032e179216f2fc2851afaf9/uuid_utils-0.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:e9064805881c30dd80a4189a0da7130e3d684de353ea36edd99c1b994bdf429e", size = 175544, upload-time = "2026-06-18T13:36:33.75Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/aff8b0456e8a63672fa89ea9c773f7547a31ff7b596a40f226bf148921a3/uuid_utils-0.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:3324bac95084e63e28553c92fac5a0394c636a76e03e50a7dab0c0bbddf87fa5", size = 173972, upload-time = "2026-06-18T13:36:35.076Z" }, + { url = "https://files.pythonhosted.org/packages/0e/48/8c9fee7d75571f2f4b2386eac798fe5f826155d13797f7c86d45eb3fdc23/uuid_utils-0.16.2-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8b8e325e61f918caf74ca540e3384b81e6e22aea782e57f615d15fc9773b96c8", size = 571003, upload-time = "2026-06-18T13:36:36.42Z" }, + { url = "https://files.pythonhosted.org/packages/de/78/754eaaa49509be6fdb705de61d1e3889de32002132d5f00e8c1e5d212da3/uuid_utils-0.16.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9282677ebf2ea5b437c20d16e75bcd7629bdc205018f95557b33b76868d8bb5b", size = 290244, upload-time = "2026-06-18T13:36:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e2/bfcbcf7eb9dfb17701104c569ed771eb359737bc70b7309e439610d089ef/uuid_utils-0.16.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e9ca7f5e215373cc9c147172170a0b1a4ab0dee9cc62fe446d9b075f31e3241", size = 328551, upload-time = "2026-06-18T13:36:39.605Z" }, + { url = "https://files.pythonhosted.org/packages/72/bf/bbdbc39d1421953edcee0bad13a1893521a636eccf381580f53e530a4feb/uuid_utils-0.16.2-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43cc72a92694d08ade8faadacf928857d9cceb84b449473246ae4e4f263d7d22", size = 335468, upload-time = "2026-06-18T13:36:41Z" }, + { url = "https://files.pythonhosted.org/packages/04/2a/e8d4e6f1f2d2e567cf6e3202d125afe7da52ad7680bba048b106c09f01b9/uuid_utils-0.16.2-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:511b5fde12d29c37a9badd399af62105bb2f4696aa10eb18be74e7b9ca84413a", size = 450984, upload-time = "2026-06-18T13:36:42.635Z" }, + { url = "https://files.pythonhosted.org/packages/6a/da/ddb1dcf0fe9bfcb0dfcddec8ae52c8f95e7088e44719f58477f5fb2c5586/uuid_utils-0.16.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:585d3adf73afa60348bf2bd529491c640a692350e76d8ff3974455e273aadfe7", size = 327940, upload-time = "2026-06-18T13:36:44.138Z" }, + { url = "https://files.pythonhosted.org/packages/37/fb/39305fbfffee1fdaccdb88fc0499ac9dcb7289a77ebc31938dcdd933cf95/uuid_utils-0.16.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ae5fa2007fd26d26f7b09e76259d5ca99bec191616207ca929f8dca12da08129", size = 355368, upload-time = "2026-06-18T13:36:45.682Z" }, + { url = "https://files.pythonhosted.org/packages/b4/70/b708edc3b776d7624b4354f43d443f14d951d3ac4d7d8867d94f2e59c3ae/uuid_utils-0.16.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9b4520521aa46a2582fe1829c535fe60b78999b89257db998df3816eb895bdf3", size = 178221, upload-time = "2026-06-18T13:36:47.291Z" }, +] + +[[package]] +name = "uv" +version = "0.11.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/8e/2777bf40df3c634f2a522baaa2995d2bd5901461fc5d8730c04c39fa6c75/uv-0.11.25.tar.gz", hash = "sha256:458e731778e7b5cc870710397859c23e766703e7bc0695f23b3eb15080745ba6", size = 4329198, upload-time = "2026-06-27T00:49:00.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/a7/dc836daca0db76178cfe4e40ee95fa2c5f96a3d7b3cda0bdde5291aef8b8/uv-0.11.25-py3-none-linux_armv6l.whl", hash = "sha256:d6f965a79fc7539a12139ce981caa0cbf7d9d3bd4ea3daadaf174ab4d7fb6e42", size = 25153689, upload-time = "2026-06-27T00:48:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/d60eabb616db232ed0e682c55d858d0980175a92336429c4e0ee82a160e6/uv-0.11.25-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:610650cbaa0a9b18015da39d2c28d736d287a5a124e49296d8fdef5e4022e980", size = 24149272, upload-time = "2026-06-27T00:48:13.917Z" }, + { url = "https://files.pythonhosted.org/packages/ab/fd/9f72373e340c6abcd3eb9257d69b442434b65e90e1f7f53f333abdac11b2/uv-0.11.25-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f7a78fc8d0c5e764e9fa39c99066db47a0bc465b023feed90812e3c0a6b5eb0d", size = 22885453, upload-time = "2026-06-27T00:48:16.641Z" }, + { url = "https://files.pythonhosted.org/packages/ee/c1/7cbb3f6b4a021ce7c1c453d1412d3b4b68d7d7585d15d91a57e9ab0a317e/uv-0.11.25-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:e3480640983e0b8e509eeb67882837e620bdd820f8776948a5f13ebbb4481d04", size = 24935172, upload-time = "2026-06-27T00:48:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/df/d4/862a534ab4aae28ad2c9145ea5af3021eefe225e4e1c983278bb34b53b9e/uv-0.11.25-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:b180b12237b4e04692491fc6796584a9a8bdf4c7332bd2a769caf096b97885d0", size = 24665125, upload-time = "2026-06-27T00:48:22.408Z" }, + { url = "https://files.pythonhosted.org/packages/91/2a/70ef2f2a212d89d23f90e1c8017c080fae93bec21cd9f14795498a6e6f89/uv-0.11.25-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:69d14ffd0a4b050f8a70f64aacb09b8dfdfb1cb30a6351fb17b48f273f95c58c", size = 24653836, upload-time = "2026-06-27T00:48:24.858Z" }, + { url = "https://files.pythonhosted.org/packages/44/2b/a7beea057778f092daa5aeda7aeb3f8bfa78f4a0622245adb449fdbb3d20/uv-0.11.25-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61ef11d9967a38109e6e8e3d20d1f743fa08033c32bce274d6ccd9a9abb5d305", size = 26069304, upload-time = "2026-06-27T00:48:28.05Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e8/bb8ab595a27035b565cedd5d601ae2b85cf6aba6f68cd6a97eae88dd7250/uv-0.11.25-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3febca65ec5bc336ddaf7e4f724704f2c894c16839723df14865ee00b4acf38d", size = 26992784, upload-time = "2026-06-27T00:48:30.584Z" }, + { url = "https://files.pythonhosted.org/packages/17/90/bbfa0653e992877e5ea78ab3186fb6a5a122ebef5913b6809ef6d06fcf9b/uv-0.11.25-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:850ba0018ff170c3a9baaf9b5fe8b23393b6b77ee4ea6b2e2315fdb8d7c388f7", size = 26151002, upload-time = "2026-06-27T00:48:33.739Z" }, + { url = "https://files.pythonhosted.org/packages/91/01/f5a01fd777ce501cc59eb71775f3bbacac258a65a64939f07804c2279c98/uv-0.11.25-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2bc05e17ae3e1f232abf93e7dcfb3b68702dfcde34a00c29cbce7e07d1ecbfb", size = 26334122, upload-time = "2026-06-27T00:48:36.247Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c4/f8b3b6b1bb48a452d98c02909e229f7ae317300618d6dc334c883742339e/uv-0.11.25-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:57fbd47e924242fd347d0c209d95711d8ea61db8d8780962d0f30ccde2c854a3", size = 25055505, upload-time = "2026-06-27T00:48:39.418Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7d/aece221faba22804d8b4f913903358dc2d45193babf2a44a49f79eadd25b/uv-0.11.25-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:f42de9e7d63a28a4fe76a522077813656de38b5acda20b4db63857d260c1ff13", size = 25660097, upload-time = "2026-06-27T00:48:42.398Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7d/b835ed56eab281054efbfbcbbe1249621908c6e4c721a878904cbed2f418/uv-0.11.25-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2c1cfe97dce56c997dfa3214bdb8955b7b34cceea7505520185e22ad99c0eb6b", size = 25759374, upload-time = "2026-06-27T00:48:45.069Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fd/28c990fd18330aa01383bae738be113f4ac5d9d414f92790947326c6781c/uv-0.11.25-py3-none-musllinux_1_1_i686.whl", hash = "sha256:fbff70ae9fa4da9fb6823ae4fdaf77a65c9520e13b6d1d0241ba56e4b121b7aa", size = 25329343, upload-time = "2026-06-27T00:48:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/70/48/16ff9dbe5f308cb547e227971cbcaf3c905797da12a4116b620e2acc09cb/uv-0.11.25-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:41b37e724f41eb4c3794bbdd82ddeebb4b5850d4ada8cccb2906ef9e5aa0f83b", size = 26477125, upload-time = "2026-06-27T00:48:50.251Z" }, + { url = "https://files.pythonhosted.org/packages/f6/8b/4146a55d3ee6b2b67c3dece3ef6be220c443abafc2dc56baa2f74d6b36a8/uv-0.11.25-py3-none-win32.whl", hash = "sha256:86d4759fec9b46f61944d6e9ef1f5eaa2c5fbe2db5ddb59492d9174b08fcf39c", size = 23896598, upload-time = "2026-06-27T00:48:53.01Z" }, + { url = "https://files.pythonhosted.org/packages/64/66/784f9fb457b640dbb96cdd175f4902235b42ffe740dbaee9ea5fc649d8b2/uv-0.11.25-py3-none-win_amd64.whl", hash = "sha256:560b0fbaa6356af533923a349658c21d4f410d16e835787d8a05da451d4ee859", size = 26862205, upload-time = "2026-06-27T00:48:55.667Z" }, + { url = "https://files.pythonhosted.org/packages/85/cb/d7bb121261db0b829f38b1e86b80d8103afd050b8a5c5c9fe4a988d2f1fc/uv-0.11.25-py3-none-win_arm64.whl", hash = "sha256:79f166cd1b84f855e9d2768221d59b403869648289fd884d58ad4299edfb4d9e", size = 25153588, upload-time = "2026-06-27T00:48:58.495Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.49.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/a5/81f987504738e6defeed61ec1c47e2aefab3c35d8eeb87e1b3f38cf28254/virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8", size = 4578798, upload-time = "2026-06-16T16:23:58.603Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "wrapt" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/a4/282c8e64300a59fc834518a54bf0afabb4ff9218b5fa76958b450459a844/wrapt-2.2.2.tar.gz", hash = "sha256:0788e321027c999bf221b667bd4a54aaefd1a36283749a860ac3eb77daed0302", size = 129068, upload-time = "2026-06-20T23:49:44.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/15/0c2d55168707465abfc41f33c0b23d792a5fa9b65c26983606940900a120/wrapt-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f1a2ff355ece6a111ca7a20dc86df6659c9205d3fcee674ca34f2a2854fd4e73", size = 80782, upload-time = "2026-06-20T23:47:44.367Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b5/5c0b093eb48f8a062ef6267d3cb36e9bb1b88440181f6545a383c60efdf8/wrapt-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55b9a899e6fff5444f229d30aa6e9ac92d2216d9d60f33c771b5d76a760d5f8e", size = 81678, upload-time = "2026-06-20T23:47:45.857Z" }, + { url = "https://files.pythonhosted.org/packages/34/f3/de70937472dd3e8a4e6811192f9c6075efdffd4a2cd9b4596bf160f89668/wrapt-2.2.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a2d78c363f97d8bd718ee40432c66395685e9e98528ccaa423c3355d1715a26d", size = 159671, upload-time = "2026-06-20T23:47:47.345Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/40aed2330e7f02ecf74386ffcfef9ccb7108c6a430f15b6a252b663b1bed/wrapt-2.2.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d619e1eed9bd4f6ed9f24cd61971aa086fa86505289628d464bcf8a2c2e3f328", size = 160785, upload-time = "2026-06-20T23:47:48.759Z" }, + { url = "https://files.pythonhosted.org/packages/45/04/aa5309beed5344b00220ae6b3b24055852192656194c27947bee1736306a/wrapt-2.2.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:518b0c5e323511ec56a38894802ddd5e1222626484e68efe63f201854ad788e5", size = 153699, upload-time = "2026-06-20T23:47:50.177Z" }, + { url = "https://files.pythonhosted.org/packages/01/df/2def7e99d1fe87eea413f95f671924cdddcb08823b1ffd212748dfa6d062/wrapt-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4bccea5cdecffa9dd70e343741f0e41e0a16619313d04b72f78bb525162ebcd0", size = 159695, upload-time = "2026-06-20T23:47:51.602Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f6/a906d01a2ce12157bad2404957b3e2140da354b8a70b2fa48bbf282871c0/wrapt-2.2.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:209112cafd963710a05d199aae431d79a28bc76eb8e6d1bbbb8ad24340722cae", size = 152813, upload-time = "2026-06-20T23:47:53.03Z" }, + { url = "https://files.pythonhosted.org/packages/02/49/bc0086292d239575b4c08f4cf8a4079fa58abbad58ec23abf84833a283ed/wrapt-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5a5290e4bf2f332fc29ce72ffb9a2fff678aaac047e2e9f5f7165cd7792e099", size = 158809, upload-time = "2026-06-20T23:47:54.391Z" }, + { url = "https://files.pythonhosted.org/packages/55/83/8fbd034de1f3e907edaa18786d5dd8f6932874edee0826c7cecb5cab03a1/wrapt-2.2.2-cp311-cp311-win32.whl", hash = "sha256:5499236ad1dc116012e2a5dd943f3f31af12fce452128e2bbcbd55a7d3d4d14c", size = 77414, upload-time = "2026-06-20T23:47:55.882Z" }, + { url = "https://files.pythonhosted.org/packages/7e/9c/23695baa331c6de4e874c3d78b8e0bed92e1d2a274e665b29858f6841672/wrapt-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:8636809939152be6ae20a6cef0fed9fe60f411b47847d0426a826884b469e971", size = 80368, upload-time = "2026-06-20T23:47:57.237Z" }, + { url = "https://files.pythonhosted.org/packages/08/49/40cefc342bf89b234a4490d741290fce781774b831aefb39c25471da96c9/wrapt-2.2.2-cp311-cp311-win_arm64.whl", hash = "sha256:5d0a142f7af07caeb5e5da87493162a7b8efa19ba919e550a746f7446e13fb30", size = 79489, upload-time = "2026-06-20T23:47:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/2a/85/180b40628b23772692a0c76e8030114e1c0ae068470ed531919f0a5f2a4a/wrapt-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8417fd3c674d3c8023d080292d29301531a12daf8bd938dd419710dd2f464f2b", size = 81484, upload-time = "2026-06-20T23:47:59.924Z" }, + { url = "https://files.pythonhosted.org/packages/94/f2/21c90f2a16689702e2aaff45795b11018dff2c9b1242bac10d225483f676/wrapt-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e7070c7472582e31af3dfc2622b2381a0df7435110a9388ed8db5ffbce67efb", size = 82151, upload-time = "2026-06-20T23:48:01.303Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b3/7e6e9fcf4fe7e1b69a49fe6cc5a44e8224bab6283c5233c97e132f14908e/wrapt-2.2.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2e096c9d39a59b35b63c9aacfbbbec2088ff51ff1fc31051acc60a07f42f273a", size = 169828, upload-time = "2026-06-20T23:48:02.719Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/894f132d857ed5a9904d937baf368badcbe5ea9e436e2f1930fe21c9f1f0/wrapt-2.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d1a6050405bf334be33bf66296f113563622972a34900ae6fa60fd283a1a900", size = 171544, upload-time = "2026-06-20T23:48:04.266Z" }, + { url = "https://files.pythonhosted.org/packages/29/de/3c833e03725b477e9ea34028224dd21a48781830101e4e036f77e8b6b102/wrapt-2.2.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10adb01371408c6de504a6658b9886480f1a4919a83752748a387a504a21df79", size = 160663, upload-time = "2026-06-20T23:48:05.708Z" }, + { url = "https://files.pythonhosted.org/packages/33/be/27edce350b24e3054d9d047f65f16d4c4d4c1f3f31c4278a1f8a95c723c8/wrapt-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3442eee2a5798f9b451f1b2cd7518ce8b7e28a2a364696c414460a0e295c012a", size = 169387, upload-time = "2026-06-20T23:48:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c4/9fd9679af8bf38e146652c7f47b6b352c3e5795b4ad1c0b7f94e15ac2aa7/wrapt-2.2.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6c99012a22f735a85eed7c4b86a3e99c30fdd57d9e115b2b45f796264b58d0bf", size = 158849, upload-time = "2026-06-20T23:48:08.91Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c2/aa6c0c2206803068c6859dabe01f8c84c43744da93d4c67b8946d21655ee/wrapt-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b686cfc008776a3952d6213cb296ed7f45d782a8453936406faa89eac0835ab", size = 168147, upload-time = "2026-06-20T23:48:10.374Z" }, + { url = "https://files.pythonhosted.org/packages/42/63/3eb25da41049d20ae18fcab2dd8b056e02387c4bfa626cbdfb7c3b872e4f/wrapt-2.2.2-cp312-cp312-win32.whl", hash = "sha256:ef2cce266b5b0b07e19fa82e59673b81142b7a3607c8ed1254113d048ed668da", size = 77734, upload-time = "2026-06-20T23:48:11.769Z" }, + { url = "https://files.pythonhosted.org/packages/da/09/0390e008a305360948fa9ce69507d041ac12cb2ee5d28e34467e2ee79391/wrapt-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:abf8c20a2d72ee69e16328b3c91342c446e723bfe48bfcc4dded3b9722ac027f", size = 80585, upload-time = "2026-06-20T23:48:13.117Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b3/84c445c66969f2d3457276b183a48c91097d59bbef9af6c075366b0f8c36/wrapt-2.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:c6c64c5d02578bc4c4bca4f0aef1504de933c1d5b4ac2710b9131111459506c8", size = 79553, upload-time = "2026-06-20T23:48:14.5Z" }, + { url = "https://files.pythonhosted.org/packages/43/fc/f32f4b22c6511173c11d9e541ab4e7d8467a0f1b3455acaf784115d31ff8/wrapt-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e8b648270c613720a202d9a45ebabc33261b22c3a839b115ac5bce8c0bb0d69", size = 81296, upload-time = "2026-06-20T23:48:15.881Z" }, + { url = "https://files.pythonhosted.org/packages/72/06/4d117d5d77a9344776c0248b24dae3d3dd2f58e5f765fa08cf887072e719/wrapt-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6fb7e94e8fe3e4c3067bb1653a91cce7c5e83acc119fdd41501b1bf74654617", size = 81841, upload-time = "2026-06-20T23:48:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/15/ff/63ad96f98eb58a742b1a20d80f21da88924405910149950b912368150468/wrapt-2.2.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb18fc51e813df0d9c98049e3bf2298a5495a648602040e21fa3c7329371159e", size = 167882, upload-time = "2026-06-20T23:48:18.764Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/8bb62d8933df7acf3247194e6e9fc68edf9d2fa203252c89c94b319dd472/wrapt-2.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94b00b00f806eb3ef2abe9049ed45994a81ee9284884d96e6b8314927c6cea3d", size = 167411, upload-time = "2026-06-20T23:48:20.315Z" }, + { url = "https://files.pythonhosted.org/packages/17/09/8789dcb09ee1de715727db7521aabbb68ffa68dfade3a49468440cfced49/wrapt-2.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:62415fd095bc590b842b6d092f2b5d9ccbaeb7e0b28535c03dcea2718b48636b", size = 158607, upload-time = "2026-06-20T23:48:21.728Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/66e02562d53ee67d841f175e38e3c993c2d78a3e104c576cad61c028b43c/wrapt-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a41e758d80dc0ab8c210f641ac892009d356cf1f955d97db544c8dd317b4d14c", size = 166367, upload-time = "2026-06-20T23:48:23.177Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a3/832ac4e41222fb263b3042d42c2f08d305db7d0f0c9b1d3a271a9eede8f6/wrapt-2.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b84cd4058001c9727b0e9980b7a9e66325b5ca748b1b578e822cade1bc6b304f", size = 157176, upload-time = "2026-06-20T23:48:24.711Z" }, + { url = "https://files.pythonhosted.org/packages/b7/01/1bd5e4d2df9c0178989ac8da9186543465388588ee2ef153e2591accebef/wrapt-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:26fc73a1b15e0946d2942b9a4426d162b51676338327dc067ccd8d2d76385f94", size = 167025, upload-time = "2026-06-20T23:48:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/1c/69/583ed25291ab53e1ec117135fb1c33425e2f46d2bc8f29c17f7a94cf4274/wrapt-2.2.2-cp313-cp313-win32.whl", hash = "sha256:3c4095803491f6ef72128914c28ec05bbad9758433bb35f6715a3e9c8e46fb2d", size = 77605, upload-time = "2026-06-20T23:48:27.643Z" }, + { url = "https://files.pythonhosted.org/packages/29/68/e69fc6d06e1523c68e0d00f95c9aed1158ce9908ee41603f7f2eae3d5db6/wrapt-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:2cb07f414fab25dbe6b5c7398e1491423a5c81a6209533639969a6c928d474a4", size = 80508, upload-time = "2026-06-20T23:48:29.013Z" }, + { url = "https://files.pythonhosted.org/packages/55/21/fe7a393d9e5dc0923bed8f5d857e9dcff210f1fa0888c02cc8f3ffaa55aa/wrapt-2.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:1fc7691f070220215cccb2a20836b9adbaecb8ff22ad47abe63de5f110994fac", size = 79565, upload-time = "2026-06-20T23:48:30.429Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e5/c120d13bf5091164f68c3c1657e84f16f57e71d978421b626393ac5bd7eb/wrapt-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ec8f83949028366531383603139403cac7a826e4011955813cdd640017845ce5", size = 83264, upload-time = "2026-06-20T23:48:31.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b0/d4a1eb97e0e286625bdf21bc7f702637f9607787ffbbdb5ec14d50c79dbf/wrapt-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b481fb0c40d9fd90a5809911208da700987d373a20a4709dc9e3944af7a6bec", size = 83791, upload-time = "2026-06-20T23:48:33.482Z" }, + { url = "https://files.pythonhosted.org/packages/18/1e/f060df47755e87b57684cee7bfc1362b204df55fac96ffebc0631b697b79/wrapt-2.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0065a3b657cec06813b4241d2462ccec287f6863103d7445b725fb3a889736f9", size = 203399, upload-time = "2026-06-20T23:48:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/c4/de/2316a757a1abb6453700b79d83e532146dcef2611348282d4d8889792161/wrapt-2.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f7424af5c5c345b7f26490e097f74a2ef45b3d08b664dc33571aee3bd3b56c", size = 210461, upload-time = "2026-06-20T23:48:36.569Z" }, + { url = "https://files.pythonhosted.org/packages/ed/29/d1160785ae18ca2495a6d82a21154103d74f656c9fd457fb35f6b11b965a/wrapt-2.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07fdcb012821859168641acf68afad61ef9783cf37100af85f152550e9677194", size = 195313, upload-time = "2026-06-20T23:48:38.175Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2d/7caa9598ae61a9cf0989cc501739cbeeb7d650ab3193cca1407b9af0c6ab/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f90038ab58fafb584801ca62d72384d7d5225d93c76f7b773c22fae545bd8066", size = 206116, upload-time = "2026-06-20T23:48:39.804Z" }, + { url = "https://files.pythonhosted.org/packages/ac/02/281ea1088b8650d865f311b35cf86fd21df89128e2909714f1161e01c9d0/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c5d7825491bfa2d08b97e9557768987952c7b9ae687d06c3320b40a37ccb7f20", size = 192668, upload-time = "2026-06-20T23:48:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/be/7d/976e2d5b4b5c5babda40974edd54d0a5585cb60132ed86b46f4b80239b16/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ad520e6daa9bbf136f14de735474dbec7dcc0891f718e1d274ce8dc92e645af", size = 198891, upload-time = "2026-06-20T23:48:43.056Z" }, + { url = "https://files.pythonhosted.org/packages/59/b7/e47651797c097f75a37e2ce86dcf04048ff576f3a674f7c558df7b5e9622/wrapt-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:25904acb9475f46c24fe0423dbc8fda8cc5fbc282ab3dc6e72e919748c53f4e9", size = 78537, upload-time = "2026-06-20T23:48:44.509Z" }, + { url = "https://files.pythonhosted.org/packages/d1/6f/9fa5d59fb06d890defb5a8f727ce6a14d2932c8760153f96956628559fee/wrapt-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:305d4c247d61c4115794a169141823c62f719525ddb90b23aa332741c77d2c28", size = 82005, upload-time = "2026-06-20T23:48:46.391Z" }, + { url = "https://files.pythonhosted.org/packages/15/80/4c7bd9873d1f9f7d138d93556b500469dbe24f42710b877519c2b9eb380d/wrapt-2.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c20279cd1a29800815d7b2d6338b60a6c6e78263f9d6e62e0eda251ba9cae2d0", size = 80762, upload-time = "2026-06-20T23:48:47.964Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/7fd9c3f83b2c74cbfc572a0b88aa37431e04bd8aed70d2c0efd3464206de/wrapt-2.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0e64826f920c42d9d9f87e8cc09ffae66c51ede12d59061a5a426deb9aa71745", size = 81341, upload-time = "2026-06-20T23:48:49.39Z" }, + { url = "https://files.pythonhosted.org/packages/4b/68/1bfa43100dd90d4ef74a05897b86275cf57e1313ca14aae2545bc9f872c9/wrapt-2.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcaa5e1451bd8751d7bd1568dfa3321c78092a52a7ecb5d1a0f18a5791e1fd00", size = 81921, upload-time = "2026-06-20T23:48:50.986Z" }, + { url = "https://files.pythonhosted.org/packages/74/eb/df7b7f0b631dbbc750f39be27d8b55f65777d8ac86da80e12be41a644c4b/wrapt-2.2.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0abfd648dac9ac9c5b3aa9b523d27f1789046640b58dcd5652a720ddb325e1fc", size = 167713, upload-time = "2026-06-20T23:48:52.598Z" }, + { url = "https://files.pythonhosted.org/packages/4d/9a/d1bd36f6d088c8e652a9383cabbd49af30b8c576302a7eccddbab6963e3f/wrapt-2.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4bfd8d1eb438153eff8b8cfe87f032ba65731e1ce06138b5090f745a33f6f95", size = 166779, upload-time = "2026-06-20T23:48:54.33Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ae/24ffacd4187fac2740a1972093929e836dea092d42c87d728cd98fee11a6/wrapt-2.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c427c9d06d859848a69f0d928fe28b5c33a941b2265d10a0e1f15cd244f1ee33", size = 158407, upload-time = "2026-06-20T23:48:55.944Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ed/974427668249a356051e8d67d47fa54ef6c777f0fcf3bae9d292c047d4b6/wrapt-2.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4250b43d1a129d947e083c4dc6baf333c9bb34edd26f912d5b0457841fc858ab", size = 166594, upload-time = "2026-06-20T23:48:57.617Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5f/e1d7c6e4523f78db2fbd7826babd0348da1d5e0834c4f918b9ab5757dfae/wrapt-2.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:173e5bb5ca350a6e0abab60b7ec7cdd7992a814cb14b4de670a28f067f105663", size = 157068, upload-time = "2026-06-20T23:48:59.171Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c1/7ebd1027f00700c0b0233b20aceef2b4784294ed64971424c4a78e069e34/wrapt-2.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:aa14b01804bce36c6d63d7b6a4f55df390f29f8648cc13a1f40b166f4d54680d", size = 166470, upload-time = "2026-06-20T23:49:00.737Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/974e471a6a978b8180186b8a9dc5ae3361ce269a967190b709b8ce17abfb/wrapt-2.2.2-cp314-cp314-win32.whl", hash = "sha256:58f9f8d637c9a6e245c6ef5b109b67ec187d2faed23d1405656b51d96e0a5b56", size = 78062, upload-time = "2026-06-20T23:49:02.327Z" }, + { url = "https://files.pythonhosted.org/packages/49/ec/e1281156cdc7a66693838ad7a0865ad641c74abd337a957d668b575aaffb/wrapt-2.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:385cb1866f20479e83299af585375bfa0a4b0c6c9907a981483ea782ea8ae406", size = 80832, upload-time = "2026-06-20T23:49:03.837Z" }, + { url = "https://files.pythonhosted.org/packages/45/7d/1b6b5ddd94005a2dac97a4490c9838f3154977850d633abcb65b30089437/wrapt-2.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:8ffbeaea6771a6eba6e6eeb09767864995726bc8240bb54baf88a9bb1db34d5c", size = 80029, upload-time = "2026-06-20T23:49:05.237Z" }, + { url = "https://files.pythonhosted.org/packages/b0/33/9ebcf8aafe91c601127cbd93708c16aa8f688f34a10bf004046803ecdc4f/wrapt-2.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:09f811d43f6f33ec7515f0be76b159569f4057ab54d3e079c3204dddb90afa2a", size = 83357, upload-time = "2026-06-20T23:49:06.632Z" }, + { url = "https://files.pythonhosted.org/packages/39/38/ec45b635153327b52e52732a0ea980e5f00b7efba65f9e018828f1e69daa/wrapt-2.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a795d3c06e5fbf9ea2f13196180b77aeab1b4685917256ee0d014cc163d90063", size = 83794, upload-time = "2026-06-20T23:49:08.098Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ea/1a89e6d3b7a83c3affe5c09cde77792c947e63e4bc85ad84cd5bb9abb0d8/wrapt-2.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:45c2f2768e790c9f8db90f239ef23a2af8e7570f25a35619ef902df4a738447f", size = 203362, upload-time = "2026-06-20T23:49:09.811Z" }, + { url = "https://files.pythonhosted.org/packages/19/d8/3b58763d9863b5a73771c0d97110f9595d248db454009e07e1535ee905a4/wrapt-2.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbf00ee0cb55ec24e2b0995a71942b85b21a066db8f3f46e1dbfdb9433ffba81", size = 210449, upload-time = "2026-06-20T23:49:11.521Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6f/17fd9e053103d8be148d20d5d7505facc72d5fe1f9127973904ceaed79cf/wrapt-2.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2252f77663651b89255895f58cc6ac08fcb206d4371813e5af61bb62d4f7689c", size = 195349, upload-time = "2026-06-20T23:49:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/d0d1ccaaa12cb7dccf28a23f0279a608ba498f71e81d949d5ed54bcfd5c1/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cd7181ab1c31192ff5219269830744b5a62020b3a6d433588c4f1c95b8f8bff", size = 206099, upload-time = "2026-06-20T23:49:15.051Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/e8aa07b619890a2aa6cde1931b1887abb08820721b564a5f80b7ca3f3aa0/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6fe35fd51b74867d8b80174c277bd6bbf6a73e443f908129dc531c4b688a20d5", size = 192728, upload-time = "2026-06-20T23:49:16.854Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f0/1819fb50f0d3c9bd758d8a83b56f1b470dee8b5b8eac8702b7c137cea9d4/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11d95fc2fbad3163596c39d440e6f21ca9fccece74b56e30a37ac2fca786a07c", size = 198842, upload-time = "2026-06-20T23:49:18.504Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/e88313f16a99930b899ef970d91c281544a470749a359decad994483bbda/wrapt-2.2.2-cp314-cp314t-win32.whl", hash = "sha256:d8a15813215f33fa83667bfc978b300e35669ea8bb424e970a1426bcb7bc6cca", size = 79059, upload-time = "2026-06-20T23:49:20.107Z" }, + { url = "https://files.pythonhosted.org/packages/a0/4f/ac12fda57a55068a094ec42851fb0a40e8489d8941863d517452de62e507/wrapt-2.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d09db0f7e8357060d3c38fc22a018aba683a796bf184360fd1a58f6fc180dc77", size = 82462, upload-time = "2026-06-20T23:49:21.631Z" }, + { url = "https://files.pythonhosted.org/packages/48/a7/df732dac86d9b2027c56bd163dbc883e037b16c3469614752e148d219c61/wrapt-2.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:f32fe639c39561ccc187bcae17e9271be0eb45f1c2952510d2f29b33ab577347", size = 81182, upload-time = "2026-06-20T23:49:23.199Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d2/6317eb6d4554855bbf12d61857774af34747bf88a42c19bf306de67e2fa3/wrapt-2.2.2-py3-none-any.whl", hash = "sha256:5bad217350f19ce99ca5b5e71d406765ea86fe541628426772b657375ee1c048", size = 61460, upload-time = "2026-06-20T23:49:42.966Z" }, +] + +[[package]] +name = "xxhash" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/ed/07e560876a4458987511461187b285071f53cde49dd5b25cd8c51091522b/xxhash-3.8.0.tar.gz", hash = "sha256:d72b2204f37840b0f16f34192c09b994b97bd25823d723d47a1eddfacf06eb43", size = 86107, upload-time = "2026-06-27T08:17:28.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/1b/73aaae7755372ff0cd5788c9955abb64b34d519dd84f2f4f081e2082119b/xxhash-3.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:08c34553cd7ceb3bfcfca344dc70305a45430429b5d58a67750f2a58364f638f", size = 34641, upload-time = "2026-06-27T08:12:47.579Z" }, + { url = "https://files.pythonhosted.org/packages/53/08/fdb1cb1001ed15b1f74a8eb70457dbdcd6df8375e27e3fe0d0225dbab170/xxhash-3.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:842d147983110e5a4f533f98f4f5bc851a08c7ca00aaa30649e8d5f9a6d4e47a", size = 32316, upload-time = "2026-06-27T08:12:48.695Z" }, + { url = "https://files.pythonhosted.org/packages/d7/05/c004e99c4292a9dde76c9157e8e51c73c6db2dd7e4a876712e6a6113e3b0/xxhash-3.8.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:37c9943e18f569f76a8b7d5d01bfe0716f7762c396096ceb42a47eb3d5ecf641", size = 220196, upload-time = "2026-06-27T08:12:49.964Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b2/8696a2008d59c3dc9346b26f7d64f5ec342cacc4051664e3b0201354fe58/xxhash-3.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21f6797afdc7abb0ffae059a0d1619c84a5368115bc0abd48f9803ab56a5d35e", size = 240908, upload-time = "2026-06-27T08:12:51.544Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/2415c55a17f525bcfa38b5b51d69381d6485b1c320eff373b263403b5e6b/xxhash-3.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5875d99d3540367d43779551dd22c813420b84a103e418d791095b9808fdca57", size = 264445, upload-time = "2026-06-27T08:12:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/23/25/056d30ed2e500d0a993e4589da8cdbe50cbf4809c1b1ac84f6f9559d99ba/xxhash-3.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1a54ad5a2a96cdf1ee7a935d38bc63daa6095530095a916f644f1ab76604ced5", size = 241295, upload-time = "2026-06-27T08:12:54.703Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/5d8c9b65ae05725c2ea8f331705e1382fc4817911eb159450aecb2905c6b/xxhash-3.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b32e50dd85f0b67b2b95eb59cd3242052f6b27b70e9e73b27629686c592e3ea3", size = 473113, upload-time = "2026-06-27T08:12:56.159Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d4/734dd8e6eaa03b0c4e3044127755221ebf153260a3c5de0382430486fcaf/xxhash-3.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4208fb85c950ddf7118b040bca15179c3bf9b7eb8bebe5e6ef067fc8af16a7", size = 220001, upload-time = "2026-06-27T08:12:57.869Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cc/a0d92359d499db55f83fe6de13188125515319b968bd627b591a0984c454/xxhash-3.8.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9f17e09b035f2a0139536da53deb392b62ee259dc2a2189be12b06a7dd50489b", size = 309757, upload-time = "2026-06-27T08:12:59.438Z" }, + { url = "https://files.pythonhosted.org/packages/bc/dd/a20949401cfb9c940ef858d93b41ded90382ff4be0f7e8a5249edd95ff18/xxhash-3.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7d6dbb976d6e3b3be51bad16b13de7f4980e6aebd0aa51c5a14dfcc0fedd495e", size = 237596, upload-time = "2026-06-27T08:13:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/6963ee0c245a69d9c4a2583da603915f9288f1df23700a0ec705239ef014/xxhash-3.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:281897e5c516769694c999f5c50fd1e9acb27acbff187282a8ac77c38b6a9be5", size = 268683, upload-time = "2026-06-27T08:13:02.577Z" }, + { url = "https://files.pythonhosted.org/packages/db/ea/3489cde91ccd91230efbb2351a6d9358e8a63a9954cb8f071fa9c32a2558/xxhash-3.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8fba3d08c246201a1a0a6cece53a0b3b0890fc16adbe1edb245fcfcbf4eb0ce2", size = 224882, upload-time = "2026-06-27T08:13:04.21Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f6/179847064c92a07bba7381e9cd7132c380a17aad31e176a2d6f6e73eed48/xxhash-3.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:14ebc1559e8a9a481d0d5506b87678942fcdfa794d4aa55cdd2a0fb175d4245a", size = 239563, upload-time = "2026-06-27T08:13:05.96Z" }, + { url = "https://files.pythonhosted.org/packages/2d/83/dd599670efd161d31fba4149e20694f140ae5707068d38ac480dac1c8cd5/xxhash-3.8.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5e7a3e3bbe3a56bff70acc9b72576670e793b0184de3d1b9cda2bf697d17f630", size = 300148, upload-time = "2026-06-27T08:13:07.495Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/a474f136610594b464ad813f6badf00b931211a69fc86542c21daf5d2a4d/xxhash-3.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9c71e3755a8320d29c351126d550930349be22b44bac1a559caf12ab78b53e9f", size = 442448, upload-time = "2026-06-27T08:13:09.467Z" }, + { url = "https://files.pythonhosted.org/packages/75/86/054032919fc73b72917054cf731be76be3a984e8f53b1d0ba6f22fb9cffc/xxhash-3.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:715c611582004e75010517b919776c5dbc00aae03054dc9fd72484a23fd1862f", size = 216755, upload-time = "2026-06-27T08:13:10.902Z" }, + { url = "https://files.pythonhosted.org/packages/3c/16/2eb382a78f12e3fde1c735b57607498c0efe897e8859484d69d9446bba55/xxhash-3.8.0-cp311-cp311-win32.whl", hash = "sha256:41a30a1d0ba978238742a374875c15979e0faed0a65294f3ff4d9410057ee8b6", size = 31851, upload-time = "2026-06-27T08:13:12.281Z" }, + { url = "https://files.pythonhosted.org/packages/dd/53/a07ad4dbdc32118b3bd190f5d54ee2ed28c1a0a994b52ae493435cfb4de7/xxhash-3.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:43705f917b8b817d6994851bf3725b98b4c95e64186404d9a6dbc1acf12fd140", size = 32655, upload-time = "2026-06-27T08:13:13.394Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/d76bef62a288a1f2441404b33cb757047cf555cd5956b36ed718a38b81e9/xxhash-3.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:35c5d843bb7ac1dfdb125ef4181fe4c2e01c2275856e6b699de89e9eb5c69c8d", size = 29128, upload-time = "2026-06-27T08:13:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/17/2e/4b7c3ab28b7a54ac17eae7e02471c49609d6fc5900856a455feeb847a2a3/xxhash-3.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fc4bd14f873cd0b420f6f1ff5b5cd0dbfeb05b044a11bb9345bcbbf9749636e3", size = 34623, upload-time = "2026-06-27T08:13:16.696Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/09eea3e1bba6a59d64599cb8fba39f2a0872d06e85420eae989a4da61a9d/xxhash-3.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:31904979198e913239cb61b49f5b849696aeb3b03340da815d1491ec74dcc602", size = 32318, upload-time = "2026-06-27T08:13:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/01/59/688bbae31e4e2d6d6eb92acbd3837c0e44ff8c7d435e6da922844ff6efda/xxhash-3.8.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7338ad13f2b273a1ef0ea97b2db0a059fdb3a1a29298bfa145937c0e4152d341", size = 220461, upload-time = "2026-06-27T08:13:19.311Z" }, + { url = "https://files.pythonhosted.org/packages/2d/de/71484ce0dab2fa4a475705d1ebc37a17ff02d40e5df6767b3255cc53120e/xxhash-3.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54e80e803cb34c8a1d278b491e543af40a588d288589c3e6becc991d5328b46b", size = 241110, upload-time = "2026-06-27T08:13:20.844Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f9/1ac88f02e7df7898541490260b21f2b7f7bd2b233038a0cbd3a3b1bffdc2/xxhash-3.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:353953ea18f5c3fbdd13936fb536aacfb47d5bc06eef0919b1a355df61f7cc31", size = 264779, upload-time = "2026-06-27T08:13:22.485Z" }, + { url = "https://files.pythonhosted.org/packages/25/49/7ea1f128d2fe948ed679020f97a0896cdc6c975da5cc69b53a4a9c4a5def/xxhash-3.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d761f983a315630eff18c2fec7360c6b6946f82748026e779336eb8141ef3eba", size = 242609, upload-time = "2026-06-27T08:13:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/a0/da/7d237278dfa1c48722c31010c84a328a317b8885429c8cb6ae4a8fa3e3db/xxhash-3.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f3786a9beb9a3b76241cb7db5f5388b460682c12204236389e3221963fc626a6", size = 473472, upload-time = "2026-06-27T08:13:25.877Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5f/980fda82620a07d80026b4df371cbca12fca0fd94d7087c4ec5d898da76f/xxhash-3.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c94f5a9a775f36cc522fa2a7e8e2cec512e252d2ac056759f753dc68a79ffc", size = 220374, upload-time = "2026-06-27T08:13:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/14/71/efa37bc3e91e1c801972bcef99eab877fcbd17ec10aca16c550ee2951107/xxhash-3.8.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:55ce59f9af37ac861947b43ea3ce7b294b5de77a1234b558d0f07ffad0197624", size = 310220, upload-time = "2026-06-27T08:13:28.804Z" }, + { url = "https://files.pythonhosted.org/packages/9d/48/19e40320044dc7051e8446505f18557d5661853b87a8770ad399325bb3c8/xxhash-3.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3afa1422a32c7c8e79ad5121dc21eaa5cee9e9e67bffca3f15d15d220d371908", size = 238100, upload-time = "2026-06-27T08:13:30.378Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0d/588499f4d7cd064864ada7adfb9e8785f88a988f1332ed4c1be73d249c15/xxhash-3.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:551fda694938be910529452a89175137c58b4739e41fadff3c047e24b1d74a3b", size = 268937, upload-time = "2026-06-27T08:13:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/54/18/fb2ad593572a33d1b6864b33047b8ca7269273a3c56107b5fd33e0b9c8fb/xxhash-3.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512eb937c9457e6057e230e005c4709dd2ab63a5989f854d69f31db905750a62", size = 224910, upload-time = "2026-06-27T08:13:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/63/9e/b880f9ed61b73492e24bb962d76aeb63f18ccb895f0edfb52e20d45ed6f2/xxhash-3.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4931ea93840f750a908efebaf23c71004feacc1a4649ef601b96d400a505c9a9", size = 240742, upload-time = "2026-06-27T08:13:35.237Z" }, + { url = "https://files.pythonhosted.org/packages/3f/89/fc682f93e54e486fc338b26a7d6d0d5cb0ab366269273c2608ac62b51afb/xxhash-3.8.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2fd4b60e8d9fc3923f39079f185b3425e6d76636fcb66d82a33dd7eba7c30f2f", size = 300527, upload-time = "2026-06-27T08:13:36.997Z" }, + { url = "https://files.pythonhosted.org/packages/80/71/a4b4122afb2d17ad69e0922cfeddb5ad5c25b02f37eed3dd3819d42e5f55/xxhash-3.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1da00075f1605794298878cb587f7533329693e2a0c45bbd25d6353644add675", size = 443195, upload-time = "2026-06-27T08:13:38.719Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e5/ed3930f5dc90f4b1bab5ac3be099e8b2e81c1262d85e4adb5f2758e30d23/xxhash-3.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba73801c87d44fa37b2a5feab3004f0a654506027bf032ceb154d94bb74ea772", size = 217252, upload-time = "2026-06-27T08:13:41.179Z" }, + { url = "https://files.pythonhosted.org/packages/44/ae/128ea5794387ca54bb4084566db20dbdfc9c21cb17b67d3fcb403927b5ba/xxhash-3.8.0-cp312-cp312-win32.whl", hash = "sha256:0b0836dee6022e22ba516ebfa8f76c6e4bda08d6c166c553e40867bac89e4a54", size = 31890, upload-time = "2026-06-27T08:13:42.568Z" }, + { url = "https://files.pythonhosted.org/packages/4f/04/a6c182dc566c88e8d1a497d22cc4ffdcfcc0a9fa80325efa6cd4b9002c54/xxhash-3.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3bc2a09b98b8f85c75208cd2b2d2aecf40c77ecb2d72f6bf9757db51a98d3499", size = 32677, upload-time = "2026-06-27T08:13:43.705Z" }, + { url = "https://files.pythonhosted.org/packages/93/b5/aeda4e79f962c8d58ec60cb20a5abfe91c9f7d62e626f69f6659bc0bd0c4/xxhash-3.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:208e6a8b93426896d803224e9fabe26f8b9c651e8381a80b1fa31812faa091e3", size = 29155, upload-time = "2026-06-27T08:13:44.903Z" }, + { url = "https://files.pythonhosted.org/packages/ec/1f/96f43c5c7c7c4d44721f8d2e5d74698c667a30283c4b10a7e50a56804ee3/xxhash-3.8.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:36434c1d1b0a4729df1fa26ab11bffed1ba52666c0beb605c98a995b470cd143", size = 38508, upload-time = "2026-06-27T08:13:46.152Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d9/7d5d6af4876c6481f2e0acb2dda64dd5209574bf7ba1ad4f6af7a1f8d473/xxhash-3.8.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:a5e6497cefcb2d67f1745c66df9718a99112583af6cc2b70da0312a2eb939f1e", size = 36542, upload-time = "2026-06-27T08:13:47.497Z" }, + { url = "https://files.pythonhosted.org/packages/32/ff/66fed439d78c5a09a1491a85af29bf8923b516530116731a9ac6b14dee2b/xxhash-3.8.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5b00b82f1be708da9404fefd658cf5cf3be5ee3be2aae4bfe3b874255badd342", size = 31102, upload-time = "2026-06-27T08:13:48.721Z" }, + { url = "https://files.pythonhosted.org/packages/56/b8/9fae0399281095f8aca1f32b21947b3c3c75ad6021b255c5c6e4b11d3866/xxhash-3.8.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:38b0cb0ab7f283413b7cace2bf710d7cf8f702ea82cbc683908691d52028a89b", size = 32096, upload-time = "2026-06-27T08:13:50.138Z" }, + { url = "https://files.pythonhosted.org/packages/61/a4/e53d162c74a8a2950dc063969914387b0680da4c7c20ad17744ec03a3b0a/xxhash-3.8.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:084312171a9798dea85e924b2674f5e1a44933050a1ea1cb1c6b1364e004c66c", size = 34585, upload-time = "2026-06-27T08:13:51.572Z" }, + { url = "https://files.pythonhosted.org/packages/69/f5/e12397e3f2c4917b6572e103a3277cd27cc56330e304bba61d195d7e5224/xxhash-3.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6a1a9e845bd3bbc57d9356819e0d198fe23282e0576b398a6282a0f8fdc75aef", size = 34622, upload-time = "2026-06-27T08:13:52.818Z" }, + { url = "https://files.pythonhosted.org/packages/70/80/c053dc51af5c942229689a0e9cb66fdc999bbd840f645e761f5ab73cbb17/xxhash-3.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ffbde09743ebaf8957b8426948fbe85eab5e5de0d29eec407fcff5a2812a3cc", size = 32320, upload-time = "2026-06-27T08:13:54.04Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a3/294171b67dfe770e1293edcf2a3f7e41302cdb8aefb258585312191b3ffe/xxhash-3.8.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a6dee3952c2b6e82e7f1dbc5dbc6167f9c84126851def7926e32827c2816169c", size = 220532, upload-time = "2026-06-27T08:13:55.448Z" }, + { url = "https://files.pythonhosted.org/packages/80/c3/d141bfdeca785c8c680abf867d4b52a5e64a55d90df242c3141a3e58c4b2/xxhash-3.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf8ff8e12416c9fa05b43c7509b9332d6ffc4090413c4e7a1dee8599763b6d59", size = 241215, upload-time = "2026-06-27T08:13:57.047Z" }, + { url = "https://files.pythonhosted.org/packages/09/5a/aeaf35143a6f3d44db73298e861405bdd9c9dacaedfc369cb43d9fd65282/xxhash-3.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cebbb322df4d97d8ef2704f49ed2f6f21f6702fafa0dc0c2a6ae70e904205689", size = 264615, upload-time = "2026-06-27T08:13:58.912Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3e/f8ca782bb34f99693faab70a7989bcc84f62ffe93c9a4cca464a33507a4b/xxhash-3.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9a8d08707b4100ebce598fc59fadf04b42d79b855818d6994f8f0fffd1df8edb", size = 242682, upload-time = "2026-06-27T08:14:00.483Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/ddbee4ff1542c2e88e72269a5a6bd18c3f26a80c2514e0918f5d1f3e9ec5/xxhash-3.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cf5427602dda15d8ce3c6d870d29bf07d43975f59c9d6d3f7f6f93a901b28b12", size = 473551, upload-time = "2026-06-27T08:14:02.17Z" }, + { url = "https://files.pythonhosted.org/packages/25/f5/a680d48dddab37ab2fd9189ca03f775e29e3627122e30790816d7eb365af/xxhash-3.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97d7bd715ea5050b6c9638b52c62adf3055b648ef6eee6892a4cd9697b530191", size = 220485, upload-time = "2026-06-27T08:14:03.765Z" }, + { url = "https://files.pythonhosted.org/packages/22/b1/7ac129b74981c07f1ff9c649f204465e86f83f9f29b2ebdc70d91514c365/xxhash-3.8.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cd25bbbab37d898f6e5a90905ce6ae2c1f8bd6668c07cef406fb3e8c8c570dd", size = 310307, upload-time = "2026-06-27T08:14:05.366Z" }, + { url = "https://files.pythonhosted.org/packages/67/e6/43e673411249dd63f6cd974523a1b32fad75cf5453e363bc8f44af215fb9/xxhash-3.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3e30e5c057f483c3c53a11b53eba091a737cb19dfead36c8b23bf5beb4a169cd", size = 238164, upload-time = "2026-06-27T08:14:07.149Z" }, + { url = "https://files.pythonhosted.org/packages/e5/95/87f8baf41f63130f3637104b7a610f82b20106332fc6e289c8dbf7955d0e/xxhash-3.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:07dd44d992ebd456752bc25b1c42cd172d94bd8cb24049300449ad0716081c3a", size = 269062, upload-time = "2026-06-27T08:14:08.834Z" }, + { url = "https://files.pythonhosted.org/packages/38/c9/3369b497cd1f926b930c52fd2400606f177790d887b49f9e86bddcc24562/xxhash-3.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3118600a3102d4707dc1c485dbc3acbbbf37819069ad3e7854e77b923745d76b", size = 225007, upload-time = "2026-06-27T08:14:10.689Z" }, + { url = "https://files.pythonhosted.org/packages/34/c8/03dceb86a8128858ac105bd6e282d62b3db6fd421a79bd8a9f6b8cdc47a7/xxhash-3.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ed37b0c95d8fb3fbaad5e13cc0a9727eb8739d1d54b2adef28108c250cada3a", size = 240815, upload-time = "2026-06-27T08:14:12.195Z" }, + { url = "https://files.pythonhosted.org/packages/47/a5/ebd43eeb1af1dd8f0201943688b20958e99d3f6eb36481fb8c37b55ef139/xxhash-3.8.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bb043da412e478e7b1db3407051124b85b133803794d3809ad6d92870b304fc7", size = 300632, upload-time = "2026-06-27T08:14:13.916Z" }, + { url = "https://files.pythonhosted.org/packages/df/24/c873e41a3c00dacc385c8ff08c007723f6a528922c1cea7fd9684e86dae7/xxhash-3.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:196fc132683d9311a0bdce8388ee52bfa07fdc1987cc428a27956e47ccd7b50d", size = 443293, upload-time = "2026-06-27T08:14:15.446Z" }, + { url = "https://files.pythonhosted.org/packages/4f/1b/c671272fe28f70574e3c574d58465f26460154bcc68876121872afa1c14d/xxhash-3.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfb5411af3b77c75e99db100aa15c5ba623c85d72c565e4d7a0ed1a986ff766e", size = 217327, upload-time = "2026-06-27T08:14:17.28Z" }, + { url = "https://files.pythonhosted.org/packages/57/43/b45a52f795812cb769b6ac159e69b605d18b1c067749e63dcac159e90064/xxhash-3.8.0-cp313-cp313-win32.whl", hash = "sha256:6d1d6179e26830c6690fac63f76d372f69714b977e12ca9c42188a60f51c59f5", size = 31898, upload-time = "2026-06-27T08:14:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/a1/42/2bd70e4eec25dc5990652979d708d4d7c999793d7d5af5d0e48ab4374dc1/xxhash-3.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7c92427a56a12f4d5c7bb26dbb9e9a4658c313ecb6c2f1dca349902e3822df07", size = 32680, upload-time = "2026-06-27T08:14:20.277Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c8/2fe61edb6144183cf094035a8c5354c65a073127acf6379655ed1e705b70/xxhash-3.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:9fc8453642c1c6d38b4fbac8901c2452ce1fa88b27f003bfee6703cbfae9bd63", size = 29157, upload-time = "2026-06-27T08:14:21.674Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b8/81d17a993b9a4750ba426ce966421681bb4b8e82a460cd346756491b8cc2/xxhash-3.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:efcacb644a915f010dc477447b045e5dcde1afaa40d16b2f0f8e7cd99c9e1635", size = 34897, upload-time = "2026-06-27T08:14:23.044Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/f5a368e3273440b3ea58fbd3f0b08c19f552b25ca59f43f5732ca96d2126/xxhash-3.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d1e0dbc510cff94c5efbcc2b82c28b41519fad09b5b1f9f3d99c63e3940e49a0", size = 32630, upload-time = "2026-06-27T08:14:24.603Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ab/f424359c91c55f564fbbe4e454a126eb522471109f67376f20ad19c5e663/xxhash-3.8.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ff19d016a41c90d1f519005887191896b6da1274e1d5d48b347e17eb798ffc5a", size = 225874, upload-time = "2026-06-27T08:14:25.992Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c2/434579ef9235123b6c9bfa89c5614e0001e988613b91557b24aa326d9faa/xxhash-3.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aafc3eab99c50508852e34307e9565933bf128cad084cac7d2471b7ab1743de0", size = 249705, upload-time = "2026-06-27T08:14:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6c/3c0c917331ca3c71f826cedce2127f230624e2b49b992472dd5e9e72101c/xxhash-3.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5e521368ed79ae6c4d31e1e417726643c49d7d6e286f4fdabf9a8330ed8a8ff7", size = 274716, upload-time = "2026-06-27T08:14:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f3/a8bb98d3307c67e88be9642dff52854c3de3f488f95989b60ff69c8dcc42/xxhash-3.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6a0127688d116ec0c225e7e1f744e3f206de2b8822ffeb31a9ab5cc6384f92c5", size = 252019, upload-time = "2026-06-27T08:14:31.247Z" }, + { url = "https://files.pythonhosted.org/packages/f7/73/fab69a2e5b6353dde643209fe9b6adf4fbd64c888e531deffc476bfb2635/xxhash-3.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:22c0b17da2f9fea0f8836538512249871b359141616bad44c58d238b5f011f40", size = 482024, upload-time = "2026-06-27T08:14:32.973Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/ba34099b5278097ec9c68c0b740719813553bfd11ca17e7353de6d2a41e3/xxhash-3.8.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d49465646b1a5e3b1729c5f636e05676a2fb52e203e3b22a5411c416c4c5302", size = 226655, upload-time = "2026-06-27T08:14:34.608Z" }, + { url = "https://files.pythonhosted.org/packages/76/0c/90aba4708a37fe752b324a7cbf10058eaa33e892cdd62751ff17a5137b93/xxhash-3.8.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2853dea1e30ed00ca87dd87d76da5da063d302b823b3fb80ccd18421de0f251", size = 319583, upload-time = "2026-06-27T08:14:36.419Z" }, + { url = "https://files.pythonhosted.org/packages/38/46/42e349e2d3017b2688f4cb301742c37c438e77963e3fef711edce2fc5c65/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:82f0102a2a3760287b7cd7f9e0a30edd4c3b18762ed1a242208d43c8e2bcf30b", size = 246000, upload-time = "2026-06-27T08:14:38.104Z" }, + { url = "https://files.pythonhosted.org/packages/ee/15/741b947ae3c768e82018c46846f8616f6aa9b5042649f318a1a6897defe3/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:b8414a66a7524596d841cad5dc1adab6ce76848db5ab2b83db911fbdab1417af", size = 275455, upload-time = "2026-06-27T08:14:39.841Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b4/a9db84c9458fc8f53eaf0051377d1e9eecd9f330fb1225640027417a309d/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0dbaa73df10414ea1e41b98691a9d8241d4c47ad8d02c726587a3cda05278e53", size = 231209, upload-time = "2026-06-27T08:14:41.543Z" }, + { url = "https://files.pythonhosted.org/packages/20/92/60a868cd34851746d0b0d95dced0f42867c7c00606f6e5dba85b70b232ce/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:43fc9aaba10ab4267c90793601f60d35c3c9caa1544eceb483618a71ad9ce7da", size = 250416, upload-time = "2026-06-27T08:14:43.193Z" }, + { url = "https://files.pythonhosted.org/packages/7a/6a/168ca46a4679c32aae9246caa1fddf35981d6304487e45e992b3d4530324/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ec5eb3d28fbb9802c6d2526f772133a06c91d6f03756fcc67c834b642ffdd51d", size = 309764, upload-time = "2026-06-27T08:14:44.79Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/13646b348c07679c818791ab2d35415db5cb20f3bc77daaa255909a401b4/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:2b77c301b644cd9b4d0749a3291081ec2048a6bef7fe0487c993bbba3efb9ce0", size = 448650, upload-time = "2026-06-27T08:14:46.562Z" }, + { url = "https://files.pythonhosted.org/packages/59/9a/3d244b2acf6bbd86a363817ee09084b4684e8e11840663e19869e9e0d952/xxhash-3.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d7ece11a132325353890a144c30119073617a1299c593ca29b96c315b07e1edd", size = 223572, upload-time = "2026-06-27T08:14:48.294Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c7/143410d026a6e0d86dc69037ec2a3b8db810a54e7f443b340ac17612be2e/xxhash-3.8.0-cp313-cp313t-win32.whl", hash = "sha256:b21db84df7b9d54d9e4195a964243c1b32d745c6fbc0cfcfffee1d4bd297196a", size = 32301, upload-time = "2026-06-27T08:14:49.687Z" }, + { url = "https://files.pythonhosted.org/packages/6c/db/2240b0638161637b2f310231748a7a6a06c79fb43a3adb34c96f359762bf/xxhash-3.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0643b7d9f598f6da6f1f6b899f4358250d0fb853242e2d712cbde27bf5a99d29", size = 33221, upload-time = "2026-06-27T08:14:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d8/52038e4fa5baf4f00654a225516168d02908edfec7ca104fbefc58af394f/xxhash-3.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:4bbacf2e938526969f8ab3334d4ac3da14ea059e1dfd1339a92f9091467e750f", size = 29294, upload-time = "2026-06-27T08:14:52.778Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ef/a09907aa28bdcdf6810d5c26656b154c60c0f06bb8db8442a1192d9c227a/xxhash-3.8.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:557e2a7cc0b6a634cf9c8e5c975d96b7da796fdeb1824569d760cf0f25b6f33f", size = 38365, upload-time = "2026-06-27T08:14:54.166Z" }, + { url = "https://files.pythonhosted.org/packages/d2/4d/d991ff77bc489c2231025e64e570502156d573c7bff69c917589cc307089/xxhash-3.8.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:dad744d1613cbfddb844dad93adbffbd51c3e9f53ceea9568f7c3b94bedc19a4", size = 36477, upload-time = "2026-06-27T08:14:55.427Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0e/553eab001f1e274da73da074968cdc8be8cacfb318937ab9871b8e1909cb/xxhash-3.8.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:953f29b22c04b123cf3cd2e08bccde3a73184aeda5a1038e0054cb3355644120", size = 31116, upload-time = "2026-06-27T08:14:56.897Z" }, + { url = "https://files.pythonhosted.org/packages/55/d5/d0f4dbe7b4d9ce0125f16e45ec0be5e04f6a172edb4e2fa551c4f2eb5d7a/xxhash-3.8.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:aa699e0253ceffecf41cae858d0a11f2439d6874a0890b556387bffe11dc1c08", size = 32112, upload-time = "2026-06-27T08:14:58.126Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2f/b332c7bede6a676343f2c9c8dea233c8c82753eaeda6f7a2c321d8c58ca3/xxhash-3.8.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e232c82466babc13e956d53aa84d0149660ed6886bc195248bb4d03bf2eca301", size = 34618, upload-time = "2026-06-27T08:14:59.458Z" }, + { url = "https://files.pythonhosted.org/packages/b3/5b/2bf3c9e61c7cf8f53bce937af45e22b72bb1f224d5afb20352beba0d628d/xxhash-3.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7f75fd1c6a5028f345cd4a8c52f4774d2e5b7809fa58111c60a5502b528914a4", size = 34739, upload-time = "2026-06-27T08:15:00.863Z" }, + { url = "https://files.pythonhosted.org/packages/64/b6/e88521f5736c181b89bfb7ab756f0ca658a8a1ecece7277b75e167717614/xxhash-3.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b49d7e09b211a1ad658dbe2dbf6561eb92f2e6926bd1101e2d023178371f2d6f", size = 32332, upload-time = "2026-06-27T08:15:02.383Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a2/fba440739fa5f86d2c28738c202e88d3dd063290c8bbb20e183c5334456a/xxhash-3.8.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ceb702bc8e56b7f1f1413d42aa294045b9a0e4c9888e07edc5cd153e8c4c948f", size = 220479, upload-time = "2026-06-27T08:15:03.785Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1c/4a1639efec16416695d6c7bc6b224d3f607e0b8cbe2409fa81081a849d1c/xxhash-3.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f3c96e06bdb122e8cc84f5c7088579f3102b828efd62e9dc964a9d17c7b89e", size = 241409, upload-time = "2026-06-27T08:15:05.439Z" }, + { url = "https://files.pythonhosted.org/packages/92/d1/8ce471f8d6752384f972fd5f6363f2e8d8b867a89fbd724c6dbd91d2bb98/xxhash-3.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:415a8d06ac9bea36b1e06b603a347e0f62401042a97d7bfccec8ae2da12ad784", size = 264433, upload-time = "2026-06-27T08:15:07.027Z" }, + { url = "https://files.pythonhosted.org/packages/95/77/400a281683fd39c54e2ac497fa67bdf886baaadb8c0ba58f7e1ea1d7692e/xxhash-3.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f5ccdd2deb5dce31201cc0eec94388cce97e681429073db50903fab0a0a8a0d", size = 242835, upload-time = "2026-06-27T08:15:08.703Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a6/edda651cfa0ba8e921791e93468fae655b63894d89730fcbfe46704f0d0a/xxhash-3.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1a6cf81bc699d3a5ebfcf2fdb2a7bd2e096708d7de193f6f322944a02ba00953", size = 473800, upload-time = "2026-06-27T08:15:10.503Z" }, + { url = "https://files.pythonhosted.org/packages/dd/da/50f764ec6a93d3961fce294567e41bfca0e66d168deed354a3dc90ebeba6/xxhash-3.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4d12a04d7ffc0359f0eadc4535a53cab113044c8d2f262c7e9a56950a5ed50e", size = 220677, upload-time = "2026-06-27T08:15:12.622Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/9fe4ed5aac6f38629cc83b34f84748b83ad8295a578ec6a49d8bf896cafb/xxhash-3.8.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d209373fcb66138c652cf843385ee60866e50158a7869bbbf8b322d9a822b765", size = 310385, upload-time = "2026-06-27T08:15:14.384Z" }, + { url = "https://files.pythonhosted.org/packages/83/f5/1147e03c0553ed22bbae9ce47503c37ee0c5f95592aae10f339c25f61de9/xxhash-3.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b88a3fe28277811e599efa6e1c96abce8a77d60dd79c94da7a9b5c377c172b7b", size = 238330, upload-time = "2026-06-27T08:15:16.201Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/92daf66c1966c84da5c97a06ced1480208d3a3bd465cb0630565ec00d1b9/xxhash-3.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5d5a888a5ef997cb35f1aad346eb861cd87ecfe24f5e25d5aa4c9fd1bd3950c2", size = 268667, upload-time = "2026-06-27T08:15:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c0/080c1a92972667e183c04b03f33c877f8ec61cfa3570e61731077286648d/xxhash-3.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:de2836e0329c01555957a603dcd113c337c577081153d691c12a51c5be3282b0", size = 224934, upload-time = "2026-06-27T08:15:19.972Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d5/cbc4e5b2bee10c94cba05b5bb2b8033e7ef44ae742583fdafcd9188e33ed/xxhash-3.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4bc74eedb0dd5827b3be748bacf9fdb50004037a3e16c7ddb5defae2682cef71", size = 240870, upload-time = "2026-06-27T08:15:22.04Z" }, + { url = "https://files.pythonhosted.org/packages/76/f7/09679b00e192b741b65c230440c4f7e6df3251a9ad427a518ddf262ec71a/xxhash-3.8.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:c571b03d59e339b010dc84f15a6f1cff80212f3a3116c2a71e2303c95065b1f6", size = 300683, upload-time = "2026-06-27T08:15:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1b/f43ec36e8c6a20c77be0bcca23f0b133ed8a0312681500d1676eebd71924/xxhash-3.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:87626acdd6e2d762c588a4ffe94258c5ef34fb6049a4a3b25019bdb7f9267a9b", size = 443407, upload-time = "2026-06-27T08:15:25.504Z" }, + { url = "https://files.pythonhosted.org/packages/45/2e/a3e3a779c5e4789daf975e05cc1c7f11bae724a03855120029d4592c8e63/xxhash-3.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:076d8a4fb290af952826922aa42a46bfc64caa31662ce4e2925a445d0e6ce57f", size = 217559, upload-time = "2026-06-27T08:15:27.234Z" }, + { url = "https://files.pythonhosted.org/packages/44/da/1c1e078ac290afff304a541a2a60965beb369ad65b4f30ec93ea1e0b7210/xxhash-3.8.0-cp314-cp314-win32.whl", hash = "sha256:52f8c7c9833d947e60df830671f6eca810d7c667051243985a561c79f1a3d545", size = 32602, upload-time = "2026-06-27T08:15:28.809Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7a/d455cb83d5e3c94046234294fb5dbbe5da600d1bbdf76b9527756920cce9/xxhash-3.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:4fbfcb7dd307e23189a71050f6e27746926590330f37d5fd2ffcb8ea78de1f42", size = 33393, upload-time = "2026-06-27T08:15:30.166Z" }, + { url = "https://files.pythonhosted.org/packages/89/8f/1b14471f617bc96edbb9566099a162d918a981381c398114726cc600b76c/xxhash-3.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:ecef1e65b4715c7326002073763fe94cc44c756a0698508abb915ab3d6be6e3d", size = 30007, upload-time = "2026-06-27T08:15:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/51ad2f9f784121c8057ef1ba36362f58d4595cbcad16322941f5b73eb53d/xxhash-3.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:02ed856a765cb6e006168595d9455ac8c3c4d60cc04cd47a158a1ac677d68f0f", size = 34957, upload-time = "2026-06-27T08:15:33.292Z" }, + { url = "https://files.pythonhosted.org/packages/1b/14/175c573ae4fac48bf21a82e5b9ceec75d64c520c51ca08de3105de539438/xxhash-3.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:eec30461a7b457611098ba7ab09363e36c8b2645b4687fb6f3d405bb646e3410", size = 32635, upload-time = "2026-06-27T08:15:34.766Z" }, + { url = "https://files.pythonhosted.org/packages/96/08/f83efabd350a50c31c851b88891e318a6f07bdbf40a43d0f7bb6cedade7f/xxhash-3.8.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b471744912d1ce5dd6d3975b7525e77518359ebf3aa1bd7d501e199f5ae488ea", size = 225969, upload-time = "2026-06-27T08:15:36.35Z" }, + { url = "https://files.pythonhosted.org/packages/7c/78/2b6d12da9cf572c84d93b88ecbf9bf6539a7c5219bde128b214396b97c8b/xxhash-3.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3748d71202bf3f279e77cb8b273b6d0f29d1bcaefb6ce6cb03b95f358863ba37", size = 249851, upload-time = "2026-06-27T08:15:38.087Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/755eeb1882634983b24e6375a95ed233228dc48f0ef12655388bf3c7eeaf/xxhash-3.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3b3bf59ea94b2a23b0f992769804ab9401d5cdcd9df0062fe2cd78a491ae8851", size = 274842, upload-time = "2026-06-27T08:15:39.808Z" }, + { url = "https://files.pythonhosted.org/packages/77/f2/09b1231cad17c314e51664c4a004c919108ec59aba10f9a28fa061e7b8be/xxhash-3.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:40f061aa5379eba249e9367b179515571e632be6d1b6f55ac139e6fe3d08463c", size = 252218, upload-time = "2026-06-27T08:15:42.105Z" }, + { url = "https://files.pythonhosted.org/packages/b2/24/de756d55547953494eb6775aea92e258035647b3ecb8547618cd549001e1/xxhash-3.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:680d70896a61fc920cc717a0a8fe8a9fb5858c563184666e31874caa54a16d9e", size = 482135, upload-time = "2026-06-27T08:15:44.476Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/b8147633e32f98ef2b4bb0dfca82f0f63e2b02ff179f20664af64c4216a7/xxhash-3.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14973fbdee136588e57447401b521f466a42faca41eecdf35123c73103512ca8", size = 226776, upload-time = "2026-06-27T08:15:46.597Z" }, + { url = "https://files.pythonhosted.org/packages/29/37/ba051d8f0380d3cf845b23ba058a17d32025846463eb6bf885887fc8effe/xxhash-3.8.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:96c6bca2486cdc58b125966817a92a6abe6ef1fab86b2f8798a7e93488782540", size = 319738, upload-time = "2026-06-27T08:15:48.394Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/36e0a27dd27ffa3f7b521650cbcd52a00fb86b71343ffadb642374e8263c/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0b1109ae238e932d8482f9cb568b56a405cc73bc7a36b837844087f1298dd218", size = 246136, upload-time = "2026-06-27T08:15:50.981Z" }, + { url = "https://files.pythonhosted.org/packages/fe/73/2663dbf4c09386a9dcc8a94d7a14b4609ed4bad8180ced5b848e60a9b660/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1da5db0863400eade7c5a31969754d1392189f26b4105f6631da2c6c7ea3bccc", size = 275568, upload-time = "2026-06-27T08:15:52.735Z" }, + { url = "https://files.pythonhosted.org/packages/d6/58/f3ce1bc3bb3971191f6521273ddae98d3c610bcefbbed5327c3b3627c12f/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c61b5a0f21ace5e886f177cce43826d85a7c84e35a9e17cb6d1b4ac0b7a7d833", size = 231314, upload-time = "2026-06-27T08:15:54.73Z" }, + { url = "https://files.pythonhosted.org/packages/4d/51/835706a36cdc00e5b638fba9b22218b3d40d23a7677c923feca8a3f55b98/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1db4f27835a450c7e729bc9330c6e702113711cea1f873d646e3a31fe96a9732", size = 250521, upload-time = "2026-06-27T08:15:56.853Z" }, + { url = "https://files.pythonhosted.org/packages/c1/47/b0b62caa3caee58ab9de8969f66aef1c3729886f3ff60e173fda3f2762be/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4788a470f946df34383abc6cd345088c13f897a5ee580c4cdd12b1d32ad218ef", size = 309926, upload-time = "2026-06-27T08:15:58.704Z" }, + { url = "https://files.pythonhosted.org/packages/69/c4/60e6d18a0e131c7af622374af9deede15d3c47d8e5e7221933481b57b319/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3b6dfa83096cb1e54d082acebaf67f0c42667c56dc48ba536a76cac08d46391e", size = 448812, upload-time = "2026-06-27T08:16:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/12/9f/c9627daa052be39a932d0e17c6bf6a9041d2cde3afacbded9196acf70261/xxhash-3.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:57ec0ba5299a9a7df376063c139f5826ff0c89b438703939af3d252c31ca96a4", size = 223639, upload-time = "2026-06-27T08:16:02.784Z" }, + { url = "https://files.pythonhosted.org/packages/a9/38/92916e008a84c1f1a9aef82e4363cdc478a722ff69e59c6afbf93d3d1fda/xxhash-3.8.0-cp314-cp314t-win32.whl", hash = "sha256:d9a61f23b999baeb84102aba767b1b3e94958eab94e6c11b08927e7dc4200795", size = 33078, upload-time = "2026-06-27T08:16:04.639Z" }, + { url = "https://files.pythonhosted.org/packages/31/7c/e413bc75121d9628bf023b2ed251411ca3a447cf00cd9aa3438ab17f6c67/xxhash-3.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:61069b260fff84116235bb93845f319284dc6b42527c215af59264f4c2ee3468", size = 33953, upload-time = "2026-06-27T08:16:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/f6/eb/21a96e218375bd8b6ecd6d07cf60c8ff1a046e93cdedc3cf7bc3309edf7b/xxhash-3.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:73cecd431b4f572d38fcf1a7fe85b30eb987778ef9e7a70bc9ffcf2d64810e6f", size = 30164, upload-time = "2026-06-27T08:16:08.009Z" }, + { url = "https://files.pythonhosted.org/packages/96/84/9bb3cc67475ac7678476b30eed2f1140431f06386d637534194037c0624f/xxhash-3.8.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ba14843f20df2dce6ff6684411a56ae53da44336546c55f8947e70aebb8cdd21", size = 32604, upload-time = "2026-06-27T08:17:19.291Z" }, + { url = "https://files.pythonhosted.org/packages/42/6d/e98f9dd62c89e8895e4f3b525b6dbc3efcf27e2b99800e51388c59eb96dd/xxhash-3.8.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ec6666a5311beae3f6cb5f2fd28c2b77e2df32702c8206f45c786a6ef81b3751", size = 29787, upload-time = "2026-06-27T08:17:21.001Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/e7844a65c62d6d78747e4d149508d65a3df6fb65d72322c2526789e9f600/xxhash-3.8.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1ec9afdd53ac5f4fd1d8918807ba6c35ba62269086af794884b9f168a73331ea", size = 43155, upload-time = "2026-06-27T08:17:22.721Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5d/652c47481053fabc33ea229540bd330a45f68d7a5277f45e6cf879c29965/xxhash-3.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68594a54be2eb5992d9b0d0a0ec7c32a7a8e930f06d6cb951d69708055680994", size = 38137, upload-time = "2026-06-27T08:17:24.295Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/7b6e961a03ee713cbdbaa3d2cf3ddd33453a4d4112bbde58f2f607ab64d2/xxhash-3.8.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:591d5eb256abf59438800ace2730ac33f77bc6ab8c3623fab1ea24d9d8b28f3a", size = 34376, upload-time = "2026-06-27T08:17:25.688Z" }, + { url = "https://files.pythonhosted.org/packages/da/aa/95d36393bf732df516a2dcf4fd7e9e851bc033a5970e30774b972137f4da/xxhash-3.8.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7f4eecf800275e62b6bcb41e65f361f2277cc886c2bff4e299959d701e5fcf93", size = 32798, upload-time = "2026-06-27T08:17:27.188Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +]