diff --git a/.github/actions/changes/action.yaml b/.github/actions/changes/action.yaml index 80893adba3..1c3d16e92b 100644 --- a/.github/actions/changes/action.yaml +++ b/.github/actions/changes/action.yaml @@ -43,6 +43,9 @@ outputs: guardrails-benchmark: description: "'true' if the nemo-guardrails plugin or guardrails service changed" value: ${{ steps.filter.outputs.guardrails-benchmark }} + deployments-openshell: + description: "'true' if the nemo-deployments plugin changed (OpenShell backend coverage)" + value: ${{ steps.filter.outputs.deployments-openshell }} ngc-metadata: description: "'true' if NGC metadata scripts, tests, or assets changed" value: ${{ steps.filter.outputs.ngc-metadata }} @@ -109,6 +112,8 @@ runs: guardrails-benchmark: - 'plugins/nemo-guardrails/**' - 'services/guardrails/**' + deployments-openshell: + - 'plugins/nemo-deployments/**' ngc-metadata: - '.github/scripts/ngc_metadata.py' - '.github/scripts/tests/test_ngc_metadata.py' diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a2cee01fb4..79e8a6bf35 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -45,6 +45,7 @@ jobs: helm: ${{ steps.changes.outputs.helm }} cpu-smoke: ${{ steps.changes.outputs.cpu-smoke }} guardrails-benchmark: ${{ steps.changes.outputs.guardrails-benchmark }} + deployments-openshell: ${{ steps.changes.outputs.deployments-openshell }} ngc-metadata: ${{ steps.changes.outputs.ngc-metadata }} auth-idp: ${{ steps.changes.outputs.auth-idp }} steps: @@ -1560,6 +1561,41 @@ jobs: retention-days: 7 path: web/packages/studio/playwright-report/ + deployments-openshell-tests: + # The openshell extra is a platform-restricted manylinux_2_39 wheel that the + # shared unit-test job does not install, so the OpenShell backend tests skip + # there. Install the extra in this isolated, path-filtered job and exercise the + # backend for real. Kept separate so a wheel-install failure fails only this + # small job, not the whole unit suite (mirrors the nemo-guardrails extra job). + name: Deployments OpenShell backend tests + needs: [changes] + if: > + !cancelled() && ( + github.event_name == 'workflow_dispatch' || + needs.changes.outputs.deployments-openshell == 'true' + ) + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - name: Install uv + # No python-version pin: uv resolves the interpreter from the workspace + # requires-python (>=3.12), so this job can never fall back to an + # unsupported version. + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + with: + enable-cache: true + cache-dependency-glob: uv.lock + - name: Run OpenShell backend unit tests + run: make test-deployments-openshell + env: + _TYPER_FORCE_DISABLE_TERMINAL: "1" + guardrails-benchmark: # Parallel matrix jobs (one NMP per variant) so the two sweeps don't # share mocks or contend on :8080. `guardrails-benchmark-analyze` merges diff --git a/Makefile b/Makefile index 6ad81cc916..d512d1a5bb 100644 --- a/Makefile +++ b/Makefile @@ -408,6 +408,14 @@ endif @echo "Running tests for package: $(PACKAGE)..." uv run --frozen pytest -v packages/$(PACKAGE)/tests/ +.PHONY: test-deployments-openshell +test-deployments-openshell: ## Run OpenShell deployment backend unit tests with the platform-restricted [openshell] extra installed + # The openshell extra is not part of the default sync (platform-restricted + # wheel), so the shared unit-test job skips these. Install it just here and + # run the backend's tests for real (mirrors the nemo-guardrails --extra bench job). + uv run --frozen --package nemo-deployments-plugin --extra openshell \ + pytest -v plugins/nemo-deployments/tests/unit/backends/openshell/ + .PHONY: test-service test-service: ## Run tests for a specific service (usage: make test-service SERVICE=evaluator) ifndef SERVICE diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/SKILL.md b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/SKILL.md index 3448ea0b30..d00e256143 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/SKILL.md +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/SKILL.md @@ -15,6 +15,7 @@ not-for: - nemo-spec (use to write the spec file before building) - nemo-try-agent (use to query a deployed agent) - nemo-setup (use to install the platform first) + - deploy-sandbox (use to deploy the built agent as a governed OpenShell sandbox) - superpowers:brainstorming (use for unrelated design work) compatibility: nemo-platform >= 0.1.0; running platform (run nemo-setup first — uses `nemo services run`, no Docker); requires agents plugin installed; writes files to agents/; runs nemo CLI commands; defers platform-health probing to `nemo-status`; LangGraph + NAT under the hood; macOS or Linux; safe under sandbox. maturity: active @@ -70,6 +71,8 @@ Verification: confirm the deployment reached ready state. If `DEPLOY_NOT_READY`: jump to the recovery table at the bottom. +> **Governed sandbox deployment.** To deploy this agent as a policy-governed OpenShell sandbox instead of the default executor (Landlock filesystem isolation plus default-deny network egress, so its model traffic can only reach the platform), use the `deploy-sandbox` skill once the image is built. It swaps this step's deploy path for the `openshell-local` executor and an auto-generated SandboxPolicy. + ## Step 2: Try the agent Invoke with one question from each category in the spec. @@ -207,7 +210,7 @@ Run the success-criteria question from the spec through `nemo agents invoke` onc ## If verification fails | Symptom | Cause | Recovery | -|---|---|---| +| --- | --- | --- | | `agents plugin unavailable` | `plugins/nemo-agents` not installed | Re-run the install loop from `nemo-setup` Step 3 for that package only | | `DEPLOY_NOT_READY` after wait | Container startup error or YAML rejected | Run `.venv/bin/nemo agents deployments get $AGENT_NAME`; check status detail and logs | | YAML rejected with `extra fields` | Top-level keys beyond `functions`, `llms`, `workflow`, `intercepts`, `middleware` | Strip extras from the YAML; only those five top-level keys are valid | diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py index 9d3c8c8727..b6c49bdf00 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py @@ -21,6 +21,7 @@ ``nemo.skills`` → :func:`discover_skills` — ``() -> Path`` callable ``nemo.docs`` → :func:`discover_docs` — ``() -> Path | dict`` callable ``nemo.executors`` → :func:`discover_executors` — ``Executor`` class +``nemo.sandbox_profiles`` → :func:`discover_sandbox_profiles` — :class:`~nemo_platform_plugin.sandbox.SandboxImageProfile` instance ``nemo.inference_middleware`` → :func:`discover_inference_middleware` — :class:`~nemo_platform_plugin.inference_middleware.NemoInferenceMiddleware` subclass (typed, IGW instantiates) ``nemo.customization.contributors`` → :func:`discover_customization_contributors` — :class:`~nemo_platform_plugin.customization_contributor.CustomizationContributor` instance (typed, customization router instantiates) ``nemo.seed`` → :func:`discover_seed_jobs` — :class:`~nemo_platform_plugin.seed.NemoSeedJob` subclass (typed, platform instantiates) @@ -61,6 +62,7 @@ from nemo_platform_plugin.inference_middleware import NemoInferenceMiddleware from nemo_platform_plugin.interface import PluginManifest from nemo_platform_plugin.job import NemoJob +from nemo_platform_plugin.sandbox import SandboxImageProfile from nemo_platform_plugin.seed import NemoSeedJob from nemo_platform_plugin.service import NemoService @@ -480,6 +482,18 @@ def discover_executors() -> dict[str, Any]: return discover("nemo.executors") +def discover_sandbox_profiles() -> dict[str, SandboxImageProfile]: + """Wrapper: discover ``nemo.sandbox_profiles`` → :class:`SandboxImageProfile`. + + Each entry-point value is a + :class:`~nemo_platform_plugin.sandbox.SandboxImageProfile` describing what an + image needs to run under a sandbox runtime. The entry-point name is the + runtime name (e.g. ``"openshell"``), used by ``nemo agents package + --sandbox-runtime `` to resolve the right profile. + """ + return cast(dict[str, SandboxImageProfile], discover("nemo.sandbox_profiles")) + + def _instantiate_customization_contributor(loaded: object) -> CustomizationContributor: from nemo_platform_plugin.customization_contributor import CustomizationContributor diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/sandbox.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/sandbox.py new file mode 100644 index 0000000000..fa39828b01 --- /dev/null +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/sandbox.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Cross-plugin contract for sandbox-runtime image requirements. + +A sandbox runtime (e.g. OpenShell) runs an agent image under a supervisor that +imposes requirements on the image: a dedicated non-root user resolved by name, +extra OS packages for its network/policy setup, and so on. Those requirements +are *provider* knowledge, but the *packager* (``nemo agents package``) is what +physically bakes them into the image. + +To keep the packager provider-agnostic, a provider plugin registers one +:class:`SandboxImageProfile` under the ``nemo.sandbox_profiles`` entry-point +group; the packager discovers it by name (via +:func:`nemo_platform_plugin.discovery.discover_sandbox_profiles`) and renders +whatever it is handed. The packager never imports the provider. + +These are plain, dependency-light dataclasses so a provider can describe its +profile without importing the runtime's heavy client libraries. +""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class SandboxUser: + """A user (and its primary group) a sandbox runtime requires in the image. + + A runtime typically resolves the user by name, so the uid is not load-bearing; + ``system=True`` keeps it out of the uid range a packager may reclaim for its own + default runtime user. + """ + + name: str + group: str | None = None + """Primary group name. Defaults to :attr:`name` when ``None``.""" + system: bool = True + create_home: bool = True + home: str | None = None + """Home directory. Defaults to ``/home/`` when ``None``.""" + shell: str = "/bin/bash" + + def resolved_group(self) -> str: + return self.group or self.name + + def resolved_home(self) -> str: + return self.home or f"/home/{self.name}" + + +@dataclass(frozen=True) +class SandboxImageProfile: + """Declarative image requirements for running under a sandbox runtime. + + Registered by a provider plugin under the ``nemo.sandbox_profiles`` + entry-point group and consumed by ``nemo agents package + --sandbox-runtime ``. + """ + + name: str + description: str = "" + apt_packages: tuple[str, ...] = () + users: tuple[SandboxUser, ...] = () diff --git a/packages/nmp_platform/config/local.yaml b/packages/nmp_platform/config/local.yaml index cd723a93d3..d2d1164c88 100644 --- a/packages/nmp_platform/config/local.yaml +++ b/packages/nmp_platform/config/local.yaml @@ -21,12 +21,12 @@ service: {} auth: enabled: false - allow_unsigned_jwt: true # local CLI: nemo auth login --unsigned-token + allow_unsigned_jwt: true # local CLI: nemo auth login --unsigned-token policy_decision_point_provider: embedded policy_decision_point_base_url: "http://localhost:8080" # Low timeouts for fast test feedback (same as integration tests) policy_data_refresh_interval: 2 - bundle_cache_seconds: 15 # 0 = refresh on every authz call for instant permission changes + bundle_cache_seconds: 15 # 0 = refresh on every authz call for instant permission changes admin_email: "admin@example.com" # --- Azure AD OIDC (NeMo Platform dev deployment) --- @@ -47,7 +47,6 @@ entities: {} # Jobs service configuration jobs: - # Explicitly register the subprocess executor at profile "default". This opts # the documented `cpu/default` plugin steps (Data Designer create, Evaluator # metrics, Anonymizer, hello-world, etc.) into the cpu→subprocess translation @@ -107,9 +106,9 @@ jobs: storage: # Additional volume for E2E tests additional_volume_mounts: - - volume_name: nmp-additional-volume - mount_path: /mnt/additional_storage - allow_create_volume: true + - volume_name: nmp-additional-volume + mount_path: /mnt/additional_storage + allow_create_volume: true subprocess: working_directory: /tmp/nmp-subprocess-jobs cleanup_completed_jobs_immediately: false @@ -130,6 +129,25 @@ deployments: pull_images: false port_range_start: 49152 port_range_end: 49251 + # OpenShell-backed sandboxed agent deployments, opt-in per deployment. Each + # sandbox gets a generated default-deny SandboxPolicy. + - name: openshell-local + backend: openshell + config: + # :17670 is OpenShell's documented docker-driver default. NOT :8080, which the + # platform owns. + gateway_endpoint: http://127.0.0.1:17670 + # A packaged agent image chowns /workspace to its own 'agent' user, which the + # sandbox user cannot write, so `nat serve` runs from a sandbox-writable dir. + serve_workdir: /home/sandbox + # null grants the sandbox NO direct egress: the agent reaches models through + # OpenShell's gateway-managed inference.local route and the gateway makes the + # upstream call. Set a platform_egress block for direct egress instead, or + # default_policy_path to pin a hand-written policy. Route wiring: + # plugins/nemo-deployments/examples/openshell/DEMO.ipynb. + platform_egress: null + # Docker stays the default so ordinary deployments and the e2e harness keep their + # docker/k8s path. default_executor: local-docker models: diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/cli.py b/plugins/nemo-agents/src/nemo_agents_plugin/cli.py index 8f5ffcc6c5..c8f76b2158 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/cli.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/cli.py @@ -373,6 +373,15 @@ def package( allow_root: bool = typer.Option( False, "--allow-root", help="Disable non-root USER hardening in the rendered Dockerfile." ), + sandbox_runtime: Optional[str] = typer.Option( + None, + "--sandbox-runtime", + help=( + "Render an image compatible with a sandbox runtime (e.g. 'openshell'). " + "Discovers the runtime's image profile and bakes in its required apt " + "packages + users so the image can run inside that sandbox supervisor." + ), + ), generate_ignore: bool = typer.Option( True, "--ignore/--no-ignore", help="Generate a .dockerignore file alongside the Dockerfile." ), @@ -419,6 +428,7 @@ def package( format=format, template=template, allow_root=allow_root, + sandbox_runtime=sandbox_runtime, agent_version=agent_version, agent_author=agent_author, generate_ignore=generate_ignore, @@ -444,6 +454,7 @@ def package( python_version=python_version, uv_version=uv_version, allow_root=allow_root, + sandbox_runtime=sandbox_runtime, agent_version=agent_version, agent_author=agent_author, template_path=template, @@ -556,6 +567,7 @@ def _package_render_only( format: str, template: Optional[str], allow_root: bool, + sandbox_runtime: Optional[str], agent_version: Optional[str], agent_author: Optional[str], generate_ignore: bool, @@ -582,6 +594,7 @@ def _package_render_only( nat_version=nat_version, uv_version=uv_version, allow_root=allow_root, + sandbox_runtime=sandbox_runtime, agent_version=agent_version, agent_author=agent_author, template_path=template, diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/container/builder.py b/plugins/nemo-agents/src/nemo_agents_plugin/container/builder.py index 484718567e..a31c41ab90 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/container/builder.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/container/builder.py @@ -99,6 +99,7 @@ def build_agent_image( python_version: str | None = None, uv_version: str | None = None, allow_root: bool = False, + sandbox_runtime: str | None = None, agent_version: str | None = None, agent_author: str | None = None, template_path: str | None = None, @@ -192,6 +193,7 @@ def build_agent_image( nat_version=nat_version, uv_version=uv_version, allow_root=allow_root, + sandbox_runtime=sandbox_runtime, agent_version=agent_version, agent_author=agent_author, template_path=template_path, diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/container/sandbox.py b/plugins/nemo-agents/src/nemo_agents_plugin/container/sandbox.py new file mode 100644 index 0000000000..0481d93986 --- /dev/null +++ b/plugins/nemo-agents/src/nemo_agents_plugin/container/sandbox.py @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Render sandbox-runtime image profiles into Dockerfile fragments. + +This is the packager's *mechanism*: it discovers a provider-supplied +:class:`~nemo_platform_plugin.sandbox.SandboxImageProfile` by name and turns its +declarative fields (apt packages, users) into shell fragments the Dockerfile +template interpolates. The provider (e.g. ``nemo-deployments[openshell]``) owns +*what* an image needs; this module owns *how* it is baked in, so +``nemo agents package`` never imports anything runtime-specific. +""" + +from __future__ import annotations + +import re + +from nemo_platform_plugin.discovery import discover_sandbox_profiles +from nemo_platform_plugin.sandbox import SandboxImageProfile + +# Profile values originate from trusted provider code, not end users, but these +# fragments are interpolated into a Dockerfile so validate against tight +# charsets as defense-in-depth against shell/Dockerfile injection. +_APT_RE = re.compile(r"^[a-z0-9][a-z0-9._+-]*$") +_NAME_RE = re.compile(r"^[a-z_][a-z0-9_-]*$") +_PATH_RE = re.compile(r"^/[A-Za-z0-9_./-]+$") + + +def resolve_sandbox_profile(name: str) -> SandboxImageProfile: + """Resolve a registered sandbox runtime by name, or raise ``ValueError``. + + The error lists the runtimes that are actually installed so the operator + knows whether they are missing the provider package (e.g. the + ``nemo-deployments[openshell]`` extra). + """ + profiles = discover_sandbox_profiles() + profile = profiles.get(name) + if profile is None: + available = ", ".join(sorted(profiles)) or "(none installed)" + raise ValueError( + f"unknown sandbox runtime {name!r}. Installed runtimes: {available}. " + "Install the provider that registers it (e.g. 'nemo-deployments[openshell]')." + ) + return profile + + +def _validate(token: str, pattern: re.Pattern[str], kind: str) -> str: + if not pattern.match(token): + raise ValueError(f"invalid {kind} {token!r} in sandbox profile {pattern.pattern!r}") + return token + + +def render_apt_packages(profile: SandboxImageProfile) -> str: + """Return the profile's apt packages as a space-joined, validated string.""" + return " ".join(_validate(pkg, _APT_RE, "apt package") for pkg in profile.apt_packages) + + +def render_user_setup(profile: SandboxImageProfile) -> str: + """Return a single shell command creating the profile's users, or ``""``. + + Each user becomes ``groupadd ... && useradd ...``; multiple users are + chained with ``&&``. The result is designed to sit after ``RUN `` on one + logical Dockerfile line (with escaped continuations for readability). + """ + parts: list[str] = [] + for user in profile.users: + name = _validate(user.name, _NAME_RE, "user name") + group = _validate(user.resolved_group(), _NAME_RE, "group name") + home = _validate(user.resolved_home(), _PATH_RE, "home dir") + shell = _validate(user.shell, _PATH_RE, "shell") + sysflag = "--system " if user.system else "" + groupadd = f"groupadd {sysflag}{group}" + useradd = f"useradd {sysflag}--gid {group}" + if user.create_home: + useradd += f" --create-home --home-dir {home}" + useradd += f" --shell {shell} {name}" + parts.append(f"{groupadd} && \\\n {useradd}") + return " && \\\n ".join(parts) diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/container/template.py b/plugins/nemo-agents/src/nemo_agents_plugin/container/template.py index 61e4d01f42..28ac12fbbd 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/container/template.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/container/template.py @@ -113,7 +113,7 @@ def is_plugin_managed(path: Path) -> bool: UV_LINK_MODE=copy RUN apt-get update && \\ - apt-get install -y --no-install-recommends g++ gcc ca-certificates curl && \\ + apt-get install -y --no-install-recommends g++ gcc ca-certificates curl{% if sandbox_apt_packages %} {{ sandbox_apt_packages }}{% endif %} && \\ update-ca-certificates && \\ rm -rf /var/lib/apt/lists/* @@ -161,6 +161,14 @@ def is_plugin_managed(path: Path) -> bool: ENV NAT_CONFIG_FILE={{ config_file_path }} ENV PATH="/workspace/.venv/bin:$PATH" +{% if sandbox_user_setup %} +# Sandbox-runtime compatibility ({{ sandbox_runtime }}). The supervisor resolves +# this user by name, so the uid is not load-bearing; --system keeps it out of the +# 1000/1001 range the agent user reclaims below. /opt and /workspace/.venv are +# already world r-x (chmod a+rX above), so this user can read the venv and exec +# the interpreter; its home dir gives NAT a writable cache/config location. +RUN {{ sandbox_user_setup }} +{% endif %} {% if not allow_root %} # Some modern base images (notably Ubuntu 24.04 "noble" and the NVIDIA base # images derived from it) ship with a default unprivileged user at @@ -215,6 +223,9 @@ class RenderParams: has_pyproject: bool = False config_file_path: str = "/workspace/config.yaml" allow_root: bool = False + sandbox_runtime: str = "" + sandbox_apt_packages: str = "" + sandbox_user_setup: str = "" agent_id: str = "" agent_name: str = "" agent_version: str = "" @@ -313,6 +324,7 @@ def render_dockerfile( nat_version: str | None = None, uv_version: str | None = None, allow_root: bool = False, + sandbox_runtime: str | None = None, agent_version: str | None = None, agent_author: str | None = None, template_path: str | None = None, @@ -329,6 +341,10 @@ def render_dockerfile( nat_version: NAT version (required). uv_version: Override ``uv`` version. allow_root: When True, skip non-root USER creation. + sandbox_runtime: Name of a registered sandbox runtime (e.g. + ``"openshell"``). When set, the runtime's image profile is + discovered via the ``nemo.sandbox_profiles`` entry-point group and + its required apt packages + users are baked into the image. agent_version: Override agent version label. agent_author: Override agent author label. template_path: Path to an external Jinja2 template file. @@ -373,6 +389,21 @@ def render_dockerfile( resolved_nat = resolve_value("nat_version", nat_version) contract_version = _get_contract_version() + sandbox_runtime_name = "" + sandbox_apt_packages = "" + sandbox_user_setup = "" + if sandbox_runtime: + from nemo_agents_plugin.container.sandbox import ( + render_apt_packages, + render_user_setup, + resolve_sandbox_profile, + ) + + profile = resolve_sandbox_profile(sandbox_runtime) + sandbox_runtime_name = profile.name + sandbox_apt_packages = render_apt_packages(profile) + sandbox_user_setup = render_user_setup(profile) + if metadata is None: metadata = extract_agent_metadata( agent_config, @@ -390,6 +421,9 @@ def render_dockerfile( has_pyproject=has_pyproject, config_file_path=config_file_path, allow_root=allow_root, + sandbox_runtime=sandbox_runtime_name, + sandbox_apt_packages=sandbox_apt_packages, + sandbox_user_setup=sandbox_user_setup, contract_version=contract_version, agent_id=metadata["agent_id"], agent_name=metadata["agent_name"], diff --git a/plugins/nemo-agents/tests/unit/test_container.py b/plugins/nemo-agents/tests/unit/test_container.py index df056ad6e6..ae091495f0 100644 --- a/plugins/nemo-agents/tests/unit/test_container.py +++ b/plugins/nemo-agents/tests/unit/test_container.py @@ -186,6 +186,69 @@ def test_allow_root_skips_user(self, agent_config: Path) -> None: assert "USER agent" not in result assert "groupadd" not in result + @staticmethod + def _fake_profile(): + from nemo_platform_plugin.sandbox import SandboxImageProfile, SandboxUser + + return SandboxImageProfile( + name="openshell", + apt_packages=("iproute2", "nftables"), + users=( + SandboxUser(name="sandbox", system=True, create_home=True, home="/home/sandbox", shell="/bin/bash"), + ), + ) + + def test_sandbox_runtime_off_by_default(self, agent_config: Path) -> None: + from nemo_agents_plugin.container.template import render_dockerfile + + result = render_dockerfile(agent_config, None, nat_version="1.4.0") + + # Vanilla images must not carry any sandbox-runtime extras. + assert "iproute2" not in result + assert "nftables" not in result + assert "sandbox" not in result + + def test_sandbox_runtime_renders_discovered_profile(self, agent_config: Path) -> None: + from nemo_agents_plugin.container.template import render_dockerfile + + # The packager discovers profiles by name; it never imports a provider. + with patch( + "nemo_agents_plugin.container.sandbox.discover_sandbox_profiles", + return_value={"openshell": self._fake_profile()}, + ): + result = render_dockerfile(agent_config, None, nat_version="1.4.0", sandbox_runtime="openshell") + + assert "iproute2 nftables" in result + assert "groupadd --system sandbox" in result + assert ( + "useradd --system --gid sandbox --create-home --home-dir /home/sandbox --shell /bin/bash sandbox" in result + ) + # Normal-container hardening + entrypoint are untouched. + assert "USER agent" in result + assert "exec nat serve" in result + + def test_sandbox_runtime_unknown_raises(self, agent_config: Path) -> None: + from nemo_agents_plugin.container.template import render_dockerfile + + with patch("nemo_agents_plugin.container.sandbox.discover_sandbox_profiles", return_value={}): + with pytest.raises(ValueError, match="unknown sandbox runtime"): + render_dockerfile(agent_config, None, nat_version="1.4.0", sandbox_runtime="nope") + + def test_sandbox_runtime_independent_of_allow_root(self, agent_config: Path) -> None: + from nemo_agents_plugin.container.template import render_dockerfile + + with patch( + "nemo_agents_plugin.container.sandbox.discover_sandbox_profiles", + return_value={"openshell": self._fake_profile()}, + ): + result = render_dockerfile( + agent_config, None, nat_version="1.4.0", sandbox_runtime="openshell", allow_root=True + ) + + # The sandbox user is required whether or not the agent user is created. + assert "groupadd --system sandbox" in result + assert "USER agent" not in result + def test_oci_labels_present(self, agent_config: Path) -> None: from nemo_agents_plugin.container.template import render_dockerfile diff --git a/plugins/nemo-deployments/examples/openshell/DEMO.ipynb b/plugins/nemo-deployments/examples/openshell/DEMO.ipynb new file mode 100644 index 0000000000..b971b9555a --- /dev/null +++ b/plugins/nemo-deployments/examples/openshell/DEMO.ipynb @@ -0,0 +1,577 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1955f713", + "metadata": {}, + "source": [ + "# OpenShell sandboxed deployment demo\n", + "\n", + "Walks the create, package, deploy, and invoke flow for an agent deployed as a policy-governed OpenShell sandbox on a local NeMo Platform.\n", + "\n", + "How to run this:\n", + "\n", + "- Use a python3 kernel from the repo's workspace env, the one with the `nemo_platform` SDK installed (e.g. the repo `.venv`).\n", + "- The platform-API steps use the NeMo Python SDK (`NeMoPlatform`), the agent invoke uses `httpx`, and the egress check shells out to the `openshell` CLI.\n", + "- Two services must be running first, each in its own terminal (the OpenShell gateway and `nemo services run`). See \"Before you begin\". The notebook runs only the steps that terminate.\n", + "\n", + "## What it shows\n", + "\n", + "An ordinary NAT ReAct agent is deployed through the standard NeMo deployments API, but the executor is `openshell-local`, so the agent runs inside an OpenShell sandbox under a generated SandboxPolicy:\n", + "\n", + "- Landlock filesystem/process isolation. The agent runs as the unprivileged `sandbox` user with a default-deny filesystem allowlist.\n", + "- Zero-egress model access. The agent reaches models through OpenShell's gateway-managed `inference.local` route, so the sandbox is granted no direct network egress. The model call is brokered over the supervisor's gateway channel rather than through a policy egress rule; a call from the sandbox to anywhere else (e.g. `example.com`) is blocked at the boundary.\n", + "\n", + "The same agent image, deployed unchanged, gets its model access brokered by the gateway, and the sandbox itself can reach nothing on the network directly." + ] + }, + { + "cell_type": "markdown", + "id": "c88d1cfb", + "metadata": {}, + "source": [ + "## Before you begin\n", + "\n", + "Everything you need on this machine:\n", + "\n", + "1. Docker, since the OpenShell gateway and the sandboxes run as containers.\n", + "2. The `openshell` CLI on your PATH (from the OpenShell install).\n", + "3. The repo workspace env with the deployments backend's `openshell` extra. A bootstrapped workspace already has it, and the cell below installs it. Packaging (Step 1) also needs the agents `container` extra; if `nemo agents package` reports a missing `python-on-whales`, run `uv pip install -e 'plugins/nemo-agents[container]'`.\n", + "4. Two services running, each in its own terminal (both are long-running foreground processes): the OpenShell gateway on `:17670` (Prerequisite 1) and the NeMo platform with the `openshell-local` executor on `:8080` (Prerequisite 2).\n", + "5. A model your Inference Gateway serves. `nemo models list` shows the options; you pick one and wire `inference.local` before Step 1.\n", + "\n", + "The `openshell` SDK (`openshell>=0.0.83`) is a normal PyPI dependency of the plugin's `[openshell]` extra, and a workspace `uv sync` resolves it. Then run the cells top to bottom." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b83f0289", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "# install the deployments backend's openshell extra (a bootstrapped workspace already has it)\n", + "uv sync --package nemo-deployments-plugin --extra openshell" + ] + }, + { + "cell_type": "markdown", + "id": "6c521e68", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "The two services from **Before you begin**, in detail. See `SETUP.md` at the repo root for more on the platform; the gateway is fully set up below.\n", + "\n", + "### 1. OpenShell gateway on `:17670` (separate terminal)\n", + "\n", + "`:17670` is OpenShell's documented docker default. It must NOT be `:8080`: that belongs to the NeMo platform. Two things steer you onto `:8080` and cause the collision, so avoid both: OpenShell's gateway container image defaults its CMD to `--port 8080` (the `deploy/docker` compose file publishes `:8080` unchanged), and the CLI's throwaway `openshell gateway add http://127.0.0.1:8080` example.\n", + "\n", + "Use the compose file shipped in this directory (`docker-compose.yml` + `gateway.toml`), pinned to the docker driver, plaintext, on `:17670`. Run these **in their own terminal**:\n", + "\n", + "```bash\n", + "# one-time: generate the JWT signing keys the docker driver needs to mint\n", + "# sandbox tokens (writes /var/lib/openshell/tls/jwt/*). Re-run only if the\n", + "# /var/lib/openshell state dir is wiped.\n", + "docker run --rm --user 0 -v /var/lib/openshell:/var/lib/openshell \\\n", + " ghcr.io/nvidia/openshell/gateway:0.0.92 generate-certs \\\n", + " --output-dir /var/lib/openshell/tls \\\n", + " --server-san 127.0.0.1 --server-san localhost --server-san host.openshell.internal\n", + "\n", + "docker compose -f plugins/nemo-deployments/examples/openshell/docker-compose.yml up -d\n", + "openshell gateway add http://127.0.0.1:17670 --local --name docker-dev\n", + "```\n", + "\n", + "Tear the gateway down after the demo with `docker compose -f plugins/nemo-deployments/examples/openshell/docker-compose.yml down`.\n", + "\n", + "The cell below **verifies** the CLI reaches the gateway. The endpoint column must read `:17670`, not `:8080`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c9bf760f", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "openshell gateway list # * docker-dev http://127.0.0.1:17670 ... plaintext\n", + "openshell sandbox list # reaches the gateway (e.g. \"No sandboxes found.\")" + ] + }, + { + "cell_type": "markdown", + "id": "c261264c", + "metadata": {}, + "source": [ + "### 2. NeMo platform with the `openshell-local` executor (separate terminal)\n", + "\n", + "The executor lives in `packages/nmp_platform/config/local.yaml`, which is NOT the config `nemo services run` loads by default (that bundled default has no `deployments:` block at all). You MUST pass it with `--config`, or the executor never registers and every deploy below fails with an unknown-executor error. The registered block:\n", + "\n", + "```yaml\n", + "deployments:\n", + " executors:\n", + " - name: openshell-local\n", + " backend: openshell\n", + " config:\n", + " gateway_endpoint: http://127.0.0.1:17670\n", + " serve_workdir: /home/sandbox # sandbox-writable CWD for `nat serve`\n", + " platform_egress: null # agent uses inference.local (gateway-managed), so the\n", + " # sandbox needs NO direct egress -> pure default-deny\n", + " # No default_executor: sandbox is opt-in; the deploy in Step 3 names it.\n", + "```\n", + "\n", + "Start the platform **in its own terminal**, bound to `0.0.0.0`. That matters: the `inference.local` route is dialed from the sandbox, not from the gateway process. The docker driver writes a literal `172.18.0.1 host.openshell.internal` into each sandbox's `/etc/hosts` (the sandbox network's gateway address), so the platform has to be listening there; on `--host 127.0.0.1` every model call fails with a 503. It also puts an unauthenticated dev platform on every interface, LAN included, so do not do this on an untrusted network. This process runs in the foreground and does not return, which is why it does not live in a notebook cell:\n", + "\n", + "```bash\n", + "export NMP_BASE_URL=http://localhost:8080\n", + "nemo services run --host 0.0.0.0 --port 8080 \\\n", + " --config packages/nmp_platform/config/local.yaml\n", + "```\n", + "\n", + "### 3. A sandbox-compatible agent image\n", + "\n", + "Built in Step 1 below. Packaging needs the agents plugin `container` extra (`python-on-whales`). If `nemo agents package` fails with a missing `python-on-whales`, install the extra (this is the targeted fix; `uv sync --all-extras` also works):\n", + "\n", + "```bash\n", + "uv pip install -e 'plugins/nemo-agents[container]'\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "e307474d", + "metadata": {}, + "source": [ + "### Build the SDK client\n", + "\n", + "Set `NMP_BASE_URL` once (the `nemo` CLI cells below read it too), then build one `NeMoPlatform` client in **direct mode**: passing `base_url` explicitly skips config-file bootstrap and auth injection, which the local platform needs none of. Every platform-API cell below reuses this `client`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85ffbeff", + "metadata": {}, + "outputs": [], + "source": [ + "%env NMP_BASE_URL=http://localhost:8080" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb75d3dd", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import os\n", + "import subprocess\n", + "\n", + "import httpx\n", + "from nemo_platform import NeMoPlatform\n", + "\n", + "WORKSPACE = \"default\"\n", + "# Direct mode: an explicit base_url means no config bootstrap and no auth headers\n", + "# (fine for the local platform). Honors the NMP_BASE_URL set above.\n", + "client = NeMoPlatform(base_url=os.environ[\"NMP_BASE_URL\"], workspace=WORKSPACE)\n", + "\n", + "# The deployments API has no typed SDK resource yet, so we drive it through the\n", + "# client's untyped raw-request escape hatch against this base path.\n", + "DEPLOY = f\"/apis/deployments/v2/workspaces/{WORKSPACE}\"\n", + "print(\"platform:\", client.base_url)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d666f11d", + "metadata": {}, + "outputs": [], + "source": [ + "# readiness gate for the platform started in prerequisite 2\n", + "r = httpx.get(f\"{os.environ['NMP_BASE_URL']}/health/ready\", timeout=5)\n", + "r.raise_for_status()\n", + "print(r.json()) # {'status': 'ready'}" + ] + }, + { + "cell_type": "markdown", + "id": "5ce4cab0", + "metadata": {}, + "source": [ + "## Pick a model, then wire inference.local\n", + "\n", + "The agent reaches models through OpenShell's gateway-managed **`inference.local`** route: the gateway routes `https://inference.local/v1` to a registered provider (the platform Inference Gateway) and injects the real credential, so the sandbox needs **no direct egress**. First pick a model the Inference Gateway serves, then do the one-time operator wiring on the gateway." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d9e9f98c", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "# pick a model the Inference Gateway serves; you set it as MODEL below\n", + "nemo models list" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ca448af1", + "metadata": {}, + "outputs": [], + "source": [ + "MODEL = \"default/openai-openai-gpt-4o-mini\" # <- set to a model `nemo models list` shows above\n", + "os.environ[\"MODEL\"] = MODEL # so the operator-wiring cell below can read it\n", + "print(\"model:\", MODEL)" + ] + }, + { + "cell_type": "markdown", + "id": "746bd651", + "metadata": {}, + "source": [ + "**Operator setup (once per gateway).** Register the platform Inference Gateway as an `openai` provider and bind `inference.local` to it. `host.openshell.internal` is the gateway's alias for its own host (where the platform listens). The credential is a dummy, since the gateway injects the real one. `inference set` verifies the endpoint gateway-side." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae2ec743", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "openshell provider create --name nemo-igw --type openai \\\n", + " --credential OPENAI_API_KEY=empty \\\n", + " --config OPENAI_BASE_URL=http://host.openshell.internal:8080/apis/inference-gateway/v2/workspaces/default/openai/-/v1 \\\n", + " || echo \"(provider may already exist; use 'openshell provider update' to change its config)\"\n", + "openshell inference set --provider nemo-igw --model \"$MODEL\"\n", + "openshell inference get" + ] + }, + { + "cell_type": "markdown", + "id": "68b37c90", + "metadata": {}, + "source": [ + "## Step 1: package the agent as a sandbox image\n", + "\n", + "The agent config is a checked-in, greppable file, `agent/config.yaml`, next to this notebook. It is a normal NAT ReAct config, already sandbox-ready (LLM pointed at `https://inference.local/v1`, `nemo_files` telemetry tracer removed), with the model id left as a deploy-time param.\n", + "\n", + "`nemo agents package` uses that file's folder as the Docker build context. The `agent/` folder holds only `config.yaml`, so the context stays clean, and it bakes to `/workspace/config.yaml`, the path Step 2's serve command loads. The model is injected at deploy via `nat serve --override llms.llm.model_name `, and nothing is written to `/tmp`." + ] + }, + { + "cell_type": "markdown", + "id": "712997db", + "metadata": {}, + "source": [ + "Build the image with the OpenShell runtime profile (adds the `sandbox` user, etc.).\n", + "\n", + "The `--sandbox-runtime openshell` flag renders a provider-neutral sandbox profile into the image build. The config is baked to `/workspace/config.yaml`, which is exactly what Step 2's serve command loads." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5e7ed23d", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "nemo agents package \\\n", + " --agent agent/config.yaml \\\n", + " --nat-version 1.8.0 \\\n", + " --sandbox-runtime openshell \\\n", + " --tag nemo-agent-igw:test" + ] + }, + { + "cell_type": "markdown", + "id": "aa54f8b2", + "metadata": {}, + "source": [ + "## Step 2: create the DeploymentConfig\n", + "\n", + "Describe the container: the image, the `nat serve` command, and the port. The serve command is what the backend runs inside the sandbox. The model id is injected via `--override`, and the agent reaches the LLM through `inference.local`, so there is no proxy handling or egress plumbing here.\n", + "\n", + "`client.post(..., cast_to=object)` returns the parsed JSON as a dict. A non-2xx response (e.g. a duplicate name) raises `APIStatusError` whose message is the response body, so the error is loud, not swallowed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ba072e0", + "metadata": {}, + "outputs": [], + "source": [ + "cfg = client.post(\n", + " f\"{DEPLOY}/deployment-configs\",\n", + " cast_to=object, # untyped raw request -> parsed JSON dict\n", + " body={\n", + " \"name\": \"igw-agent-cfg\",\n", + " \"containers\": [\n", + " {\n", + " \"name\": \"agent\",\n", + " \"image\": \"nemo-agent-igw:test\",\n", + " \"command\": [\n", + " \"/workspace/.venv/bin/nat\",\n", + " \"serve\",\n", + " \"--config_file\",\n", + " \"/workspace/config.yaml\",\n", + " \"--override\",\n", + " \"llms.llm.model_name\",\n", + " MODEL, # deploy-time model id (see above)\n", + " \"--host\",\n", + " \"0.0.0.0\",\n", + " \"--port\",\n", + " \"9000\",\n", + " ],\n", + " \"ports\": [{\"containerPort\": 9000, \"name\": \"http\"}],\n", + " }\n", + " ],\n", + " },\n", + ")\n", + "print(json.dumps(cfg, indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "2a016303", + "metadata": {}, + "source": [ + "## Step 3: deploy (executor = openshell-local)\n", + "\n", + "Create the Deployment. Sandbox is opt-in, so you must name the executor. There is no default sandbox executor, which is deliberate: ordinary deployments and the e2e harness keep their normal docker/k8s path.\n", + "\n", + "Behind the scenes the `OpenShellDeploymentBackend`:\n", + "\n", + "1. Generates a SandboxPolicy: default-deny filesystem (`read_only: /opt` for the interpreter; `read_write: /home/sandbox, /tmp, /dev/shm` for the Dask runtime `nat serve` spins up), `run_as_user: sandbox`, and a single egress rule to `platform_egress` (the Inference Gateway). A mandatory `nemo_platform` egress rule is always injected, so a hand-written policy can never sever the sandbox's path back to the platform.\n", + "2. Creates the sandbox from the image and applies the policy.\n", + "3. Runs the serve command in the sandbox-writable `serve_workdir` (`/home/sandbox`). The image legitimately owns `/workspace` as its `agent` user, which the `sandbox` user cannot write.\n", + "4. Exposes the service through the gateway and reports the URL." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "901b0f6c", + "metadata": {}, + "outputs": [], + "source": [ + "dep = client.post(\n", + " f\"{DEPLOY}/deployments\",\n", + " cast_to=object,\n", + " body={\n", + " \"name\": \"igw-agent\",\n", + " \"deployment_config\": \"igw-agent-cfg\",\n", + " \"executor\": \"openshell-local\",\n", + " },\n", + ")\n", + "print(json.dumps(dep, indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "f139aa4e", + "metadata": {}, + "source": [ + "## Step 4: wait for READY and read the endpoint\n", + "\n", + "The reconciler drives the deployment from `PENDING` to `STARTING` to `READY` (typically well under a minute). Re-run the poll cell until it prints `READY`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf1bf2cf", + "metadata": {}, + "outputs": [], + "source": [ + "# poll status -- re-run until this prints READY\n", + "dep = client.get(f\"{DEPLOY}/deployments/igw-agent\", cast_to=object)\n", + "print(dep[\"status\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f2fee311", + "metadata": {}, + "outputs": [], + "source": [ + "# grab the exposed endpoint once READY; `url` persists to later cells\n", + "dep = client.get(f\"{DEPLOY}/deployments/igw-agent\", cast_to=object)\n", + "url = dep[\"endpoints\"][0][\"url\"]\n", + "print(url) # e.g. http://--.openshell.localhost:17670/" + ] + }, + { + "cell_type": "markdown", + "id": "32ef1e5e", + "metadata": {}, + "source": [ + "## Step 5: invoke the agent\n", + "\n", + "Ask a question that forces both a tool call and an LLM call. The agent's LLM call goes to `inference.local`, which the gateway routes to the platform Inference Gateway, so the sandbox makes no direct egress. This cell hits the agent's own NAT-served endpoint (via the openshell gateway), not the platform API, so it is a plain `httpx` POST rather than the SDK client.\n", + "\n", + "Other routes the served workflow exposes: `POST /v1/chat/completions`, `/chat`, `/v1/workflow`, and `/generate/stream`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3b9a7dd", + "metadata": {}, + "outputs": [], + "source": [ + "dep = client.get(f\"{DEPLOY}/deployments/igw-agent\", cast_to=object)\n", + "url = dep[\"endpoints\"][0][\"url\"]\n", + "r = httpx.post(\n", + " f\"{url}generate\",\n", + " json={\"input_message\": \"What is the current date and time? Use your current_datetime tool\"},\n", + " timeout=60,\n", + ")\n", + "r.raise_for_status()\n", + "print(r.json()) # {'value': 'The current date and time is 2026-... +0000.'}" + ] + }, + { + "cell_type": "markdown", + "id": "e651afa8", + "metadata": {}, + "source": [ + "### Debugging a 502 on invoke\n", + "\n", + "A 502 means the gateway reached the sandbox but the agent's `nat serve` (port 9000) isn't answering, because it crashed or hasn't come up. `READY` does not gate on serve health (the detached serve exec is unsupervised), so:\n", + "\n", + "1. Give it a moment and retry the invoke. `nat serve` plus its Dask runtime can take 30-60s after `READY`.\n", + "2. If it persists, read the serve log inside the sandbox. The backend launches `nat serve` with its output redirected to `/tmp/nemo-serve.log`, and the cell below cats it.\n", + "\n", + "The most common root cause is that the `MODEL` set in Step 1 isn't served on your Inference Gateway (`nemo models list`), so `nat serve` fails at startup. Next-most-common is that the config didn't bake to `/workspace/config.yaml`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3938326", + "metadata": {}, + "outputs": [], + "source": [ + "# read the agent's serve log from inside the sandbox to see why it 502s\n", + "dep = client.get(f\"{DEPLOY}/deployments/igw-agent\", cast_to=object)\n", + "sbx = dep[\"endpoints\"][0][\"url\"].split(\"://\", 1)[-1].split(\"--\", 1)[0]\n", + "res = subprocess.run(\n", + " [\"openshell\", \"sandbox\", \"exec\", \"--name\", sbx, \"--\", \"cat\", \"/tmp/nemo-serve.log\"],\n", + " capture_output=True,\n", + " text=True,\n", + " stdin=subprocess.DEVNULL,\n", + " timeout=30,\n", + ")\n", + "print(res.stdout or \"(serve log empty -- process may not have started yet)\")\n", + "print(res.stderr, end=\"\")" + ] + }, + { + "cell_type": "markdown", + "id": "cfb9e098", + "metadata": {}, + "source": [ + "### (Optional) prove the egress policy\n", + "\n", + "Show that the sandbox can reach nothing on the network directly. The `example.com` call is blocked by the policy default-deny. Step 5's LLM call still worked, because it went through the gateway-managed `inference.local` route rather than sandbox egress. A nonzero exit from the sandboxed `curl` is the PASS." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "347e0b1d", + "metadata": {}, + "outputs": [], + "source": [ + "# sandbox name is nmp-; derive it from the endpoint URL (or read `openshell sandbox list`)\n", + "dep = client.get(f\"{DEPLOY}/deployments/igw-agent\", cast_to=object)\n", + "sbx = dep[\"endpoints\"][0][\"url\"].split(\"://\", 1)[-1].split(\"--\", 1)[0]\n", + "print(\"sandbox:\", sbx)\n", + "\n", + "# A NONZERO exit from the inner curl is the PASS: example.com must be blocked by the\n", + "# default-deny egress policy. The inner `curl -m 5` self-aborts, and the outer timeout\n", + "# is a backstop so a wedged `exec` can never hang the notebook.\n", + "try:\n", + " res = subprocess.run(\n", + " [\"openshell\", \"sandbox\", \"exec\", \"--name\", sbx, \"--\", \"curl\", \"-sS\", \"-m\", \"5\", \"https://example.com\"],\n", + " capture_output=True,\n", + " text=True,\n", + " stdin=subprocess.DEVNULL,\n", + " timeout=30,\n", + " )\n", + " print(\n", + " \"BLOCKED as expected (policy default-deny)\"\n", + " if res.returncode != 0\n", + " else f\"UNEXPECTED: egress to example.com succeeded\\n{res.stdout}\"\n", + " )\n", + "except subprocess.TimeoutExpired:\n", + " print(\"exec timed out (30s) -- inconclusive; check `openshell sandbox list`\")\n", + "# meanwhile Step 5's LLM call went via inference.local (gateway-routed; no sandbox egress)" + ] + }, + { + "cell_type": "markdown", + "id": "039ad20d", + "metadata": {}, + "source": [ + "## Step 6: clean up" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "739fc5be", + "metadata": {}, + "outputs": [], + "source": [ + "client.delete(f\"{DEPLOY}/deployments/igw-agent\", cast_to=httpx.Response) # 204 -> tears down the sandbox\n", + "client.delete(f\"{DEPLOY}/deployment-configs/igw-agent-cfg\", cast_to=httpx.Response)\n", + "print(\"deleted deployment + config\")" + ] + }, + { + "cell_type": "markdown", + "id": "c210719b", + "metadata": {}, + "source": [ + "## Known limitations\n", + "\n", + "- The NAT config is baked into the image; deploy-time config injection is a follow-up.\n", + "- `inference.local` requires the one-time operator wiring (provider + `inference set`) per gateway; the deployment backend does not auto-register it yet.\n", + "- The deployments API has no typed SDK resource yet, so the platform-API cells use the untyped `client.post(..., cast_to=object)`; when the typed resource lands these become `client.deployments.*`.\n", + "\n", + "## Don't forget the gateway\n", + "\n", + "After the demo, tear the gateway down in its terminal:\n", + "\n", + "```bash\n", + "docker compose -f plugins/nemo-deployments/examples/openshell/docker-compose.yml down\n", + "```" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/plugins/nemo-deployments/examples/openshell/agent/config.yaml b/plugins/nemo-deployments/examples/openshell/agent/config.yaml new file mode 100644 index 0000000000..099fe98800 --- /dev/null +++ b/plugins/nemo-deployments/examples/openshell/agent/config.yaml @@ -0,0 +1,44 @@ +# NAT ReAct agent for the OpenShell sandbox demo (see ../DEMO.ipynb). +# +# This file is the source of truth for the demo agent. `nemo agents package` +# uses this folder as the Docker build context (it holds only this file, so the +# context stays clean) and bakes it to /workspace/config.yaml, which the serve +# command loads. +# +# Deploy-time params are `${VAR}` refs. NAT expands them from env at config load +# (nat/utils/io/yaml_tools.py -> expandvars, surrounded_vars_only=True). An unset +# `${VAR}` expands to empty -> YAML null -> a loud validation error, which is the +# desired fail-fast. The demo notebook injects the model via +# `nat serve --override llms.llm.model_name ` (robust; no env dependency), +# and you can alternatively set NEMO_DEFAULT_MODEL on the container env. +# +# NOTE: no `general.telemetry` block. The stock react-agent example enables the +# `nemo_files` tracing plugin, which is NOT installed in the packaged sandbox +# image, so `nat serve` would reject the config. + +functions: + wiki: + _type: wiki_search + clock: + _type: current_datetime + +llms: + llm: + _type: openai + # OpenShell gateway-managed inference: the gateway routes https://inference.local to a + # registered provider (the platform Inference Gateway) and injects the real credential. + # The sandbox needs no direct egress and no proxy handling. Operator wiring (once per gateway): + # openshell provider create --name nemo-igw --type openai --credential OPENAI_API_KEY=empty \ + # --config OPENAI_BASE_URL=http://host.openshell.internal:8080/apis/inference-gateway/v2/workspaces/default/openai/-/v1 + # openshell inference set --provider nemo-igw --model + base_url: https://inference.local/v1 + api_key: not-used # MUST be non-empty; the gateway injects the real credential + model_name: ${NEMO_DEFAULT_MODEL} # deploy-time model id; notebook overrides via `nat serve --override` + temperature: 0.0 + +workflow: + _type: react_agent + tool_names: [wiki, clock] + llm_name: llm + verbose: false + parse_agent_response_max_retries: 3 diff --git a/plugins/nemo-deployments/examples/openshell/docker-compose.yml b/plugins/nemo-deployments/examples/openshell/docker-compose.yml new file mode 100644 index 0000000000..23f232c7dd --- /dev/null +++ b/plugins/nemo-deployments/examples/openshell/docker-compose.yml @@ -0,0 +1,97 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# OpenShell gateway for the NeMo sandbox-deployment demo (Docker driver). +# +# Adapted from OpenShell's deploy/docker/docker-compose.yml, pinned to :17670 +# (OpenShell's documented docker default) instead of :8080, because the NeMo +# platform owns :8080. Three changes beyond the port were proven necessary for +# docker sandboxes to actually run (the stock OpenShell compose omits them): +# 1. the port is set via the `command:` override below, because compose's +# `command: []` does NOT clear the image's baked `--port 8080` CLI flag; +# 2. the port is published on all host interfaces (not 127.0.0.1-only) so +# sandbox containers can reach the gateway at the host bridge IP; +# 3. gateway.toml carries JWT signing keys (docker sandboxes need them) that +# must be generated once first (see below). +# +# Bring up: +# # one-time JWT key generation (writes /var/lib/openshell/tls/jwt/*): +# docker run --rm --user 0 -v /var/lib/openshell:/var/lib/openshell \ +# ghcr.io/nvidia/openshell/gateway:0.0.92 generate-certs \ +# --output-dir /var/lib/openshell/tls --server-san host.openshell.internal +# docker compose up -d # from this directory (examples/openshell/) +# openshell gateway add http://127.0.0.1:17670 --local --name docker-dev +# Tear down: docker compose down +# +# Sandbox containers are created and torn down by the gateway, not by this file. +# This is Docker-outside-of-Docker: the gateway uses the host Docker socket to +# create sibling sandbox containers on the host. + +services: + gateway: + # Pinned, and kept in step with the `openshell` client pin in the plugin's + # pyproject: the gateway enforces request limits the client does not, so :latest + # turns a gateway release into an unexplained deployment failure. + image: ghcr.io/nvidia/openshell/gateway:${IMAGE_TAG:-0.0.92} + restart: unless-stopped + + # The image ENTRYPOINT is the gateway binary and its CMD bakes + # `--bind-address 0.0.0.0 --port 8080` as explicit CLI flags, which beat + # gateway.toml (precedence: CLI flag > env > TOML). `command: []` does NOT + # clear it (Compose treats [] as "no override" and uses the image CMD), so + # the bind must be set here as a real command override to move off :8080. + # Everything else stays in gateway.toml. + command: ["--bind-address", "0.0.0.0", "--port", "17670"] + + # Local-dev: run as root so the gateway can write the extracted supervisor + # binary to /var/lib/openshell and access /var/run/docker.sock without a GID + # dance. Distroless images have no /etc/passwd, so use the numeric UID. + # Production should use a dedicated non-root UID with docker-group access. + user: "0" + + # Host aliases resolved INSIDE THE GATEWAY CONTAINER, e.g. when the gateway dials + # a provider URL written as host.openshell.internal (`openshell inference set` + # validates the platform endpoint that way). Docker Desktop (macOS/Windows) + # provides both automatically; Linux needs these explicit host-gateway aliases. + # Sandboxes do NOT inherit them: the docker driver writes its own + # host.openshell.internal / host.docker.internal entries into each sandbox, + # pointing at the sandbox network's gateway address. + extra_hosts: + - "host.docker.internal:host-gateway" + - "host.openshell.internal:host-gateway" + + ports: + # gRPC / control-plane API. Published on all host interfaces (NOT + # 127.0.0.1-only): sandbox containers reach the gateway via + # host.openshell.internal, which resolves to the host's docker bridge + # gateway IP, so the port must be reachable there, not just on loopback. + # Internal bind port and published port MUST match for that routing. + - "${OPENSHELL_PORT:-17670}:17670" + + volumes: + # Docker socket: lets the gateway create and manage sandbox containers. + - /var/run/docker.sock:/var/run/docker.sock + # Data dir bind-mounted at the SAME absolute path on host and container so + # the supervisor binary the gateway extracts is resolvable by the host + # Docker daemon when it creates sandbox containers. + - type: bind + source: /var/lib/openshell + target: /var/lib/openshell + bind: + create_host_path: true + # Gateway + driver settings. Compose resolves ./ relative to THIS file's + # directory (examples/openshell/), not the caller's CWD. + - type: bind + source: ./gateway.toml + target: /etc/openshell/gateway.toml + read_only: true + + environment: + OPENSHELL_GATEWAY_CONFIG: /etc/openshell/gateway.toml + # DB URL is env-only (blocked in the TOML file). Plain SQLite path, no + # credentials, safe to inline. Swap for a real DSN via secret if needed. + OPENSHELL_DB_URL: "sqlite:/var/lib/openshell/gateway.db?mode=rwc" + # OS-level path vars (not part of the gateway config schema) so the + # extracted supervisor lands in the bind-mounted, same-path data dir. + XDG_DATA_HOME: /var/lib/openshell + HOME: /var/lib/openshell diff --git a/plugins/nemo-deployments/examples/openshell/gateway.toml b/plugins/nemo-deployments/examples/openshell/gateway.toml new file mode 100644 index 0000000000..7d371470fe --- /dev/null +++ b/plugins/nemo-deployments/examples/openshell/gateway.toml @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# OpenShell gateway config for the NeMo sandbox-deployment demo (Docker driver). +# +# Adapted from OpenShell's deploy/docker/gateway.toml, with one deliberate +# change: the gateway is pinned to :17670 (OpenShell's documented docker +# default) instead of :8080, because the NeMo platform owns :8080 and running +# the gateway there collides with it. Mounted read-only into the gateway +# container by docker-compose.yml and loaded via OPENSHELL_GATEWAY_CONFIG. + +[openshell] +version = 1 + +[openshell.gateway] +# NOTE: the main bind is set by docker-compose.yml's `command:` override, not +# here. The image bakes `--port 8080` as a CLI flag that beats this TOML +# (CLI > env > TOML), and `command: []` does not clear it, so set it there. +# health_bind_address has no baked flag, so it is honored from this file. +health_bind_address = "127.0.0.1:17671" +log_level = "info" +compute_drivers = ["docker"] +disable_tls = true + +[openshell.gateway.auth] +# Local-dev escape hatch: accept CLI/API calls without mTLS or OIDC. Sandbox +# supervisors still authenticate with the gateway-minted JWTs below. Never set +# this true on a shared gateway. +allow_unauthenticated_users = true + +[openshell.gateway.gateway_jwt] +# Docker sandboxes REQUIRE the gateway to mint sandbox JWTs, so these signing +# keys must exist. Generate them once before `docker compose up` (see the +# docker-compose.yml header / DEMO.ipynb prerequisites for the generate-certs +# command that writes them under /var/lib/openshell/tls/jwt). +signing_key_path = "/var/lib/openshell/tls/jwt/signing.pem" +public_key_path = "/var/lib/openshell/tls/jwt/public.pem" +kid_path = "/var/lib/openshell/tls/jwt/kid" +gateway_id = "docker-dev" +ttl_secs = 3600 + +[openshell.drivers.docker] +# Default image for `openshell sandbox create` without --from. +default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +# Supervisor image the openshell-sandbox binary is extracted from on first +# start. Cached to XDG_DATA_HOME and reused on restart (no manual extract). +supervisor_image = "ghcr.io/nvidia/openshell/supervisor:latest" +image_pull_policy = "IfNotPresent" +sandbox_namespace = "openshell" +# Sandbox callback address. The Docker driver keeps only the scheme and swaps +# in host.openshell.internal + the gateway bind port, so the gateway must be +# published on 17670 on the host (see docker-compose.yml). +grpc_endpoint = "http://host.openshell.internal:17670" diff --git a/plugins/nemo-deployments/examples/openshell/local-sandbox-policy.yaml b/plugins/nemo-deployments/examples/openshell/local-sandbox-policy.yaml new file mode 100644 index 0000000000..5fe0cdf2a7 --- /dev/null +++ b/plugins/nemo-deployments/examples/openshell/local-sandbox-policy.yaml @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Local-dev SandboxPolicy for OpenShell-backed NAT agent deployments. +# +# The backend GENERATES an equivalent default-deny policy from the executor's +# `platform_egress` config + the sandbox filesystem defaults, so this +# file is not required. It stays as (a) a readable reference for what the generator +# emits, and (b) an OVERRIDE example: point the `openshell` executor's +# `default_policy_path` at it in packages/nmp_platform/config/local.yaml to pin a +# hand-written policy instead. The mandatory platform egress rule is injected either +# way, so a policy here can never sever the sandbox's path back to the platform. +# +# What each block is for (all verified against a live gateway): +# filesystem_policy.read_only /opt holds the uv-managed interpreter the venv +# nat launcher execs; /usr,/bin,/lib,/etc are the base. +# filesystem_policy.read_write /home/sandbox is the serve workdir (NAT writes +# /.tmp); /dev/shm is needed by nat[most]'s +# Dask cluster (POSIX semaphores). +# process.run_as_* the workload runs as the image's 'sandbox' user. +# network_policies.igw egress is restricted to the NeMo Inference +# Gateway ONLY (host.docker.internal:8080 on the +# docker driver); everything else is blocked. The +# connecting binary is the venv/uv python, so it +# must be listed here (not curl). + +version: 1 + +filesystem_policy: + include_workdir: true + read_only: [/usr, /bin, /lib, /etc, /opt, /proc, /dev/urandom, /var/log] + read_write: [/workspace, /home/sandbox, /sandbox, /tmp, /dev/null, /dev/shm] + +landlock: + compatibility: best_effort + +process: + run_as_user: sandbox + run_as_group: sandbox + +network_policies: + igw: + name: nemo-inference-gateway + endpoints: + - { host: host.docker.internal, port: 8080, protocol: rest, enforcement: enforce, access: full } + binaries: + - { path: /usr/bin/curl } + - { path: /opt/uv/python/cpython-3.13.7-linux-aarch64-gnu/bin/python3.13 } + - { path: /workspace/.venv/bin/python3.13 } diff --git a/plugins/nemo-deployments/pyproject.toml b/plugins/nemo-deployments/pyproject.toml index ab3acfab1c..49d20abcfb 100644 --- a/plugins/nemo-deployments/pyproject.toml +++ b/plugins/nemo-deployments/pyproject.toml @@ -9,16 +9,24 @@ dependencies = [ "nemo-platform-sdk", "nemo-platform-plugin", "pydantic>=2.10.6", + "pyyaml>=6.0", ] version = "0.0.0" [project.optional-dependencies] docker = ["docker>=7.0"] k8s = ["kubernetes>=30.1.0"] +openshell = ["grpcio>=1.78.0", "openshell>=0.0.92", "protobuf>=6.31.1"] [project.entry-points."nemo.services"] deployments = "nemo_deployments_plugin.service:DeploymentsService" +[project.entry-points."nemo.sandbox_profiles"] +openshell = "nemo_deployments_plugin.backends.openshell.sandbox_profile:PROFILE" + +[project.entry-points."nemo.skills"] +deployments = "nemo_deployments_plugin.skills:skills_dir" + [project.entry-points."nemo.controllers"] deployments = "nemo_deployments_plugin.controller:DeploymentsController" @@ -36,7 +44,20 @@ nemo-platform-plugin = { workspace = true } [dependency-groups] -dev = ["pytest>=8.3.4", "pytest-asyncio>=0.25.3", "httpx>=0.27", "fastapi>=0.115", "docker>=7.0", "kubernetes>=30.1.0"] +dev = [ + "pytest>=8.3.4", + "pytest-asyncio>=0.25.3", + "httpx>=0.27", + "fastapi>=0.115", + "docker>=7.0", + "kubernetes>=30.1.0", + # openshell/grpcio/protobuf are intentionally NOT in this dev group. openshell is a + # platform-restricted wheel (manylinux_2_39 / macOS 13 arm64, no sdist); a dev-group + # entry would make `uv sync --all-packages` (bootstrap) hard-fail on any host with + # glibc < 2.39 or older macOS, because the lock marker gates arch/OS but cannot + # express glibc. They live in the `[openshell]` extra instead, and the dedicated CI + # job (make test-deployments-openshell) installs that extra to cover the backend. +] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/base.py b/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/base.py index 4956ac31c1..16c5d2c30c 100644 --- a/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/base.py +++ b/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/base.py @@ -15,6 +15,17 @@ from pydantic import BaseModel, Field +class MissingBackendDependencyError(RuntimeError): + """A backend's optional dependency (its packaging extra) is not installed. + + Backends whose substrate SDK ships as an optional extra (e.g. ``openshell``) + raise this when that SDK is absent, so the executor registry can skip the + executor with a warning instead of failing the whole deployments service at + startup. Subclasses ``RuntimeError`` so existing ``except RuntimeError`` paths + keep working. + """ + + class BackendStatusUpdate(BaseModel): """Status projection returned by backends — consumed by the reconciler (758).""" diff --git a/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py b/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py new file mode 100644 index 0000000000..19b334874b --- /dev/null +++ b/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py @@ -0,0 +1,738 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""OpenShell substrate backend for the deployments plugin. + +Runs a deployment as an OpenShell sandbox: create the sandbox from the container +image, launch the serve command as a detached process (the supervisor is PID 1, so +the workload is started via ``ExecSandbox`` rather than the image entrypoint), and +publish each container port with ``ExposeService`` to obtain a gateway-routable URL. + +The proto stubs and gRPC client come from the ``openshell`` SDK (``openshell._proto``). +Each sandbox gets a ``SandboxPolicy``: either a hand-written YAML (``default_policy_path``) +or a generated default-deny policy built from the platform egress + the sandbox +filesystem defaults, with the platform egress rule always injected as mandatory so +the agent's own fs paths and its path back to the platform are permitted. Not yet +covered and tracked separately: +- persistent volumes; +- supervisor-managed (restartable) workload command; the detached exec is not + supervised, so the reconciler probes its liveness and reports FAILED when it + dies, rather than restarting it. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import logging +import shlex +from typing import TYPE_CHECKING, Any, Literal, cast + +from nemo_deployments_plugin.backends.base import ( + BackendStatusUpdate, + DeploymentBackend, + LogResult, + MissingBackendDependencyError, + VolumeStatusUpdate, +) +from nemo_deployments_plugin.backends.labels import ( + CONFIG_NAME_LABEL, + DEPLOYMENT_NAME_LABEL, + DEPLOYMENT_WORKSPACE_LABEL, + MANAGED_BY_KEY, + deployment_identity_labels, + managed_by_label_selector, +) +from nemo_deployments_plugin.backends.openshell.config import OpenShellExecutorConfig +from nemo_deployments_plugin.backends.openshell.policy import ( + DEFAULT_EGRESS_BINARIES, + PlatformEgress, + SandboxFilesystem, + build_sandbox_policy, + generate_policy_dict, + inject_platform_egress, + load_policy_dict, + normalize_loaded_policy, +) +from nemo_deployments_plugin.constants import MANAGED_BY_LABEL +from nemo_deployments_plugin.entities import Container, DeploymentConfig +from nemo_deployments_plugin.secrets import SecretResolutionError, resolve_deployment_config_secrets +from nemo_deployments_plugin.types import DeploymentStatus, Endpoint +from nemo_platform.resources.entities import AsyncEntitiesResource +from nemo_platform_plugin.entity_client import NemoEntitiesClient, NemoEntityNotFoundError + +if TYPE_CHECKING: + import grpc + from openshell._proto import openshell_pb2 as pb # ty: ignore[unresolved-import] + from openshell._proto import openshell_pb2_grpc as pb_grpc # ty: ignore[unresolved-import] + +logger = logging.getLogger(__name__) + +_OPENSHELL_INSTALL_HINT = ( + "The 'openshell' package is required for OpenShellDeploymentBackend. " + "Install it with: uv sync --package nemo-deployments-plugin --extra openshell" +) + +_SERVE_LOG = "/tmp/nemo-serve.log" +# Marker the serve launcher writes so read_status (stateless across reconcile polls) +# does not relaunch the workload on a later poll. Lives in a policy read-write path. +# Holds the launch time as epoch seconds, which is the only clock the probe has for +# deciding whether a missing pidfile is a slow start or a launcher that never ran. +_LAUNCH_MARKER = "/tmp/nemo-serve.launched" +_SERVE_PIDFILE = "/tmp/nemo-serve.pid" + +# OpenShell rejects a longer sandbox or service name with INVALID_ARGUMENT: three +# segments plus two "--" delimiters must fit a 63-char DNS label. +_MAX_ROUTABLE_NAME_LEN = 19 + +# Seconds after launch that a missing pidfile stops being a slow start and becomes proof +# the launcher's background shell never ran. +_SERVE_PID_GRACE_SECONDS = 30 + +# Liveness probe exit codes. The launcher backgrounds the workload and marks the launch +# unconditionally, so the marker only proves the shell accepted the `&`. The pidfile is +# what proves something actually started, hence the third state: absent-but-recent is +# pending (wait another poll), absent-past-grace is dead. +_SERVE_DEAD_EXIT = 9 +_SERVE_PENDING_EXIT = 10 +_LIVENESS_PROBE = f""" +if test -f {_SERVE_PIDFILE}; then + kill -0 "$(cat {_SERVE_PIDFILE})" 2>/dev/null || exit {_SERVE_DEAD_EXIT} + exit 0 +fi +started=$(cat {_LAUNCH_MARKER} 2>/dev/null) || exit {_SERVE_PENDING_EXIT} +case "$started" in '' | *[!0-9]*) exit {_SERVE_PENDING_EXIT} ;; esac +if [ "$(($(date +%s) - started))" -ge {_SERVE_PID_GRACE_SECONDS} ]; then + exit {_SERVE_DEAD_EXIT} +fi +exit {_SERVE_PENDING_EXIT} +""" +_LOG_TAIL_LINES = 20 + +# Cached on first status read; needs the proto enums so it cannot be built at import +# time (see _ensure_openshell). None until built. +_PHASE_TO_STATUS: dict[int, DeploymentStatus] | None = None + + +def _ensure_openshell() -> None: + """Import grpc + the openshell proto stubs, binding them as module globals. + + Deferred so importing this backend (and the registry that eagerly imports it) + does not require the optional ``openshell`` package, matching the docker/k8s + backends. Only ``TYPE_CHECKING`` imports the names, so the type checker sees pure + modules (never ``None``); the runtime binding lives in ``globals()``. Constructing + the backend calls this and raises a clear error when the package is absent. + """ + if globals().get("pb") is not None: + return + try: + import grpc + from openshell._proto import openshell_pb2 as pb # ty: ignore[unresolved-import] + from openshell._proto import openshell_pb2_grpc as pb_grpc # ty: ignore[unresolved-import] + except ImportError as exc: + raise MissingBackendDependencyError(_OPENSHELL_INSTALL_HINT) from exc + globals().update(grpc=grpc, pb=pb, pb_grpc=pb_grpc) + + +def _phase_to_status(phase: int) -> DeploymentStatus: + global _PHASE_TO_STATUS + if _PHASE_TO_STATUS is None: + _PHASE_TO_STATUS = { + pb.SANDBOX_PHASE_UNSPECIFIED: "UNKNOWN", + pb.SANDBOX_PHASE_PROVISIONING: "STARTING", + pb.SANDBOX_PHASE_READY: "READY", + pb.SANDBOX_PHASE_ERROR: "FAILED", + pb.SANDBOX_PHASE_DELETING: "DELETING", + pb.SANDBOX_PHASE_UNKNOWN: "UNKNOWN", + } + return _PHASE_TO_STATUS.get(phase, "UNKNOWN") + + +class OpenShellDeploymentBackend(DeploymentBackend): + """Manage deployments as OpenShell sandboxes via the gateway gRPC API.""" + + def init(self) -> None: + _ensure_openshell() + self._executor_config = OpenShellExecutorConfig.model_validate(self._config) + self._entities = NemoEntitiesClient(AsyncEntitiesResource(self._sdk)) + # Build the policy once (fail fast on a bad path/shape). The gateway default + # policy would not permit the agent's own exec paths, so we always apply one. + self._policy = self._build_executor_policy() + self._channel = self._create_channel() + self._stub = pb_grpc.OpenShellStub(self._channel) + + def _build_executor_policy(self) -> Any: + """The SandboxPolicy applied to created sandboxes. + + A hand-written YAML (``default_policy_path``) if given, else a generated + default-deny policy. When ``platform_egress`` is configured, its rule is + injected as mandatory so the sandbox can always reach the platform. + When ``platform_egress`` is null the sandbox gets no direct + egress at all, correct for gateway-managed inference (inference.local), + which the supervisor brokers so no policy egress rule is needed. The + supervisor still dials the platform itself, at the sandbox network's + gateway address, so the platform must listen there (``--host 0.0.0.0``). + """ + cfg = self._executor_config.platform_egress + egress = ( + PlatformEgress( + host=cfg.host, + port=cfg.port, + protocol=cfg.protocol, + tls=cfg.tls, + access=cfg.access, + binaries=tuple(cfg.binaries) or DEFAULT_EGRESS_BINARIES, + ) + if cfg is not None + else None + ) + compat = self._executor_config.landlock_compatibility + path = self._executor_config.default_policy_path + if path: + policy_dict = normalize_loaded_policy(load_policy_dict(path), landlock_compatibility=compat) + else: + policy_dict = generate_policy_dict( + filesystem=SandboxFilesystem(landlock_compatibility=compat), egress=egress + ) + if egress is not None: + inject_platform_egress(policy_dict, egress) + return build_sandbox_policy(policy_dict) + + def _create_channel(self) -> grpc.Channel: + target = self._executor_config.grpc_target() + if self._executor_config.use_insecure(): + return grpc.insecure_channel(target) + tls = self._executor_config.tls + ca = client_cert = client_key = None + if tls is not None: + ca = _read_bytes(tls.ca_cert_path) + client_cert = _read_bytes(tls.client_cert_path) + client_key = _read_bytes(tls.client_key_path) + credentials = grpc.ssl_channel_credentials( + root_certificates=ca, + private_key=client_key, + certificate_chain=client_cert, + ) + return grpc.secure_channel(target, credentials) + + def shutdown(self) -> None: + channel = getattr(self, "_channel", None) + if channel is not None: + channel.close() + + async def _load_deployment_config(self, workspace: str, config_name: str) -> DeploymentConfig: + return await self._entities.get(DeploymentConfig, config_name, workspace=workspace) + + async def create_deployment( + self, + *, + workspace: str, + name: str, + config_name: str, + labels: dict[str, str], + backend_config: dict[str, Any], + ) -> BackendStatusUpdate: + del backend_config # per-deployment openshell overrides: needs entity field + SDK regen (follow-up) + sandbox_nm = _sandbox_name(workspace, name) + + existing = await self._try_get_sandbox(sandbox_nm) + if existing is not None: + if _sandbox_matches(existing, workspace, name): + return await self.read_status(workspace=workspace, name=name) + return BackendStatusUpdate( + status="FAILED", + status_message=f"Sandbox name collision: {sandbox_nm} exists with different labels", + ) + + try: + config = await self._load_deployment_config(workspace, config_name) + config = await resolve_deployment_config_secrets(self._sdk, config) + except NemoEntityNotFoundError: + return BackendStatusUpdate( + status="FAILED", + status_message=f"DeploymentConfig '{config_name}' not found in workspace '{workspace}'", + ) + except SecretResolutionError as exc: + return BackendStatusUpdate(status="FAILED", status_message=str(exc)) + except Exception as exc: + logger.exception("Failed to load deployment config %s/%s", workspace, config_name) + return BackendStatusUpdate( + status="FAILED", + status_message=f"Failed to load deployment config: {exc}", + ) + + if not config.containers: + return BackendStatusUpdate(status="FAILED", status_message="DeploymentConfig has no containers") + container = config.containers[0] + if not container.image: + return BackendStatusUpdate(status="FAILED", status_message="containers[0].image is required") + + serve_command = list(container.command) + list(container.args) + if not serve_command: + return BackendStatusUpdate( + status="FAILED", + status_message=( + "openshell backend requires containers[0].command (the serve command); " + "the image entrypoint is not run by the sandbox supervisor" + ), + ) + + env: dict[str, str] = {} + unresolved: list[str] = [] + for var in container.env: + if var.value is not None: + env[var.name] = var.value + elif var.value_from is not None or var.secret_ref is not None: + unresolved.append(var.name) + if unresolved: + return BackendStatusUpdate( + status="FAILED", + status_message=( + "Cannot resolve a value for environment variable(s): " + f"{', '.join(sorted(unresolved))}. valueFrom is not supported by the openshell " + "backend and secret references must resolve to a value." + ), + ) + all_labels = { + **labels, + **config.labels, + **deployment_identity_labels( + workspace, + name, + config.restart_policy, + config_name=config_name, + backoff_limit=config.backoff_limit, + ), + } + + template = pb.SandboxTemplate(image=container.image, environment=env, labels=all_labels) + spec = pb.SandboxSpec(template=template, environment=env, policy=self._policy) + request = pb.CreateSandboxRequest(spec=spec, name=sandbox_nm, labels=all_labels) + + try: + await self._unary(self._stub.CreateSandbox, request) + except grpc.RpcError as exc: + return BackendStatusUpdate( + status="FAILED", + status_message=f"CreateSandbox failed: {_rpc_detail(exc)}", + ) + + # Return as soon as the sandbox is created. Provisioning (wait for READY, launch + # the serve command, expose ports) is driven by read_status across the reconciler's + # poll cycles, so a slow sandbox never blocks the serial reconcile loop. + return BackendStatusUpdate( + status="STARTING", + status_message=f"Sandbox {sandbox_nm} created; awaiting READY", + ) + + async def read_status(self, *, workspace: str, name: str) -> BackendStatusUpdate: + sandbox_nm = _sandbox_name(workspace, name) + try: + response = await self._unary(self._stub.GetSandbox, pb.GetSandboxRequest(name=sandbox_nm)) + except grpc.RpcError as exc: + if _rpc_code(exc) == grpc.StatusCode.NOT_FOUND: + return BackendStatusUpdate(status="LOST", status_message=f"Sandbox {sandbox_nm} not found") + return BackendStatusUpdate( + status="UNKNOWN", + status_message=f"GetSandbox error: {_rpc_detail(exc)}", + error_details={"error": _rpc_detail(exc), "sandbox": sandbox_nm}, + ) + + sandbox = response.sandbox + phase = sandbox.status.phase + # Not READY yet: report the phase and let the reconciler keep polling. + if phase != pb.SANDBOX_PHASE_READY: + status = _phase_to_status(phase) + message = _condition_message(sandbox.status) or f"Sandbox phase {pb.SandboxPhase.Name(phase)}" + return BackendStatusUpdate(status=status, status_message=message) + + # Sandbox READY: drive provisioning (launch the serve command, expose ports) on + # the reconciler's poll cycles rather than blocking create. + try: + return await self._advance_provisioning(sandbox, sandbox_nm, workspace) + except grpc.RpcError as exc: + # A transient gateway error mid-provisioning (e.g. the serve-marker probe) must not + # escape raw into the reconciler; report UNKNOWN and let the next poll retry. The + # deliberate provisioning-failure paths already return FAILED and clean up, so this + # only catches unhandled transients. + return BackendStatusUpdate( + status="UNKNOWN", + status_message=f"Provisioning RPC error: {_rpc_detail(exc)}", + error_details={"error": _rpc_detail(exc), "sandbox": sandbox_nm}, + ) + + async def delete_deployment(self, workspace: str, name: str) -> BackendStatusUpdate: + sandbox_nm = _sandbox_name(workspace, name) + try: + await self._unary(self._stub.DeleteSandbox, pb.DeleteSandboxRequest(name=sandbox_nm)) + except grpc.RpcError as exc: + if _rpc_code(exc) == grpc.StatusCode.NOT_FOUND: + return BackendStatusUpdate( + status="SUCCEEDED", + status_message=f"Sandbox {sandbox_nm} already gone", + ) + return BackendStatusUpdate( + status="FAILED", + status_message=f"DeleteSandbox failed: {_rpc_detail(exc)}", + ) + return BackendStatusUpdate(status="SUCCEEDED", status_message=f"Sandbox {sandbox_nm} deleted") + + async def list_managed_deployment_names(self) -> list[str]: + request = pb.ListSandboxesRequest(label_selector=managed_by_label_selector()) + try: + response = await self._unary(self._stub.ListSandboxes, request) + except grpc.RpcError: + logger.warning("Failed to list managed sandboxes", exc_info=True) + return [] + + seen: set[str] = set() + for sandbox in response.sandboxes: + sandbox_labels = dict(sandbox.metadata.labels) + if sandbox_labels.get(MANAGED_BY_KEY) != MANAGED_BY_LABEL: + continue + ws = sandbox_labels.get(DEPLOYMENT_WORKSPACE_LABEL) + dep_name = sandbox_labels.get(DEPLOYMENT_NAME_LABEL) + if ws and dep_name: + seen.add(f"{ws}/{dep_name}") + return sorted(seen) + + async def get_logs(self, *, workspace: str, name: str, tail: int = 100) -> LogResult: + """Tail the workload's own output, falling back to the supervisor log. + + ``GetSandboxLogs`` cannot see ``_SERVE_LOG``: it is fed by the supervisor's + tracing layer, not by the workload's stdout. + """ + sandbox_nm = _sandbox_name(workspace, name) + try: + sandbox = await self._unary(self._stub.GetSandbox, pb.GetSandboxRequest(name=sandbox_nm)) + sandbox_id = sandbox.sandbox.metadata.id or sandbox_nm + exit_code, output = await self._exec_detached( + sandbox_id, ["/bin/sh", "-lc", f"tail -n {int(tail)} {_SERVE_LOG}"] + ) + if exit_code == 0: + lines = output.splitlines() + return LogResult(lines=lines, truncated=len(lines) >= tail) + response = await self._unary( + self._stub.GetSandboxLogs, + pb.GetSandboxLogsRequest(sandbox_id=sandbox_id, lines=tail), + ) + except grpc.RpcError as exc: + if _rpc_code(exc) == grpc.StatusCode.NOT_FOUND: + return LogResult(lines=[f"Sandbox {sandbox_nm} not found"]) + return LogResult(lines=[f"Failed to fetch logs: {_rpc_detail(exc)}"]) + lines = [_format_log_line(line) for line in response.logs] + return LogResult(lines=lines, truncated=response.buffer_total > len(lines)) + + async def create_volume( + self, + *, + workspace: str, + name: str, + size: str, + access_modes: list[str], + backend_config: dict[str, Any], + ) -> VolumeStatusUpdate: + del workspace, name, size, access_modes, backend_config + return VolumeStatusUpdate( + status="FAILED", + status_message="Volumes are not yet supported by the openshell backend", + ) + + async def read_volume_status( + self, + *, + workspace: str, + name: str, + backend_config: dict[str, Any] | None = None, + ) -> VolumeStatusUpdate: + del workspace, name, backend_config + return VolumeStatusUpdate( + status="FAILED", + status_message="Volumes are not yet supported by the openshell backend", + ) + + async def delete_volume( + self, + workspace: str, + name: str, + *, + backend_config: dict[str, Any] | None = None, + ) -> VolumeStatusUpdate: + del workspace, name, backend_config + return VolumeStatusUpdate(status="RELEASED", status_message="No openshell volume to delete") + + # --- helpers --- + + async def _unary(self, method: Any, request: Any) -> Any: + return await asyncio.to_thread(method, request, timeout=self._executor_config.request_timeout_seconds) + + async def _advance_provisioning(self, sandbox: Any, sandbox_nm: str, workspace: str) -> BackendStatusUpdate: + """Advance a READY sandbox toward a serving deployment, idempotently. + + Called from read_status on each poll once the sandbox is READY: launch the + detached serve command (once, guarded by a marker file) and expose its ports. + Provisioning thus happens across reconcile cycles instead of blocking create. + On a provisioning failure the sandbox is deleted so it does not leak behind a + terminal FAILED deployment. + """ + sandbox_id = sandbox.metadata.id or sandbox_nm + + # Fast path: ports already exposed on a prior poll -> the deployment is serving, + # as long as the workload is alive. An exposed service proves routing, not health. + endpoints = await self._list_endpoints(sandbox_nm) + if endpoints: + failure = await self._serve_failure(sandbox_id) + if failure is not None: + return failure + return BackendStatusUpdate(status="READY", status_message="Sandbox serving", endpoints=endpoints) + + config_name = dict(sandbox.metadata.labels).get(CONFIG_NAME_LABEL) + if not config_name: + return BackendStatusUpdate(status="FAILED", status_message="Sandbox is missing its deployment-config label") + try: + config = await self._load_deployment_config(workspace, config_name) + except Exception as exc: + return BackendStatusUpdate(status="FAILED", status_message=f"Failed to load deployment config: {exc}") + container = config.containers[0] + + # Launch the serve command once; the launcher writes a marker so a later poll + # does not relaunch it (read_status keeps no state across calls). + if not await self._serve_launched(sandbox_id): + failure = await self._launch_serve(sandbox_id, list(container.command) + list(container.args)) + if failure is not None: + await self._delete_sandbox_best_effort(sandbox_nm) + return failure + return BackendStatusUpdate(status="STARTING", status_message="Serve command launched") + + # Never expose a port to a workload that is not demonstrably running. The launch + # marker only proves the launcher shell accepted the `&`, so a live pid is what + # stops a launch that started nothing from advancing to READY. + state = await self._serve_state(sandbox_id) + if state == "dead": + return await self._serve_dead_update(sandbox_id) + if state == "pending": + return BackendStatusUpdate(status="STARTING", status_message="Serve launched; awaiting serve pid") + + # Serve launched but ports not yet exposed: expose them. + try: + endpoints = await self._expose_ports(sandbox_nm, container) + except grpc.RpcError as exc: + await self._delete_sandbox_best_effort(sandbox_nm) + return BackendStatusUpdate(status="FAILED", status_message=f"ExposeService failed: {_rpc_detail(exc)}") + if not endpoints: + return BackendStatusUpdate(status="STARTING", status_message="Serve launched; awaiting endpoints") + return BackendStatusUpdate(status="READY", status_message="Sandbox serving", endpoints=endpoints) + + async def _serve_launched(self, sandbox_id: str) -> bool: + """Whether the serve command has already been launched (marker file present).""" + exit_code, _ = await self._exec_detached(sandbox_id, ["/bin/sh", "-lc", f"test -f {_LAUNCH_MARKER}"]) + return exit_code == 0 + + async def _serve_state(self, sandbox_id: str) -> Literal["alive", "dead", "pending"]: + """Whether the serve process is running, known dead, or not yet accounted for. + + Only a probe that positively proves death reports ``dead``, so a flaky exec RPC + or an unreadable marker never flaps a healthy deployment. Everything undecided is + ``pending``, which callers treat as "not yet safe to expose". + """ + exit_code, _ = await self._exec_detached(sandbox_id, ["/bin/sh", "-lc", _LIVENESS_PROBE]) + if exit_code == _SERVE_DEAD_EXIT: + return "dead" + if exit_code in (None, 0): + return "alive" + return "pending" + + async def _serve_dead_update(self, sandbox_id: str) -> BackendStatusUpdate: + """A FAILED update carrying the workload's own last output. + + The sandbox is kept (unlike the provisioning-failure paths) so its log stays + readable through ``get_logs``. + """ + tail = await self._serve_log_tail(sandbox_id) + message = "Serve process exited; deployment is not serving" + if tail: + message = f"{message}. Last output: {tail}" + return BackendStatusUpdate(status="FAILED", status_message=message) + + async def _serve_failure(self, sandbox_id: str) -> BackendStatusUpdate | None: + """A FAILED update when the serve process is known dead, else None. + + Used where the deployment is already serving, so only positive evidence of death + demotes it; a pending probe leaves it alone. + """ + if await self._serve_state(sandbox_id) != "dead": + return None + return await self._serve_dead_update(sandbox_id) + + async def _serve_log_tail(self, sandbox_id: str, lines: int = _LOG_TAIL_LINES) -> str: + """Last lines of the workload log, or "" when it cannot be read.""" + try: + exit_code, output = await self._exec_detached( + sandbox_id, ["/bin/sh", "-lc", f"tail -n {int(lines)} {_SERVE_LOG}"] + ) + except grpc.RpcError: + logger.warning("Failed to read the serve log for sandbox %s", sandbox_id, exc_info=True) + return "" + return output.strip() if exit_code in (None, 0) else "" + + async def _launch_serve(self, sandbox_id: str, serve_command: list[str]) -> BackendStatusUpdate | None: + """Launch the detached serve command, writing the launch marker on success. + + Returns None on success, or a FAILED update if the launcher itself failed (bad + workdir/shell or a non-zero launcher exit). The backgrounded serve process is + not supervised, so this catches launch-time failures, not later serve crashes. + """ + serve = shlex.join(serve_command) + # The inner shell records its own pid and then execs, so the pidfile holds the + # workload's pid whether or not setsid forks. The marker is written synchronously + # so a poll racing the background start does not relaunch, and holds the launch + # time so the probe can age out a pidfile that never appears. + inner = f"echo $$ >{_SERVE_PIDFILE}; exec {serve} >{_SERVE_LOG} 2>&1" + launch = f"setsid /bin/sh -c {shlex.quote(inner)} /dev/null 2>&1 & date +%s >{_LAUNCH_MARKER}" + workdir = self._executor_config.serve_workdir + if workdir: + launch = f"cd {shlex.quote(workdir)} && {launch}" + try: + exit_code, output = await self._exec_detached(sandbox_id, ["/bin/sh", "-lc", launch]) + except grpc.RpcError as exc: + return BackendStatusUpdate( + status="FAILED", status_message=f"Failed to launch serve command: {_rpc_detail(exc)}" + ) + if exit_code not in (None, 0): + detail = output.strip() + message = f"Serve command launch exited with code {exit_code}" + if detail: + message = f"{message}: {detail}" + return BackendStatusUpdate(status="FAILED", status_message=message) + return None + + async def _delete_sandbox_best_effort(self, sandbox_nm: str) -> None: + """Best-effort DeleteSandbox to avoid leaking a partially-provisioned sandbox. + + Swallows all errors (including a NOT_FOUND, which makes this idempotent) so a + cleanup failure never masks the original provisioning failure. + """ + try: + await self._unary(self._stub.DeleteSandbox, pb.DeleteSandboxRequest(name=sandbox_nm)) + except Exception: + logger.warning("Failed to clean up sandbox %s after provisioning failure", sandbox_nm, exc_info=True) + + async def _try_get_sandbox(self, sandbox_nm: str) -> Any | None: + try: + response = await self._unary(self._stub.GetSandbox, pb.GetSandboxRequest(name=sandbox_nm)) + except grpc.RpcError as exc: + if _rpc_code(exc) == grpc.StatusCode.NOT_FOUND: + return None + raise + return response.sandbox + + async def _exec_detached(self, sandbox_id: str, command: list[str]) -> tuple[int | None, str]: + """Run a command, draining its event stream. Returns (exit_code, combined output).""" + timeout = self._executor_config.request_timeout_seconds + request = pb.ExecSandboxRequest(sandbox_id=sandbox_id, command=command, timeout_seconds=timeout) + + def _drain() -> tuple[int | None, str]: + exit_code: int | None = None + chunks: list[str] = [] + for event in self._stub.ExecSandbox(request, timeout=timeout): + if event.HasField("stdout"): + chunks.append(event.stdout.data.decode("utf-8", errors="replace")) + elif event.HasField("stderr"): + chunks.append(event.stderr.data.decode("utf-8", errors="replace")) + elif event.HasField("exit"): + exit_code = event.exit.exit_code + return exit_code, "".join(chunks) + + return await asyncio.to_thread(_drain) + + async def _list_endpoints(self, sandbox_nm: str) -> list[Endpoint]: + """Return the sandbox's currently exposed services as endpoints.""" + try: + response = await self._unary(self._stub.ListServices, pb.ListServicesRequest(sandbox=sandbox_nm)) + except grpc.RpcError: + logger.warning("Failed to list services for sandbox %s", sandbox_nm, exc_info=True) + return [] + return [Endpoint(name=svc.endpoint.service_name, url=svc.url, protocol="http") for svc in response.services] + + async def _expose_ports(self, sandbox_nm: str, container: Container) -> list[Endpoint]: + endpoints: list[Endpoint] = [] + for port in container.ports: + service = _service_name(port) + request = pb.ExposeServiceRequest( + sandbox=sandbox_nm, + service=service, + target_port=port.container_port, + ) + response = await self._unary(self._stub.ExposeService, request) + endpoints.append(Endpoint(name=service, url=response.url, protocol="http")) + return endpoints + + +def _sandbox_name(workspace: str, name: str) -> str: + """OpenShell sandbox name, within ``_MAX_ROUTABLE_NAME_LEN``. + + The limit rules out the shared ``dep---`` scheme, so the human-readable + workspace/name mapping is carried in identity labels (used by list/idempotency) instead. + + The digest is a non-crypto short id; the length is chosen to fit the routable-name limit. + """ + digest = hashlib.sha256(f"{workspace}/{name}".encode()).hexdigest()[:14] + return f"nmp-{digest}" + + +def _service_name(port: Any) -> str: + """Routable service name for a container port, within ``_MAX_ROUTABLE_NAME_LEN``. + + Port names are author-supplied, so an over-long one is truncated with a digest suffix: + the digest keeps distinct names distinct, and avoids the trailing ``-`` a bare + truncation can leave behind (invalid as a DNS label). + """ + raw = port.name or f"port-{port.container_port}" + if len(raw) <= _MAX_ROUTABLE_NAME_LEN: + return raw + digest = hashlib.sha256(raw.encode()).hexdigest()[:6] + prefix = raw[: _MAX_ROUTABLE_NAME_LEN - len(digest) - 1].rstrip("-") + return f"{prefix}-{digest}" + + +def _sandbox_matches(sandbox: Any, workspace: str, name: str) -> bool: + sandbox_labels = dict(sandbox.metadata.labels) + return ( + sandbox_labels.get(MANAGED_BY_KEY) == MANAGED_BY_LABEL + and sandbox_labels.get(DEPLOYMENT_WORKSPACE_LABEL) == workspace + and sandbox_labels.get(DEPLOYMENT_NAME_LABEL) == name + ) + + +def _condition_message(status: Any) -> str: + for condition in reversed(status.conditions): + if condition.message: + return condition.message + return "" + + +def _format_log_line(line: Any) -> str: + source = getattr(line, "source", "") + message = getattr(line, "message", "") or getattr(line, "line", "") + return f"[{source}] {message}".strip() if source else str(message) + + +def _rpc_code(exc: grpc.RpcError) -> grpc.StatusCode | None: + getter = getattr(exc, "code", None) + if not callable(getter): + return None + return cast("grpc.StatusCode", getter()) + + +def _rpc_detail(exc: grpc.RpcError) -> str: + code = _rpc_code(exc) + details_getter = getattr(exc, "details", None) + details = details_getter() if callable(details_getter) else str(exc) + return f"{code.name if code is not None else 'UNKNOWN'}: {details}" + + +def _read_bytes(path: str | None) -> bytes | None: + if not path: + return None + with open(path, "rb") as handle: + return handle.read() diff --git a/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/config.py b/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/config.py new file mode 100644 index 0000000000..49cbe4537d --- /dev/null +++ b/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/config.py @@ -0,0 +1,143 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Executor-level OpenShell backend configuration. + +These knobs configure a named ``openshell`` executor instance (how to reach the +OpenShell gateway), not per-deployment entity ``backend_config``. +""" + +from __future__ import annotations + +from typing import Literal +from urllib.parse import urlparse + +from pydantic import BaseModel, ConfigDict, Field, model_validator + + +class OpenShellTLSConfig(BaseModel): + """mTLS material for an https gateway. Unused for local plaintext gateways.""" + + model_config = ConfigDict(extra="forbid") + + ca_cert_path: str | None = Field(default=None, description="Path to the CA bundle that signed the gateway cert.") + client_cert_path: str | None = Field(default=None, description="Path to the client certificate (mTLS).") + client_key_path: str | None = Field(default=None, description="Path to the client private key (mTLS).") + + +class PlatformEgressConfig(BaseModel): + """The NeMo platform endpoint a sandbox must always be able to reach. + + Environment-specific: on the docker driver a sandbox reaches the platform at + host.docker.internal:8080; an in-cluster (k8s) driver uses the platform Service + address. This becomes the single allowed rule in a generated default-deny + ``SandboxPolicy`` and is re-injected into any static or + user-supplied policy so a deployment can never lose its path home. + """ + + model_config = ConfigDict(extra="forbid") + + host: str = Field(default="host.docker.internal", description="Platform host reachable from inside a sandbox.") + port: int = Field(default=8080, ge=1, description="Platform port (the inference gateway / API listener).") + # Value sets track openshell/proto/sandbox.proto (NetworkAccessRule), which is broader than + # the summaries above imply: protocol also allows "graphql" and "" (L4-only), tls allows + # "passthrough". + protocol: Literal["rest", "websocket", "graphql", "sql", ""] = Field( + default="rest", + description='OpenShell L7 protocol: "rest", "websocket", "graphql", "sql", or "" for L4-only.', + ) + tls: Literal["terminate", "passthrough", ""] = Field( + default="", + description='TLS handling: "terminate" for HTTPS L7 intercept, "passthrough" (or "") for no L7 interception.', + ) + access: Literal["read-only", "read-write", "full"] = Field( + default="full", + description='OpenShell access preset: "read-only", "read-write", or "full".', + ) + binaries: list[str] = Field( + default_factory=list, + description=( + "Binaries permitted to open the egress connection (the venv/uv python that makes LLM " + "calls, not curl). Empty uses the backend default set." + ), + ) + + +class OpenShellExecutorConfig(BaseModel): + """Knobs for a named openshell executor instance.""" + + model_config = ConfigDict(extra="forbid") + + gateway_endpoint: str = Field( + default="http://127.0.0.1:17670", + description=( + "OpenShell gateway endpoint as a URL (http://host:port or https://host:port). " + "The gRPC target is the same host:port; http implies plaintext, https implies TLS." + ), + ) + insecure: bool | None = Field( + default=None, + description="Force plaintext (True) or TLS (False). When None, derived from the endpoint scheme.", + ) + tls: OpenShellTLSConfig | None = Field(default=None, description="mTLS material for an https gateway.") + request_timeout_seconds: int = Field( + default=120, + ge=1, + description="Per-RPC deadline for control-plane calls (create/get/expose/delete/logs).", + ) + default_policy_path: str | None = Field( + default=None, + description=( + "Path to a hand-written SandboxPolicy YAML applied to created sandboxes. When unset, a " + "default-deny policy is generated from platform_egress + the sandbox filesystem defaults. " + "When platform_egress is set it is injected as mandatory; set platform_egress: null to " + "disable it entirely (e.g. gateway-managed inference via inference.local)." + ), + ) + platform_egress: PlatformEgressConfig | None = Field( + default_factory=PlatformEgressConfig, + description=( + "Platform endpoint a sandbox reaches directly. Drives the generated default policy " + "(when default_policy_path is unset) and is injected into every policy as a mandatory " + "egress rule. Set to null to grant the sandbox NO direct egress, correct when the agent " + "reaches models through the gateway-managed inference.local route, which needs no " + "sandbox egress rule." + ), + ) + serve_workdir: str = Field( + default="/home/sandbox", + description=( + "Working directory for the detached serve command. Must be writable by the sandbox " + "user: a packaged agent image chowns /workspace to its own 'agent' user, so NAT's " + "per-run temp dir is written here instead. Empty string disables the chdir." + ), + ) + landlock_compatibility: Literal["best_effort", "hard_requirement"] = Field( + default="best_effort", + description=( + "Landlock mode for the generated policy and for hand-written policies that omit a " + "landlock block. 'best_effort' runs on kernels without Landlock but ships NO " + "filesystem confinement there (fail-open); 'hard_requirement' fails the sandbox " + "closed on such kernels (fail-closed) at the cost of breaking hosts without Landlock." + ), + ) + + @model_validator(mode="after") + def _validate_endpoint(self) -> OpenShellExecutorConfig: + parsed = urlparse(self.gateway_endpoint) + if parsed.scheme not in ("http", "https") or not parsed.hostname: + raise ValueError("gateway_endpoint must be a URL like http://host:port or https://host:port") + return self + + def grpc_target(self) -> str: + """host:port for the gRPC channel, derived from the endpoint URL.""" + parsed = urlparse(self.gateway_endpoint) + host = parsed.hostname + port = parsed.port or (443 if parsed.scheme == "https" else 80) + return f"{host}:{port}" + + def use_insecure(self) -> bool: + """Whether to open a plaintext channel (explicit override, else scheme-derived).""" + if self.insecure is not None: + return self.insecure + return urlparse(self.gateway_endpoint).scheme == "http" diff --git a/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/policy.py b/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/policy.py new file mode 100644 index 0000000000..ed8dbe1c41 --- /dev/null +++ b/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/policy.py @@ -0,0 +1,276 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Build and generate an OpenShell ``SandboxPolicy`` proto. + +Two producers, one proto: + +- ``load_sandbox_policy`` reads a hand-written policy YAML. +- ``generate_sandbox_policy`` builds a default-deny policy structurally from a + sandbox's own filesystem shape + the platform egress it must reach. + +Both go through ``build_sandbox_policy`` so the mapping from the documented YAML +shape (``version`` / ``filesystem_policy`` / ``landlock`` / ``process`` / +``network_policies``) to the proto lives in one place. Everything here is a +property of the *sandbox*, not of a deployment: it takes plain structured inputs +and returns a policy, so a future non-deployment consumer (a sandboxed job, a +tool runner) can reuse it without importing deployment types. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Literal + +import yaml +from google.protobuf import json_format +from nemo_deployments_plugin.backends.base import MissingBackendDependencyError + +if TYPE_CHECKING: + from openshell._proto import sandbox_pb2 as sb # ty: ignore[unresolved-import] + +# Default filesystem/process shape for a NAT-agent OpenShell image, verified against +# a live gateway: /opt holds the uv-managed interpreter the venv nat +# launcher execs; /workspace is the agent venv; /home/sandbox is the serve workdir +# (NAT writes /.tmp); /dev/shm is needed by nat[most]'s Dask cluster (POSIX +# semaphores). These describe the image, so they belong with the sandbox, not the +# deployment. +DEFAULT_READ_ONLY: tuple[str, ...] = ("/usr", "/bin", "/lib", "/etc", "/opt", "/proc", "/dev/urandom", "/var/log") +DEFAULT_READ_WRITE: tuple[str, ...] = ("/workspace", "/home/sandbox", "/sandbox", "/tmp", "/dev/null", "/dev/shm") +DEFAULT_RUN_AS_USER = "sandbox" + +# Landlock compatibility default. "best_effort" runs on kernels without Landlock but +# degrades to NO filesystem confinement there (fail-open); "hard_requirement" fails +# the sandbox closed on such kernels. Kept as the default to preserve the local-dev +# docker-driver happy path; harden via OpenShellExecutorConfig.landlock_compatibility. +DEFAULT_LANDLOCK_COMPATIBILITY: Literal["best_effort", "hard_requirement"] = "best_effort" + +# The binary that opens the egress socket for NAT's LLM calls is the venv/uv python, +# NOT curl. The exact uv-managed interpreter path is image/patch-version specific +# (e.g. /opt/uv/python/cpython-3.13.7-.../bin/python3.13); an executor can add it via +# its platform_egress config. OpenShell matches binary paths exactly, with no prefix or +# glob support, so an egress rule has to pin the interpreter patch version. +DEFAULT_EGRESS_BINARIES: tuple[str, ...] = ("/workspace/.venv/bin/python3.13", "/usr/bin/curl") + +# Map key for the mandatory platform egress rule. Reserved: injected into every +# policy, so a user rule at this key is overwritten rather than merged. +PLATFORM_EGRESS_KEY = "nemo_platform" + + +# Values the supervisor recognises for the two free-form string fields that fail OPEN when +# unrecognised: anything but "hard_requirement" becomes best-effort Landlock (no filesystem +# confinement on a kernel without Landlock), and an unset enforcement takes the proto's +# "audit" default instead of blocking. The proto cannot type these, so they are checked here. +LANDLOCK_COMPATIBILITIES = ("best_effort", "hard_requirement") +ENFORCEMENTS = ("enforce", "audit") +DEFAULT_ENFORCEMENT = "enforce" + +# OpenShell's baseline policy is version 1; an omitted version would parse as the proto's 0. +DEFAULT_POLICY_VERSION = 1 + + +@dataclass(frozen=True) +class SandboxFilesystem: + """Filesystem + process shape a sandbox image needs to boot and run the agent.""" + + read_only: tuple[str, ...] = DEFAULT_READ_ONLY + read_write: tuple[str, ...] = DEFAULT_READ_WRITE + include_workdir: bool = True + run_as_user: str = DEFAULT_RUN_AS_USER + run_as_group: str = DEFAULT_RUN_AS_USER + landlock_compatibility: str = DEFAULT_LANDLOCK_COMPATIBILITY + + +@dataclass(frozen=True) +class PlatformEgress: + """The one egress a sandbox must always be allowed: the NeMo platform (Inference Gateway/entities/files). + + Environment-specific (docker driver -> host.docker.internal:8080; k8s -> the + platform Service). This is the sole allowed rule in a generated default-deny + policy, and is re-injected into any static/overridden policy so a deployment can + never lose its path home. + """ + + host: str + port: int + protocol: str = "rest" + tls: str = "" + access: str = "full" + enforcement: str = "enforce" + binaries: tuple[str, ...] = DEFAULT_EGRESS_BINARIES + name: str = "nemo-platform-egress" + key: str = PLATFORM_EGRESS_KEY + + +def load_policy_dict(path: str) -> dict[str, Any]: + """Read a policy YAML file and return the parsed mapping (not yet a proto).""" + with open(path, encoding="utf-8") as handle: + data = yaml.safe_load(handle) or {} + if not isinstance(data, dict): + raise ValueError(f"sandbox policy file {path} must contain a YAML mapping") + return data + + +def normalize_loaded_policy( + data: dict[str, Any], *, landlock_compatibility: str = DEFAULT_LANDLOCK_COMPATIBILITY +) -> dict[str, Any]: + """Inject safe defaults into a hand-written policy mapping before it becomes a proto. + + A loaded YAML that omits ``process`` would run as the image-default user (possibly + root); one that omits ``landlock`` would ship no filesystem confinement. The + generated path always sets both, so mirror it here: default the process block to + the sandbox user/group and default the landlock block. Blocks the author wrote are + kept as-is. Mutates and returns ``data``. + """ + process = data.get("process") + if not isinstance(process, dict): + process = {} + data["process"] = process + process.setdefault("run_as_user", DEFAULT_RUN_AS_USER) + process.setdefault("run_as_group", DEFAULT_RUN_AS_USER) + + landlock = data.get("landlock") + if not isinstance(landlock, dict): + data["landlock"] = {"compatibility": landlock_compatibility} + else: + landlock.setdefault("compatibility", landlock_compatibility) + return data + + +def load_sandbox_policy(path: str) -> Any: + """Read a policy YAML file and return a ``SandboxPolicy`` proto.""" + return build_sandbox_policy(normalize_loaded_policy(load_policy_dict(path))) + + +def generate_policy_dict(*, filesystem: SandboxFilesystem, egress: PlatformEgress | None) -> dict[str, Any]: + """Generate a default-deny policy mapping. + + When ``egress`` is given, the platform egress rule is the sole allowed network + rule. When ``None`` (e.g. gateway-managed inference via ``inference.local``, + which needs no sandbox egress), the policy allows no egress at all, a pure + default-deny policy. + """ + return { + "version": 1, + "filesystem_policy": { + "include_workdir": filesystem.include_workdir, + "read_only": list(filesystem.read_only), + "read_write": list(filesystem.read_write), + }, + "landlock": {"compatibility": filesystem.landlock_compatibility}, + "process": {"run_as_user": filesystem.run_as_user, "run_as_group": filesystem.run_as_group}, + "network_policies": ({egress.key: _platform_egress_rule(egress)} if egress is not None else {}), + } + + +def inject_platform_egress(policy: dict[str, Any], egress: PlatformEgress) -> dict[str, Any]: + """Ensure the mandatory platform egress rule is present, overwriting any rule at its key. + + Applied to every policy (generated, static YAML, or a future user override) so a + policy can never sever the sandbox's path back to the platform. Mutates and + returns ``policy``. + """ + network = policy.setdefault("network_policies", {}) + if not isinstance(network, dict): + raise ValueError("network_policies must be a mapping of rule name -> rule") + network[egress.key] = _platform_egress_rule(egress) + return policy + + +def generate_sandbox_policy(*, filesystem: SandboxFilesystem, egress: PlatformEgress | None = None) -> Any: + """Generate a default-deny ``SandboxPolicy`` proto from structured inputs. + + ``egress=None`` yields a policy with no egress at all (gateway-managed inference). + """ + return build_sandbox_policy(generate_policy_dict(filesystem=filesystem, egress=egress)) + + +_OPENSHELL_INSTALL_HINT = ( + "The 'openshell' package is required to build OpenShell sandbox policies. " + "Install it with: uv sync --package nemo-deployments-plugin --extra openshell" +) + + +def _ensure_sb() -> None: + """Import the openshell sandbox proto lazily and bind it as a module global, so + this module loads without the optional ``openshell`` package (matching the + docker/k8s backends). The first policy build triggers this. + """ + if globals().get("sb") is not None: + return + try: + from openshell._proto import sandbox_pb2 as sb # ty: ignore[unresolved-import] + except ImportError as exc: + raise MissingBackendDependencyError(_OPENSHELL_INSTALL_HINT) from exc + globals()["sb"] = sb + + +def _with_proto_field_names(data: dict[str, Any]) -> dict[str, Any]: + """Rename the documented ``filesystem_policy`` key to the proto's ``filesystem``.""" + if "filesystem_policy" not in data: + return data + renamed = dict(data) + filesystem = renamed.pop("filesystem_policy") + if "filesystem" in renamed and renamed["filesystem"] != filesystem: + raise ValueError("sandbox policy sets both filesystem_policy and filesystem; keep one") + renamed["filesystem"] = filesystem + return renamed + + +def _apply_defaults_and_check(policy: Any) -> None: + """Fill the defaults the proto cannot express, and check the fields it cannot type. + + The supervisor reads ``compatibility`` and ``enforcement`` as free-form strings and + treats anything it does not recognise as the weaker setting, so an unrecognised value + here would ship a policy that reads as confined but is not. + """ + if not policy.version: + policy.version = DEFAULT_POLICY_VERSION + if policy.HasField("landlock") and policy.landlock.compatibility not in LANDLOCK_COMPATIBILITIES: + raise ValueError( + f"invalid sandbox policy: landlock.compatibility must be one of " + f"{', '.join(LANDLOCK_COMPATIBILITIES)}, got '{policy.landlock.compatibility}'" + ) + for key, rule in policy.network_policies.items(): + for endpoint in rule.endpoints: + if not endpoint.enforcement: + endpoint.enforcement = DEFAULT_ENFORCEMENT + elif endpoint.enforcement not in ENFORCEMENTS: + raise ValueError( + f"invalid sandbox policy: network_policies.{key} enforcement must be one of " + f"{', '.join(ENFORCEMENTS)}, got '{endpoint.enforcement}'" + ) + + +def build_sandbox_policy(data: dict[str, Any]) -> Any: + """Parse a policy mapping into a ``SandboxPolicy`` proto, rejecting anything unknown. + + The proto is the policy schema, so it does the structural validation: a misspelled or + unknown key raises here instead of being dropped on the way to a weaker policy, and + every field OpenShell supports is accepted, including ones this backend never writes. + """ + _ensure_sb() + try: + policy = json_format.ParseDict(_with_proto_field_names(data), sb.SandboxPolicy()) + except json_format.ParseError as exc: + raise ValueError(f"invalid sandbox policy: {exc}") from exc + _apply_defaults_and_check(policy) + return policy + + +def _platform_egress_rule(egress: PlatformEgress) -> dict[str, Any]: + """The platform egress rule in the policy-YAML dict shape.""" + endpoint: dict[str, Any] = { + "host": egress.host, + "port": egress.port, + "protocol": egress.protocol, + "enforcement": egress.enforcement, + "access": egress.access, + } + if egress.tls: + endpoint["tls"] = egress.tls + return { + "name": egress.name, + "endpoints": [endpoint], + "binaries": [{"path": p} for p in egress.binaries], + } diff --git a/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/sandbox_profile.py b/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/sandbox_profile.py new file mode 100644 index 0000000000..19c973e7f0 --- /dev/null +++ b/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/sandbox_profile.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""OpenShell sandbox image profile. + +Registered under the ``nemo.sandbox_profiles`` entry-point group so +``nemo agents package --sandbox-runtime openshell`` can bake OpenShell's +supervisor requirements into an agent image without the packager importing +anything OpenShell-specific. + +Intentionally dependency-light (no gRPC / client imports) so profile discovery +works even when the ``nemo-deployments[openshell]`` extra is not installed. +""" + +from __future__ import annotations + +from nemo_platform_plugin.sandbox import SandboxImageProfile, SandboxUser + +# Without ``nftables`` the supervisor falls back to a degraded policy mode. The +# glibc >= 2.39 floor its binary needs is described rather than enforced: the +# packager's default base (noble) already satisfies it. +PROFILE = SandboxImageProfile( + name="openshell", + description=( + "OpenShell sandbox supervisor: non-root 'sandbox' user, iproute2 for " + "network-namespace setup, nftables for full policy enforcement, and a " + "glibc >= 2.39 base image." + ), + apt_packages=("iproute2", "nftables"), + users=( + SandboxUser( + name="sandbox", + system=True, + create_home=True, + home="/home/sandbox", + shell="/bin/bash", + ), + ), +) diff --git a/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/registry.py b/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/registry.py index 5c3fbc66d6..9915586b78 100644 --- a/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/registry.py +++ b/plugins/nemo-deployments/src/nemo_deployments_plugin/backends/registry.py @@ -9,9 +9,10 @@ from dataclasses import dataclass from typing import Any, Self -from nemo_deployments_plugin.backends.base import DeploymentBackend +from nemo_deployments_plugin.backends.base import DeploymentBackend, MissingBackendDependencyError from nemo_deployments_plugin.backends.docker.backend import DockerDeploymentBackend from nemo_deployments_plugin.backends.k8s.backend import K8sDeploymentBackend +from nemo_deployments_plugin.backends.openshell.backend import OpenShellDeploymentBackend from nemo_platform import AsyncNeMoPlatform logger = logging.getLogger(__name__) @@ -19,6 +20,7 @@ BACKEND_CLASSES: dict[str, type[DeploymentBackend]] = { "docker": DockerDeploymentBackend, "k8s": K8sDeploymentBackend, + "openshell": OpenShellDeploymentBackend, } @@ -61,7 +63,20 @@ def from_config( for spec in specs: if spec.backend not in classes: raise UnknownBackendTypeError(f"Unknown backend type '{spec.backend}' for executor '{spec.name}'.") - executors[spec.name] = classes[spec.backend](sdk, spec.config) + try: + executors[spec.name] = classes[spec.backend](sdk, spec.config) + except MissingBackendDependencyError as exc: + # An opt-in backend whose optional extra isn't installed (e.g. + # 'openshell') must not take down the whole deployments service: + # skip just that executor. Resolving it later raises + # ExecutorNotFoundError, and a `default_executor` that can't be + # built still fails fast via the check below. + logger.warning( + "Skipping executor '%s': backend '%s' is unavailable (%s)", + spec.name, + spec.backend, + exc, + ) if default_executor and default_executor not in executors: raise ExecutorNotFoundError(f"default_executor '{default_executor}' is not registered.") except Exception: diff --git a/plugins/nemo-deployments/src/nemo_deployments_plugin/skills.py b/plugins/nemo-deployments/src/nemo_deployments_plugin/skills.py new file mode 100644 index 0000000000..dad5836db2 --- /dev/null +++ b/plugins/nemo-deployments/src/nemo_deployments_plugin/skills.py @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Skills directory exposure, registered under ``nemo.skills``. + +Returns the path to the ``skills/`` directory inside this package so the +platform can discover and load skill markdown files shipped with the plugin +(currently: ``deploy-sandbox``). +""" + +from __future__ import annotations + +from pathlib import Path + + +def skills_dir() -> Path: + """Return the directory containing this plugin's skills.""" + return Path(__file__).parent / "skills" diff --git a/plugins/nemo-deployments/src/nemo_deployments_plugin/skills/deploy-sandbox/SKILL.md b/plugins/nemo-deployments/src/nemo_deployments_plugin/skills/deploy-sandbox/SKILL.md new file mode 100644 index 0000000000..01b0f01a7a --- /dev/null +++ b/plugins/nemo-deployments/src/nemo_deployments_plugin/skills/deploy-sandbox/SKILL.md @@ -0,0 +1,384 @@ +--- +name: deploy-sandbox +description: >- + Deploys an already-built NAT agent as a policy-governed OpenShell sandbox on a + local NeMo Platform, so the same agent image gets Landlock filesystem isolation + and a pure default-deny network policy (the sandbox reaches nothing on the + network directly; its model calls are brokered by the OpenShell gateway through + the inference.local route) for free. Covers the proven wire-inference.local -> + package -> DeploymentConfig -> deploy (executor openshell-local) -> wait -> + query -> zero-egress proof -> cleanup flow. Use when the user asks to deploy an + agent as a sandbox, sandbox a deployment, run an agent under a Landlock/egress + policy, or use the openshell-local executor. Trigger keywords - deploy sandbox, + sandboxed agent, openshell deployment, openshell-local executor, sandbox policy, + egress policy, landlock, governed deployment, network egress governance, + inference.local. +triggers: + - deploy the agent as a sandbox + - sandbox the deployment + - deploy with openshell + - openshell-local executor + - run the agent under an egress policy + - governed sandbox deployment + - landlock the agent +not-for: + - nemo-build-agent (use to scaffold + build the agent image first) + - nemo-try-agent (use to query an already-deployed agent) + - nemo-setup (use to install and start the platform first) + - nemo-status (use for a read-only health dashboard) +compatibility: >- + nemo-platform >= 0.1.0; requires the nemo-deployments plugin installed with the + openshell extra (`openshell>=0.0.92` from PyPI); a running OpenShell docker-driver + gateway on :17670 (not :8080, which the platform owns) whose JWT signing keys were + generated once with `generate-certs`; a NeMo Platform reachable from inside a sandbox + at the sandbox network's gateway address (`--host 0.0.0.0`) with the `openshell-local` + executor loaded via + `--config packages/nmp_platform/config/local.yaml`; the gateway's `inference.local` + route wired to the platform Inference Gateway (`openshell provider create` + + `openshell inference set`); the agents plugin for the packaging step. Docker-driver + specific (sandboxes reach the platform at `host.openshell.internal:8080`). Linux + (glibc >= 2.39) only: verified live end-to-end there, and the `inference.local` hop is + unverified on macOS (Docker Desktop resolves the sandbox's `host.openshell.internal` + inside its Linux VM, not to the Mac's loopback). +maturity: experimental +license: Apache-2.0 +user-invocable: true +allowed-tools: [Bash, Read, Write, Edit] +--- + +# Deploy an agent as a governed OpenShell sandbox + +Deploy an ordinary NAT agent through the standard NeMo deployments API, but with the executor set to `openshell-local` so the agent runs inside an OpenShell sandbox under an automatically generated SandboxPolicy: Landlock filesystem isolation, `run_as_user: sandbox`, and a pure default-deny network policy. The agent reaches models through OpenShell's gateway-managed `inference.local` route, so the sandbox itself is granted no direct network egress at all: the model call is brokered over the supervisor's gateway channel rather than through a policy egress rule, and a call from the sandbox to anywhere else is blocked at the boundary. + +The headline: the same agent image, deployed unchanged, gets its model access brokered by the gateway and network-egress governance for free. Its model calls resolve through `https://inference.local/v1`; a direct call to anywhere else is blocked at the sandbox boundary. + +The full worked runbook with narration beats lives in the notebook at `plugins/nemo-deployments/examples/openshell/DEMO.ipynb`. The already-correct demo agent config lives at `plugins/nemo-deployments/examples/openshell/agent/config.yaml`, and a readable reference for the policy the backend generates (plus an override example) lives at `plugins/nemo-deployments/examples/openshell/local-sandbox-policy.yaml`. This skill is the concrete command path; read those files for context and overrides. + +## When to use + +- The user has a built (or buildable) NAT agent and wants it deployed under sandbox isolation with a governed default-deny network policy. +- The user explicitly asks for the `openshell-local` executor, a SandboxPolicy, or Landlock/egress governance. + +If the agent image does not exist yet, run `nemo-build-agent` first to scaffold and validate the NAT workflow, then return here to deploy it as a sandbox. This skill does not author agent YAML. + +## Pre-flight + +Verify these prerequisites before touching the deploy steps. Do not improvise around a missing one; stop and tell the user what is not running. + +Commands below assume `nemo` and `openshell` are on your PATH. In a repo checkout they live in the workspace venv, so prefix with `.venv/bin/` (e.g. `.venv/bin/openshell`, `.venv/bin/nemo`). + +1. **OpenShell gateway reachable on `:17670`, with its JWT signing keys generated.** + `:17670` is OpenShell's documented docker default. It must NOT be `:8080`: that + belongs to the NeMo platform (the gateway container image and the CLI's `openshell + gateway add http://127.0.0.1:8080` example both steer onto `:8080`, the collision + trap). Docker-driver sandboxes REQUIRE the gateway to mint sandbox JWTs, so you must + generate the signing keys once before the gateway starts (see `gateway.toml`'s + `[openshell.gateway.gateway_jwt]` and the `docker-compose.yml` header). Run the + one-time key generation, then bring the compose stack up and register it: + + ```bash + # one-time: generate the JWT signing keys the docker driver needs to mint + # sandbox tokens (writes /var/lib/openshell/tls/jwt/*). Re-run only if the + # /var/lib/openshell state dir is wiped. + docker run --rm --user 0 -v /var/lib/openshell:/var/lib/openshell \ + ghcr.io/nvidia/openshell/gateway:0.0.92 generate-certs \ + --output-dir /var/lib/openshell/tls \ + --server-san 127.0.0.1 --server-san localhost --server-san host.openshell.internal + + docker compose -f plugins/nemo-deployments/examples/openshell/docker-compose.yml up -d + openshell gateway add http://127.0.0.1:17670 --local --name docker-dev + ``` + + Confirm the CLI reaches the gateway and the endpoint column reads `:17670`, not `:8080`: + + ```bash + openshell gateway list # * docker-dev http://127.0.0.1:17670 ... plaintext + openshell sandbox list # reaches the gateway (e.g. "No sandboxes found.") + ``` + + Tear the gateway down after the demo with `docker compose -f + plugins/nemo-deployments/examples/openshell/docker-compose.yml down`. + +2. **NeMo Platform up, listening on all interfaces, started with the executor config.** The + `openshell-local` executor lives in `packages/nmp_platform/config/local.yaml`, + which is NOT the config `nemo services run` loads by default (that bundled default + has no `deployments:` openshell executor), so it must be passed with `--config`. + Start (or restart) it as: + + ```bash + export NMP_BASE_URL=http://localhost:8080 + nemo services run --host 0.0.0.0 --port 8080 \ + --config packages/nmp_platform/config/local.yaml + curl -sf http://localhost:8080/health/ready # {"status":"ready"} + ``` + + If the platform is down, route to `nemo-setup`. `--host 0.0.0.0` matters: the + `inference.local` route is dialed from the sandbox, not from the gateway process. The + docker driver writes a literal `172.18.0.1 host.openshell.internal` into each + sandbox's `/etc/hosts` (the sandbox network's gateway address), so the platform has + to be listening there. On `--host 127.0.0.1` every model call fails with a 503. It + also exposes an unauthenticated dev platform on every interface, LAN included, so do + not run this on an untrusted network. If the platform is up but was started + without `--config`, the executor is absent and Step 4 fails with an unknown-executor + error; restart with the `--config` flag above. The executor block that ships in that + file: + + ```yaml + deployments: + executors: + - name: local-docker # ordinary docker deployments (the default) + backend: docker + config: { pull_images: false, port_range_start: 9000, port_range_end: 9100 } + - name: openshell-local + backend: openshell + config: + gateway_endpoint: http://127.0.0.1:17670 + serve_workdir: /home/sandbox # sandbox-writable CWD for `nat serve` + platform_egress: null # agent uses inference.local (gateway-managed), + # so the sandbox needs NO direct egress -> + # a pure default-deny SandboxPolicy + default_executor: local-docker # sandbox is opt-in; Step 4 names the executor + ``` + + `platform_egress: null` is the shipped default and yields a policy with no egress + rules at all. See the Alternate branch at the end if you deliberately want the older + direct-egress path. + +3. **Packaging and backend dependencies present.** Step 2 (`nemo agents package`) needs + `python-on-whales` (the agents plugin `container` extra), and the OpenShell backend + needs the `openshell` SDK (the deployments plugin ships it in its `openshell` extra): + + ```bash + .venv/bin/python -c "import python_on_whales, openshell" && echo DEPS_OK || echo DEPS_MISSING + ``` + + If `DEPS_MISSING`, install the extras: `uv sync --package nemo-deployments-plugin + --extra openshell` for the backend, and `uv pip install -e + 'plugins/nemo-agents[container]'` for packaging. Base `make bootstrap-python` does + not install the platform-restricted `openshell` wheel or the agents `container` + extra, so both need this step. + +Convenience variable for the curl steps: + +```bash +BASE=http://localhost:8080/apis/deployments/v2/workspaces/default +``` + +## Step 1: Pick a model and wire the `inference.local` route + +The agent reaches models through OpenShell's gateway-managed `inference.local` route: the gateway routes `https://inference.local/v1` to a registered provider (the platform Inference Gateway) and injects the real credential, so the sandbox needs no direct egress. First pick a model the Inference Gateway serves, then do the one-time operator wiring on the gateway. + +```bash +nemo models list --all-pages # pick a model the gateway serves; it paginates, so --all-pages +MODEL='default/openai-openai-gpt-4o-mini' # example only; set to a model the list above shows +``` + +`model_name` MUST be a model that actually exists on this platform's Inference Gateway. A model the gateway cannot resolve makes `nat serve` exit inside the sandbox, which the reconciler reports as `FAILED` with the serve log tail rather than a `READY` deployment that 502s. Prefer a gpt-4o-mini-class model for clean ReAct tool-calling; avoid verbose reasoning models that emit empty content. + +Register the platform Inference Gateway as an `openshell` provider and bind `inference.local` to it (once per gateway). `host.openshell.internal` is the gateway's alias for its own host, where the platform listens. The credential is a dummy: the gateway injects the real one. + +```bash +openshell provider create --name nemo-igw --type openai \ + --credential OPENAI_API_KEY=empty \ + --config OPENAI_BASE_URL=http://host.openshell.internal:8080/apis/inference-gateway/v2/workspaces/default/openai/-/v1 \ + || echo "(provider may already exist; use 'openshell provider update' to change its config)" +openshell inference set --provider nemo-igw --model "$MODEL" +openshell inference get # verifies the route gateway-side +``` + +`inference set` verifies the endpoint gateway-side, so a clean `inference get` is your confirmation the route is live before you deploy. + +## Step 2: Package the agent as a sandbox image + +Use the checked-in demo config `plugins/nemo-deployments/examples/openshell/agent/config.yaml`. It is a normal NAT ReAct config that is already sandbox-ready: its LLM points at `https://inference.local/v1`, the `general.telemetry` / `nemo_files` tracer block is removed, and the model id is left as a deploy-time param. Its `agent/` folder holds only `config.yaml`, so it is a clean Docker build context. + +Two properties of this config are mandatory for the sandbox path, so preserve them if you start from your own agent instead: + +- **LLM points at `inference.local`.** `base_url: https://inference.local/v1`, and `api_key` is a non-empty placeholder (`not-used`): it MUST be non-empty because the gateway swaps in the real credential, but an empty string makes NAT reject the config. +- **No `general.telemetry` / `nemo_files` block.** The stock `react-agent.yml` example enables the `nemo_files` tracing plugin, which is NOT installed in the packaged sandbox image, so `nat serve` rejects the config and exits, and the deployment lands in `FAILED` with the config error in its status message. Remove the whole `general.telemetry` block before packaging. (The demo config already has it removed.) + +The `wiki` tool is registered in the demo config but its egress to Wikipedia is blocked under the zero-egress policy, so only the `current_datetime` path is exercised below. Trim the `wiki` tool from `tool_names`/`functions` if you want no dead tools. + +If you start from your own agent, copy it into its own clean directory named `config.yaml` and edit the copy (not the repo file): + +```bash +mkdir -p /tmp/igw-agent +cp .yml /tmp/igw-agent/config.yaml +# then: set base_url: https://inference.local/v1, keep api_key non-empty, +# remove the general.telemetry block, leave model_name as ${NEMO_DEFAULT_MODEL} +``` + +The clean directory and the `config.yaml` name both matter (verified the hard way): `nemo agents package` uses the agent file's **parent directory as the Docker build context**, so a shared dir like bare `/tmp` can hold a file that breaks `docker buildx` (`failed to unmarshal ... invalid UTF-8`); and it bakes the file into the image at **`/workspace/`**, so naming it `config.yaml` makes it land at `/workspace/config.yaml`, the exact path Step 3's serve command loads. On a name mismatch `nat serve` never finds its config and exits, and the deployment reports `FAILED` with that error rather than serving. + +Build the image with the OpenShell runtime profile (adds the `sandbox` user and required packages). Use the shipped demo config: + +```bash +nemo agents package \ + --agent plugins/nemo-deployments/examples/openshell/agent/config.yaml \ + --nat-version 1.8.0 \ + --sandbox-runtime openshell \ + --tag nemo-agent-igw:test +``` + +The config is baked into the image at `/workspace/config.yaml`, which is exactly what Step 3's serve command loads. The model id is injected at deploy time via `nat serve --override llms.llm.model_name ` (Step 3), so the unset `${NEMO_DEFAULT_MODEL}` in the baked config is fine. + +Verification: `docker image inspect nemo-agent-igw:test >/dev/null 2>&1 && echo IMAGE_OK || echo IMAGE_MISSING`. If `IMAGE_MISSING`, the build failed; surface the `nemo agents package` output and stop. + +## Step 3: Create the DeploymentConfig + +Describe the container: the image, the `nat serve` command the backend runs inside the sandbox (with the model injected via `--override`), and the port. The agent reaches the LLM through `inference.local`, so there is no proxy handling or egress plumbing here. + +```bash +curl -sf -X POST "$BASE/deployment-configs" -H 'content-type: application/json' -d "{ + \"name\": \"igw-agent-cfg\", + \"containers\": [{ + \"name\": \"agent\", + \"image\": \"nemo-agent-igw:test\", + \"command\": [\"/workspace/.venv/bin/nat\",\"serve\",\"--config_file\",\"/workspace/config.yaml\",\"--override\",\"llms.llm.model_name\",\"$MODEL\",\"--host\",\"0.0.0.0\",\"--port\",\"9000\"], + \"ports\": [{\"containerPort\": 9000, \"name\": \"http\"}] + }] +}" | jq . +``` + +`$MODEL` here MUST be the same id you passed to `openshell inference set` in Step 1, so the model the agent requests matches the route the gateway forces. + +## Step 4: Deploy (executor = openshell-local) + +Sandbox is opt-in: name the executor explicitly (`"executor": "openshell-local"`). The default executor is `local-docker`, so ordinary deployments and the e2e harness keep their normal docker/k8s path. + +```bash +curl -sf -X POST "$BASE/deployments" -H 'content-type: application/json' -d '{ + "name": "igw-agent", + "deployment_config": "igw-agent-cfg", + "executor": "openshell-local" +}' | jq . +``` + +Behind the scenes the `OpenShellDeploymentBackend` generates a pure default-deny SandboxPolicy (read-only `/opt` for the interpreter; read-write `/home/sandbox`, `/tmp`, `/dev/shm` for the Dask runtime `nat serve` spins up; `run_as_user: sandbox`; and, because `platform_egress` is null, no network egress rules at all), creates the sandbox from the image and applies the policy, runs the serve command in the sandbox-writable `serve_workdir` (`/home/sandbox`), and exposes the service through the gateway. The agent's model calls leave the sandbox only through the gateway-routed `inference.local` path. + +## Step 5: Wait for READY and read the endpoint + +The reconciler drives `PENDING -> STARTING -> READY`, typically well under a minute. + +```bash +for i in $(seq 1 30); do + status=$(curl -sf "$BASE/deployments/igw-agent" | jq -r '.status') + echo "$status" + [ "$status" = "READY" ] && break + [ "$status" = "FAILED" ] && { echo "DEPLOY_FAILED"; break; } + sleep 3 +done +URL=$(curl -sf "$BASE/deployments/igw-agent" | jq -r '.endpoints[0].url') +echo "$URL" # e.g. http://--.openshell.localhost:17670/ +``` + +If the status reaches `FAILED` or never leaves `PENDING`/`STARTING`, jump to the recovery table below. Do not proceed to invoke until `READY`. `READY` gates on the sandbox being up, the serve launcher exiting cleanly, and the serve process still being alive on the poll, but not on `nat serve` having finished binding its port, so give serve a moment after `READY`. A serve process that exits (bad config, unresolvable model) flips the deployment to `FAILED` on the next poll, with the tail of its log in the status message. + +## Step 6: Invoke the agent + +Ask a question that forces both a tool call and an LLM call. The agent's LLM call goes to `inference.local`, which the gateway routes to the platform Inference Gateway, so the sandbox makes no direct egress. + +```bash +curl -sf -X POST "${URL}generate" -H 'content-type: application/json' \ + -d '{"input_message":"What is the current date and time? Use your current_datetime tool"}' | jq . +# -> {"value":"The current date and time is 2026-... +0000."} +``` + +Other routes the served workflow exposes: `POST /v1/chat/completions`, `/chat`, `/v1/workflow`, and `/generate/stream`. + +Verification: the response must contain a non-empty `value`. A `502 Service endpoint is not reachable` means the gateway reached the sandbox but `nat serve` (port 9000) is not answering, because it crashed or has not come up. Give it 30-60s and retry; if it persists, read the serve log inside the sandbox: + +```bash +SBX=$(curl -sf "$BASE/deployments/igw-agent" | jq -r '.endpoints[0].url' | sed -E 's#^https?://##; s#--.*##') +openshell sandbox exec --name "$SBX" -- cat /tmp/nemo-serve.log +``` + +The most common root causes are: the `MODEL` is not served on your Inference Gateway (`nemo models list --all-pages`), the config did not bake to `/workspace/config.yaml`, or a leftover `general.telemetry` block made NAT reject the config. An empty or error `value` means the LLM call failed at the gateway or the model misbehaved. Either way, check the recovery table. + +## Step 7: Prove the sandbox has zero direct egress + +The security punchline. Show the sandbox can reach nothing on the network directly: + +```bash +# the sandbox name is nmp-; derive it from the endpoint URL (or read `openshell sandbox list`) +SBX=$(curl -sf "$BASE/deployments/igw-agent" | jq -r '.endpoints[0].url' | sed -E 's#^https?://##; s#--.*##') +openshell sandbox exec --name "$SBX" -- curl -sS -m 5 https://example.com # BLOCKED (policy default-deny) +# a NONZERO exit from the inner curl is the PASS +# meanwhile Step 6's LLM call still worked: it went via inference.local (gateway-routed, not sandbox egress) +``` + +## Step 8: Clean up + +```bash +curl -sf -X DELETE "$BASE/deployments/igw-agent" # tears down the sandbox +curl -sf -X DELETE "$BASE/deployment-configs/igw-agent-cfg" +``` + +Tear down the gateway too when you are done: + +```bash +docker compose -f plugins/nemo-deployments/examples/openshell/docker-compose.yml down +``` + +## Gotchas (all verified against a live gateway) + +- **The `inference.local` route must be wired before invoke.** The agent hits `https://inference.local/v1`; if you never ran `openshell provider create` + `openshell inference set` (Step 1), the gateway has no route and `nat serve`'s model calls fail. `openshell inference get` should show the `nemo-igw` provider and your model. +- **Remove `general.telemetry` / `nemo_files` before packaging.** The stock `react-agent.yml` enables the `nemo_files` tracer, which is not installed in the sandbox image, so `nat serve` rejects the config and the deployment goes `FAILED`. The shipped `agent/config.yaml` already removes it. +- **Keep `api_key` non-empty.** `base_url: https://inference.local/v1` with an empty `api_key` makes NAT reject the config. Use a placeholder like `not-used`; the gateway injects the real credential. +- **Serve workdir must be sandbox-writable.** The image owns `/workspace` as its `agent` user, which the `sandbox` user cannot write. `serve_workdir` is `/home/sandbox`; the policy grants read-write on `/home/sandbox`, `/tmp`, and `/dev/shm` (Dask needs POSIX semaphores under `/dev/shm`). +- **Model choice matters for ReAct, and the model must exist on the Inference Gateway.** Use a `model_name` your `nemo models list --all-pages` actually shows; switchyard names drift between platforms and a missing one makes `nat serve` exit (endpoint 502). A gpt-4o-mini-class model gives clean tool-calling; verbose reasoning models can emit empty content that breaks the ReAct loop. +- **The `wiki_search` tool cannot reach Wikipedia under the zero-egress policy.** It stays registered in the demo config, but its external egress is blocked, so only the `current_datetime` path is exercised. Trim `wiki` from `tool_names`/`functions` if you want no dead tools. +- **Platform must bind 0.0.0.0.** The `inference.local` route is dialed from the sandbox, not from the gateway process: the docker driver writes a literal `172.18.0.1 host.openshell.internal` into each sandbox's `/etc/hosts` (the sandbox network's gateway address), so the platform has to be listening there. On `--host 127.0.0.1` every model call fails with a 503. `0.0.0.0` puts an unauthenticated dev platform on your LAN; do not do it on untrusted wifi. +- **`openshell inference set` validates from the host, not from the sandbox.** It prints `Validated Endpoints` whenever the CLI or gateway can reach the URL, which is true even for a loopback-only platform. A green validation followed by a 503 at invoke is exactly that case: the route is fine, the platform is not reachable at the sandbox network's gateway address. +- **Docker sandboxes need gateway-minted JWTs.** If the gateway logs complain about missing signing keys, run the one-time `generate-certs` command in Pre-flight step 1 before `docker compose up`. +- **`sandbox exec` rejects newlines in args.** To push a file into a sandbox by hand, base64-encode it (`echo | base64 -d > f`) or use `sandbox create --upload `. +- **Endpoint host is docker-driver specific.** The exposed `*.openshell.localhost:17670` URL and the sandbox's `host.openshell.internal` hop to the platform both assume the docker driver. + +## If verification fails + +| Symptom | Cause | Recovery | +| --- | --- | --- | +| `openshell gateway list` / `sandbox list` cannot reach the gateway | Gateway not running | Start the docker-driver gateway (Pre-flight step 1: `generate-certs` once, then `docker compose up -d`); re-check | +| Gateway logs about missing JWT signing keys | `generate-certs` never run | Run the one-time `generate-certs` command (Pre-flight step 1), then restart the gateway | +| `health/ready` not `ready` | Platform down | Route to `nemo-setup`, return when ready | +| Deploy `FAILED` with an unknown/unregistered executor error | Platform started without the executor config | Restart with `nemo services run --host 0.0.0.0 --port 8080 --config packages/nmp_platform/config/local.yaml` (the bundled default config has no openshell executor) | +| `openshell gateway list` shows the endpoint on `:8080` | Gateway collides with the platform port | Recreate the gateway on `:17670` (docker driver, plaintext) per Pre-flight step 1; `:8080` belongs to the platform | +| Deploy stuck `PENDING`/`STARTING` | Image missing sandbox user, or serve command wrong | `openshell sandbox list`; inspect the sandbox; confirm Step 2 used `--sandbox-runtime openshell` | +| Deploy `FAILED` | Policy or gateway rejected the sandbox, the serve launcher exited non-zero, or the serve process died after launch | Check platform logs (`nemo services logs -n 100`) and the deployments reconciler output; the FAILED status message carries the launcher exit detail or the serve log tail. `nemo deployments logs ` returns the workload's own log | +| Invoke returns `502 Service endpoint is not reachable` | `nat serve` is alive but has not bound `:9000` yet | Retry after 30-60s. If it persists while the deployment stays `READY`, read the serve log with `nemo deployments logs `, or in the sandbox: `openshell sandbox exec --name -- cat /tmp/nemo-serve.log`. Common causes: baked config path does not match `--config_file`; a leftover `general.telemetry` block; or a `model_name` the Inference Gateway cannot resolve. Confirm the config is where serve looks (`openshell sandbox exec --name -- ls /workspace` -> `config.yaml`) and that `MODEL` exists (`nemo models list --all-pages`) | +| Invoke returns 503 `inference service unavailable` (in the response or the serve log) | The `inference.local` route resolved, but the gateway's upstream hop to the platform failed | Almost always a platform bound to `127.0.0.1`; restart it with `--host 0.0.0.0`. Confirm the address the sandbox actually dials with `openshell sandbox exec --name -- cat /etc/hosts` (look for `host.openshell.internal`) and check the platform answers there (`curl -sf http://:8080/health/ready`). A clean `openshell inference set` does NOT rule this out: it validates from the host | +| Invoke returns empty/error `value`, or serve log shows connection refused to `inference.local` | `inference.local` route not wired, or model misbehaved | Confirm `openshell inference get` shows the `nemo-igw` provider + your model; re-run Step 1's `provider create` / `inference set`; try a gpt-4o-mini-class model | +| `example.com` reachable in Step 7 | Egress policy not applied | Confirm executor is `openshell-local` and the generated default-deny policy is attached to the sandbox | +| `ModuleNotFoundError: openshell` at deploy, or `python-on-whales` missing at package | workspace extras not installed | `uv sync --package nemo-deployments-plugin --extra openshell` and `uv pip install -e 'plugins/nemo-agents[container]'` | + +Do not claim the deployment succeeded until Step 6 prints a non-empty `value`. + +## Alternate: direct-egress (agent -> host.docker.internal:8080) + +The shipped design routes model traffic through `inference.local` and grants the sandbox no direct egress. If you deliberately want the older direct-egress path instead (the agent calls the Inference Gateway itself at `host.docker.internal:8080`), you must **edit** `packages/nmp_platform/config/local.yaml` to set `platform_egress` on the executor (it is NOT the shipped default): + +```yaml +config: + gateway_endpoint: http://127.0.0.1:17670 + serve_workdir: /home/sandbox + platform_egress: + host: host.docker.internal + port: 8080 + binaries: [/workspace/.venv/bin/python3.13] # the venv python opens the socket, NOT curl +``` + +Then the sandbox's generated policy has a single egress rule to the Inference Gateway. In this branch you also skip Step 1's `inference.local` wiring and instead point the agent config's `base_url` directly at the Inference Gateway: `http://host.docker.internal:8080/apis/inference-gateway/v2/workspaces/default/openai/-/v1`. Note: the connecting binary must be on the egress allowlist (the venv/uv python that makes the LLM call, not curl); a hand-written override that only lists curl silently blocks `nat`. Restart the platform with `--config` after editing so the change takes effect. + +## Related skills + +- `nemo-build-agent`: scaffold, validate, and build the NAT agent image before deploying it here. +- `nemo-try-agent`: query an already-deployed agent. +- `nemo-status`: read-only health dashboard for platform, agents, providers, and models. +- `inference`: register providers and virtual models the agent's Inference Gateway model name resolves to. + +## Known limitations + +- The NAT config is baked into the image; deploy-time config injection (`config_files`) is a follow-up. +- `inference.local` requires the one-time operator wiring (provider + `inference set`) per gateway; the deployment backend does not auto-register it yet. +- The exposed endpoint host and the sandbox's `host.openshell.internal` hop to the platform are docker-driver specific. +- The verified end-to-end flow (agent packaged, deployed through `openshell-local` to READY, invoke returning a real `value` via `inference.local`, direct egress to `example.com` blocked) depends on the `openshell-local` executor wiring landing in your platform config (loaded via `--config`) and the `inference.local` route being wired on the gateway. diff --git a/plugins/nemo-deployments/src/nemo_deployments_plugin/skills/deploy-sandbox/tests.json b/plugins/nemo-deployments/src/nemo_deployments_plugin/skills/deploy-sandbox/tests.json new file mode 100644 index 0000000000..f2398a39a2 --- /dev/null +++ b/plugins/nemo-deployments/src/nemo_deployments_plugin/skills/deploy-sandbox/tests.json @@ -0,0 +1,65 @@ +{ + "skill": "deploy-sandbox", + "tests": [ + { + "type": "explicit", + "prompt": "Use the deploy-sandbox skill to deploy my agent under an OpenShell sandbox.", + "expected_skill": "deploy-sandbox" + }, + { + "type": "explicit", + "prompt": "Run deploy-sandbox for nemo-agent-igw:test with the openshell-local executor.", + "expected_skill": "deploy-sandbox" + }, + { + "type": "explicit", + "prompt": "Deploy this built image as a sandbox and show me the openshell egress policy proof.", + "expected_skill": "deploy-sandbox" + }, + { + "type": "implicit", + "prompt": "Deploy this agent as a sandbox so its model traffic can only reach the platform gateway.", + "expected_skill": "deploy-sandbox" + }, + { + "type": "implicit", + "prompt": "I want my agent to run under a Landlock and default-deny egress policy when deployed.", + "expected_skill": "deploy-sandbox" + }, + { + "type": "implicit", + "prompt": "Govern my agent's network egress at deploy time on the openshell-local executor.", + "expected_skill": "deploy-sandbox" + }, + { + "type": "contextual", + "prompt": "Scaffold a new NAT agent from my spec and generate its workflow YAML.", + "expected_skill_not": "deploy-sandbox" + }, + { + "type": "contextual", + "prompt": "The platform is up and my agent is deployed. Now just ask it a question about refund policy.", + "expected_skill_not": "deploy-sandbox" + }, + { + "type": "contextual", + "prompt": "Install the platform and start the OpenShell gateway before I do anything else.", + "expected_skill_not": "deploy-sandbox" + }, + { + "type": "negative-control", + "prompt": "Build me a new React component for the settings page.", + "expected_skill_not": "deploy-sandbox" + }, + { + "type": "negative-control", + "prompt": "Write a SQL query to aggregate monthly revenue by region.", + "expected_skill_not": "deploy-sandbox" + }, + { + "type": "negative-control", + "prompt": "Summarize this PDF and pull out the action items.", + "expected_skill_not": "deploy-sandbox" + } + ] +} diff --git a/plugins/nemo-deployments/tests/integration/backends/openshell/test_openshell_backend.py b/plugins/nemo-deployments/tests/integration/backends/openshell/test_openshell_backend.py new file mode 100644 index 0000000000..214bf3513b --- /dev/null +++ b/plugins/nemo-deployments/tests/integration/backends/openshell/test_openshell_backend.py @@ -0,0 +1,119 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Integration test for OpenShellDeploymentBackend against a live gateway. + +Mocks only the entities client (feeding a DeploymentConfig directly, like the docker +integration test) and drives the real backend against the OpenShell gateway. Skipped +unless the gateway is reachable. +""" + +from __future__ import annotations + +import asyncio +import os +import socket +import subprocess +import time +from unittest.mock import AsyncMock, MagicMock, patch +from urllib.parse import urlparse + +import pytest +from nemo_deployments_plugin.backends.registry import BACKEND_CLASSES +from nemo_deployments_plugin.constants import MANAGED_BY_LABEL +from nemo_deployments_plugin.entities import Container, ContainerPort, DeploymentConfig + +GATEWAY = os.environ.get("OPENSHELL_GATEWAY_ENDPOINT", "http://127.0.0.1:17670") +IMAGE = os.environ.get("OPENSHELL_TEST_IMAGE", "ghcr.io/nvidia/openshell-community/sandboxes/base:latest") + + +def _gateway_reachable(endpoint: str) -> bool: + parsed = urlparse(endpoint) + host = parsed.hostname or "127.0.0.1" + port = parsed.port or (443 if parsed.scheme == "https" else 80) + try: + with socket.create_connection((host, port), timeout=2): + return True + except OSError: + return False + + +pytestmark = [ + pytest.mark.skipif("openshell" not in BACKEND_CLASSES, reason="OpenShell backend not registered"), + pytest.mark.skipif(not _gateway_reachable(GATEWAY), reason=f"OpenShell gateway not reachable at {GATEWAY}"), +] + + +async def _poll_until_ready(backend, *, attempts: int = 60, delay: float = 2.0): + """Drive read_status until the deployment leaves STARTING, mirroring the reconciler.""" + status = None + for _ in range(attempts): + status = await backend.read_status(workspace="itest", name="rt") + if status.status != "STARTING": + return status + await asyncio.sleep(delay) + return status + + +def _make_backend(): + mock_entities = AsyncMock() + with ( + patch("nemo_deployments_plugin.backends.openshell.backend.AsyncEntitiesResource"), + patch("nemo_deployments_plugin.backends.openshell.backend.NemoEntitiesClient", return_value=mock_entities), + ): + from nemo_deployments_plugin.backends.openshell.backend import OpenShellDeploymentBackend + + backend = OpenShellDeploymentBackend(MagicMock(), {"gateway_endpoint": GATEWAY}) + backend._entities = mock_entities + return backend, mock_entities + + +async def test_openshell_roundtrip_serves_http() -> None: + backend, mock_entities = _make_backend() + mock_entities.get.return_value = DeploymentConfig( + name="rt-cfg", + workspace="itest", + containers=[ + Container( + name="web", + image=IMAGE, + command=["python3", "-m", "http.server"], + args=["8000", "--bind", "0.0.0.0"], + ports=[ContainerPort(containerPort=8000, name="http")], + ) + ], + ) + + try: + created = await backend.create_deployment( + workspace="itest", + name="rt", + config_name="rt-cfg", + labels={"managed-by": MANAGED_BY_LABEL}, + backend_config={}, + ) + assert created.status == "STARTING", created.status_message + + # Provisioning runs on read_status across polls, so the endpoint exists only at READY. + status = await _poll_until_ready(backend) + assert status.status == "READY", status.status_message + assert status.endpoints, "expected an exposed endpoint" + url = status.endpoints[0].url + + code = "000" + for _ in range(20): + proc = subprocess.run( + ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "10", url], + capture_output=True, + text=True, + ) + code = proc.stdout.strip() + if code not in ("000", "502", "503", "504"): + break + time.sleep(1) + assert code == "200", f"served endpoint {url} returned HTTP {code}" + + assert "itest/rt" in await backend.list_managed_deployment_names() + finally: + await backend.delete_deployment("itest", "rt") + backend.shutdown() diff --git a/plugins/nemo-deployments/tests/unit/backends/openshell/conftest.py b/plugins/nemo-deployments/tests/unit/backends/openshell/conftest.py new file mode 100644 index 0000000000..fc7a442594 --- /dev/null +++ b/plugins/nemo-deployments/tests/unit/backends/openshell/conftest.py @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Shared fixtures for OpenShell backend unit tests.""" + +from __future__ import annotations + +from collections.abc import Iterator +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from nemo_deployments_plugin.backends.openshell.backend import OpenShellDeploymentBackend + + +@pytest.fixture +def mock_sdk() -> MagicMock: + return MagicMock() + + +@pytest.fixture +def mock_entities() -> AsyncMock: + return AsyncMock() + + +@pytest.fixture +def mock_stub() -> MagicMock: + stub = MagicMock() + # read_status re-derives endpoints via ListServices; default to none. + stub.ListServices.return_value = MagicMock(services=[]) + return stub + + +@pytest.fixture +def openshell_backend( + mock_sdk: MagicMock, mock_entities: AsyncMock, mock_stub: MagicMock +) -> Iterator[OpenShellDeploymentBackend]: + with ( + patch("nemo_deployments_plugin.backends.openshell.backend.AsyncEntitiesResource"), + patch("nemo_deployments_plugin.backends.openshell.backend.NemoEntitiesClient", return_value=mock_entities), + patch("grpc.insecure_channel", return_value=MagicMock()), + ): + backend = OpenShellDeploymentBackend(mock_sdk, {"gateway_endpoint": "http://127.0.0.1:17670"}) + backend._stub = mock_stub + backend._entities = mock_entities + yield backend diff --git a/plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_backend_mocked.py b/plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_backend_mocked.py new file mode 100644 index 0000000000..67c1092f19 --- /dev/null +++ b/plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_backend_mocked.py @@ -0,0 +1,579 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Mocked-stub unit tests for OpenShellDeploymentBackend.""" + +from __future__ import annotations + +import os +import subprocess +import time +from pathlib import Path +from unittest.mock import AsyncMock, MagicMock, patch + +import grpc +import pytest +from nemo_deployments_plugin.backends.labels import ( + CONFIG_NAME_LABEL, + DEPLOYMENT_NAME_LABEL, + DEPLOYMENT_WORKSPACE_LABEL, + MANAGED_BY_KEY, +) +from nemo_deployments_plugin.backends.openshell.backend import ( + _LAUNCH_MARKER, + _LIVENESS_PROBE, + _MAX_ROUTABLE_NAME_LEN, + _SERVE_DEAD_EXIT, + _SERVE_PENDING_EXIT, + _SERVE_PID_GRACE_SECONDS, + _SERVE_PIDFILE, + OpenShellDeploymentBackend, + _sandbox_name, + _service_name, +) +from nemo_deployments_plugin.backends.registry import BACKEND_CLASSES +from nemo_deployments_plugin.constants import MANAGED_BY_LABEL +from nemo_deployments_plugin.entities import Container, ContainerPort, DeploymentConfig, EnvVar +from nemo_deployments_plugin.secrets import SecretResolutionError + +pytest.importorskip("openshell") # platform-restricted extra; skip where not installed (e.g. CI) + +from openshell._proto import openshell_pb2 as pb # ty: ignore[unresolved-import] + + +class FakeRpcError(grpc.RpcError): + def __init__(self, code: grpc.StatusCode, details: str = "boom") -> None: + self._code = code + self._details = details + + def code(self) -> grpc.StatusCode: + return self._code + + def details(self) -> str: + return self._details + + +def _not_found() -> FakeRpcError: + return FakeRpcError(grpc.StatusCode.NOT_FOUND, "missing") + + +def _sandbox( + phase: int, *, sandbox_id: str = "sid", labels: dict | None = None, config_name: str = "cfg1" +) -> MagicMock: + lbls = dict(labels or {}) + if config_name: + lbls[CONFIG_NAME_LABEL] = config_name + return MagicMock( + sandbox=MagicMock( + metadata=MagicMock(id=sandbox_id, labels=lbls), + status=MagicMock(phase=phase, conditions=[]), + ) + ) + + +def _config( + *, + command: tuple[str, ...] = ("python3", "-m", "http.server"), + args: tuple[str, ...] = ("8000",), + with_port: bool = True, +) -> DeploymentConfig: + ports = [ContainerPort(containerPort=8000, name="http")] if with_port else [] + return DeploymentConfig( + name="cfg1", + workspace="default", + containers=[Container(name="web", image="img:latest", command=list(command), args=list(args), ports=ports)], + ) + + +def _exec_events(exit_code: int, *, stdout: str = "", stderr: str = "") -> list[MagicMock]: + """Build a realistic ExecSandbox event stream: optional stdout/stderr then an exit event.""" + events: list[MagicMock] = [] + if stdout: + out = MagicMock() + out.HasField.side_effect = lambda field: field == "stdout" + out.stdout.data = stdout.encode() + events.append(out) + if stderr: + err = MagicMock() + err.HasField.side_effect = lambda field: field == "stderr" + err.stderr.data = stderr.encode() + events.append(err) + exit_event = MagicMock() + exit_event.HasField.side_effect = lambda field: field == "exit" + exit_event.exit.exit_code = exit_code + events.append(exit_event) + return events + + +def _config_with_env(env: list[EnvVar]) -> DeploymentConfig: + return DeploymentConfig( + name="cfg1", + workspace="default", + containers=[ + Container( + name="web", + image="img:latest", + command=["python3", "-m", "http.server"], + args=["8000"], + ports=[ContainerPort(containerPort=8000, name="http")], + env=env, + ) + ], + ) + + +def test_registry_contains_openshell() -> None: + assert BACKEND_CLASSES["openshell"] is OpenShellDeploymentBackend + + +def test_sandbox_name_within_limit_and_deterministic() -> None: + name = _sandbox_name("a-long-workspace-name", "a-long-deployment-name") + assert name.startswith("nmp-") + assert len(name) <= _MAX_ROUTABLE_NAME_LEN + assert name == _sandbox_name("a-long-workspace-name", "a-long-deployment-name") + assert name != _sandbox_name("other", "name") + + +def test_service_name_clamped_and_distinct() -> None: + short = _service_name(ContainerPort(containerPort=8000, name="http")) + assert short == "http" + + unnamed = _service_name(ContainerPort(containerPort=8000)) + assert unnamed == "port-8000" + + long_a = _service_name(ContainerPort(containerPort=8000, name="a-very-long-port-name-one")) + long_b = _service_name(ContainerPort(containerPort=8001, name="a-very-long-port-name-two")) + assert len(long_a) <= _MAX_ROUTABLE_NAME_LEN + assert not long_a.endswith("-") + assert long_a != long_b, "truncation must not collapse distinct port names onto one service" + + +@pytest.mark.parametrize( + ("phase", "expected"), + [ + # READY is not here: a READY sandbox triggers the read_status provisioning state + # machine (launch/expose), covered by the read_status provisioning tests below. + (pb.SANDBOX_PHASE_PROVISIONING, "STARTING"), + (pb.SANDBOX_PHASE_ERROR, "FAILED"), + (pb.SANDBOX_PHASE_DELETING, "DELETING"), + ], +) +async def test_read_status_maps_phase( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock, phase: int, expected: str +) -> None: + mock_stub.GetSandbox.return_value = _sandbox(phase) + update = await openshell_backend.read_status(workspace="default", name="srv") + assert update.status == expected + + +async def test_read_status_includes_exposed_endpoints( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock +) -> None: + mock_stub.GetSandbox.return_value = _sandbox(pb.SANDBOX_PHASE_READY) + svc = MagicMock(url="http://nmp-x--http.openshell.localhost:18080/") + svc.endpoint.service_name = "http" + mock_stub.ListServices.return_value = MagicMock(services=[svc]) + + update = await openshell_backend.read_status(workspace="default", name="srv") + + assert update.status == "READY" + assert [e.url for e in update.endpoints] == ["http://nmp-x--http.openshell.localhost:18080/"] + assert update.endpoints[0].name == "http" + + +async def test_read_status_not_found_is_lost( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock +) -> None: + mock_stub.GetSandbox.side_effect = _not_found() + update = await openshell_backend.read_status(workspace="default", name="srv") + assert update.status == "LOST" + + +async def test_create_requires_serve_command( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock, mock_entities: AsyncMock +) -> None: + mock_stub.GetSandbox.side_effect = _not_found() # not already present + mock_entities.get.return_value = _config(command=(), args=()) + update = await openshell_backend.create_deployment( + workspace="default", name="srv", config_name="cfg1", labels={}, backend_config={} + ) + assert update.status == "FAILED" + assert "command" in update.status_message + + +async def test_create_returns_starting_after_create( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock, mock_entities: AsyncMock +) -> None: + # Create returns STARTING right after CreateSandbox; provisioning (launch/expose) is + # deferred to read_status so a slow sandbox never blocks the reconcile loop. + mock_entities.get.return_value = _config() + mock_stub.GetSandbox.side_effect = _not_found() # idempotency probe: absent + mock_stub.CreateSandbox.return_value = MagicMock() + + update = await openshell_backend.create_deployment( + workspace="default", name="srv", config_name="cfg1", labels={"managed-by": MANAGED_BY_LABEL}, backend_config={} + ) + + assert update.status == "STARTING" + mock_stub.CreateSandbox.assert_called_once() + mock_stub.ExecSandbox.assert_not_called() + mock_stub.ExposeService.assert_not_called() + + +async def test_delete_idempotent_when_missing( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock +) -> None: + mock_stub.DeleteSandbox.side_effect = _not_found() + update = await openshell_backend.delete_deployment("default", "srv") + assert update.status == "SUCCEEDED" + + +async def test_list_managed_deployment_names( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock +) -> None: + sandbox = MagicMock( + metadata=MagicMock( + labels={ + MANAGED_BY_KEY: MANAGED_BY_LABEL, + DEPLOYMENT_WORKSPACE_LABEL: "default", + DEPLOYMENT_NAME_LABEL: "srv", + } + ) + ) + mock_stub.ListSandboxes.return_value = MagicMock(sandboxes=[sandbox]) + names = await openshell_backend.list_managed_deployment_names() + assert names == ["default/srv"] + + +async def test_get_logs_formats_lines(openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock) -> None: + # No workload log yet (the tail exec reports no exit code) -> supervisor logs. + mock_stub.GetSandbox.return_value = _sandbox(pb.SANDBOX_PHASE_READY, sandbox_id="sid") + log_line = MagicMock(source="sandbox") + log_line.message = "serving on 8000" + mock_stub.GetSandboxLogs.return_value = MagicMock(logs=[log_line], buffer_total=1) + + result = await openshell_backend.get_logs(workspace="default", name="srv", tail=10) + assert result.lines == ["[sandbox] serving on 8000"] + + +async def test_get_logs_returns_workload_log( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock +) -> None: + mock_stub.GetSandbox.return_value = _sandbox(pb.SANDBOX_PHASE_READY, sandbox_id="sid") + mock_stub.ExecSandbox.return_value = _exec_events(0, stdout="starting nat\nlistening on 8000\n") + + result = await openshell_backend.get_logs(workspace="default", name="srv", tail=10) + + assert result.lines == ["starting nat", "listening on 8000"] + mock_stub.GetSandboxLogs.assert_not_called() + + +async def test_create_volume_unsupported(openshell_backend: OpenShellDeploymentBackend) -> None: + update = await openshell_backend.create_volume( + workspace="default", name="data", size="1Gi", access_modes=["ReadWriteOnce"], backend_config={} + ) + assert update.status == "FAILED" + + +# --- #2: secrets resolved; unresolved env fails FAILED, no silent drop --- + + +async def test_create_resolves_plain_env_into_sandbox( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock, mock_entities: AsyncMock +) -> None: + mock_entities.get.return_value = _config_with_env([EnvVar(name="FOO", value="bar")]) + mock_stub.GetSandbox.side_effect = _not_found() # idempotency probe: absent + mock_stub.CreateSandbox.return_value = MagicMock() + + update = await openshell_backend.create_deployment( + workspace="default", name="srv", config_name="cfg1", labels={}, backend_config={} + ) + + assert update.status == "STARTING" + spec = mock_stub.CreateSandbox.call_args.args[0].spec + assert spec.environment["FOO"] == "bar" + + +async def test_create_fails_on_unresolved_value_from( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock, mock_entities: AsyncMock +) -> None: + mock_stub.GetSandbox.side_effect = _not_found() # not already present + mock_entities.get.return_value = _config_with_env( + [EnvVar(name="POD_IP", valueFrom={"fieldRef": {"fieldPath": "status.podIP"}})] + ) + + update = await openshell_backend.create_deployment( + workspace="default", name="srv", config_name="cfg1", labels={}, backend_config={} + ) + + assert update.status == "FAILED" + assert "POD_IP" in update.status_message + mock_stub.CreateSandbox.assert_not_called() + + +async def test_create_fails_when_secret_resolution_errors( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock, mock_entities: AsyncMock +) -> None: + mock_stub.GetSandbox.side_effect = _not_found() + mock_entities.get.return_value = _config() + with patch( + "nemo_deployments_plugin.backends.openshell.backend.resolve_deployment_config_secrets", + side_effect=SecretResolutionError("Platform secret access failed"), + ): + update = await openshell_backend.create_deployment( + workspace="default", name="srv", config_name="cfg1", labels={}, backend_config={} + ) + + assert update.status == "FAILED" + assert "secret" in update.status_message.lower() + mock_stub.CreateSandbox.assert_not_called() + + +# --- #6/#7/#8: read_status drives provisioning; failures clean up the sandbox --- + + +async def test_read_status_launches_serve_when_ready( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock, mock_entities: AsyncMock +) -> None: + # READY, no services yet, marker absent -> launch serve, report STARTING. + mock_entities.get.return_value = _config() + mock_stub.GetSandbox.return_value = _sandbox(pb.SANDBOX_PHASE_READY) + mock_stub.ExecSandbox.side_effect = [_exec_events(1), _exec_events(0)] # marker absent, launch ok + + update = await openshell_backend.read_status(workspace="default", name="srv") + + assert update.status == "STARTING" + mock_stub.ExposeService.assert_not_called() + + +async def test_read_status_transient_rpc_error_is_unknown( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock, mock_entities: AsyncMock +) -> None: + # A transient RPC error on the serve-marker probe must not escape read_status; it maps to + # UNKNOWN (retry next poll) rather than deleting the sandbox or relaunching. + mock_entities.get.return_value = _config() + mock_stub.GetSandbox.return_value = _sandbox(pb.SANDBOX_PHASE_READY) + mock_stub.ExecSandbox.side_effect = FakeRpcError(grpc.StatusCode.UNAVAILABLE, "gateway blip") + + update = await openshell_backend.read_status(workspace="default", name="srv") + + assert update.status == "UNKNOWN" + mock_stub.DeleteSandbox.assert_not_called() + + +async def test_read_status_exposes_after_launch( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock, mock_entities: AsyncMock +) -> None: + # READY, no services, marker present -> expose ports -> READY with endpoint. + mock_entities.get.return_value = _config() + mock_stub.GetSandbox.return_value = _sandbox(pb.SANDBOX_PHASE_READY) + mock_stub.ExecSandbox.return_value = _exec_events(0) # marker present + mock_stub.ExposeService.return_value = MagicMock(url="http://nmp-x--http.openshell.localhost:17670/") + + update = await openshell_backend.read_status(workspace="default", name="srv") + + assert update.status == "READY" + assert [e.url for e in update.endpoints] == ["http://nmp-x--http.openshell.localhost:17670/"] + + +async def test_read_status_fails_when_serve_process_died( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock +) -> None: + # Ports exposed but the workload is gone. + mock_stub.GetSandbox.return_value = _sandbox(pb.SANDBOX_PHASE_READY) + svc = MagicMock(url="http://nmp-x--http.openshell.localhost:17670/") + svc.endpoint.service_name = "http" + mock_stub.ListServices.return_value = MagicMock(services=[svc]) + mock_stub.ExecSandbox.side_effect = [ + _exec_events(9), # liveness probe: pid is gone + _exec_events(0, stdout="ValueError: unknown model\n"), # log tail + ] + + update = await openshell_backend.read_status(workspace="default", name="srv") + + assert update.status == "FAILED" + assert "unknown model" in update.status_message + mock_stub.DeleteSandbox.assert_not_called() # kept so get_logs can still read the log + + +async def test_read_status_does_not_expose_a_dead_workload( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock, mock_entities: AsyncMock +) -> None: + # Launched on an earlier poll, process already dead. + mock_entities.get.return_value = _config() + mock_stub.GetSandbox.return_value = _sandbox(pb.SANDBOX_PHASE_READY) + mock_stub.ExecSandbox.side_effect = [ + _exec_events(0), # marker present + _exec_events(9), # liveness probe: pid is gone + _exec_events(0, stdout="Traceback: boom\n"), # log tail + ] + + update = await openshell_backend.read_status(workspace="default", name="srv") + + assert update.status == "FAILED" + assert "boom" in update.status_message + mock_stub.ExposeService.assert_not_called() + + +async def test_read_status_stays_ready_when_liveness_is_undecidable( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock +) -> None: + # Already serving and the probe cannot decide: only proof of death demotes a + # deployment, so an undecided probe must not flap it. + mock_stub.GetSandbox.return_value = _sandbox(pb.SANDBOX_PHASE_READY) + svc = MagicMock(url="http://nmp-x--http.openshell.localhost:17670/") + svc.endpoint.service_name = "http" + mock_stub.ListServices.return_value = MagicMock(services=[svc]) + mock_stub.ExecSandbox.return_value = _exec_events(10) + + update = await openshell_backend.read_status(workspace="default", name="srv") + + assert update.status == "READY" + + +async def test_read_status_withholds_expose_until_the_serve_pid_exists( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock, mock_entities: AsyncMock +) -> None: + # The launcher marks the launch unconditionally, so a marker alone proves nothing. + # Until a pid is recorded the deployment stays STARTING rather than advancing to + # expose and reporting READY over a workload that may never have started. + mock_entities.get.return_value = _config() + mock_stub.GetSandbox.return_value = _sandbox(pb.SANDBOX_PHASE_READY) + mock_stub.ExecSandbox.side_effect = [ + _exec_events(0), # marker present + _exec_events(10), # liveness probe: no pidfile yet, still inside the grace window + ] + + update = await openshell_backend.read_status(workspace="default", name="srv") + + assert update.status == "STARTING" + mock_stub.ExposeService.assert_not_called() + + +async def test_read_status_fails_when_the_serve_pid_never_appears( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock, mock_entities: AsyncMock +) -> None: + # Past the grace window a missing pidfile is proof the launcher's background shell + # never ran, which is a failed deployment rather than a slow one. + mock_entities.get.return_value = _config() + mock_stub.GetSandbox.return_value = _sandbox(pb.SANDBOX_PHASE_READY) + mock_stub.ExecSandbox.side_effect = [ + _exec_events(0), # marker present + _exec_events(9), # liveness probe: grace elapsed with no pidfile + _exec_events(0, stdout="sh: 1: nat: not found\n"), # log tail + ] + + update = await openshell_backend.read_status(workspace="default", name="srv") + + assert update.status == "FAILED" + assert "not found" in update.status_message + mock_stub.ExposeService.assert_not_called() + + +async def test_read_status_deletes_sandbox_when_launch_rpc_fails( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock, mock_entities: AsyncMock +) -> None: + mock_entities.get.return_value = _config() + mock_stub.GetSandbox.return_value = _sandbox(pb.SANDBOX_PHASE_READY) + mock_stub.ExecSandbox.side_effect = [_exec_events(1), FakeRpcError(grpc.StatusCode.INTERNAL, "exec boom")] + + update = await openshell_backend.read_status(workspace="default", name="srv") + + assert update.status == "FAILED" + mock_stub.DeleteSandbox.assert_called_once() + + +async def test_read_status_fails_on_nonzero_launch_exit( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock, mock_entities: AsyncMock +) -> None: + mock_entities.get.return_value = _config() + mock_stub.GetSandbox.return_value = _sandbox(pb.SANDBOX_PHASE_READY) + mock_stub.ExecSandbox.side_effect = [_exec_events(1), _exec_events(127, stderr="sh: setsid: not found")] + + update = await openshell_backend.read_status(workspace="default", name="srv") + + assert update.status == "FAILED" + assert "127" in update.status_message + assert "setsid" in update.status_message + mock_stub.ExposeService.assert_not_called() + mock_stub.DeleteSandbox.assert_called_once() + + +async def test_read_status_deletes_sandbox_when_expose_fails( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock, mock_entities: AsyncMock +) -> None: + mock_entities.get.return_value = _config() + mock_stub.GetSandbox.return_value = _sandbox(pb.SANDBOX_PHASE_READY) + mock_stub.ExecSandbox.return_value = _exec_events(0) # marker present -> expose path + mock_stub.ExposeService.side_effect = FakeRpcError(grpc.StatusCode.INTERNAL, "expose boom") + + update = await openshell_backend.read_status(workspace="default", name="srv") + + assert update.status == "FAILED" + mock_stub.DeleteSandbox.assert_called_once() + + +async def test_read_status_cleanup_swallows_delete_error( + openshell_backend: OpenShellDeploymentBackend, mock_stub: MagicMock, mock_entities: AsyncMock +) -> None: + # Cleanup must not mask the original provisioning failure even if DeleteSandbox errors. + mock_entities.get.return_value = _config() + mock_stub.GetSandbox.return_value = _sandbox(pb.SANDBOX_PHASE_READY) + mock_stub.ExecSandbox.return_value = _exec_events(0) + mock_stub.ExposeService.side_effect = FakeRpcError(grpc.StatusCode.INTERNAL, "expose boom") + mock_stub.DeleteSandbox.side_effect = FakeRpcError(grpc.StatusCode.INTERNAL, "delete boom") + + update = await openshell_backend.read_status(workspace="default", name="srv") + + assert update.status == "FAILED" + assert "expose" in update.status_message.lower() + mock_stub.DeleteSandbox.assert_called_once() + + +def _run_probe(tmp_path: Path, *, pid: str | None, marker: str | None) -> int: + """Run the real liveness probe against tmp files, returning its exit code. + + The probe is shell, not Python, so the three-state logic is only meaningful when + executed by a shell. Paths are rewritten to tmp_path so the test never touches the + constants' real locations. + """ + pidfile = tmp_path / "serve.pid" + marker_file = tmp_path / "serve.launched" + if pid is not None: + pidfile.write_text(pid) + if marker is not None: + marker_file.write_text(marker) + script = _LIVENESS_PROBE.replace(_SERVE_PIDFILE, str(pidfile)).replace(_LAUNCH_MARKER, str(marker_file)) + return subprocess.run(["/bin/sh", "-c", script], capture_output=True, check=False).returncode + + +def test_liveness_probe_reports_a_live_pid_as_alive(tmp_path: Path) -> None: + # The test process itself: alive, and owned by this user so kill -0 is permitted + # (an unowned pid fails with EPERM and would read as dead). + assert _run_probe(tmp_path, pid=str(os.getpid()), marker=str(int(time.time()))) == 0 + + +def test_liveness_probe_reports_a_reaped_pid_as_dead(tmp_path: Path) -> None: + dead = subprocess.Popen(["/bin/sh", "-c", "exit 0"]) + dead.wait() + assert _run_probe(tmp_path, pid=str(dead.pid), marker=str(int(time.time()))) == _SERVE_DEAD_EXIT + + +def test_liveness_probe_is_pending_while_the_pidfile_is_still_expected(tmp_path: Path) -> None: + # Launched a moment ago with no pid recorded yet: a slow start, not a failure. + assert _run_probe(tmp_path, pid=None, marker=str(int(time.time()))) == _SERVE_PENDING_EXIT + + +def test_liveness_probe_ages_a_missing_pidfile_into_dead(tmp_path: Path) -> None: + # This is the hole the three-state probe closes: the launcher marks the launch + # unconditionally, so without the grace check a pidfile that never appears reads as + # healthy forever and the deployment advances to READY over nothing. + stale = int(time.time()) - _SERVE_PID_GRACE_SECONDS - 1 + assert _run_probe(tmp_path, pid=None, marker=str(stale)) == _SERVE_DEAD_EXIT + + +@pytest.mark.parametrize("marker", [None, "", "not-a-timestamp"]) +def test_liveness_probe_is_pending_when_the_marker_is_unusable(tmp_path: Path, marker: str | None) -> None: + # No usable launch time means no basis to call it dead, so stay undecided rather + # than fail a deployment on a garbled marker. + assert _run_probe(tmp_path, pid=None, marker=marker) == _SERVE_PENDING_EXIT diff --git a/plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_executor_config.py b/plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_executor_config.py new file mode 100644 index 0000000000..d977c1e7aa --- /dev/null +++ b/plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_executor_config.py @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for OpenShellExecutorConfig.""" + +from __future__ import annotations + +import pytest +from nemo_deployments_plugin.backends.openshell.config import OpenShellExecutorConfig +from pydantic import ValidationError + + +def test_defaults_target_local_plaintext() -> None: + config = OpenShellExecutorConfig() + assert config.grpc_target() == "127.0.0.1:17670" + assert config.use_insecure() is True + + +def test_https_endpoint_implies_tls() -> None: + config = OpenShellExecutorConfig(gateway_endpoint="https://gw.example:443") + assert config.grpc_target() == "gw.example:443" + assert config.use_insecure() is False + + +def test_insecure_override_forces_plaintext() -> None: + config = OpenShellExecutorConfig(gateway_endpoint="https://gw.example:443", insecure=True) + assert config.use_insecure() is True + + +def test_invalid_endpoint_rejected() -> None: + with pytest.raises(ValidationError): + OpenShellExecutorConfig(gateway_endpoint="not-a-url") + + +def test_platform_egress_defaults_to_local_docker() -> None: + config = OpenShellExecutorConfig() + assert config.platform_egress is not None + assert config.platform_egress.host == "host.docker.internal" + assert config.platform_egress.port == 8080 + assert config.platform_egress.binaries == [] # empty -> backend uses its default set + + +def test_platform_egress_is_overridable() -> None: + config = OpenShellExecutorConfig.model_validate( + {"platform_egress": {"host": "10.0.0.5", "port": 9000, "binaries": ["/opt/py/bin/python3.13"]}} + ) + assert config.platform_egress is not None + assert config.platform_egress.host == "10.0.0.5" + assert config.platform_egress.port == 9000 + assert config.platform_egress.binaries == ["/opt/py/bin/python3.13"] + + +def test_platform_egress_can_be_disabled() -> None: + # null platform_egress -> the sandbox gets no direct egress at all, correct for + # gateway-managed inference (inference.local), which the supervisor brokers outside + # the policy's egress rules. + config = OpenShellExecutorConfig(platform_egress=None) + assert config.platform_egress is None + + +def test_unknown_config_key_is_rejected() -> None: + # extra="forbid": a typo'd key (platform_egres) becomes a loud error instead of silently + # falling back to the default egress, which would be a security-relevant divergence. + with pytest.raises(ValidationError): + OpenShellExecutorConfig.model_validate({"gateway_endpoint": "http://127.0.0.1:17670", "platform_egres": None}) + + +def test_platform_egress_rejects_unknown_key() -> None: + with pytest.raises(ValidationError): + OpenShellExecutorConfig.model_validate({"platform_egress": {"host": "h", "prtocol": "rest"}}) + + +def test_platform_egress_accepts_proto_value_sets() -> None: + # Value sets follow openshell/proto/sandbox.proto, not the shorter summaries: "graphql"/"" + # protocol and "passthrough" tls are valid and must not be rejected. + config = OpenShellExecutorConfig.model_validate( + {"platform_egress": {"protocol": "graphql", "tls": "passthrough", "access": "read-only"}} + ) + assert config.platform_egress is not None + assert (config.platform_egress.protocol, config.platform_egress.tls, config.platform_egress.access) == ( + "graphql", + "passthrough", + "read-only", + ) + + +def test_platform_egress_rejects_invalid_protocol() -> None: + with pytest.raises(ValidationError): + OpenShellExecutorConfig.model_validate({"platform_egress": {"protocol": "ftp"}}) diff --git a/plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_policy.py b/plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_policy.py new file mode 100644 index 0000000000..0bad5d1bf4 --- /dev/null +++ b/plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_policy.py @@ -0,0 +1,319 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the policy YAML -> SandboxPolicy proto builder.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + +import pytest +from nemo_deployments_plugin.backends.openshell.policy import ( + PLATFORM_EGRESS_KEY, + PlatformEgress, + SandboxFilesystem, + build_sandbox_policy, + generate_policy_dict, + generate_sandbox_policy, + inject_platform_egress, + load_sandbox_policy, + normalize_loaded_policy, +) + +# Building a real SandboxPolicy proto needs the platform-restricted 'openshell' extra; +# skip those cases where it isn't installed (e.g. CI). The dict-only helpers below +# (generate_policy_dict / inject_platform_egress) run everywhere. +requires_openshell = pytest.mark.skipif( + importlib.util.find_spec("openshell") is None, + reason="openshell extra not installed (platform-restricted wheel)", +) + +_FULL = { + "version": 1, + "filesystem_policy": { + "include_workdir": True, + "read_only": ["/usr", "/opt"], + "read_write": ["/home/sandbox", "/dev/shm"], + }, + "landlock": {"compatibility": "best_effort"}, + "process": {"run_as_user": "sandbox", "run_as_group": "sandbox"}, + "network_policies": { + "igw": { + "name": "nemo-igw", + "endpoints": [ + { + "host": "host.docker.internal", + "port": 8080, + "protocol": "rest", + "enforcement": "enforce", + "access": "full", + } + ], + "binaries": [{"path": "/usr/bin/curl"}, {"path": "/workspace/.venv/bin/python3.13"}], + } + }, +} + + +@requires_openshell +def test_build_sandbox_policy_full() -> None: + policy = build_sandbox_policy(_FULL) + + assert policy.version == 1 + assert policy.filesystem.include_workdir is True + assert list(policy.filesystem.read_only) == ["/usr", "/opt"] + assert "/dev/shm" in policy.filesystem.read_write + assert policy.landlock.compatibility == "best_effort" + assert policy.process.run_as_user == "sandbox" + assert policy.process.run_as_group == "sandbox" + + assert "igw" in policy.network_policies + rule = policy.network_policies["igw"] + assert rule.name == "nemo-igw" + assert rule.endpoints[0].host == "host.docker.internal" + assert rule.endpoints[0].port == 8080 + assert rule.endpoints[0].access == "full" + assert [b.path for b in rule.binaries] == ["/usr/bin/curl", "/workspace/.venv/bin/python3.13"] + + +@requires_openshell +def test_build_sandbox_policy_minimal() -> None: + policy = build_sandbox_policy({}) + assert policy.version == 1 + assert not policy.filesystem.read_only + assert not policy.network_policies + + +@requires_openshell +def test_build_sandbox_policy_accepts_filesystem_alias() -> None: + # proto field name 'filesystem' is accepted as well as the YAML 'filesystem_policy' + policy = build_sandbox_policy({"filesystem": {"read_write": ["/tmp"]}}) + assert list(policy.filesystem.read_write) == ["/tmp"] + + +def _egress() -> PlatformEgress: + return PlatformEgress(host="host.docker.internal", port=8080, binaries=("/workspace/.venv/bin/python3.13",)) + + +def test_generate_policy_dict_is_default_deny() -> None: + policy = generate_policy_dict(filesystem=SandboxFilesystem(), egress=_egress()) + + # exactly one network rule: the platform egress. everything else denied. + assert list(policy["network_policies"]) == [PLATFORM_EGRESS_KEY] + rule = policy["network_policies"][PLATFORM_EGRESS_KEY] + assert rule["endpoints"][0] == { + "host": "host.docker.internal", + "port": 8080, + "protocol": "rest", + "enforcement": "enforce", + "access": "full", + } + assert [b["path"] for b in rule["binaries"]] == ["/workspace/.venv/bin/python3.13"] + # sandbox exec/write paths + run-as come from the filesystem shape. + assert "/opt" in policy["filesystem_policy"]["read_only"] + assert "/workspace" in policy["filesystem_policy"]["read_write"] + assert "/home/sandbox" in policy["filesystem_policy"]["read_write"] + assert "/dev/shm" in policy["filesystem_policy"]["read_write"] + assert policy["process"]["run_as_user"] == "sandbox" + + +def test_generate_policy_dict_no_egress_is_pure_default_deny() -> None: + # egress=None (gateway-managed inference.local) -> no network rules at all. + policy = generate_policy_dict(filesystem=SandboxFilesystem(), egress=None) + assert policy["network_policies"] == {} + # filesystem/process shape still applied. + assert "/opt" in policy["filesystem_policy"]["read_only"] + assert policy["process"]["run_as_user"] == "sandbox" + + +@requires_openshell +def test_generate_sandbox_policy_no_egress() -> None: + policy = generate_sandbox_policy(filesystem=SandboxFilesystem(), egress=None) + assert not policy.network_policies + + +@requires_openshell +def test_generate_sandbox_policy_builds_proto() -> None: + policy = generate_sandbox_policy(filesystem=SandboxFilesystem(), egress=_egress()) + assert PLATFORM_EGRESS_KEY in policy.network_policies + rule = policy.network_policies[PLATFORM_EGRESS_KEY] + assert rule.endpoints[0].host == "host.docker.internal" + assert rule.endpoints[0].port == 8080 + assert [b.path for b in rule.binaries] == ["/workspace/.venv/bin/python3.13"] + assert policy.process.run_as_user == "sandbox" + + +def test_inject_platform_egress_overwrites_user_rule_and_keeps_others() -> None: + # a user policy that tries to point the platform key elsewhere, plus an unrelated rule. + policy = { + "network_policies": { + PLATFORM_EGRESS_KEY: {"name": "evil", "endpoints": [{"host": "attacker.example", "port": 443}]}, + "pypi": {"name": "pypi", "endpoints": [{"host": "pypi.org", "port": 443}]}, + } + } + inject_platform_egress(policy, _egress()) + + rule = policy["network_policies"][PLATFORM_EGRESS_KEY] + assert rule["endpoints"][0]["host"] == "host.docker.internal" # ours, not the user's + assert "pypi" in policy["network_policies"] # unrelated rule preserved + + +def test_inject_platform_egress_adds_rule_when_absent() -> None: + policy: dict = {"version": 1} + inject_platform_egress(policy, _egress()) + assert PLATFORM_EGRESS_KEY in policy["network_policies"] + + +def test_platform_egress_tls_omitted_when_empty() -> None: + plain = generate_policy_dict(filesystem=SandboxFilesystem(), egress=_egress()) + assert "tls" not in plain["network_policies"][PLATFORM_EGRESS_KEY]["endpoints"][0] + + secure = generate_policy_dict( + filesystem=SandboxFilesystem(), + egress=PlatformEgress(host="h", port=443, tls="terminate"), + ) + assert secure["network_policies"][PLATFORM_EGRESS_KEY]["endpoints"][0]["tls"] == "terminate" + + +@requires_openshell +def test_load_sandbox_policy_reads_yaml(tmp_path: Path) -> None: + policy_file = tmp_path / "policy.yaml" + policy_file.write_text( + "version: 1\nprocess:\n run_as_user: sandbox\n run_as_group: sandbox\n", + encoding="utf-8", + ) + policy = load_sandbox_policy(str(policy_file)) + assert policy.process.run_as_user == "sandbox" + + +# --- finding #5: fail-open on loaded-policy + Landlock paths --- + + +@requires_openshell +def test_network_rule_defaults_enforcement_to_enforce() -> None: + # a loaded/override rule that omits `enforcement` must block (proto default is audit). + policy = build_sandbox_policy({"network_policies": {"r": {"endpoints": [{"host": "h", "port": 8080}]}}}) + assert policy.network_policies["r"].endpoints[0].enforcement == "enforce" + + +@requires_openshell +def test_build_sandbox_policy_full_keeps_explicit_enforcement() -> None: + # explicit enforcement is preserved (regression guard for the new default). + policy = build_sandbox_policy(_FULL) + assert policy.network_policies["igw"].endpoints[0].enforcement == "enforce" + + +def test_normalize_loaded_policy_injects_sandbox_process_when_absent() -> None: + data = normalize_loaded_policy({"version": 1}) + assert data["process"]["run_as_user"] == "sandbox" + assert data["process"]["run_as_group"] == "sandbox" + + +def test_normalize_loaded_policy_injects_landlock_when_absent() -> None: + data = normalize_loaded_policy({"version": 1}) + assert data["landlock"]["compatibility"] == "best_effort" + + +def test_normalize_loaded_policy_preserves_authored_blocks() -> None: + data = normalize_loaded_policy( + { + "process": {"run_as_user": "custom", "run_as_group": "custom"}, + "landlock": {"compatibility": "hard_requirement"}, + } + ) + assert data["process"]["run_as_user"] == "custom" + assert data["landlock"]["compatibility"] == "hard_requirement" + + +def test_normalize_loaded_policy_honors_landlock_override() -> None: + data = normalize_loaded_policy({"version": 1}, landlock_compatibility="hard_requirement") + assert data["landlock"]["compatibility"] == "hard_requirement" + + +def test_normalize_loaded_policy_fills_partial_process() -> None: + data = normalize_loaded_policy({"process": {"run_as_group": "grp"}}) + assert data["process"]["run_as_user"] == "sandbox" # missing key filled + assert data["process"]["run_as_group"] == "grp" # authored key kept + + +@requires_openshell +def test_load_sandbox_policy_injects_defaults_when_blocks_absent(tmp_path: Path) -> None: + policy_file = tmp_path / "policy.yaml" + policy_file.write_text("version: 1\n", encoding="utf-8") + policy = load_sandbox_policy(str(policy_file)) + assert policy.process.run_as_user == "sandbox" + assert policy.process.run_as_group == "sandbox" + assert policy.landlock.compatibility == "best_effort" + + +def test_generate_policy_dict_honors_landlock_compatibility() -> None: + # the knob threads through SandboxFilesystem into the generated policy dict. + policy = generate_policy_dict(filesystem=SandboxFilesystem(landlock_compatibility="hard_requirement"), egress=None) + assert policy["landlock"]["compatibility"] == "hard_requirement" + + +def test_executor_config_landlock_default_and_validation() -> None: + from nemo_deployments_plugin.backends.openshell.config import OpenShellExecutorConfig + + assert OpenShellExecutorConfig().landlock_compatibility == "best_effort" + assert ( + OpenShellExecutorConfig(landlock_compatibility="hard_requirement").landlock_compatibility == "hard_requirement" + ) + with pytest.raises(ValueError): + OpenShellExecutorConfig(landlock_compatibility="bogus") + + +# --- policy overrides are validated strictly --- + + +@requires_openshell +def test_build_sandbox_policy_rejects_landlock_key_typo() -> None: + # A typo'd key must not leave the sandbox on the fail-open landlock default. + with pytest.raises(ValueError, match="compatibilty"): + build_sandbox_policy({"landlock": {"compatibilty": "hard_requirement"}}) + + +@requires_openshell +def test_build_sandbox_policy_rejects_unknown_top_level_key() -> None: + with pytest.raises(ValueError, match="filesystem_polcy"): + build_sandbox_policy({"version": 1, "filesystem_polcy": {"read_only": ["/usr"]}}) + + +@requires_openshell +def test_build_sandbox_policy_rejects_bad_landlock_value() -> None: + # The proto types this as a plain string and the supervisor reads anything it does not + # recognise as best-effort, so the value set is checked here. + with pytest.raises(ValueError, match="landlock.compatibility"): + build_sandbox_policy({"landlock": {"compatibility": "hard-requirement"}}) + + +@requires_openshell +def test_build_sandbox_policy_rejects_bad_enforcement_value() -> None: + with pytest.raises(ValueError, match="enforcement"): + build_sandbox_policy({"network_policies": {"r": {"endpoints": [{"host": "h", "enforcement": "off"}]}}}) + + +@requires_openshell +def test_build_sandbox_policy_accepts_field_this_backend_never_writes() -> None: + # Structural validation comes from the proto, so a valid OpenShell policy this backend + # does not generate still round-trips. + policy = build_sandbox_policy( + {"network_policies": {"r": {"endpoints": [{"host": "h", "allowed_ips": ["10.0.0.1"], "ports": [443]}]}}} + ) + assert list(policy.network_policies["r"].endpoints[0].allowed_ips) == ["10.0.0.1"] + + +@requires_openshell +def test_build_sandbox_policy_accepts_both_filesystem_keys() -> None: + by_alias = build_sandbox_policy({"filesystem_policy": {"read_only": ["/usr"]}}) + by_name = build_sandbox_policy({"filesystem": {"read_only": ["/usr"]}}) + assert by_alias == by_name + + +@requires_openshell +def test_load_sandbox_policy_rejects_typo_in_file(tmp_path: Path) -> None: + policy_file = tmp_path / "policy.yaml" + policy_file.write_text("version: 1\nlandlock:\n compatibilty: hard_requirement\n", encoding="utf-8") + with pytest.raises(ValueError, match="invalid sandbox policy"): + load_sandbox_policy(str(policy_file)) diff --git a/plugins/nemo-deployments/tests/unit/backends/openshell/test_sandbox_profile.py b/plugins/nemo-deployments/tests/unit/backends/openshell/test_sandbox_profile.py new file mode 100644 index 0000000000..67758f2f04 --- /dev/null +++ b/plugins/nemo-deployments/tests/unit/backends/openshell/test_sandbox_profile.py @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""The OpenShell sandbox image profile and its entry-point registration.""" + +from __future__ import annotations + +from nemo_deployments_plugin.backends.openshell.sandbox_profile import PROFILE + + +def test_openshell_profile_shape() -> None: + assert PROFILE.name == "openshell" + assert "iproute2" in PROFILE.apt_packages + assert "nftables" in PROFILE.apt_packages + sandbox_users = [u for u in PROFILE.users if u.name == "sandbox"] + assert len(sandbox_users) == 1 + assert sandbox_users[0].system is True + assert sandbox_users[0].resolved_home() == "/home/sandbox" + + +def test_openshell_profile_is_discoverable() -> None: + # Registered under nemo.sandbox_profiles so the agent packager can find it + # by name without importing anything OpenShell-specific. + from nemo_platform_plugin import discovery + + discovery.discover.cache_clear() + discovery.discover_entry_points.cache_clear() + profiles = discovery.discover_sandbox_profiles() + + assert "openshell" in profiles + assert profiles["openshell"].name == "openshell" diff --git a/plugins/nemo-deployments/tests/unit/test_registry.py b/plugins/nemo-deployments/tests/unit/test_registry.py index 2dd66dd285..84592cca83 100644 --- a/plugins/nemo-deployments/tests/unit/test_registry.py +++ b/plugins/nemo-deployments/tests/unit/test_registry.py @@ -10,7 +10,13 @@ import pytest from docker.errors import NotFound -from nemo_deployments_plugin.backends.base import BackendStatusUpdate, DeploymentBackend, LogResult, VolumeStatusUpdate +from nemo_deployments_plugin.backends.base import ( + BackendStatusUpdate, + DeploymentBackend, + LogResult, + MissingBackendDependencyError, + VolumeStatusUpdate, +) from nemo_deployments_plugin.backends.docker.backend import DockerDeploymentBackend from nemo_deployments_plugin.backends.registry import ( ExecutorNotFoundError, @@ -121,6 +127,11 @@ def init(self) -> None: raise RuntimeError("init failed") +class _MissingDepBackend(_StubBackend): + def init(self) -> None: + raise MissingBackendDependencyError("openshell extra not installed") + + def test_registry_rolls_back_on_partial_init(backend_classes: dict[str, type[DeploymentBackend]]) -> None: sdk = AsyncNeMoPlatform(base_url="http://localhost:8080") classes = {**backend_classes, "fail": _FailingBackend} @@ -143,6 +154,42 @@ def shutdown(self) -> None: assert shutdown_calls == ["shutdown"] +def test_registry_skips_backend_with_missing_dependency( + backend_classes: dict[str, type[DeploymentBackend]], +) -> None: + # An opt-in backend whose optional extra is absent is skipped, not fatal: + # other executors still register and the service can boot. + sdk = AsyncNeMoPlatform(base_url="http://localhost:8080") + classes = {**backend_classes, "sandbox": _MissingDepBackend} + registry = ExecutorRegistry.from_config( + sdk, + [ + ExecutorSpec(name="ok", backend="docker", config={}), + ExecutorSpec(name="sandbox-local", backend="sandbox", config={}), + ], + backend_classes=classes, + ) + assert registry.registered_names() == ["ok"] + with pytest.raises(ExecutorNotFoundError): + registry.resolve("sandbox-local") + + +def test_registry_missing_dependency_default_executor_still_fails( + backend_classes: dict[str, type[DeploymentBackend]], +) -> None: + # A skipped backend is fine as a secondary executor, but if it is the + # configured default the registry must still fail fast. + sdk = AsyncNeMoPlatform(base_url="http://localhost:8080") + classes = {**backend_classes, "sandbox": _MissingDepBackend} + with pytest.raises(ExecutorNotFoundError): + ExecutorRegistry.from_config( + sdk, + [ExecutorSpec(name="sandbox-local", backend="sandbox", config={})], + default_executor="sandbox-local", + backend_classes=classes, + ) + + def test_multiple_docker_executors_distinct_config() -> None: sdk = AsyncNeMoPlatform(base_url="http://localhost:8080") with _patched_docker_init(): diff --git a/sdk/python/nemo-platform/src/nemo_platform/skills/nemo-build-agent/SKILL.md b/sdk/python/nemo-platform/src/nemo_platform/skills/nemo-build-agent/SKILL.md index 3448ea0b30..d00e256143 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/skills/nemo-build-agent/SKILL.md +++ b/sdk/python/nemo-platform/src/nemo_platform/skills/nemo-build-agent/SKILL.md @@ -15,6 +15,7 @@ not-for: - nemo-spec (use to write the spec file before building) - nemo-try-agent (use to query a deployed agent) - nemo-setup (use to install the platform first) + - deploy-sandbox (use to deploy the built agent as a governed OpenShell sandbox) - superpowers:brainstorming (use for unrelated design work) compatibility: nemo-platform >= 0.1.0; running platform (run nemo-setup first — uses `nemo services run`, no Docker); requires agents plugin installed; writes files to agents/; runs nemo CLI commands; defers platform-health probing to `nemo-status`; LangGraph + NAT under the hood; macOS or Linux; safe under sandbox. maturity: active @@ -70,6 +71,8 @@ Verification: confirm the deployment reached ready state. If `DEPLOY_NOT_READY`: jump to the recovery table at the bottom. +> **Governed sandbox deployment.** To deploy this agent as a policy-governed OpenShell sandbox instead of the default executor (Landlock filesystem isolation plus default-deny network egress, so its model traffic can only reach the platform), use the `deploy-sandbox` skill once the image is built. It swaps this step's deploy path for the `openshell-local` executor and an auto-generated SandboxPolicy. + ## Step 2: Try the agent Invoke with one question from each category in the spec. @@ -207,7 +210,7 @@ Run the success-criteria question from the spec through `nemo agents invoke` onc ## If verification fails | Symptom | Cause | Recovery | -|---|---|---| +| --- | --- | --- | | `agents plugin unavailable` | `plugins/nemo-agents` not installed | Re-run the install loop from `nemo-setup` Step 3 for that package only | | `DEPLOY_NOT_READY` after wait | Container startup error or YAML rejected | Run `.venv/bin/nemo agents deployments get $AGENT_NAME`; check status detail and logs | | YAML rejected with `extra fields` | Top-level keys beyond `functions`, `llms`, `workflow`, `intercepts`, `middleware` | Strip extras from the YAML; only those five top-level keys are valid | diff --git a/uv.lock b/uv.lock index 6136ff714b..0094ca4f50 100644 --- a/uv.lock +++ b/uv.lock @@ -255,16 +255,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402, upload-time = "2026-06-07T21:06:40.311Z" }, { url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448, upload-time = "2026-06-07T21:06:43.813Z" }, { url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854, upload-time = "2026-06-07T21:06:45.391Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884, upload-time = "2026-06-07T21:06:47.413Z" }, - { url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034, upload-time = "2026-06-07T21:06:50.165Z" }, - { url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054, upload-time = "2026-06-07T21:06:52.035Z" }, { url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278, upload-time = "2026-06-07T21:06:54.049Z" }, - { url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795, upload-time = "2026-06-07T21:06:55.911Z" }, { url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397, upload-time = "2026-06-07T21:06:57.777Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504, upload-time = "2026-06-07T21:07:00.009Z" }, - { url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806, upload-time = "2026-06-07T21:07:02.064Z" }, - { url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707, upload-time = "2026-06-07T21:07:03.942Z" }, - { url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121, upload-time = "2026-06-07T21:07:05.987Z" }, { url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580, upload-time = "2026-06-07T21:07:07.873Z" }, { url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" }, { url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" }, @@ -272,16 +264,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" }, { url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" }, { url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" }, - { url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" }, { url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" }, - { url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" }, { url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" }, - { url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" }, - { url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" }, - { url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" }, { url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" }, ] @@ -712,15 +696,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8 wheels = [ { 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/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/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/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" }, @@ -752,29 +732,13 @@ sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b0 wheels = [ { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, - { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, - { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, - { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, - { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, - { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, - { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, - { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, - { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, - { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, - { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, - { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, - { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, ] @@ -850,21 +814,13 @@ sdist = { url = "https://files.pythonhosted.org/packages/46/9e/d8e40b29b6269a845 wheels = [ { url = "https://files.pythonhosted.org/packages/32/7b/8e526f6ffb9983c0c6d082e358df4b20fe1a9e95f453e704bc7a25ef4aab/clickhouse_driver-0.2.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:188775d38ff7cb36e7045441aabf3a6a8751127d8b37b6eb1b1518494eaac5bd", size = 207193, upload-time = "2025-11-10T22:47:55.146Z" }, { url = "https://files.pythonhosted.org/packages/65/96/40f274896abf287c378575f025c602fa4e834278930dd63574ff548815c4/clickhouse_driver-0.2.10-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ff5cba860df61845d6ae12f31d4a70ff4ae3be4e6a8a876e68af8aa4b0e45bc", size = 1046187, upload-time = "2025-11-10T22:47:58.246Z" }, - { url = "https://files.pythonhosted.org/packages/0c/80/7b6e110c3b803fa8b3f8cdba0e08553a62c5f64e5ad57e56de3ea95cd9e1/clickhouse_driver-0.2.10-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fad865009d96de44d548f1691ed92adee971f72c001cf4466b3ba2ac7d9db47b", size = 1088806, upload-time = "2025-11-10T22:47:59.834Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d6/7f77bd00fc01df9db2e573de21bbc1f66083549d004864b892877dee8a76/clickhouse_driver-0.2.10-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cf0fe791e7c2adc0ab41d4770953c00f8a88bdd7e3ee83bb849a661a6c93d4ef", size = 1109839, upload-time = "2025-11-10T22:48:01.405Z" }, { url = "https://files.pythonhosted.org/packages/55/f7/57a80ff9cc44a333021e2caf8d35fc23da6ec7b602bbc3bf8dfac0253a6e/clickhouse_driver-0.2.10-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5744daafdd0ff7520c6ae95a78211a0ff5c2cfb3513a20f5602d2bc7eed580d", size = 1049773, upload-time = "2025-11-10T22:48:03.089Z" }, { url = "https://files.pythonhosted.org/packages/f6/3e/fcf8e9cb9edc717ce6c467a9ec7c96b4495d5f8ec4859175952149fbdaa8/clickhouse_driver-0.2.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f02f6c9f71ae5c06e3b760d3d9f4f758b32acf6f71504b6d90bacca9abbfec18", size = 1006817, upload-time = "2025-11-10T22:48:05.038Z" }, - { url = "https://files.pythonhosted.org/packages/95/ab/1bc25a385012c03595b91311d8341205a5790375207d80425e2285055d42/clickhouse_driver-0.2.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6df571410f149e16e0a0e5529f1c2a9e41bb62b9357a3c8b0bd0647d6bb0fd1e", size = 1051047, upload-time = "2025-11-10T22:48:07.115Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e1/9dd7331d08495beacf4291a6fbe5514fd0f6f8d53014121a8d70d8bd6c1e/clickhouse_driver-0.2.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1e891162226a44fa169bdc996efd49b22bcf59372c35118ec5785e936fe97178", size = 1052014, upload-time = "2025-11-10T22:48:08.608Z" }, { url = "https://files.pythonhosted.org/packages/ee/e9/af10e0ddbbd90c4ead933effff1b8914bc687bd52a70d244404db4c91529/clickhouse_driver-0.2.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3a261947ba0cf0034d044c30563ad151d1cf8156a5ff419b017c423b4235e0ac", size = 1020937, upload-time = "2025-11-10T22:48:10.993Z" }, { url = "https://files.pythonhosted.org/packages/34/92/ee5a2d7a812b65d9690e46222218f33064c4bd44f3535b1ba564fb4b528b/clickhouse_driver-0.2.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8be64c77d58d4a33b3c957cdb7c5a4deeac56bf93f4188dbfb5c5454eb04c985", size = 205158, upload-time = "2025-11-10T22:48:17.745Z" }, { url = "https://files.pythonhosted.org/packages/03/00/6c532a0aea89e3d09dd4150b1df0b92e787a306b8711d54d003d18fd1ddd/clickhouse_driver-0.2.10-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23abafd0c883ccc1baea527c1d05a6bc0c59aae6c29ae65e1b84d498b265f8c0", size = 1033476, upload-time = "2025-11-10T22:48:19.239Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9b/137ea1ff9539da77cd022331ec4fa079cbefbd4ebbcb5c51bdd7dcd0bca0/clickhouse_driver-0.2.10-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:77ffe2063469c637c5e57bf0713ca1b617b612d55a8392799f97e34c353e6908", size = 1079495, upload-time = "2025-11-10T22:48:20.744Z" }, - { url = "https://files.pythonhosted.org/packages/ec/1c/e13766af7e4e174c6f17b1fbc5a078b28584f53adc91f103caacc73f569b/clickhouse_driver-0.2.10-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df2d77779fcc1ddb68614b75bf45b8db61cf63f42a03d5624ce6922a305e609f", size = 1100658, upload-time = "2025-11-10T22:48:22.277Z" }, { url = "https://files.pythonhosted.org/packages/41/e5/0686ad3ef1b594c16e8b13394c73ee4860fd025d70211a360f797dd7a28a/clickhouse_driver-0.2.10-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e46e31e4b14626571819e669341a3017376ce935d25b2cc0bfea9343b1b562", size = 1034175, upload-time = "2025-11-10T22:48:24.117Z" }, { url = "https://files.pythonhosted.org/packages/d8/32/fea4e971297b50e5af3318fd90d400269ae1c74ad4d83a9453b89f578d3c/clickhouse_driver-0.2.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7435d1ff2bc577aeedf8f01d94b5777af382484f8973a9c5018d5afd0dd175c", size = 995963, upload-time = "2025-11-10T22:48:25.824Z" }, - { url = "https://files.pythonhosted.org/packages/02/c4/d42f2b69ab5903e5bc9119b179f55c9aef79fe667f77cab4d8ae90492dcd/clickhouse_driver-0.2.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b60c7e3321214eec4568811bcd953836671fa078c57f6607f236414447636de2", size = 1044626, upload-time = "2025-11-10T22:48:27.927Z" }, - { url = "https://files.pythonhosted.org/packages/78/36/043b6b2d967396172a60f10bf26de2c83248857f9a1e75b481f02218d1d7/clickhouse_driver-0.2.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13fdf6571e20ac79992605ad65058296ac0f2437c1e7428a98dd6d173753119e", size = 1045772, upload-time = "2025-11-10T22:48:29.439Z" }, { url = "https://files.pythonhosted.org/packages/0c/cf/bc5c807cbe68ce9eeac6a1997b937c81774ca86b2ab593c6efb9121a9f08/clickhouse_driver-0.2.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06b6b683af086f9049d0c5e7e660fb76013439efa640e6c8ff6673622c3838fa", size = 1006716, upload-time = "2025-11-10T22:48:31.086Z" }, ] @@ -913,29 +869,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, - { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, - { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, - { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, - { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, - { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, - { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, - { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, - { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] @@ -953,11 +897,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/85/6379d42181bfc713094f081360fc5784d6c816b599d45e7f082502d173ce/cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225", size = 4696243, upload-time = "2026-06-09T22:32:33.446Z" }, { url = "https://files.pythonhosted.org/packages/9c/87/c85d147b53323c7eb4d850920c8901377323c2a0ff8d79c262d4fee89aa2/cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691", size = 4713235, upload-time = "2026-06-09T22:31:40.141Z" }, { url = "https://files.pythonhosted.org/packages/79/58/67cbf8cf1ee7c54b439ca07bbecf8362c07afc11a3724fea70f745784add/cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242", size = 4702323, upload-time = "2026-06-09T22:31:42.191Z" }, - { url = "https://files.pythonhosted.org/packages/89/c6/24266ac10c47f6cd2a865f4446062b466da1d1f10b27189eac00e61bf0c9/cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08", size = 5300085, upload-time = "2026-06-09T22:31:58.703Z" }, { url = "https://files.pythonhosted.org/packages/d2/bb/cc4b78784f97efc8c5874c2a9743708d172be6663024b34a0467885ae0c8/cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6", size = 4746137, upload-time = "2026-06-09T22:31:31.01Z" }, - { url = "https://files.pythonhosted.org/packages/1f/52/0c44de3f5267f8fbe8e835138017522a333436166e406f0db9b9e6e3033f/cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8", size = 4333867, upload-time = "2026-06-09T22:32:28.096Z" }, { url = "https://files.pythonhosted.org/packages/9a/2e/772d7adbfa931537bc401640b7cac9976bff689bda187833e5d63b428e49/cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429", size = 4701805, upload-time = "2026-06-09T22:31:38.284Z" }, - { url = "https://files.pythonhosted.org/packages/f8/a3/b06844f303873493c963caf581c04df31c7035e0c1b0f02c4814d319ec80/cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f", size = 5258461, upload-time = "2026-06-09T22:31:04.187Z" }, { url = "https://files.pythonhosted.org/packages/9f/13/8b765e2e12b07c74941caadb9d1c8fdc006c4dfbf2b8f2d610519758954d/cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f", size = 4745488, upload-time = "2026-06-09T22:32:30.07Z" }, { url = "https://files.pythonhosted.org/packages/2e/aa/48972bce55049b32a94f4907eda4d75fa385aad8a39506cc2fc72196ecf0/cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41", size = 4830256, upload-time = "2026-06-09T22:31:43.868Z" }, { url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117, upload-time = "2026-06-09T22:31:05.845Z" }, @@ -965,11 +906,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196, upload-time = "2026-06-09T22:31:54.74Z" }, { url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782, upload-time = "2026-06-09T22:31:45.615Z" }, { url = "https://files.pythonhosted.org/packages/66/c2/81a4fb4e4373c500bb526bc337ac5719dd31dd15b970b84a238168c6aa08/cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577", size = 4696618, upload-time = "2026-06-09T22:31:11.564Z" }, - { url = "https://files.pythonhosted.org/packages/e5/0b/aa68b221dde92d09cb29a024ede17550ee21e77a404e59fc093c82bb51e1/cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1", size = 5289970, upload-time = "2026-06-09T22:31:20.368Z" }, { url = "https://files.pythonhosted.org/packages/78/13/fba657f958d2af66ea959a4ba01212632089249d34af1ae48054136344d7/cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d", size = 4731873, upload-time = "2026-06-09T22:31:22.253Z" }, - { url = "https://files.pythonhosted.org/packages/4c/4c/9a964756d24a26b3e34dfcb16f961b89838786e6700b635b0d1e3adff4b6/cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6", size = 4330804, upload-time = "2026-06-09T22:31:36.56Z" }, { url = "https://files.pythonhosted.org/packages/4b/0f/a10f3a6eb12950a10e3a874070283aa2dd5875b2bfd15fad8a3e17b3f13e/cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46", size = 4696217, upload-time = "2026-06-09T22:31:13.351Z" }, - { url = "https://files.pythonhosted.org/packages/f3/6f/5cd12f951165ea73ef85266775d97e4c763b2474ccfd816dd69d3a18d6f8/cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401", size = 5245252, upload-time = "2026-06-09T22:32:02.193Z" }, { url = "https://files.pythonhosted.org/packages/68/ab/8aaa12e4516ec4464033ab79b6f3b592bd5a92102467c4ace8a0d970203f/cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b", size = 4731388, upload-time = "2026-06-09T22:32:04.019Z" }, { url = "https://files.pythonhosted.org/packages/1b/24/50027ea4dca85ec1f40688f3c24fb32ccacd520583c9592c3cc95628e6fb/cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1", size = 4824186, upload-time = "2026-06-09T22:32:18.707Z" }, { url = "https://files.pythonhosted.org/packages/52/41/04cb5eb17085ade6f50cc611fb657df6a0f5885350de8764ece89c050197/cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475", size = 4964539, upload-time = "2026-06-09T22:31:18.793Z" }, @@ -1597,23 +1535,13 @@ sdist = { url = "https://files.pythonhosted.org/packages/dd/00/dab9ca274cf1fde19 wheels = [ { url = "https://files.pythonhosted.org/packages/95/97/f1e34c8224dc373c6fab5b33e33be0d184751fdc27013af3278b1e4e6e6c/fastar-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ec841a69fea73361c6df6d9183915c09e9ce3bd96493763fa46019e79918400", size = 627422, upload-time = "2026-03-20T14:25:20.318Z" }, { url = "https://files.pythonhosted.org/packages/fe/cf/b6ad68b2ab1d7b74b0d38725d817418016bdd64880b36108be80d2460b4d/fastar-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de264da9e8ef6407aa0b23c7c47ed4e34fde867e7c1f6e3cb98945a93e5f89f2", size = 760583, upload-time = "2026-03-20T14:23:50.447Z" }, - { url = "https://files.pythonhosted.org/packages/b8/96/086116ad46e3b98f6c217919d680e619f2857ffa6b5cc0d7e46e4f214b83/fastar-0.9.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75c70be3a7da3ff9342f64c15ec3749c13ef56bc28e69075d82d03768532a8d0", size = 758000, upload-time = "2026-03-20T14:24:03.471Z" }, - { url = "https://files.pythonhosted.org/packages/9b/e6/ea642ea61eea98d609343080399a296a9ff132bd0492a6638d6e0d9e41a7/fastar-0.9.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a734506b071d2a8844771fe735fbd6d67dd0eec80eef5f189bbe763ebe7a0b8", size = 923647, upload-time = "2026-03-20T14:24:16.875Z" }, - { url = "https://files.pythonhosted.org/packages/c6/3e/53874aad61e4a664af555a2aa7a52fe46cfadd423db0e592fa0cfe0fa668/fastar-0.9.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8eac084ab215aaf65fa406c9b9da1ac4e697c3d3a1a183e09c488e555802f62d", size = 816528, upload-time = "2026-03-20T14:24:42.048Z" }, { url = "https://files.pythonhosted.org/packages/41/df/d663214d35380b07a24a796c48d7d7d4dc3a28ec0756edbcb7e2a81dc572/fastar-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb62e2369834fb23d26327157f0a2dbec40b230c709fa85b1ce96cf010e6fbf", size = 819050, upload-time = "2026-03-20T14:25:08.352Z" }, - { url = "https://files.pythonhosted.org/packages/7c/5a/455b53f11527568100ba6d5847635430645bad62d676f0bae4173fc85c90/fastar-0.9.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:f2f399fffb74bcd9e9d4507e253ace2430b5ccf61000596bda41e90414bcf4f2", size = 885257, upload-time = "2026-03-20T14:24:28.86Z" }, { url = "https://files.pythonhosted.org/packages/4f/dd/0a8ea7b910293b07f8c82ef4e6451262ccf2a6f2020e880f184dc4abd6c2/fastar-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87006c8770dfc558aefe927590bbcdaf9648ca4472a9ee6d10dfb7c0bda4ce5b", size = 968135, upload-time = "2026-03-20T14:25:45.614Z" }, - { url = "https://files.pythonhosted.org/packages/6b/cb/5c7e9231d6ba00e225623947068db09ddd4e401800b0afaf39eece14bfee/fastar-0.9.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d012644421d669d9746157193f4eafd371e8ae56ff7aef97612a4922418664c", size = 1034940, upload-time = "2026-03-20T14:25:58.893Z" }, { url = "https://files.pythonhosted.org/packages/8b/53/6ddda28545b428d54c42f341d797046467c689616a36eae9a43ba56f2545/fastar-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:59bc500d7b6bdaf2ffb2b632bc6b0f97ddfb3bb7d31b54d61ceb00b5698d6484", size = 1025314, upload-time = "2026-03-20T14:26:24.624Z" }, { url = "https://files.pythonhosted.org/packages/77/52/f3b06867e5ca8d5b2c1c15a1563415e0037b5831f2058ee72b03960296d9/fastar-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f07c6bdeedfeb30ef459f21fa9ab06e2b6727f7e7653176d3abb7a85f447c400", size = 627615, upload-time = "2026-03-20T14:25:21.608Z" }, { url = "https://files.pythonhosted.org/packages/3f/54/e2e1b4c8512d670373047e5e585b1d1ff9ffd722b0a17647d22c9c9bd248/fastar-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:108bb46c080ca152bb331f1e0576177d36e9badba51b1d5724d2823542e0dd1f", size = 760246, upload-time = "2026-03-20T14:23:51.964Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7d/1e283dd8dbb3647049594bb477bdc053045c6fff2d3f06386d2dcacce7aa/fastar-0.9.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d17d311cfbb559154ba940972b6d07a3a7ac221a2a01208f119ad03495f01d32", size = 757024, upload-time = "2026-03-20T14:24:04.69Z" }, - { url = "https://files.pythonhosted.org/packages/87/ac/82d3cb64d318ce16c5d1a26a40b8aa570fcc9b23684221aece838c4cbada/fastar-0.9.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2ef34e7088f308e73460e1b8d9b0479a743f679816782a80db6ae87ee68714a", size = 921630, upload-time = "2026-03-20T14:24:18.155Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b8/3e7892f1a25a1a2054a20de6c846c0794b8fa361e5b9d3d00915b41e97bd/fastar-0.9.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c93bf4732d0dd6adae4a8b3bbebe19af76ee1072b7688bf39c5a1d120425a772", size = 815791, upload-time = "2026-03-20T14:24:43.28Z" }, { url = "https://files.pythonhosted.org/packages/db/5e/8fcc662db1fd0985f4f8a54e79276416565a0d1fcb8da66665b2061ead30/fastar-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a67b061b1099cf3b8b6234dd3605fa16f5078ab6b51c8d77ad7a5d11c3cf834", size = 818980, upload-time = "2026-03-20T14:25:09.545Z" }, - { url = "https://files.pythonhosted.org/packages/68/ed/37291fbd6c9b5b0905712da6191bdfc25a7dc236efbf130e3a1a7d1b9440/fastar-0.9.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:912efe3121dc1f3c05940cfa1c6b09b8868d702d24566506aa1d0d96e429923a", size = 884578, upload-time = "2026-03-20T14:24:30.584Z" }, { url = "https://files.pythonhosted.org/packages/94/19/7b3b7af978ae4f012664781554716d67549ab19ddbcb6e6d1adc04d7a5e7/fastar-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2394980cc126a3263e115600bc4ff9e7320cddde83c99fc334ab530be5b7166e", size = 967790, upload-time = "2026-03-20T14:25:46.975Z" }, - { url = "https://files.pythonhosted.org/packages/e6/38/4cce2a8e529a7d3e99e427c9bbcccd7013ff6b3ba295613e6f1c573c9e6c/fastar-0.9.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d0aff74ea98642784c941d3cd8c35943258d4b9626157858901c5b181683339b", size = 1033892, upload-time = "2026-03-20T14:26:00.22Z" }, { url = "https://files.pythonhosted.org/packages/10/4f/6ec0c123c15bbcb9a9b82e979dc81273789ebbfbb4a2b41a1a6941577c94/fastar-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c9bd8879ebf05aa247e60e454bb7568cbdd44f016b8c58e31e5398039403e61d", size = 1025768, upload-time = "2026-03-20T14:26:25.957Z" }, ] @@ -1775,37 +1703,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, - { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, - { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, - { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, - { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, - { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, - { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, - { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, - { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] @@ -1937,15 +1847,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd9 wheels = [ { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, - { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, - { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, - { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, @@ -1969,13 +1875,11 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" }, { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" }, { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" }, { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" }, @@ -2072,19 +1976,13 @@ sdist = { url = "https://files.pythonhosted.org/packages/1a/eb/8fc64f40388c29ce8 wheels = [ { url = "https://files.pythonhosted.org/packages/ea/2e/3d60b1a9e9f29a2152aa66c823bf5e399ae7be3fef310ff0de86779c5d2d/hf_transfer-0.1.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ebc4ab9023414880c8b1d3c38174d1c9989eb5022d37e814fa91a3060123eb0", size = 1343558, upload-time = "2025-01-07T10:04:42.313Z" }, { url = "https://files.pythonhosted.org/packages/fb/38/130a5ac3747f104033591bcac1c961cb1faadfdc91704f59b09c0b465ff2/hf_transfer-0.1.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8674026f21ed369aa2a0a4b46000aca850fc44cd2b54af33a172ce5325b4fc82", size = 3726676, upload-time = "2025-01-07T10:04:11.539Z" }, - { url = "https://files.pythonhosted.org/packages/15/a1/f4e27c5ad17aac616ae0849e2aede5aae31db8267a948c6b3eeb9fd96446/hf_transfer-0.1.9-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a736dfbb2c84f5a2c975478ad200c0c8bfcb58a25a35db402678fb87ce17fa4", size = 3062920, upload-time = "2025-01-07T10:04:16.297Z" }, - { url = "https://files.pythonhosted.org/packages/50/d0/2b213eb1ea8b1252ccaf1a6c804d0aba03fea38aae4124df6a3acb70511a/hf_transfer-0.1.9-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c7fc1b85f4d0f76e452765d7648c9f4bfd0aedb9ced2ae1ebfece2d8cfaf8e2", size = 3398837, upload-time = "2025-01-07T10:04:22.778Z" }, { url = "https://files.pythonhosted.org/packages/8c/8a/79dbce9006e0bd6b74516f97451a7b7c64dbbb426df15d901dd438cfeee3/hf_transfer-0.1.9-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d991376f0eac70a60f0cbc95602aa708a6f7c8617f28b4945c1431d67b8e3c8", size = 3546986, upload-time = "2025-01-07T10:04:36.415Z" }, { url = "https://files.pythonhosted.org/packages/a9/f7/9ac239b6ee6fe0bad130325d987a93ea58c4118e50479f0786f1733b37e8/hf_transfer-0.1.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e6ac4eddcd99575ed3735ed911ddf9d1697e2bd13aa3f0ad7e3904dd4863842e", size = 4071715, upload-time = "2025-01-07T10:04:53.224Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a3/0ed697279f5eeb7a40f279bd783cf50e6d0b91f24120dcf66ef2cf8822b4/hf_transfer-0.1.9-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:57fd9880da1ee0f47250f735f791fab788f0aa1ee36afc49f761349869c8b4d9", size = 3388081, upload-time = "2025-01-07T10:04:57.818Z" }, { url = "https://files.pythonhosted.org/packages/45/07/6661e43fbee09594a8a5e9bb778107d95fe38dac4c653982afe03d32bd4d/hf_transfer-0.1.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a5b366d34cd449fe9b20ef25941e6eef0460a2f74e7389f02e673e1f88ebd538", size = 3690551, upload-time = "2025-01-07T10:05:09.238Z" }, { url = "https://files.pythonhosted.org/packages/41/ba/8d9fd9f1083525edfcb389c93738c802f3559cb749324090d7109c8bf4c2/hf_transfer-0.1.9-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:8669dbcc7a3e2e8d61d42cd24da9c50d57770bd74b445c65123291ca842a7e7a", size = 1348126, upload-time = "2025-01-07T10:04:45.712Z" }, { url = "https://files.pythonhosted.org/packages/8e/a2/cd7885bc9959421065a6fae0fe67b6c55becdeda4e69b873e52976f9a9f0/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fd0167c4407a3bc4cdd0307e65ada2294ec04f1813d8a69a5243e379b22e9d8", size = 3728604, upload-time = "2025-01-07T10:04:14.173Z" }, - { url = "https://files.pythonhosted.org/packages/f6/2e/a072cf196edfeda3310c9a5ade0a0fdd785e6154b3ce24fc738c818da2a7/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee8b10afedcb75f71091bcc197c526a6ebf5c58bbbadb34fdeee6160f55f619f", size = 3064995, upload-time = "2025-01-07T10:04:18.663Z" }, - { url = "https://files.pythonhosted.org/packages/29/63/b560d39651a56603d64f1a0212d0472a44cbd965db2fa62b99d99cb981bf/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc6bd19e1cc177c66bdef15ef8636ad3bde79d5a4f608c158021153b4573509d", size = 3400839, upload-time = "2025-01-07T10:04:26.122Z" }, { url = "https://files.pythonhosted.org/packages/d6/d8/f87ea6f42456254b48915970ed98e993110521e9263472840174d32c880d/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdca9bfb89e6f8f281890cc61a8aff2d3cecaff7e1a4d275574d96ca70098557", size = 3552664, upload-time = "2025-01-07T10:04:40.123Z" }, { url = "https://files.pythonhosted.org/packages/d6/56/1267c39b65fc8f4e2113b36297320f102718bf5799b544a6cbe22013aa1d/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:89a23f58b7b7effbc047b8ca286f131b17728c99a9f972723323003ffd1bb916", size = 4073732, upload-time = "2025-01-07T10:04:55.624Z" }, - { url = "https://files.pythonhosted.org/packages/82/1a/9c748befbe3decf7cb415e34f8a0c3789a0a9c55910dea73d581e48c0ce5/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:dc7fff1345980d6c0ebb92c811d24afa4b98b3e07ed070c8e38cc91fd80478c5", size = 3390096, upload-time = "2025-01-07T10:04:59.98Z" }, { url = "https://files.pythonhosted.org/packages/e7/6e/e597b04f753f1b09e6893075d53a82a30c13855cbaa791402695b01e369f/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d2fde99d502093ade3ab1b53f80da18480e9902aa960dab7f74fb1b9e5bc5746", size = 3695243, upload-time = "2025-01-07T10:05:11.411Z" }, ] @@ -2499,17 +2397,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b5 wheels = [ { url = "https://files.pythonhosted.org/packages/9c/4a/6a2397096162b21645162825f058d1709a02965606e537e3304b02742e9b/jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744", size = 320124, upload-time = "2025-05-18T19:03:46.341Z" }, { url = "https://files.pythonhosted.org/packages/2a/85/1ce02cade7516b726dd88f59a4ee46914bf79d1676d1228ef2002ed2f1c9/jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2", size = 345330, upload-time = "2025-05-18T19:03:47.596Z" }, - { url = "https://files.pythonhosted.org/packages/75/d0/bb6b4f209a77190ce10ea8d7e50bf3725fc16d3372d0a9f11985a2b23eff/jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026", size = 369670, upload-time = "2025-05-18T19:03:49.334Z" }, - { url = "https://files.pythonhosted.org/packages/a0/f5/a61787da9b8847a601e6827fbc42ecb12be2c925ced3252c8ffcb56afcaf/jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c", size = 489057, upload-time = "2025-05-18T19:03:50.66Z" }, - { url = "https://files.pythonhosted.org/packages/12/e4/6f906272810a7b21406c760a53aadbe52e99ee070fc5c0cb191e316de30b/jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959", size = 389372, upload-time = "2025-05-18T19:03:51.98Z" }, { url = "https://files.pythonhosted.org/packages/e2/ba/77013b0b8ba904bf3762f11e0129b8928bff7f978a81838dfcc958ad5728/jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a", size = 352038, upload-time = "2025-05-18T19:03:53.703Z" }, { url = "https://files.pythonhosted.org/packages/c0/72/0d6b7e31fc17a8fdce76164884edef0698ba556b8eb0af9546ae1a06b91d/jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea", size = 523557, upload-time = "2025-05-18T19:03:56.386Z" }, { url = "https://files.pythonhosted.org/packages/2f/09/bc1661fbbcbeb6244bd2904ff3a06f340aa77a2b94e5a7373fd165960ea3/jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b", size = 514202, upload-time = "2025-05-18T19:03:57.675Z" }, { url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947, upload-time = "2025-05-18T19:04:03.347Z" }, { url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618, upload-time = "2025-05-18T19:04:04.709Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829, upload-time = "2025-05-18T19:04:06.912Z" }, - { url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034, upload-time = "2025-05-18T19:04:08.222Z" }, - { url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529, upload-time = "2025-05-18T19:04:09.566Z" }, { url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671, upload-time = "2025-05-18T19:04:10.98Z" }, { url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989, upload-time = "2025-05-18T19:04:14.261Z" }, { url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495, upload-time = "2025-05-18T19:04:15.603Z" }, @@ -2585,21 +2477,13 @@ sdist = { url = "https://files.pythonhosted.org/packages/ad/1d/68607c574dd78f030 wheels = [ { url = "https://files.pythonhosted.org/packages/9e/f6/02301a17826e0f5d253146918e52436831f43fdf018031819ab4dc2af8e4/jsonpath_rust_bindings-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:44de7464ad227028c36e8d713653b4bfe5eb7524ac1a4b0a71e8bcb3bd4f4f3a", size = 814583, upload-time = "2025-11-16T19:01:55.251Z" }, { url = "https://files.pythonhosted.org/packages/7b/63/8860fc926e25ef3dfbc61d6366932f3e106a089308e3ad6a36987fac3efe/jsonpath_rust_bindings-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c220c2d27ab6a0791e3af10e2a7c53ccd1dc2dfc8681999fed4458392aa0372", size = 831964, upload-time = "2025-11-16T19:00:17.016Z" }, - { url = "https://files.pythonhosted.org/packages/84/2d/5f16683333b298969c24b6a09b5cb071fe4d603e4e8788e5db6b82231618/jsonpath_rust_bindings-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e423363b47080830bbb4d8257c0f26bda8ee655a18c4f934952bfe4c46e8d510", size = 836196, upload-time = "2025-11-16T19:00:33.647Z" }, - { url = "https://files.pythonhosted.org/packages/da/c9/5c75bad74f27eca1853d9d58fabc4ed838e94997d65177d9f29cc9bb3229/jsonpath_rust_bindings-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:366cba544c080c08530cef0cc19922f0380f0caab6e7e5a0ddfb70de288d5abc", size = 931327, upload-time = "2025-11-16T19:00:50.823Z" }, - { url = "https://files.pythonhosted.org/packages/b7/5f/8e3a65a8053945d0c63ea8e5c11832b051e0919b342f29e1365108165472/jsonpath_rust_bindings-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7bf30e27a81d07c79cc58c86600687e5adfe0f7b1aaf8069a737085bebfaea71", size = 959445, upload-time = "2025-11-16T19:01:06.891Z" }, { url = "https://files.pythonhosted.org/packages/2f/5a/f44c4b55cecc6eb1a4b22dc2aacb9cf9f434b600706527ab619f6076ced0/jsonpath_rust_bindings-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c390c33582cd268d35b86eb0f550229e0cf26f03bb06c470db4712d6fa4dc0f", size = 947132, upload-time = "2025-11-16T19:01:38.408Z" }, { url = "https://files.pythonhosted.org/packages/5f/9d/e35fdaea0a065584d4864af8711a9be501015d4354d3eb9f61de0fedccc6/jsonpath_rust_bindings-1.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0017af7054fb6bce55863a7065ae465a9c47fd93fb94f002ca98bb8adf15101a", size = 1066860, upload-time = "2025-11-16T19:02:31.654Z" }, - { url = "https://files.pythonhosted.org/packages/8b/25/8ca3c1b67435f3a29d121c86867afdb86e02ec932c7a5343af61554c5788/jsonpath_rust_bindings-1.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9212d3746a57015fc3722488f61c4afc465d993f68371d864be8fa5b0c58d635", size = 1148553, upload-time = "2025-11-16T19:02:47.91Z" }, { url = "https://files.pythonhosted.org/packages/c6/be/708f5c15718e796d3d3fb3d139fe5dfa8aa6b0eff44adadc0bf66822d388/jsonpath_rust_bindings-1.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d21101114514d34b21ab216eef1d7bb41155311fa61284e8f2dbdb93bde41c78", size = 1133969, upload-time = "2025-11-16T19:03:21.839Z" }, { url = "https://files.pythonhosted.org/packages/7b/4c/2a7995761e247610551cb218b5fcfa9c95a542d8a38915a91579178eba73/jsonpath_rust_bindings-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f55ee1e7fdb6bb2363c40a6d6ce0285e53bd52b4ecae7bef3909eeb11a9b4cd2", size = 815031, upload-time = "2025-11-16T19:01:56.972Z" }, { url = "https://files.pythonhosted.org/packages/f1/fb/f1375e4f254fdf088ebbb397cfb42f3bdd5c7fed3349ad140f09d052ae09/jsonpath_rust_bindings-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:734eee89754c829a0fb55a30467c8a33081976375b763c907f71f7018682c26c", size = 832342, upload-time = "2025-11-16T19:00:18.79Z" }, - { url = "https://files.pythonhosted.org/packages/17/5f/bccd6178fc9655e03b01917531c08a25951b55455189a98faa13f7125d4a/jsonpath_rust_bindings-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6716caa0855dbf9d021509a3caa00a9fa7cc241930f40830c24e85d0e17a6246", size = 836616, upload-time = "2025-11-16T19:00:35.477Z" }, - { url = "https://files.pythonhosted.org/packages/e9/e6/e809c31962c161230ef136646e7a6bc1783ab9299255f043923af1d55a90/jsonpath_rust_bindings-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02373d581a093d0640e60858884d67ec93259e7b6d6bd8e5874400ad99558e00", size = 931852, upload-time = "2025-11-16T19:00:52.271Z" }, - { url = "https://files.pythonhosted.org/packages/e1/51/9d29b9f642012d545233416138c96562aefdab78d3602b61e19267fc4098/jsonpath_rust_bindings-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:146b69ce20cb9869e05a6d369f4a10b52f98e1f8575f1ac5b49e285fa2032380", size = 959626, upload-time = "2025-11-16T19:01:08.348Z" }, { url = "https://files.pythonhosted.org/packages/1c/95/696e02d5af89b95da829b79473cde3e7a1c0d73c571d1dc7c32e886e04e0/jsonpath_rust_bindings-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe44737c6c72079ef30c85f975c19fa0114c13039fe538d8c5b259007a35a0ff", size = 947152, upload-time = "2025-11-16T19:01:41.146Z" }, { url = "https://files.pythonhosted.org/packages/66/5c/b7eb6647de1721b632cccbfe3de777f1030fa0525a89b119ed70ebafc2c6/jsonpath_rust_bindings-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40c23781d28a8b126c8a2b337e4fe275cc8f35a149bda769e3ec2760dfb58b91", size = 1067196, upload-time = "2025-11-16T19:02:33.289Z" }, - { url = "https://files.pythonhosted.org/packages/c5/80/1c56c148c92f43aca6799716f549ff2463ee328c377b9c1e630d4057a607/jsonpath_rust_bindings-1.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4eacb98f80fff7d43956503ca7b42e491f7084c7b9bd8b5b6bad3f50d08480df", size = 1148940, upload-time = "2025-11-16T19:02:49.647Z" }, { url = "https://files.pythonhosted.org/packages/7e/df/b0c2fd033c5f5714a7ea4c03dabe8ab66dbaabab4fa7d9385344ab7a16e7/jsonpath_rust_bindings-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7f2a526c87a245f708dc1d8d4988c471384c369a5909b8b730e63b6a7f0c2d60", size = 1134078, upload-time = "2025-11-16T19:03:23.799Z" }, ] @@ -3125,27 +3009,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/97/a517944b20f8fd0932ad2109482bee4e29fe721416387a363306667941f6/lxml-6.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d", size = 4930895, upload-time = "2026-04-18T04:32:56.29Z" }, { url = "https://files.pythonhosted.org/packages/94/7c/e08a970727d556caa040a44773c7b7e3ad0f0d73dedc863543e9a8b931f2/lxml-6.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a", size = 5093820, upload-time = "2026-04-18T04:32:58.94Z" }, { url = "https://files.pythonhosted.org/packages/88/ee/2a5c2aa2c32016a226ca25d3e1056a8102ea6e1fe308bf50213586635400/lxml-6.1.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105", size = 5005790, upload-time = "2026-04-18T04:33:01.272Z" }, - { url = "https://files.pythonhosted.org/packages/e3/38/a0db9be8f38ad6043ab9429487c128dd1d30f07956ef43040402f8da49e8/lxml-6.1.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485", size = 5630827, upload-time = "2026-04-18T04:33:04.036Z" }, { url = "https://files.pythonhosted.org/packages/31/ba/3c13d3fc24b7cacf675f808a3a1baabf43a30d0cd24c98f94548e9aa58eb/lxml-6.1.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814", size = 5240445, upload-time = "2026-04-18T04:33:06.87Z" }, - { url = "https://files.pythonhosted.org/packages/7e/01/1da87c7b587c38d0cbe77a01aae3b9c1c49ed47d76918ef3db8fc151b1ca/lxml-6.1.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad", size = 4694949, upload-time = "2026-04-18T04:33:11.628Z" }, - { url = "https://files.pythonhosted.org/packages/a1/88/7db0fe66d5aaf128443ee1623dec3db1576f3e4c17751ec0ef5866468590/lxml-6.1.0-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54", size = 5243901, upload-time = "2026-04-18T04:33:13.95Z" }, { url = "https://files.pythonhosted.org/packages/00/a8/1346726af7d1f6fca1f11223ba34001462b0a3660416986d37641708d57c/lxml-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d", size = 5048054, upload-time = "2026-04-18T04:33:16.965Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b7/85057012f035d1a0c87e02f8c723ca3c3e6e0728bcf4cb62080b21b1c1e3/lxml-6.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69", size = 4777324, upload-time = "2026-04-18T04:33:19.832Z" }, - { url = "https://files.pythonhosted.org/packages/75/6c/ad2f94a91073ef570f33718040e8e160d5fb93331cf1ab3ca1323f939e2d/lxml-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d", size = 5645702, upload-time = "2026-04-18T04:33:22.436Z" }, - { url = "https://files.pythonhosted.org/packages/3b/89/0bb6c0bd549c19004c60eea9dc554dd78fd647b72314ef25d460e0d208c6/lxml-6.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5", size = 5232901, upload-time = "2026-04-18T04:33:26.21Z" }, { url = "https://files.pythonhosted.org/packages/a1/d9/d609a11fb567da9399f525193e2b49847b5a409cdebe737f06a8b7126bdc/lxml-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d", size = 5261333, upload-time = "2026-04-18T04:33:28.984Z" }, { url = "https://files.pythonhosted.org/packages/08/03/69347590f1cf4a6d5a4944bb6099e6d37f334784f16062234e1f892fdb1d/lxml-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45", size = 8559689, upload-time = "2026-04-18T04:31:57.785Z" }, { url = "https://files.pythonhosted.org/packages/f5/54/92ad98a94ac318dc4f97aaac22ff8d1b94212b2ae8af5b6e9b354bf825f7/lxml-6.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2", size = 4923489, upload-time = "2026-04-18T04:33:31.401Z" }, { url = "https://files.pythonhosted.org/packages/15/3b/a20aecfab42bdf4f9b390590d345857ad3ffd7c51988d1c89c53a0c73faf/lxml-6.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491", size = 5082162, upload-time = "2026-04-18T04:33:34.262Z" }, { url = "https://files.pythonhosted.org/packages/45/26/2cdb3d281ac1bd175603e290cbe4bad6eff127c0f8de90bafd6f8548f0fd/lxml-6.1.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc", size = 4993247, upload-time = "2026-04-18T04:33:36.674Z" }, - { url = "https://files.pythonhosted.org/packages/f6/05/d735aef963740022a08185c84821f689fc903acb3d50326e6b1e9886cc22/lxml-6.1.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e", size = 5613042, upload-time = "2026-04-18T04:33:39.205Z" }, { url = "https://files.pythonhosted.org/packages/ee/b8/ead7c10efff731738c72e59ed6eb5791854879fbed7ae98781a12006263a/lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2", size = 5228304, upload-time = "2026-04-18T04:33:41.647Z" }, - { url = "https://files.pythonhosted.org/packages/89/54/40d9403d7c2775fa7301d3ddd3464689bfe9ba71acc17dfff777071b4fdc/lxml-6.1.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe", size = 4700209, upload-time = "2026-04-18T04:33:47.552Z" }, - { url = "https://files.pythonhosted.org/packages/85/b2/bbdcc2cf45dfc7dfffef4fd97e5c47b15919b6a365247d95d6f684ef5e82/lxml-6.1.0-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88", size = 5232365, upload-time = "2026-04-18T04:33:50.249Z" }, { url = "https://files.pythonhosted.org/packages/48/5a/b06875665e53aaba7127611a7bed3b7b9658e20b22bc2dd217a0b7ab0091/lxml-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181", size = 5043654, upload-time = "2026-04-18T04:33:52.71Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9c/e71a069d09641c1a7abeb30e693f828c7c90a41cbe3d650b2d734d876f85/lxml-6.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24", size = 4769326, upload-time = "2026-04-18T04:33:55.244Z" }, - { url = "https://files.pythonhosted.org/packages/cc/06/7a9cd84b3d4ed79adf35f874750abb697dec0b4a81a836037b36e47c091a/lxml-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e", size = 5635879, upload-time = "2026-04-18T04:33:58.509Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f0/9d57916befc1e54c451712c7ee48e9e74e80ae4d03bdce49914e0aee42cd/lxml-6.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495", size = 5224048, upload-time = "2026-04-18T04:34:00.943Z" }, { url = "https://files.pythonhosted.org/packages/99/75/90c4eefda0c08c92221fe0753db2d6699a4c628f76ff4465ec20dea84cc1/lxml-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33", size = 5250241, upload-time = "2026-04-18T04:34:03.365Z" }, ] @@ -3208,23 +3080,17 @@ wheels = [ { 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/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/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" }, ] @@ -3448,11 +3314,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8b/72/e6d6602ce18adf4ddcd0e48f2e13590cc92a536199e52109f46f259d3c46/mmh3-5.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7", size = 40034, upload-time = "2026-03-05T15:54:23.943Z" }, { url = "https://files.pythonhosted.org/packages/e5/e2/51ed62063b44d10b06d975ac87af287729eeb5e3ed9772f7584a17983e90/mmh3-5.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006", size = 103274, upload-time = "2026-03-05T15:54:26.44Z" }, { url = "https://files.pythonhosted.org/packages/75/ce/12a7524dca59eec92e5b31fdb13ede1e98eda277cf2b786cf73bfbc24e81/mmh3-5.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825", size = 106158, upload-time = "2026-03-05T15:54:28.578Z" }, - { url = "https://files.pythonhosted.org/packages/86/1f/d3ba6dd322d01ab5d44c46c8f0c38ab6bbbf9b5e20e666dfc05bf4a23604/mmh3-5.2.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a", size = 113005, upload-time = "2026-03-05T15:54:29.767Z" }, - { url = "https://files.pythonhosted.org/packages/b6/a9/15d6b6f913294ea41b44d901741298e3718e1cb89ee626b3694625826a43/mmh3-5.2.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b", size = 120744, upload-time = "2026-03-05T15:54:30.931Z" }, { url = "https://files.pythonhosted.org/packages/76/b3/70b73923fd0284c439860ff5c871b20210dfdbe9a6b9dd0ee6496d77f174/mmh3-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166", size = 99111, upload-time = "2026-03-05T15:54:32.353Z" }, - { url = "https://files.pythonhosted.org/packages/fd/68/6e292c0853e204c44d2f03ea5f090be3317a0e2d9417ecb62c9eb27687df/mmh3-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211", size = 106437, upload-time = "2026-03-05T15:54:35.177Z" }, - { url = "https://files.pythonhosted.org/packages/dd/c6/fedd7284c459cfb58721d461fcf5607a4c1f5d9ab195d113d51d10164d16/mmh3-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000", size = 110002, upload-time = "2026-03-05T15:54:36.673Z" }, { url = "https://files.pythonhosted.org/packages/3b/ac/ca8e0c19a34f5b71390171d2ff0b9f7f187550d66801a731bb68925126a4/mmh3-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5", size = 97507, upload-time = "2026-03-05T15:54:37.804Z" }, { url = "https://files.pythonhosted.org/packages/62/fb/648bfddb74a872004b6ee751551bfdda783fe6d70d2e9723bad84dbe5311/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6", size = 39114, upload-time = "2026-03-05T15:54:45.205Z" }, { url = "https://files.pythonhosted.org/packages/95/c2/ab7901f87af438468b496728d11264cb397b3574d41506e71b92128e0373/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f", size = 39819, upload-time = "2026-03-05T15:54:46.509Z" }, @@ -3461,11 +3323,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/4c/8e3af1b6d85a299767ec97bd923f12b06267089c1472c27c1696870d1175/mmh3-5.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03", size = 40033, upload-time = "2026-03-05T15:54:50.994Z" }, { url = "https://files.pythonhosted.org/packages/bb/0d/2c5f9893b38aeb6b034d1a44ecd55a010148054f6a516abe53b5e4057297/mmh3-5.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5", size = 103299, upload-time = "2026-03-05T15:54:53.569Z" }, { url = "https://files.pythonhosted.org/packages/1c/fc/2ebaef4a4d4376f89761274dc274035ffd96006ab496b4ee5af9b08f21a9/mmh3-5.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593", size = 106222, upload-time = "2026-03-05T15:54:55.092Z" }, - { url = "https://files.pythonhosted.org/packages/57/09/ea7ffe126d0ba0406622602a2d05e1e1a6841cc92fc322eb576c95b27fad/mmh3-5.2.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4", size = 113048, upload-time = "2026-03-05T15:54:56.305Z" }, - { url = "https://files.pythonhosted.org/packages/85/57/9447032edf93a64aa9bef4d9aa596400b1756f40411890f77a284f6293ca/mmh3-5.2.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1", size = 120742, upload-time = "2026-03-05T15:54:57.453Z" }, { url = "https://files.pythonhosted.org/packages/53/82/a86cc87cc88c92e9e1a598fee509f0409435b57879a6129bf3b3e40513c7/mmh3-5.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104", size = 99132, upload-time = "2026-03-05T15:54:58.583Z" }, - { url = "https://files.pythonhosted.org/packages/e8/88/a601e9f32ad1410f438a6d0544298ea621f989bd34a0731a7190f7dec799/mmh3-5.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f", size = 106479, upload-time = "2026-03-05T15:55:01.532Z" }, - { url = "https://files.pythonhosted.org/packages/d6/5c/ce29ae3dfc4feec4007a437a1b7435fb9507532a25147602cd5b52be86db/mmh3-5.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2", size = 110030, upload-time = "2026-03-05T15:55:02.934Z" }, { url = "https://files.pythonhosted.org/packages/13/30/ae444ef2ff87c805d525da4fa63d27cda4fe8a48e77003a036b8461cfd5c/mmh3-5.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a", size = 97536, upload-time = "2026-03-05T15:55:04.135Z" }, ] @@ -3532,38 +3390,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, - { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, - { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, - { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, - { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, - { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, - { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, - { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, - { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, - { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, - { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, - { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, - { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, - { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] @@ -4032,6 +3872,7 @@ dependencies = [ { name = "nemo-platform-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-platform-sdk", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "pydantic", extra = ["email"], marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pyyaml", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] [package.optional-dependencies] @@ -4041,6 +3882,11 @@ docker = [ k8s = [ { name = "kubernetes", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] +openshell = [ + { name = "grpcio", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "openshell", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "protobuf", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] [package.dev-dependencies] dev = [ @@ -4056,13 +3902,17 @@ dev = [ requires-dist = [ { name = "docker", marker = "extra == 'docker'", specifier = ">=7.0" }, { name = "fastapi", specifier = ">=0.115" }, + { name = "grpcio", marker = "extra == 'openshell'", specifier = ">=1.78.0" }, { name = "httpx", specifier = ">=0.27" }, { name = "kubernetes", marker = "extra == 'k8s'", specifier = ">=30.1.0" }, { name = "nemo-platform-plugin", editable = "packages/nemo_platform_plugin" }, { name = "nemo-platform-sdk", editable = "sdk/python/nemo-platform" }, + { name = "openshell", marker = "extra == 'openshell'", specifier = ">=0.0.92" }, + { name = "protobuf", marker = "extra == 'openshell'", specifier = ">=6.31.1" }, { name = "pydantic", specifier = ">=2.10.6" }, + { name = "pyyaml", specifier = ">=6.0" }, ] -provides-extras = ["docker", "k8s"] +provides-extras = ["docker", "k8s", "openshell"] [package.metadata.requires-dev] dev = [ @@ -8149,6 +7999,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/be/7b/45ad1b95315b5563baa7338c8e8088bb1af66905c46e1bd1fe6ecbe30ea8/openinference_semantic_conventions-0.1.29-py3-none-any.whl", hash = "sha256:f45e0b1cf79fe407af4722bcf391a01565f0878c95be3ebcc9382245d0367cc5", size = 10582, upload-time = "2026-04-22T00:39:27.066Z" }, ] +[[package]] +name = "openshell" +version = "0.0.92" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "grpcio", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "httpx", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "protobuf", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/e3/be7c389ae1b12312b302da847b82734475d2689188d2a19645ac8c1223c8/openshell-0.0.92-py3-none-macosx_13_0_arm64.whl", hash = "sha256:3d673a98e66520eabd4b06fbff126d55718fb92c868fbfeb994adb34970a4796", size = 8470512, upload-time = "2026-07-27T15:32:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ca/604e9a4b9701f11c51ca4129699ca3a548aad734254431bdfcda48f17e15/openshell-0.0.92-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:036fa76cd89dd49375405edde55ee35f72c0a3bd22f990942a336f4ededf15ef", size = 8532736, upload-time = "2026-07-27T15:33:16.548Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fc/24a15a862925d19b1d318be2796aa996233dc30b1b9e3c4862ec1039c6f4/openshell-0.0.92-py3-none-manylinux_2_39_x86_64.whl", hash = "sha256:8a20fea53ffff0c6127ca3f6841fb4b268ced1ccb03917cdbf278e419c1e9fdf", size = 9018946, upload-time = "2026-07-27T15:33:37.985Z" }, +] + [[package]] name = "opentelemetry-api" version = "1.43.0" @@ -8448,22 +8314,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/f6/8d58b32ab32d9215973a1688aebd098252ee8af1766c0e4e36e7831f0295/orjson-3.11.8-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f", size = 229233, upload-time = "2026-03-31T16:15:12.762Z" }, { url = "https://files.pythonhosted.org/packages/a9/8b/2ffe35e71f6b92622e8ea4607bf33ecf7dfb51b3619dcfabfd36cbe2d0a5/orjson-3.11.8-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6", size = 128772, upload-time = "2026-03-31T16:15:14.237Z" }, { url = "https://files.pythonhosted.org/packages/27/d2/1f8682ae50d5c6897a563cb96bc106da8c9cb5b7b6e81a52e4cc086679b9/orjson-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8", size = 131946, upload-time = "2026-03-31T16:15:15.607Z" }, - { url = "https://files.pythonhosted.org/packages/52/4b/5500f76f0eece84226e0689cb48dcde081104c2fa6e2483d17ca13685ffb/orjson-3.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813", size = 130368, upload-time = "2026-03-31T16:15:17.066Z" }, - { url = "https://files.pythonhosted.org/packages/56/7c/ba7cb871cba1bcd5cd02ee34f98d894c6cea96353ad87466e5aef2429c60/orjson-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546", size = 146877, upload-time = "2026-03-31T16:15:19.833Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/eb9c25fc1386696c6a342cd361c306452c75e0b55e86ad602dd4827a7fd7/orjson-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506", size = 132837, upload-time = "2026-03-31T16:15:21.282Z" }, { url = "https://files.pythonhosted.org/packages/37/87/5ddeb7fc1fbd9004aeccab08426f34c81a5b4c25c7061281862b015fce2b/orjson-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f", size = 133624, upload-time = "2026-03-31T16:15:22.641Z" }, { url = "https://files.pythonhosted.org/packages/22/09/90048793db94ee4b2fcec4ac8e5ddb077367637d6650be896b3494b79bb7/orjson-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e", size = 141904, upload-time = "2026-03-31T16:15:24.435Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cf/eb284847487821a5d415e54149a6449ba9bfc5872ce63ab7be41b8ec401c/orjson-3.11.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb", size = 423742, upload-time = "2026-03-31T16:15:26.155Z" }, { url = "https://files.pythonhosted.org/packages/b3/6d/37c2589ba864e582ffe7611643314785c6afb1f83c701654ef05daa8fcc7/orjson-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25", size = 136485, upload-time = "2026-03-31T16:15:29.749Z" }, { url = "https://files.pythonhosted.org/packages/66/7f/95fba509bb2305fab0073558f1e8c3a2ec4b2afe58ed9fcb7d3b8beafe94/orjson-3.11.8-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc", size = 229180, upload-time = "2026-03-31T16:15:36.426Z" }, { url = "https://files.pythonhosted.org/packages/f6/9d/b237215c743ca073697d759b5503abd2cb8a0d7b9c9e21f524bcf176ab66/orjson-3.11.8-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559", size = 128754, upload-time = "2026-03-31T16:15:38.049Z" }, { url = "https://files.pythonhosted.org/packages/42/3d/27d65b6d11e63f133781425f132807aef793ed25075fec686fc8e46dd528/orjson-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623", size = 131877, upload-time = "2026-03-31T16:15:39.484Z" }, - { url = "https://files.pythonhosted.org/packages/dd/cc/faee30cd8f00421999e40ef0eba7332e3a625ce91a58200a2f52c7fef235/orjson-3.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c", size = 130361, upload-time = "2026-03-31T16:15:41.274Z" }, - { url = "https://files.pythonhosted.org/packages/9c/7c/ca3a3525aa32ff636ebb1778e77e3587b016ab2edb1b618b36ba96f8f2c0/orjson-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55", size = 146862, upload-time = "2026-03-31T16:15:44.341Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0c/18a9d7f18b5edd37344d1fd5be17e94dc652c67826ab749c6e5948a78112/orjson-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137", size = 132847, upload-time = "2026-03-31T16:15:46.368Z" }, { url = "https://files.pythonhosted.org/packages/23/91/7e722f352ad67ca573cee44de2a58fb810d0f4eb4e33276c6a557979fd8a/orjson-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53", size = 133637, upload-time = "2026-03-31T16:15:48.123Z" }, { url = "https://files.pythonhosted.org/packages/af/04/32845ce13ac5bd1046ddb02ac9432ba856cc35f6d74dde95864fe0ad5523/orjson-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e", size = 141906, upload-time = "2026-03-31T16:15:49.626Z" }, - { url = "https://files.pythonhosted.org/packages/02/5e/c551387ddf2d7106d9039369862245c85738b828844d13b99ccb8d61fd06/orjson-3.11.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6", size = 423722, upload-time = "2026-03-31T16:15:51.176Z" }, { url = "https://files.pythonhosted.org/packages/18/6d/0dce10b9f6643fdc59d99333871a38fa5a769d8e2fc34a18e5d2bfdee900/orjson-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b", size = 136460, upload-time = "2026-03-31T16:15:54.431Z" }, ] @@ -8475,17 +8333,13 @@ sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9 wheels = [ { 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/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" }, ] @@ -8777,35 +8631,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] @@ -8818,7 +8657,6 @@ sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a wheels = [ { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] @@ -8847,21 +8685,13 @@ sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba42464 wheels = [ { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" }, { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, - { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, - { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, - { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, - { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" }, { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, - { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" }, { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, ] @@ -8916,19 +8746,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/8e/63/4fbc14810c32d2a88 wheels = [ { url = "https://files.pythonhosted.org/packages/cb/32/fe1cc3d36a19c1ce39792b1ed151ddff5ee1d74c8801f0e93ff36e65f885/py_rust_stemmers-0.1.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d62410ada44a01e02974b85d45d82f4b4c511aae9121e5f3c1ba1d0bea9126b", size = 272021, upload-time = "2025-02-19T13:55:25.685Z" }, { url = "https://files.pythonhosted.org/packages/0a/38/b8f94e5e886e7ab181361a0911a14fb923b0d05b414de85f427e773bf445/py_rust_stemmers-0.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b28ef729a4c83c7d9418be3c23c0372493fcccc67e86783ff04596ef8a208cdf", size = 310547, upload-time = "2025-02-19T13:55:26.891Z" }, - { url = "https://files.pythonhosted.org/packages/a9/08/62e97652d359b75335486f4da134a6f1c281f38bd3169ed6ecfb276448c3/py_rust_stemmers-0.1.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a979c3f4ff7ad94a0d4cf566ca7bfecebb59e66488cc158e64485cf0c9a7879f", size = 315237, upload-time = "2025-02-19T13:55:28.116Z" }, { url = "https://files.pythonhosted.org/packages/1c/b9/fc0278432f288d2be4ee4d5cc80fd8013d604506b9b0503e8b8cae4ba1c3/py_rust_stemmers-0.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c3593d895453fa06bf70a7b76d6f00d06def0f91fc253fe4260920650c5e078", size = 324419, upload-time = "2025-02-19T13:55:29.211Z" }, { url = "https://files.pythonhosted.org/packages/6b/5b/74e96eaf622fe07e83c5c389d101540e305e25f76a6d0d6fb3d9e0506db8/py_rust_stemmers-0.1.5-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:96ccc7fd042ffc3f7f082f2223bb7082ed1423aa6b43d5d89ab23e321936c045", size = 324792, upload-time = "2025-02-19T13:55:30.948Z" }, { url = "https://files.pythonhosted.org/packages/4f/f7/b76816d7d67166e9313915ad486c21d9e7da0ac02703e14375bb1cb64b5a/py_rust_stemmers-0.1.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef18cfced2c9c676e0d7d172ba61c3fab2aa6969db64cc8f5ca33a7759efbefe", size = 488014, upload-time = "2025-02-19T13:55:32.066Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ed/7d9bed02f78d85527501f86a867cd5002d97deb791b9a6b1b45b00100010/py_rust_stemmers-0.1.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:541d4b5aa911381e3d37ec483abb6a2cf2351b4f16d5e8d77f9aa2722956662a", size = 575582, upload-time = "2025-02-19T13:55:34.005Z" }, { url = "https://files.pythonhosted.org/packages/93/40/eafd1b33688e8e8ae946d1ef25c4dc93f5b685bd104b9c5573405d7e1d30/py_rust_stemmers-0.1.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ffd946a36e9ac17ca96821963663012e04bc0ee94d21e8b5ae034721070b436c", size = 493267, upload-time = "2025-02-19T13:55:35.294Z" }, { url = "https://files.pythonhosted.org/packages/ed/be/0465dcb3a709ee243d464e89231e3da580017f34279d6304de291d65ccb0/py_rust_stemmers-0.1.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4e308fc7687901f0c73603203869908f3156fa9c17c4ba010a7fcc98a7a1c5f2", size = 272019, upload-time = "2025-02-19T13:55:39.183Z" }, { url = "https://files.pythonhosted.org/packages/ab/b6/76ca5b1f30cba36835938b5d9abee0c130c81833d51b9006264afdf8df3c/py_rust_stemmers-0.1.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9efc4da5e734bdd00612e7506de3d0c9b7abc4b89d192742a0569d0d1fe749", size = 310545, upload-time = "2025-02-19T13:55:40.339Z" }, - { url = "https://files.pythonhosted.org/packages/56/8f/5be87618cea2fe2e70e74115a20724802bfd06f11c7c43514b8288eb6514/py_rust_stemmers-0.1.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc2cc8d2b36bc05b8b06506199ac63d437360ae38caefd98cd19e479d35afd42", size = 315236, upload-time = "2025-02-19T13:55:41.55Z" }, { url = "https://files.pythonhosted.org/packages/00/02/ea86a316aee0f0a9d1449ad4dbffff38f4cf0a9a31045168ae8b95d8bdf8/py_rust_stemmers-0.1.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a231dc6f0b2a5f12a080dfc7abd9e6a4ea0909290b10fd0a4620e5a0f52c3d17", size = 324419, upload-time = "2025-02-19T13:55:42.693Z" }, { url = "https://files.pythonhosted.org/packages/2a/fd/1612c22545dcc0abe2f30fc08f30a2332f2224dd536fa1508444a9ca0e39/py_rust_stemmers-0.1.5-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5845709d48afc8b29e248f42f92431155a3d8df9ba30418301c49c6072b181b0", size = 324794, upload-time = "2025-02-19T13:55:43.896Z" }, { url = "https://files.pythonhosted.org/packages/66/18/8a547584d7edac9e7ac9c7bdc53228d6f751c0f70a317093a77c386c8ddc/py_rust_stemmers-0.1.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e48bfd5e3ce9d223bfb9e634dc1425cf93ee57eef6f56aa9a7120ada3990d4be", size = 488014, upload-time = "2025-02-19T13:55:45.088Z" }, - { url = "https://files.pythonhosted.org/packages/3b/87/4619c395b325e26048a6e28a365afed754614788ba1f49b2eefb07621a03/py_rust_stemmers-0.1.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:35d32f6e7bdf6fd90e981765e32293a8be74def807147dea9fdc1f65d6ce382f", size = 575582, upload-time = "2025-02-19T13:55:46.436Z" }, { url = "https://files.pythonhosted.org/packages/98/6e/214f1a889142b7df6d716e7f3fea6c41e87bd6c29046aa57e175d452b104/py_rust_stemmers-0.1.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:191ea8bf922c984631ffa20bf02ef0ad7eec0465baeaed3852779e8f97c7e7a3", size = 493269, upload-time = "2025-02-19T13:55:49.057Z" }, ] @@ -9065,21 +8891,13 @@ sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd2 wheels = [ { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, @@ -9125,17 +8943,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/14/5b/bb6a8bfdf13eb9808 wheels = [ { url = "https://files.pythonhosted.org/packages/55/83/8ccf04b2f9642153702c6eb22d0a0abad57014fd85879ab1f6341b5a1946/pydantic_monty-0.0.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2988d3e511131680d9de60647bfe5c697b1e4e4cad474fecf1451c53314e8520", size = 8688756, upload-time = "2026-05-29T08:30:24.677Z" }, { url = "https://files.pythonhosted.org/packages/de/b8/c7881620a812850772ae0924863d1399cbecb3e4c8c455a9c7a9c20b06f8/pydantic_monty-0.0.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c688dc7c7b28a2f389a61217bae1d50658e28f960abe33a254328cadc8a17a", size = 8171342, upload-time = "2026-05-29T08:29:44.773Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ea/6d10ea1657e303295a75a3854f6dd6b378cbd501dcd1782844107b932acd/pydantic_monty-0.0.18-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f469293f5a776231b9617787a5dd6c58048c6568833ee6848338be6391f15449", size = 8591152, upload-time = "2026-05-29T08:31:29.944Z" }, - { url = "https://files.pythonhosted.org/packages/93/fb/ab85c4676ccffd0f3b7f509a4c8b396b07c7860577def2f58a22b3fe8aef/pydantic_monty-0.0.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6e0cd4991947b8a47210985836f94c14139bc3ea06253d2f38d0d752b165517", size = 9183064, upload-time = "2026-05-29T08:31:12.776Z" }, - { url = "https://files.pythonhosted.org/packages/5c/12/11292178b487052f9e0a1ea7b3d17e1e3bfcba598fefce8cb9ed8712021e/pydantic_monty-0.0.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58b4b96863abbc0ffa5baf64779b3fbb6376cc763488b027ecaddb5052d5ff17", size = 9285440, upload-time = "2026-05-29T08:29:35.642Z" }, { url = "https://files.pythonhosted.org/packages/79/42/7afb8dde4414d84c042f2cc1b0870a7351cae2e4fbf3fef89b3aa683eca9/pydantic_monty-0.0.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1208bd5976c1254b2705559836511c86ea3cc51d9c6f688b1e3e22984715dbc", size = 9233438, upload-time = "2026-05-29T08:31:39.177Z" }, { url = "https://files.pythonhosted.org/packages/5f/46/89124cf146725e354b44685b477da6b0b5dc07a8a3af2aec309e88c55405/pydantic_monty-0.0.18-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:977168253d8f6b49bb64f128d02c4b62ee8b435fd62876268377e1f2b00cc00f", size = 8351900, upload-time = "2026-05-29T08:31:08.348Z" }, { url = "https://files.pythonhosted.org/packages/00/c5/dda512f5a9c68242faea368844aacefb54c2a13f9b40bee5ab48ccdc78c5/pydantic_monty-0.0.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c21319a091dc1ff1fccb8647dae5bb543b3f528c556319ed15c7992dfa9b5e87", size = 8901559, upload-time = "2026-05-29T08:29:26.047Z" }, { url = "https://files.pythonhosted.org/packages/c6/9c/7628423f955efb669d2cc1d3a8909bf8271b543ce27036e18229ad0e51e8/pydantic_monty-0.0.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d47976a18e3e3da0e86f8cf6068fc8a125930422dc10d3d3bf0b5410a9e9282e", size = 8689116, upload-time = "2026-05-29T08:29:30.906Z" }, { url = "https://files.pythonhosted.org/packages/c2/9c/51f8ffa4340bc1986eb9240b0756724f5fdf3c463d6d66c8cc8450e1446d/pydantic_monty-0.0.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb84fe51e3f6e00a0cc9628e0acf5904d982c8cc85d4db42b7532d071602f703", size = 8178458, upload-time = "2026-05-29T08:30:09.015Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ec/7eb84aeb86631571f9acffc91552217dc2b524b00db37b8d10517df467d1/pydantic_monty-0.0.18-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a2e86f3ba67b094d498bc8b071d5e3b8b034bb9f3006216a357c5f71d49d6132", size = 8591295, upload-time = "2026-05-29T08:29:23.357Z" }, - { url = "https://files.pythonhosted.org/packages/da/69/d5210208fa116593bd81789e2e5abb6222d38087c9c1879e18f7e7620275/pydantic_monty-0.0.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb8a412aa0336d4e0334a4e05a54b391d91fa334020bd295a280285ba17ab5ca", size = 9184647, upload-time = "2026-05-29T08:29:51.852Z" }, - { url = "https://files.pythonhosted.org/packages/ed/74/4d95c8f65072964c4cb798dbe87d2e1c1349607ab5905874bfa8a0b94de1/pydantic_monty-0.0.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1056ce3acef60ab880314caf97775c5ea41b30f9306d0dd28cefeffd42dda366", size = 9291637, upload-time = "2026-05-29T08:31:19.966Z" }, { url = "https://files.pythonhosted.org/packages/d0/40/5817780313a3e089ca6f860fbdc836d3aa33790eb72c2e8fe2edc877820e/pydantic_monty-0.0.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9593caa45b68fd07ac67bea9974effbe2a1c5453d8a106b913596b0dff6d8471", size = 9233863, upload-time = "2026-05-29T08:31:42.838Z" }, { url = "https://files.pythonhosted.org/packages/b3/55/f77565c5797502c7ba995dc23a26759cb33023590f9f2926bc4e8ab87afe/pydantic_monty-0.0.18-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:e15e18ed27a17ee607ad3bcbf82f25a9ec4d496ff493ff64cdabb83ca2174cec", size = 8358264, upload-time = "2026-05-29T08:31:32.531Z" }, { url = "https://files.pythonhosted.org/packages/1b/1f/c700eb800868d1be4078a99cb00e23fb7e5d8760c8e83b729bba27b5bf92/pydantic_monty-0.0.18-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:96d75a418d96640ff0c7f354a78fc4470a2d0f40ba69e886c2f32756d594d9a0", size = 8906664, upload-time = "2026-05-29T08:30:04.055Z" }, @@ -9498,13 +9310,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd77 wheels = [ { 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/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" }, @@ -9518,13 +9328,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/5e/eb/5a0d575de784f9a1f wheels = [ { url = "https://files.pythonhosted.org/packages/ad/c5/a3d2020ce5ccfc6aede0d45bcb870298652ac0cf199f67714d250e0cdf39/pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69", size = 176146, upload-time = "2025-06-10T15:31:50.584Z" }, { url = "https://files.pythonhosted.org/packages/e3/bb/23a9739291086ca0d3189eac7cd92b4d00e9fdc77d722ab610c35f9a82ba/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0", size = 746792, upload-time = "2025-06-10T15:31:52.304Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c2/e8825f4ff725b7e560d62a3609e31d735318068e1079539ebfde397ea03e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42", size = 786772, upload-time = "2025-06-10T15:31:54.712Z" }, { url = "https://files.pythonhosted.org/packages/35/be/58a4dcae8854f2fdca9b28d9495298fd5571a50d8430b1c3033ec95d2d0e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b", size = 778723, upload-time = "2025-06-10T15:31:56.093Z" }, { url = "https://files.pythonhosted.org/packages/86/ed/fed0da92b5d5d7340a082e3802d84c6dc9d5fa142954404c41a544c1cb92/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254", size = 758478, upload-time = "2025-06-10T15:31:58.314Z" }, { url = "https://files.pythonhosted.org/packages/f0/69/ac02afe286275980ecb2dcdc0156617389b7e0c0a3fcdedf155c67be2b80/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8", size = 799159, upload-time = "2025-06-10T15:31:59.675Z" }, { url = "https://files.pythonhosted.org/packages/0f/16/2710c252ee04cbd74d9562ebba709e5a284faeb8ada88fcda548c9191b47/pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1", size = 182879, upload-time = "2025-06-10T15:32:04.466Z" }, { url = "https://files.pythonhosted.org/packages/9a/40/ae8163519d937fa7bfa457b6f78439cc6831a7c2b170e4f612f7eda71815/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49", size = 811277, upload-time = "2025-06-10T15:32:06.214Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/28d82dbff7f87b96f0eeac79b7d972a96b4980c1e445eb6a857ba91eda00/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b", size = 831650, upload-time = "2025-06-10T15:32:08.076Z" }, { url = "https://files.pythonhosted.org/packages/e8/df/161c4566facac7d75a9e182295c223060373d4116dead9cc53a265de60b9/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a", size = 815755, upload-time = "2025-06-10T15:32:09.435Z" }, { url = "https://files.pythonhosted.org/packages/05/10/f42c48fa5153204f42eaa945e8d1fd7c10d6296841dcb2447bf7da1be5c4/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e", size = 810403, upload-time = "2025-06-10T15:32:11.051Z" }, { url = "https://files.pythonhosted.org/packages/d5/d2/e369064aa51009eb9245399fd8ad2c562bd0bcd392a00be44b2a824ded7c/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255", size = 835581, upload-time = "2025-06-10T15:32:12.897Z" }, @@ -9617,38 +9425,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, - { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, - { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, - { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, - { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, - { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, - { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, - { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, - { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, - { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, - { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, - { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, - { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, - { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, - { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, ] @@ -9764,21 +9554,13 @@ sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b wheels = [ { url = "https://files.pythonhosted.org/packages/93/b0/d4f1f3fe9eb3f8e382d45ce5b0547ea01c4b7e0b4b4eb87bcd66a1d2b888/rignore-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961", size = 820411, upload-time = "2025-11-05T20:42:24.782Z" }, { url = "https://files.pythonhosted.org/packages/4a/c8/dea564b36dedac8de21c18e1851789545bc52a0c22ece9843444d5608a6a/rignore-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", size = 897821, upload-time = "2025-11-05T20:40:52.613Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2b/ee96db17ac1835e024c5d0742eefb7e46de60020385ac883dd3d1cde2c1f/rignore-0.7.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", size = 873963, upload-time = "2025-11-05T20:41:07.49Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8c/ad5a57bbb9d14d5c7e5960f712a8a0b902472ea3f4a2138cbf70d1777b75/rignore-0.7.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", size = 1169216, upload-time = "2025-11-05T20:41:23.73Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/5b00bc2a6bc1701e6878fca798cf5d9125eb3113193e33078b6fc0d99123/rignore-0.7.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a", size = 942942, upload-time = "2025-11-05T20:41:39.393Z" }, { url = "https://files.pythonhosted.org/packages/85/e5/7f99bd0cc9818a91d0e8b9acc65b792e35750e3bdccd15a7ee75e64efca4/rignore-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd", size = 959787, upload-time = "2025-11-05T20:42:09.765Z" }, { url = "https://files.pythonhosted.org/packages/41/f7/e80f55dfe0f35787fa482aa18689b9c8251e045076c35477deb0007b3277/rignore-0.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1734dc49d1e9501b07852ef44421f84d9f378da9fbeda729e77db71f49cac28b", size = 1078647, upload-time = "2025-11-05T21:40:13.463Z" }, - { url = "https://files.pythonhosted.org/packages/d4/cf/2c64f0b6725149f7c6e7e5a909d14354889b4beaadddaa5fff023ec71084/rignore-0.7.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", size = 1139186, upload-time = "2025-11-05T21:40:31.27Z" }, { url = "https://files.pythonhosted.org/packages/7f/5e/13b249613fd5d18d58662490ab910a9f0be758981d1797789913adb4e918/rignore-0.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", size = 1127725, upload-time = "2025-11-05T21:41:05.804Z" }, { url = "https://files.pythonhosted.org/packages/f9/8f/f8daacd177db4bf7c2223bab41e630c52711f8af9ed279be2058d2fe4982/rignore-0.7.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010", size = 820150, upload-time = "2025-11-05T20:42:26.545Z" }, { url = "https://files.pythonhosted.org/packages/36/31/b65b837e39c3f7064c426754714ac633b66b8c2290978af9d7f513e14aa9/rignore-0.7.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c", size = 897406, upload-time = "2025-11-05T20:40:53.854Z" }, - { url = "https://files.pythonhosted.org/packages/ca/58/1970ce006c427e202ac7c081435719a076c478f07b3a23f469227788dc23/rignore-0.7.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116", size = 874050, upload-time = "2025-11-05T20:41:08.922Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/eb45db9f90137329072a732273be0d383cb7d7f50ddc8e0bceea34c1dfdf/rignore-0.7.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e", size = 1167835, upload-time = "2025-11-05T20:41:24.997Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f1/6f1d72ddca41a64eed569680587a1236633587cc9f78136477ae69e2c88a/rignore-0.7.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0", size = 941945, upload-time = "2025-11-05T20:41:40.628Z" }, { url = "https://files.pythonhosted.org/packages/48/6f/2f178af1c1a276a065f563ec1e11e7a9e23d4996fd0465516afce4b5c636/rignore-0.7.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7", size = 959067, upload-time = "2025-11-05T20:42:11.09Z" }, { url = "https://files.pythonhosted.org/packages/31/eb/c4f92cc3f2825d501d3c46a244a671eb737fc1bcf7b05a3ecd34abb3e0d7/rignore-0.7.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:181eb2a975a22256a1441a9d2f15eb1292839ea3f05606620bd9e1938302cf79", size = 1078365, upload-time = "2025-11-05T21:40:15.148Z" }, - { url = "https://files.pythonhosted.org/packages/26/09/99442f02794bd7441bfc8ed1c7319e890449b816a7493b2db0e30af39095/rignore-0.7.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b", size = 1139066, upload-time = "2025-11-05T21:40:32.771Z" }, { url = "https://files.pythonhosted.org/packages/e2/25/d37215e4562cda5c13312636393aea0bafe38d54d4e0517520a4cc0753ec/rignore-0.7.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360", size = 1127550, upload-time = "2025-11-05T21:41:07.648Z" }, ] @@ -9811,29 +9593,17 @@ sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b3 wheels = [ { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, ] @@ -9853,16 +9623,10 @@ version = "0.15.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" }, { url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" }, { url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" }, - { url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" }, - { url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" }, { url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" }, - { url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" }, { url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" }, - { url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" }, { url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" }, ] @@ -9903,13 +9667,8 @@ sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd2 wheels = [ { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" }, { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" }, - { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" }, - { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" }, - { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" }, { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" }, - { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" }, { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" }, - { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" }, { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" }, ] @@ -10615,12 +10374,8 @@ sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb3 wheels = [ { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, - { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, - { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, ] @@ -10847,16 +10602,10 @@ version = "0.0.56" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/55/07/fb29aea5235b0aa8ecfc4d1cc6ddf9fba8b863d67d96c6d345694d644c43/ty-0.0.56.tar.gz", hash = "sha256:84d114dc3796361c0fc72945016eabd74d46b9ee64f198cb0e485719704681e5", size = 6050123, upload-time = "2026-07-01T16:44:56.036Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/48/bce79e7ca5c1cc529d3e0d37ddd1121aea4b68a4f749974ad1cc77161871/ty-0.0.56-py3-none-linux_armv6l.whl", hash = "sha256:186d4a53e15747c947e1ec3d7eec8e345d8e40a1ca10e634c585db52497e87dd", size = 11643066, upload-time = "2026-07-01T16:44:18.374Z" }, { url = "https://files.pythonhosted.org/packages/cf/2d/b3b7a74ce8bc59ef48843ad80179bb0d9598bbd6cfc0d11d519bdf6b1352/ty-0.0.56-py3-none-macosx_11_0_arm64.whl", hash = "sha256:afd3058c0a6c5f241e814734f133008c93ee805f61c9cf4ce7412b8822b5d9ad", size = 10962270, upload-time = "2026-07-01T16:44:22.959Z" }, { url = "https://files.pythonhosted.org/packages/64/ac/6c2fd7de0304a8a7218a756af74f7e62a5e8540fdb175e0a869e51042345/ty-0.0.56-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:058b52f7a823ac13aae3cae30809dd6b5145794b64d8478f9ef38c75d79b4483", size = 11471406, upload-time = "2026-07-01T16:44:25.327Z" }, - { url = "https://files.pythonhosted.org/packages/50/b6/11d861156861c03c7726b74558f9a0e0092661aff83a4fda1279df28c425/ty-0.0.56-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c66e00c1522add1f2bbdd2e45828c953b35c306b7bef03ec9169c75a63699a0", size = 11445612, upload-time = "2026-07-01T16:44:27.531Z" }, - { url = "https://files.pythonhosted.org/packages/d7/f7/dbb4b4ccb69cd64c209ae55b1ab788ace8222c2bc1f6845be9e7cbedbf25/ty-0.0.56-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63fe3947fe0c46c69a7d950e6832ee70a9ec17321fefbff3d2e3c20baf9e5bd0", size = 12666337, upload-time = "2026-07-01T16:44:31.586Z" }, - { url = "https://files.pythonhosted.org/packages/86/e9/73f903fe4a3d9ea02f26f57c1eb07e3b1029ec92b0e8c2364718893440e3/ty-0.0.56-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71a0c1a72f9854532e710e119b6871ffe4542c8a65146f1f65dcd78fecd885b4", size = 12280247, upload-time = "2026-07-01T16:44:33.637Z" }, { url = "https://files.pythonhosted.org/packages/d6/90/cebd222495832f1a00dcd321ba25f3cab804221a4991b992c2178bec68ee/ty-0.0.56-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70d1665596494e24d8ebd198438872b5a56ec3cae5f2bcf6c673be797acc4e3c", size = 11991107, upload-time = "2026-07-01T16:44:36.122Z" }, - { url = "https://files.pythonhosted.org/packages/b7/07/8f7337a07250f42d975cdb6decf47fc5b421e6c7da5e3e7be1e85f63a7e5/ty-0.0.56-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:778f99e51558afc1dbbe48ee38ab6aae7b31390ed8c1a1ef1499b295e9f1e82f", size = 12298970, upload-time = "2026-07-01T16:44:38.243Z" }, { url = "https://files.pythonhosted.org/packages/3c/b9/a52cd59034a48f5f18c6b155cc2cc36861d874b6d0af204b12c898024c3d/ty-0.0.56-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:867bc5708e0066bb4ff6c7db524bd5deea2676c62bfe71d3303138b3be850af0", size = 11425683, upload-time = "2026-07-01T16:44:40.473Z" }, - { url = "https://files.pythonhosted.org/packages/1d/2e/48e42d33357d52eefb695c0c3fcfc96879b73668a7447d1d1e0ad774fedc/ty-0.0.56-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6012f4189c928edb330a37deb9930f982380bd4aa7c4b8e0428eec9651c7551", size = 11469258, upload-time = "2026-07-01T16:44:42.513Z" }, { url = "https://files.pythonhosted.org/packages/09/34/9d81967ff240eaa57e9249728ef7b7790747cf6d3c9a98ec86b2cfdcc8ee/ty-0.0.56-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:62619b3b0e2c6248ef30d3f0e2f2217ae9893040585be07f32324242f197cd6f", size = 12100242, upload-time = "2026-07-01T16:44:46.584Z" }, ] @@ -11153,11 +10902,8 @@ sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062c wheels = [ { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, - { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, ] @@ -11290,25 +11036,16 @@ sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d wheels = [ { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, - { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, - { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, - { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, - { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, @@ -11438,30 +11175,18 @@ sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6 wheels = [ { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, - { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, - { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, - { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, - { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, - { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, - { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, - { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, - { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, - { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, - { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, ] @@ -11492,44 +11217,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, - { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, - { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, - { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, - { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, - { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, - { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, - { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, - { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, - { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, - { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, ] @@ -11551,24 +11252,16 @@ sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529 wheels = [ { 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/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/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/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/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/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" }, ]