diff --git a/docs/get-started/example-agent.mdx b/docs/get-started/example-agent.mdx index 226f334cb9..4ef8ad549f 100644 --- a/docs/get-started/example-agent.mdx +++ b/docs/get-started/example-agent.mdx @@ -3,7 +3,10 @@ title: "Get started with an example agent" description: "Run the optimization loop from end to end on τ-Bench" --- -This provides a guide for how you can quickly load agent traces into your NeMo Platform, run the analyst agent to discover issues with that agent, and launch an experimentalist to fix those issues. This is meant as an example to show how the whole platform works. You can also jump straight to setting up your agent with NeMo Platform. +This guide runs one Tau3 Airline agent through the full loop: Harbor evaluates +the checked-in example agent, the Analyst discovers issues from its traces, and +the Experimentalist improves the same agent. You can also jump straight to +setting up your agent with NeMo Platform. ## Prerequisites @@ -34,33 +37,29 @@ uv run nemo services start --config packages/nmp_platform/config/local.yaml You should now be able to navigate to `http://localhost:8080` and see the NeMo Platform web UI. -## 3. Prepare Tau2 and run the airline agent +## 3. Run the Tau3 Airline agent with Harbor -Now that we have a running NeMo Platform, it's time to load up some data! This example uses the τ-Bench from Sierra. It's a great representation of a simplified agent that performs a business-critical task. Our first step will be to download the tau2 repository and install the dependencies to run it. - -Then, we'll run a script to go through 30 of the tasks from the Tau 2 Airline dataset. In order to make our example more realistic and representative of the kind of agent data we typically see in production, we won't record whether the tasks passed verification – we want to see if we can gain useful insights even without ground truth data. This could take up to 20 minutes, and will cost about a dollar. +Evaluate the +[checked-in Tau3 NOOA example agent](https://github.com/NVIDIA-NeMo/nemo-platform/tree/main/plugins/nemo-experimentalist/examples/tau3-nooa-agent) +against the +[`sierra-research/tau3-bench`](https://hub.harborframework.com/datasets/sierra-research/tau3-bench/latest) +dataset on Harbor Hub. The Insights testbed selects six Airline tasks, runs the +agent through Harbor, and uploads its traces and verifier rewards into NeMo +Platform. This can take 20 minutes or longer because Harbor builds task +containers and runs model-backed user simulation. ```bash -git init tmp/tau2-bench -git -C tmp/tau2-bench remote add origin https://github.com/sierra-research/tau2-bench.git -git -C tmp/tau2-bench fetch --depth 1 origin 8ebb7499622fc2be9b9d510d6f7a7653461f4f29 -git -C tmp/tau2-bench checkout --detach FETCH_HEAD -uv --directory tmp/tau2-bench sync --frozen -uv --directory tmp/tau2-bench run --frozen tau2 check-data - export NMP_BASE_URL=http://localhost:8080 +export INFERENCE_API_BASE=https://inference-api.nvidia.com/v1 export INFERENCE_API_KEY=sk-... -export OPENAI_API_KEY="$INFERENCE_API_KEY" -export OPENAI_API_BASE=https://inference-api.nvidia.com/v1 uv run --directory plugins/nemo-insights --frozen \ - python -m testbed run tau2-airline \ - --base "$NMP_BASE_URL" \ - --set include_rewards=false \ - --set tau2_repo="$PWD/tmp/tau2-bench" + python -m testbed run tau3-airline-harbor \ + --base "$NMP_BASE_URL" ``` -The testbed runs the 30-task airline train split and ingests realistic traces into the `tau2-airline` workspace. At this point you can navigate to the traces view and see the traces from the agent. +In Studio, open the `canonical-tau3-airline` workspace's Experiments area to +inspect the task results, rewards, and traces. ## 4. Run the Analyst @@ -68,16 +67,17 @@ Now that we have data in our system, we can run the analyst agent to understand If the analyst discovers issues in your application, it will create what we call an 'insight'. An insight is a human-readable description of a problem in your agentic system. The closest analogy is a bug report. They don't try to describe why an issue happened or the code you should change to fix it, and they should be understandable to a user of your agent. -Let's run it: +Run the Analyst against the recorded Harbor evaluation: ```bash -uv run --frozen nemo insights analyze \ - --agent tau2-airline \ - --workspace tau2-airline \ - --base-url "$NMP_BASE_URL" +uv run --directory plugins/nemo-insights --frozen \ + python -m testbed analyze tau3-airline-harbor \ + --live \ + --base "$NMP_BASE_URL" ``` -This will take a few minutes to run. Once it's done, you can navigate to the insights page to see the issues the analyst discovered in the τ-Bench Airline Agent. +This will take a few minutes. In Studio, open the +`canonical-tau3-airline` workspace's Insights area to review the result. ## 5. Prepare a Tau3 Airline smoke dataset diff --git a/plugins/nemo-experimentalist/Dockerfile b/plugins/nemo-experimentalist/Dockerfile new file mode 100644 index 0000000000..7fcb4acabc --- /dev/null +++ b/plugins/nemo-experimentalist/Dockerfile @@ -0,0 +1,54 @@ +# syntax=docker/dockerfile:1 +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +ARG NMP_PYTHON_BASE=nmp-python-base + +FROM ${NMP_PYTHON_BASE} AS dependencies +COPY --from=nmp-workspace pyproject.toml uv.lock README.md ./ +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen --only-group experimentalist --no-install-workspace --no-editable + +FROM dependencies AS builder +COPY --from=nmp-workspace . . +RUN --mount=type=cache,target=/root/.cache/uv \ + for package in \ + nemo-eval-author-plugin \ + nemo-experimentalist-plugin \ + nemo-insights-plugin \ + nemo-platform \ + nemo-platform-plugin \ + nemo-platform-sdk \ + nmp-common; do \ + uv build --wheel --package "$package" --out-dir /wheels; \ + done + +FROM ${NMP_PYTHON_BASE} AS runtime + +LABEL com.nvidia.nemo.experimentalist.openshell-runtime="1" + +RUN apt-get update && apt-get install -y --no-install-recommends \ + iproute2 \ + nftables \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd --system sandbox \ + && useradd --system --gid sandbox --create-home --home-dir /home/sandbox --shell /bin/bash sandbox \ + && mkdir -p /sandbox \ + && chown sandbox:sandbox /sandbox \ + && touch /etc/nemo-experimentalist-container + +COPY --from=dependencies /app/.venv /app/.venv +COPY --from=builder /wheels /wheels +RUN UV_COMPILE_BYTECODE=0 uv pip install \ + --python /app/.venv/bin/python \ + --no-deps \ + /wheels/*.whl \ + && rm -rf /wheels + +ENV HOME=/home/sandbox \ + PATH="/app/.venv/bin:$PATH" \ + XDG_CACHE_HOME=/sandbox/.cache + +WORKDIR /sandbox +USER sandbox diff --git a/plugins/nemo-experimentalist/README.md b/plugins/nemo-experimentalist/README.md index 4ccd8c7f72..635a9da25e 100644 --- a/plugins/nemo-experimentalist/README.md +++ b/plugins/nemo-experimentalist/README.md @@ -54,22 +54,52 @@ effective inputs: $NEMO experimentalist doctor ``` -## Run the Experimentalist locally +## Run the Experimentalist in OpenShell -`nemo experimentalist run` runs the local Experimentalist loop. It evaluates -a baseline agent on Harbor-compatible train and validation datasets, proposes -candidate mutations, and records its artifacts under the selected experiment -directory. +`nemo experimentalist run` resolves inputs on the trusted host, runs the +optimization loop in an OpenShell sandbox, and submits Harbor work to an +ephemeral authenticated host bridge. The sandbox receives a run-specific input +snapshot, not the repository or home directory, and it receives no Docker +socket or real bridge credential. OpenShell injects an opaque bridge placeholder +and replaces it with the host token only at the permitted network boundary. +There is no local-execution fallback. -Configure the models before running an experiment: +Trusted Python callers and tests can still invoke +`nemo_experimentalist_plugin.experimentalist.run.run_experimentalist` directly +without OpenShell. The public CLI deliberately does not expose that bypass. + +Install and configure OpenShell, then configure the optimizer model and the +dedicated direct-inference credential used by candidates: ```bash -export EXPERIMENTALIST_API_BASE=https://inference-api.nvidia.com/v1 -export EXPERIMENTALIST_API_KEY=sk-... +export EXPERIMENTALIST_API_KEY=nvapi-optimizer-key export EXPERIMENTALIST_SMART_MODEL_NAME=openai/openai/openai/gpt-5.5 export EXPERIMENTALIST_FAST_MODEL_NAME=openai/openai/openai/gpt-5-mini +export INFERENCE_API_KEY=nvapi-dedicated-experimentalist-key +export INFERENCE_API_BASE=https://inference-api.nvidia.com/v1 +export AUT_MODEL_NAME=openai/gpt-oss-120b ``` +The Experimentalist model values use NOOA's provider/routing/model identifier +format. Provider setup removes NOOA's outer client prefix while preserving the +gateway's provider/model route when deriving the OpenShell model ID. When the +optimizer and candidate share a gateway key, `EXPERIMENTALIST_API_KEY` defaults +to `INFERENCE_API_KEY`. + +Provider setup skips OpenShell 0.0.92's generic endpoint probe for the NVIDIA +Inference Hub route because its GPT-5 proxy rejects that probe shape. This does +not disable the sandbox's pinned inference route or network enforcement; the +first Experimentalist model request performs the functional endpoint check. + +The candidate key is injected only into the candidate process inside Harbor. +Use a spending-limited, easy-to-revoke credential. Source-control archival and +winner publishing are intentionally unsupported by this runtime. + +The local bridge binds all host interfaces by default because the OpenShell +Docker driver reaches it through the host-side gateway interface. On a trusted +host, set `NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_BIND` to that gateway's host IP +to restrict the listener to the interface OpenShell uses. + ### Insight-driven optimization A single local Insight in `.nemo-optimizer/insights.yaml` is selected by diff --git a/plugins/nemo-experimentalist/docker-bake.hcl b/plugins/nemo-experimentalist/docker-bake.hcl new file mode 100644 index 0000000000..55d84795b8 --- /dev/null +++ b/plugins/nemo-experimentalist/docker-bake.hcl @@ -0,0 +1,15 @@ +# Experimentalist control plane. Harbor and Docker remain host-side. +target "nmp-experimentalist-docker" { + target = "runtime" + context = "." + dockerfile = "plugins/nemo-experimentalist/Dockerfile" + contexts = { + nmp-python-base = "target:nmp-python-base" + nmp-workspace = "target:nmp-workspace" + } + cache-to = maybe_registry_cache_to("nmp-experimentalist") + cache-from = maybe_registry_cache_from("nmp-experimentalist") + tags = sha_and_maybe_latest_tags("nmp-experimentalist") + output = image_output() + platforms = get_platforms() +} diff --git a/plugins/nemo-experimentalist/pyproject.toml b/plugins/nemo-experimentalist/pyproject.toml index e0add47d4f..a478f7a91f 100644 --- a/plugins/nemo-experimentalist/pyproject.toml +++ b/plugins/nemo-experimentalist/pyproject.toml @@ -4,6 +4,7 @@ version = "0.1.0" description = "NeMo Experimentalist plugin for NeMo Platform." requires-python = ">=3.12,<3.14" dependencies = [ + "fastapi>=0.115.4", "pydantic>=2", "httpx", "harbor>=0.16", @@ -15,9 +16,14 @@ dependencies = [ "nemo-insights-plugin", "nemo-platform", "nemo-platform-plugin", + "python-multipart>=0.0.20", "tomlkit>=0.13.3", + "uvicorn>=0.34.0", ] +[project.scripts] +nemo-experimentalist-harbor-bridge = "nemo_experimentalist_plugin.harbor_bridge.service:main" + [project.entry-points."nemo.cli"] experimentalist = "nemo_experimentalist_plugin.cli:ExperimentalistCLI" diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/cli.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/cli.py index f8049e0b4a..ef81c9c06e 100644 --- a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/cli.py +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/cli.py @@ -15,6 +15,11 @@ import typer import yaml from nemo_experimentalist_plugin.client import make_client +from nemo_experimentalist_plugin.openshell.launcher import ( + OpenShellLaunchError, + launch_openshell_run, +) +from nemo_experimentalist_plugin.openshell.preparation import prepare_openshell_run from nemo_experimentalist_plugin.preflight import ( Probes, check_artifacts, @@ -45,8 +50,11 @@ from nemo_platform_plugin.cli import NemoCLI DEFAULT_WORKSPACE = "default" +_CONTAINER_MARKER = Path("/etc/nemo-experimentalist-container") _PREFLIGHT_PROBES: Probes | None = None # test seam; None → real probes +_OPEN_SHELL_PREPARER = prepare_openshell_run +_OPEN_SHELL_LAUNCHER = launch_openshell_run # Lazily imported in the experiment command: importing experimentalist.run reaches model # construction that requires EXPERIMENTALIST_API_* env at import time, and this module @@ -57,6 +65,10 @@ # TODO: Add remote train/validation dataset support when remote experiment mode is implemented. +def _inside_experimentalist_container() -> bool: + return _CONTAINER_MARKER.is_file() + + def _default_experiment_dir(profile: AgentProfile | None) -> Path: if profile is None: experiments_root = Path("tmp") @@ -94,9 +106,8 @@ def run( help=( "Baseline agent: a local directory or a git URL with an optional ref " "(e.g. ssh://git@host/group/repo.git@main). Optional in Mode 1 (the insight " - "supplies the agent) and overrides the insight's agent when given. A git " - "source records provenance and enables --config storage.publish_winner to open a " - "draft PR/MR for the winner against that ref." + "supplies the agent) and overrides the insight's agent when given. Git sources " + "are resolved on the trusted host before the credential-free snapshot enters OpenShell." ), ), agent_spec: str | None = typer.Option( @@ -203,7 +214,7 @@ def run( readable=True, ), ) -> None: - """Run offline optimization for a baseline agent (local dir or git source).""" + """Run optimization inside OpenShell with Harbor execution on the trusted host.""" if no_insight and (insight is not None or insight_id is not None): typer.echo("--no-insight cannot be combined with --insight or --insight-id", err=True) @@ -246,6 +257,7 @@ async def _flow() -> str: insight_id=effective_insight.selector, base_url=base_url_resolved, enforce_insight_agent=agent is None, + openshell=not _inside_experimentalist_container(), probes=_PREFLIGHT_PROBES, ) ) @@ -293,6 +305,22 @@ async def _flow() -> str: scratch_dir=experiment_dir_resolved / "resolved", plan=plan, ) + client = make_client(base_url_resolved) + if not _inside_experimentalist_container(): + try: + prepared = await _OPEN_SHELL_PREPARER( + inputs, + experiment_dir=experiment_dir_resolved, + client=client, + ) + return await asyncio.to_thread( + _OPEN_SHELL_LAUNCHER, + prepared, + experiment_dir=experiment_dir_resolved, + platform_url=base_url_resolved, + ) + finally: + await client.close() global run_experimentalist if run_experimentalist is None: from nemo_experimentalist_plugin.experimentalist.run import ( @@ -300,7 +328,6 @@ async def _flow() -> str: ) run_experimentalist = _run_experimentalist - client = make_client(base_url_resolved) try: return await run_experimentalist( agent=inputs.agent, @@ -321,7 +348,7 @@ async def _flow() -> str: try: output_text = asyncio.run(_flow()) - except (OSError, ValueError, yaml.YAMLError) as exc: + except (OpenShellLaunchError, OSError, ValueError, yaml.YAMLError) as exc: typer.echo(str(exc), err=True) raise typer.Exit(code=1) from None typer.echo(output_text) @@ -413,6 +440,7 @@ def doctor( insight=effective_insight.ref if effective_insight is not None else None, insight_id=effective_insight.selector if effective_insight is not None else None, base_url=base_url_resolved, + openshell=not _inside_experimentalist_container(), probes=_PREFLIGHT_PROBES, ) if profile_obj is not None: diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/analyzer.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/analyzer.py index 335441e571..b6df7e0f1d 100644 --- a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/analyzer.py +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/analyzer.py @@ -17,6 +17,7 @@ Task, TrialResult, ) +from nemo_experimentalist_plugin.experimentalist.components.evaluator.models import DependencyRuntimeError from nemo_experimentalist_plugin.experimentalist.components.trace_analyzer import ( # noqa: F401 Diagnostic, TraceAnalyzer, @@ -638,6 +639,8 @@ async def run( for task_id, result in zip(unique_tasks, rationales_list, strict=True): if isinstance(result, asyncio.CancelledError): raise result + if isinstance(result, DependencyRuntimeError): + raise result if isinstance(result, BaseException): logging.getLogger(__name__).warning(f"Rationalizer failed for {agent_id}/{task_id}: {result}") continue @@ -665,6 +668,8 @@ async def run( for (trial, _), result in zip(trial_tasks, diagnoses_list, strict=True): if isinstance(result, asyncio.CancelledError): raise result + if isinstance(result, DependencyRuntimeError): + raise result if isinstance(result, BaseException): logging.getLogger(__name__).warning( "TraceAnalyzer failed for %s/%s: %s", diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/base.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/base.py index 25261b4e21..ac593b410d 100644 --- a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/base.py +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/base.py @@ -39,6 +39,9 @@ def __init__(self, options: EvaluatorConfig, experiment_dir: Path | None = None) self.options = options self.experiment_dir = experiment_dir + def prepare_dataset(self, dataset: Dataset) -> None: + """Attach evaluator-specific task runtimes in place when required.""" + async def aggregate_results(self, results: Sequence[TrialResult]) -> dict[str, float | int]: """ Aggregate evaluation results from multiple runs. diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/factory.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/factory.py index 3b59226cec..8e6dfb0b12 100644 --- a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/factory.py +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/factory.py @@ -3,6 +3,7 @@ """Factories for evaluator-specific datasets and evaluators.""" +import os from pathlib import Path from typing import Any @@ -17,6 +18,12 @@ HarborEvaluatorConfig, ) from nemo_experimentalist_plugin.experimentalist.components.evaluator.models import Dataset, DatasetRef, Task +from nemo_experimentalist_plugin.experimentalist.components.evaluator.remote_harbor import ( + BRIDGE_URL_ENV, + OPEN_SHELL_RUNTIME_ENV, + RemoteHarborEvaluator, + RemoteHarborEvaluatorConfig, +) _SUPPORTED_EVALUATOR_TYPES = { "harbor": (HarborDataset, HarborEvaluator, HarborEvaluatorConfig), @@ -99,6 +106,12 @@ def build_evaluator( config = config.model_dump() elif not isinstance(config, dict): raise TypeError(f"{evaluator_type.capitalize()} evaluator config must be an EvaluatorConfig or dict") + if evaluator_type == "harbor" and os.environ.get(OPEN_SHELL_RUNTIME_ENV) == "1": + bridge_url = os.environ.get(BRIDGE_URL_ENV) + if not bridge_url: + raise RuntimeError(f"{BRIDGE_URL_ENV} is required inside the OpenShell runtime") + evaluator_config = RemoteHarborEvaluatorConfig.model_validate({**config, "bridge_url": bridge_url}) + return RemoteHarborEvaluator(options=evaluator_config, experiment_dir=experiment_dir) evaluator_config = _SUPPORTED_EVALUATOR_TYPES[evaluator_type][2].model_validate(config) return _SUPPORTED_EVALUATOR_TYPES[evaluator_type][1]( options=evaluator_config, experiment_dir=experiment_dir diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/harbor.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/harbor.py index 24a60ffa37..32f925d0b3 100644 --- a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/harbor.py +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/harbor.py @@ -12,6 +12,7 @@ import logging import os import re +import shlex import shutil import sys import tempfile @@ -42,6 +43,7 @@ DatasetRef, DatasetValidationError, DataValue, + DependencyCommandResult, DependencyRuntime, MetricResult, MetricSpec, @@ -187,6 +189,12 @@ class HarborEvaluatorConfig(EvaluatorConfig): artifacts: list[str] = Field(default=[]) retry: RetryConfig = Field(default=RetryConfig(exclude_exceptions=set())) import_path: str = Field(default="harbor_wrapper:WrappedAgent") + scope_import_path: bool = Field( + default=True, + description="Scope a candidate-owned import path under the candidate directory.", + ) + agent_model_name: str | None = Field(default=None) + agent_env: dict[str, str] = Field(default_factory=dict) trace_dir: str = Field(default=_TRACE_ARTIFACT_SOURCE) @@ -208,8 +216,9 @@ def context(self) -> HarborDependencyContext: class HarborDependencyContext: """Async context manager that starts and stops Harbor task dependencies.""" - def __init__(self, runtime: HarborDependencyRuntime) -> None: + def __init__(self, runtime: HarborDependencyRuntime, *, temp_root: Path | None = None) -> None: self._runtime = runtime + self._temp_root = temp_root self._environment: BaseEnvironment | None = None self._temp_dir: tempfile.TemporaryDirectory[str] | None = None @@ -247,7 +256,9 @@ async def _start_harbor_runtime(self) -> None: harbor_task = HarborTaskModel(task_path) context_id = uuid4() session_id = f"{harbor_task.short_name}__{context_id.hex[:12]}__env" - temp_dir = tempfile.TemporaryDirectory(prefix="nemo-harbor-deps-") + if self._temp_root is not None: + self._temp_root.mkdir(parents=True, exist_ok=True) + temp_dir = tempfile.TemporaryDirectory(prefix="nemo-harbor-deps-", dir=self._temp_root) trial_paths = TrialPaths(Path(temp_dir.name)) trial_paths.mkdir() @@ -312,6 +323,27 @@ async def _start_harbor_runtime(self) -> None: } ) + async def execute( + self, + command: str, + *, + stdin: str | None = None, + timeout: float = 30.0, + cwd: str = "/app", + ) -> DependencyCommandResult: + """Execute inside the active Harbor task environment.""" + if self._environment is None: + raise RuntimeError("Harbor dependency environment is not running") + wrapped = command + if stdin is not None: + wrapped = f"printf %s {shlex.quote(stdin)} | (\n{command}\n)" + result = await self._environment.exec(wrapped, cwd=cwd, timeout_sec=max(1, int(timeout))) + return DependencyCommandResult( + stdout=result.stdout or "", + stderr=result.stderr or "", + returncode=result.return_code, + ) + async def _stop_started_runtime(self) -> None: stop_error: Exception | None = None if self._environment is not None: @@ -1151,6 +1183,12 @@ def _task_metric_specs(task_dir: Path, config: dict[str, Any]) -> dict[str, Metr @staticmethod def _dependency_runtime(task_dir: Path, config: dict[str, Any]) -> DependencyRuntime: + if os.environ.get("NEMO_EXPERIMENTALIST_OPEN_SHELL_RUNTIME") == "1": + from nemo_experimentalist_plugin.experimentalist.components.evaluator.remote_harbor import ( # noqa: PLC0415 + remote_dependency_runtime_for_task, + ) + + return remote_dependency_runtime_for_task(task_dir, task_id=task_dir.name) environment_config = config.get("environment") if isinstance(config, dict) else None build_timeout = None if isinstance(environment_config, dict): @@ -1247,7 +1285,9 @@ class HarborEvaluator(Evaluator): def __init__(self, options: HarborEvaluatorConfig | None = None, experiment_dir: Path | None = None) -> None: super().__init__(options or HarborEvaluatorConfig(), experiment_dir=experiment_dir) - async def _run(self, agent: Path, dataset: Dataset, options: HarborEvaluatorConfig) -> Sequence[TrialResult]: + async def _run(self, agent: Path, dataset: Dataset, options: EvaluatorConfig) -> Sequence[TrialResult]: + if not isinstance(options, HarborEvaluatorConfig): + raise TypeError("Harbor evaluator requires HarborEvaluatorConfig") if not isinstance(dataset, HarborDataset): raise ValueError("Dataset must be a Harbor dataset") @@ -1260,6 +1300,9 @@ async def _run(self, agent: Path, dataset: Dataset, options: HarborEvaluatorConf options_dict["jobs_dir"] = experiment_dir / options.jobs_dir options_dict["job_name"] = options.job_name or f"{agent.name}-{dataset.id}" import_path: str = options_dict.pop("import_path") + scope_import_path: bool = options_dict.pop("scope_import_path") + agent_model_name: str | None = options_dict.pop("agent_model_name") + agent_env: dict[str, str] = options_dict.pop("agent_env") trace_dir: str = options_dict.pop("trace_dir", _TRACE_ARTIFACT_SOURCE) options_dict["artifacts"] = _with_trace_artifact(options_dict.get("artifacts") or [], trace_dir) force_rerun: bool = options_dict.pop("force_rerun", False) @@ -1271,8 +1314,17 @@ async def _run(self, agent: Path, dataset: Dataset, options: HarborEvaluatorConf await dataset.validate() - scoped_import_path, scoped_package = _scoped_import_path(agent_path, import_path) - agents_config = [AgentConfig(import_path=scoped_import_path)] + if scope_import_path: + resolved_import_path, scoped_package = _scoped_import_path(agent_path, import_path) + else: + resolved_import_path, scoped_package = import_path, None + agents_config = [ + AgentConfig( + import_path=resolved_import_path, + model_name=agent_model_name, + env=agent_env, + ) + ] datasets_config = [DatasetConfig(path=dataset_path, task_names=[task.id for task in dataset.tasks])] job_config = JobConfig(**options_dict, agents=agents_config, datasets=datasets_config) if force_rerun: @@ -1284,7 +1336,8 @@ async def _run(self, agent: Path, dataset: Dataset, options: HarborEvaluatorConf job = await Job.create(job_config) await job.run() finally: - _cleanup_scoped_imports(scoped_package) + if scoped_package is not None: + _cleanup_scoped_imports(scoped_package) trials = await self._trials_from_dir(job.job_dir, dataset.tasks) return trials diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/models.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/models.py index 8b59ce67f3..265f6b803a 100644 --- a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/models.py +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/models.py @@ -14,7 +14,7 @@ from contextlib import AbstractAsyncContextManager from pathlib import Path from types import TracebackType -from typing import Any, Literal, TypeAlias +from typing import Any, Literal, Protocol, TypeAlias, runtime_checkable from urllib.parse import unquote, urlparse from pydantic import BaseModel, Field, SerializeAsAny @@ -86,6 +86,31 @@ def context(self) -> AbstractAsyncContextManager[DependencyRuntime | None]: return DependencyContext(self) +class DependencyRuntimeError(RuntimeError): + """Task dependency setup or bridge transport failed.""" + + +class DependencyCommandResult(BaseModel): + """Result of a command executed inside a task dependency runtime.""" + + stdout: str = "" + stderr: str = "" + returncode: int + + +@runtime_checkable +class DependencyCommandExecutor(Protocol): + """Runtime capable of executing analyzer commands.""" + + async def execute( + self, + command: str, + *, + stdin: str | None = None, + timeout: float = 30.0, + ) -> DependencyCommandResult: ... + + async def run_dependency_command(spec: CommandSpec, phase: str) -> None: """Run a dependency command. diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/remote_harbor.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/remote_harbor.py new file mode 100644 index 0000000000..43433e9571 --- /dev/null +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/evaluator/remote_harbor.py @@ -0,0 +1,505 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""OpenShell-side Harbor evaluator and dependency client.""" + +from __future__ import annotations + +import asyncio +import logging +import os +import shutil +from collections.abc import Iterator, Sequence +from pathlib import Path +from types import TracebackType +from typing import Any, Literal +from urllib.parse import unquote, urlparse +from uuid import uuid4 + +import httpx +from nemo_experimentalist_plugin.experimentalist.components.evaluator.base import ( + Evaluator, + EvaluatorConfig, + EvaluatorType, +) +from nemo_experimentalist_plugin.experimentalist.components.evaluator.harbor import ( + HarborDataset, + HarborDependencyRuntime, + HarborEvaluatorConfig, +) +from nemo_experimentalist_plugin.experimentalist.components.evaluator.models import ( + Dataset, + DependencyCommandResult, + DependencyRuntime, + DependencyRuntimeError, + EvaluationResult, + ResourceRef, + TrialResult, + local_path_from_uri, +) +from nemo_experimentalist_plugin.harbor_bridge.archives import ( + create_directory_archive, + extract_directory_archive, +) +from nemo_experimentalist_plugin.harbor_bridge.contracts import ( + ArchiveReference, + DependencyExecRequest, + DependencyExecResponse, + DependencySession, + DependencyStartRequest, + EnvelopeTask, + EvaluationAccepted, + EvaluationEnvelope, + EvaluationState, + EvaluationStatus, + EvaluationSubmission, +) +from nemo_experimentalist_plugin.harbor_bridge.envelopes import ( + ResolvedEnvelopeTask, + TaskEnvelopePolicy, + create_overlay_directory, + resolve_envelope_task, + transport_tree_digest, +) +from pydantic import AnyHttpUrl, ConfigDict, Field, PrivateAttr, model_validator + +OPEN_SHELL_RUNTIME_ENV = "NEMO_EXPERIMENTALIST_OPEN_SHELL_RUNTIME" +BRIDGE_URL_ENV = "NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_URL" +BRIDGE_TOKEN_ENV = "NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_TOKEN" +logger = logging.getLogger(__name__) + + +def _result_resource_refs(result: EvaluationResult) -> Iterator[ResourceRef]: + for trial in result.trials: + if trial.trace is not None: + yield trial.trace + yield from trial.resources.values() + for output in trial.outputs.values(): + if isinstance(output, ResourceRef): + yield output + for metric in trial.metrics.values(): + if metric.spec is not None and metric.spec.ref is not None: + yield metric.spec.ref + + +def _bridge_headers(token_env: str, *, dependency_capability: str | None = None) -> dict[str, str]: + """Send a host token locally or an OpenShell-managed opaque placeholder.""" + token = os.environ.get(token_env) + if not token: + raise DependencyRuntimeError(f"Missing bridge token environment variable {token_env}") + headers = {"Authorization": f"Bearer {token}"} + if dependency_capability is not None: + headers["X-Nemo-Dependency-Capability"] = dependency_capability + return headers + + +class RemoteHarborEvaluatorConfig(HarborEvaluatorConfig): + """Sandbox-side transport settings; resource authority stays server-side.""" + + model_config = ConfigDict(extra="forbid") + + bridge_url: AnyHttpUrl + bridge_token_env: str = BRIDGE_TOKEN_ENV + run_profile: Literal["smoke", "standard"] = "standard" + poll_interval_sec: float = Field(default=1.0, ge=0.01, le=30) + request_timeout_sec: float = Field(default=60.0, ge=1, le=600) + evaluation_timeout_sec: float = Field(default=7200.0, ge=1, le=86_400) + max_archive_bytes: int = Field(default=512 * 1024 * 1024, ge=1) + + +class RemoteHarborDependencyRuntime(DependencyRuntime): + """Opaque bridge-backed task environment.""" + + model_config = ConfigDict(extra="forbid") + + task_id: str + base_task_id: str + envelope_id: str + envelope_digest: str + task_path: Path + overlay_policy: TaskEnvelopePolicy + bridge_url: AnyHttpUrl + bridge_token_env: str = BRIDGE_TOKEN_ENV + request_timeout_sec: float = 60.0 + max_archive_bytes: int = Field(default=512 * 1024 * 1024, ge=1) + + _session_id: str | None = PrivateAttr(default=None) + _capability: str | None = PrivateAttr(default=None) + _transport: httpx.AsyncBaseTransport | None = PrivateAttr(default=None) + + @model_validator(mode="after") + def reject_local_lifecycle(self) -> RemoteHarborDependencyRuntime: + """Keep dependency lifecycle commands behind the bridge.""" + if self.start is not None or self.readiness is not None or self.stop is not None: + raise ValueError("Remote Harbor dependencies do not accept local lifecycle commands") + return self + + def context(self) -> RemoteHarborDependencyContext: + runtime = self.model_copy(deep=False) + runtime._session_id = None + runtime._capability = None + runtime._transport = self._transport + return RemoteHarborDependencyContext(runtime) + + async def execute( + self, + command: str, + *, + stdin: str | None = None, + timeout: float = 30.0, + ) -> DependencyCommandResult: + if self._session_id is None or self._capability is None: + raise DependencyRuntimeError("Remote Harbor dependency session is not running") + command_timeout_sec = max(1, int(timeout)) + response = await self._request( + "POST", + f"/v1/dependencies/{self._session_id}/exec", + capability=self._capability, + json=DependencyExecRequest( + command=command, + stdin=stdin, + timeout_sec=command_timeout_sec, + ).model_dump(), + timeout=max(self.request_timeout_sec, command_timeout_sec + 10), + ) + if response.status_code != 200: + raise DependencyRuntimeError(f"Harbor dependency command failed with HTTP {response.status_code}") + result = DependencyExecResponse.model_validate_json(response.content) + return DependencyCommandResult( + stdout=result.stdout, + stderr=result.stderr, + returncode=result.returncode, + ) + + async def _request( + self, + method: str, + path: str, + *, + capability: str | None = None, + **kwargs: Any, + ) -> httpx.Response: + headers = _bridge_headers(self.bridge_token_env, dependency_capability=capability) + async with httpx.AsyncClient(timeout=self.request_timeout_sec, transport=self._transport) as client: + return await client.request( + method, + f"{str(self.bridge_url).rstrip('/')}{path}", + headers=headers, + **kwargs, + ) + + +class RemoteHarborDependencyContext: + def __init__(self, runtime: RemoteHarborDependencyRuntime) -> None: + self.runtime = runtime + + async def __aenter__(self) -> DependencyRuntime: + binding = ResolvedEnvelopeTask( + envelope_id=self.runtime.envelope_id, + envelope_digest=self.runtime.envelope_digest, + task_id=self.runtime.task_id, + base_task_id=self.runtime.base_task_id, + task_path=self.runtime.task_path, + policy=self.runtime.overlay_policy, + ) + root = Path.cwd() / "tmp" / "harbor-bridge" / f"dependency-{uuid4().hex}" + overlay_dir = root / "overlay" + archive = root / "overlay.tar.gz" + root.mkdir(parents=True) + try: + digest = create_overlay_directory([binding], overlay_dir) + metadata = DependencyStartRequest( + request_id=f"dependency-{uuid4().hex[:16]}", + envelope_id=binding.envelope_id, + envelope_digest=binding.envelope_digest, + task_id=binding.task_id, + base_task_id=binding.base_task_id, + overlay_digest=digest, + ) + files = None + handle = None + if digest is not None: + create_directory_archive( + overlay_dir, + archive, + max_bytes=self.runtime.max_archive_bytes, + ) + handle = archive.open("rb") + files = {"overlay": ("overlay.tar.gz", handle, "application/gzip")} + try: + response = await self.runtime._request( + "POST", + "/v1/dependencies", + data={"metadata": metadata.model_dump_json()}, + files=files, + ) + finally: + if handle is not None: + handle.close() + if response.status_code != 201: + raise DependencyRuntimeError(f"Harbor dependency startup failed with HTTP {response.status_code}") + session = DependencySession.model_validate_json(response.content) + self.runtime._session_id = session.session_id + self.runtime._capability = session.capability_token + return self.runtime + finally: + shutil.rmtree(root, ignore_errors=True) + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + traceback: TracebackType | None, + ) -> bool: + del exc_type, traceback + shutdown_error: Exception | None = None + try: + if self.runtime._session_id is not None and self.runtime._capability is not None: + response = await self.runtime._request( + "DELETE", + f"/v1/dependencies/{self.runtime._session_id}", + capability=self.runtime._capability, + ) + if response.status_code != 204: + shutdown_error = DependencyRuntimeError( + f"Harbor dependency shutdown failed with HTTP {response.status_code}" + ) + except Exception as error: + shutdown_error = error + finally: + self.runtime._session_id = None + self.runtime._capability = None + if shutdown_error is not None: + if exc is None: + raise shutdown_error + logger.warning( + "Harbor dependency shutdown failed while preserving the active body exception", + exc_info=(type(shutdown_error), shutdown_error, shutdown_error.__traceback__), + ) + return False + + +class RemoteHarborEvaluator(Evaluator): + """Submit inert candidate/overlay archives and poll the bounded bridge.""" + + evaluator_type: EvaluatorType = "harbor" + + def __init__( + self, + options: RemoteHarborEvaluatorConfig, + experiment_dir: Path | None = None, + *, + transport: httpx.AsyncBaseTransport | None = None, + ) -> None: + super().__init__(options, experiment_dir) + self._transport = transport + + def prepare_dataset(self, dataset: Dataset) -> None: + if not isinstance(dataset, HarborDataset) or dataset.source is None: + raise ValueError("Remote Harbor evaluator requires a sourced Harbor dataset") + root = local_path_from_uri(dataset.source.uri, context="Harbor dataset reference").resolve() + options = self.options + assert isinstance(options, RemoteHarborEvaluatorConfig) + for task in dataset.tasks: + if isinstance(task.dependencies, RemoteHarborDependencyRuntime): + continue + if not isinstance(task.dependencies, HarborDependencyRuntime): + raise DependencyRuntimeError( + f"Remote Harbor evaluator will not run unsupported task dependencies locally: {task.id}" + ) + task_path = local_path_from_uri(task.uri, context="Harbor task reference").resolve() + binding = resolve_envelope_task(root, task_path, task_id=task.id) + runtime = RemoteHarborDependencyRuntime.model_validate( + { + "task_id": task.id, + "base_task_id": binding.base_task_id, + "envelope_id": binding.envelope_id, + "envelope_digest": binding.envelope_digest, + "task_path": task_path, + "overlay_policy": binding.policy, + "bridge_url": options.bridge_url, + "bridge_token_env": options.bridge_token_env, + "request_timeout_sec": options.request_timeout_sec, + "max_archive_bytes": options.max_archive_bytes, + } + ) + runtime._transport = self._transport + task.dependencies = runtime + + async def _materialize_result_artifacts( + self, + client: httpx.AsyncClient, + *, + job_id: str, + result: EvaluationResult, + headers: dict[str, str], + staging: Path, + options: RemoteHarborEvaluatorConfig, + ) -> None: + archive = staging / "result-artifacts.tar.gz" + total = 0 + async with client.stream( + "GET", + f"{str(options.bridge_url).rstrip('/')}/v1/evaluations/{job_id}/artifacts", + headers=headers, + ) as response: + if response.status_code != 200: + raise RuntimeError(f"Harbor bridge artifact download failed with HTTP {response.status_code}") + expected_digest = response.headers.get("X-Nemo-Artifact-Digest") + if expected_digest is None: + raise RuntimeError("Harbor bridge artifact response omitted its digest") + with archive.open("xb") as output: + async for chunk in response.aiter_bytes(): + total += len(chunk) + if total > options.max_archive_bytes: + raise RuntimeError("Harbor bridge artifact archive exceeds the configured limit") + output.write(chunk) + + artifact_root = ((self.experiment_dir or Path.cwd()) / "remote-harbor-artifacts" / job_id).resolve() + extract_directory_archive( + archive, + artifact_root, + max_bytes=options.max_archive_bytes, + ) + if transport_tree_digest(artifact_root) != expected_digest: + shutil.rmtree(artifact_root, ignore_errors=True) + raise RuntimeError("Harbor bridge artifact digest mismatch") + + for resource in _result_resource_refs(result): + parsed = urlparse(resource.uri) + if parsed.scheme != "nemo-harbor-bridge" or not parsed.path.startswith("/artifacts/"): + continue + relative = unquote(parsed.path.removeprefix("/artifacts/")) + local = (artifact_root / relative).resolve() + if not local.is_relative_to(artifact_root) or not local.exists(): + raise RuntimeError("Harbor bridge result references a missing or unsafe artifact") + resource.uri = local.as_uri() + + async def _run( + self, + agent: Path, + dataset: Dataset, + options: EvaluatorConfig, + ) -> Sequence[TrialResult]: + if not isinstance(options, RemoteHarborEvaluatorConfig): + raise TypeError("Remote Harbor evaluator requires RemoteHarborEvaluatorConfig") + if not isinstance(dataset, HarborDataset) or dataset.source is None: + raise ValueError("Dataset must be a sourced Harbor dataset") + root = local_path_from_uri(dataset.source.uri, context="Harbor dataset reference").resolve() + bindings = [ + resolve_envelope_task( + root, + local_path_from_uri(task.uri, context="Harbor task reference").resolve(), + task_id=task.id, + ) + for task in dataset.tasks + ] + identities = {(item.envelope_id, item.envelope_digest) for item in bindings} + if len(identities) != 1: + raise ValueError("One evaluation may reference exactly one trusted task envelope") + envelope_id, envelope_digest = identities.pop() + headers = _bridge_headers(options.bridge_token_env) + + staging = (self.experiment_dir or Path.cwd()) / "tmp" / "harbor-bridge" / uuid4().hex + staging.mkdir(parents=True) + candidate_archive = staging / "candidate.tar.gz" + overlay_dir = staging / "overlay" + overlay_archive = staging / "overlay.tar.gz" + job_id = None + try: + create_directory_archive(agent, candidate_archive, max_bytes=options.max_archive_bytes) + overlay_digest = create_overlay_directory(bindings, overlay_dir) + if overlay_digest is not None: + create_directory_archive(overlay_dir, overlay_archive, max_bytes=options.max_archive_bytes) + submission = EvaluationSubmission( + request_id=f"{agent.name}-{uuid4().hex[:12]}", + envelope=EvaluationEnvelope( + id=envelope_id, + digest=envelope_digest, + tasks=[EnvelopeTask(task_id=item.task_id, base_task_id=item.base_task_id) for item in bindings], + ), + candidate=ArchiveReference(digest=transport_tree_digest(agent)), + overlay=ArchiveReference(digest=overlay_digest) if overlay_digest is not None else None, + run_profile=options.run_profile, + ) + async with httpx.AsyncClient( + timeout=options.request_timeout_sec, + transport=self._transport, + ) as client: + candidate_handle = candidate_archive.open("rb") + overlay_handle = overlay_archive.open("rb") if overlay_digest is not None else None + files = {"candidate": ("candidate.tar.gz", candidate_handle, "application/gzip")} + if overlay_handle is not None: + files["overlay"] = ("overlay.tar.gz", overlay_handle, "application/gzip") + try: + response = await client.post( + f"{str(options.bridge_url).rstrip('/')}/v1/evaluations", + headers=headers, + data={"metadata": submission.model_dump_json()}, + files=files, + ) + finally: + candidate_handle.close() + if overlay_handle is not None: + overlay_handle.close() + if response.status_code != 202: + raise RuntimeError(f"Harbor bridge submission failed with HTTP {response.status_code}") + accepted = EvaluationAccepted.model_validate_json(response.content) + job_id = accepted.job_id + deadline = asyncio.get_running_loop().time() + options.evaluation_timeout_sec + while True: + if asyncio.get_running_loop().time() >= deadline: + raise TimeoutError("Harbor bridge evaluation timed out") + response = await client.get( + f"{str(options.bridge_url).rstrip('/')}/v1/evaluations/{job_id}", + headers=headers, + ) + if response.status_code != 200: + raise RuntimeError(f"Harbor bridge polling failed with HTTP {response.status_code}") + status = EvaluationStatus.model_validate_json(response.content) + if status.state == EvaluationState.COMPLETED: + assert status.result is not None + await self._materialize_result_artifacts( + client, + job_id=job_id, + result=status.result, + headers=headers, + staging=staging, + options=options, + ) + return list(status.result.trials) + if status.state in (EvaluationState.FAILED, EvaluationState.CANCELLED): + raise RuntimeError(status.error or f"Harbor bridge evaluation {status.state}") + await asyncio.sleep(options.poll_interval_sec) + except asyncio.CancelledError: + if job_id is not None: + async with httpx.AsyncClient( + timeout=options.request_timeout_sec, + transport=self._transport, + ) as client: + await client.delete( + f"{str(options.bridge_url).rstrip('/')}/v1/evaluations/{job_id}", + headers=headers, + ) + raise + finally: + shutil.rmtree(staging, ignore_errors=True) + + +def remote_dependency_runtime_for_task(task_dir: Path, *, task_id: str) -> RemoteHarborDependencyRuntime: + """Bind a freshly materialized task to the bridge before analyzer use.""" + bridge_url = os.environ.get(BRIDGE_URL_ENV) + if not bridge_url: + raise DependencyRuntimeError(f"{BRIDGE_URL_ENV} is required inside the OpenShell runtime") + binding = resolve_envelope_task(task_dir.parent, task_dir, task_id=task_id) + return RemoteHarborDependencyRuntime.model_validate( + { + "task_id": task_id, + "base_task_id": binding.base_task_id, + "envelope_id": binding.envelope_id, + "envelope_digest": binding.envelope_digest, + "task_path": task_dir, + "overlay_policy": binding.policy, + "bridge_url": bridge_url, + } + ) diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/loop.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/loop.py index 6c8854547d..dd43627fcb 100644 --- a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/loop.py +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/loop.py @@ -439,11 +439,13 @@ async def _run(self, deps: ExperimentalistDeps) -> ExperimentalistResult: deps.evaluator_type, train_dataset_ref, ) + evaluator.prepare_dataset(train_eval_dataset) validation_eval_dataset = dataset_factory.build_dataset( deps.evaluator_type, validation_dataset_ref, ) + evaluator.prepare_dataset(validation_eval_dataset) # ---- Resolve insight (Mode 1) vs local agent (Mode 2) ----------- insight = ( @@ -492,6 +494,10 @@ async def _run(self, deps: ExperimentalistDeps) -> ExperimentalistResult: train_eval_dataset = eval_author_result.train_dataset validation_eval_dataset = eval_author_result.validation_dataset insight_eval_dataset = eval_author_result.insight_suite + evaluator.prepare_dataset(train_eval_dataset) + evaluator.prepare_dataset(validation_eval_dataset) + if insight_eval_dataset is not None: + evaluator.prepare_dataset(insight_eval_dataset) else: # Mode 2: local agent directory as baseline, no insight required. insight = None diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/rationalizer.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/rationalizer.py index 6be50d9423..ca0cb7dacf 100644 --- a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/rationalizer.py +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/rationalizer.py @@ -116,9 +116,10 @@ async def run(self, task: Task, agent_spec: Path | None = None) -> Rationale: if cached is not None: return cached async with task.start_deps() as runtime: - rationale = await self.solve(task, runtime, agent_spec=agent_spec) - rationale = await self.verify(task, runtime, rationale, agent_spec=agent_spec) - cache.store(self._workspace_path, key, rationale) + with self.shell.use_dependency_runtime(runtime): + rationale = await self.solve(task, runtime, agent_spec=agent_spec) + rationale = await self.verify(task, runtime, rationale, agent_spec=agent_spec) + cache.store(self._workspace_path, key, rationale) return rationale @strategy(CodeActStrategy(config=CodeActConfig(max_iterations=30, cell_timeout=120.0))) diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/tools.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/tools.py index 97c635b963..e591351f5a 100644 --- a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/tools.py +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/tools.py @@ -2,10 +2,16 @@ # SPDX-License-Identifier: Apache-2.0 import logging -from collections.abc import Sequence +from collections.abc import Iterator, Sequence +from contextlib import contextmanager +from contextvars import ContextVar from pathlib import Path from nemo_experimentalist_plugin.entities import Candidate +from nemo_experimentalist_plugin.experimentalist.components.evaluator.models import ( + DependencyCommandExecutor, + DependencyRuntime, +) from nooa.tools import ShellResult, ShellTools from .holdout_utils import ( @@ -49,6 +55,20 @@ def __init__( """Initialize with a working directory and an optional set of blocked path tokens.""" super().__init__(cwd=str(cwd), init_command=init_command) self._blocked_paths = tuple(blocked_paths) + self._dependency_executor: ContextVar[DependencyCommandExecutor | None] = ContextVar( + f"dependency_executor_{id(self)}", + default=None, + ) + + @contextmanager + def use_dependency_runtime(self, runtime: DependencyRuntime | None) -> Iterator[None]: + """Route shell calls through an executable task runtime while active.""" + executor = runtime if isinstance(runtime, DependencyCommandExecutor) else None + token = self._dependency_executor.set(executor) + try: + yield + finally: + self._dependency_executor.reset(token) def is_blocked(self, command: str) -> bool: """Return True if the command references any held-out split path token. @@ -85,6 +105,9 @@ async def run( if self.is_blocked(command): logger.warning(f"GuardedShellTools blocked held-out access: {command}") return ShellResult(stdout="", stderr=BLOCKED_MESSAGE, returncode=1) + if executor := self._dependency_executor.get(): + result = await executor.execute(command, stdin=stdin, timeout=timeout) + return ShellResult(stdout=result.stdout, stderr=result.stderr, returncode=result.returncode) return await super().run(command, stdin=stdin, timeout=timeout) diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/trace_analyzer.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/trace_analyzer.py index e46ae16ffe..7c5b996028 100644 --- a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/trace_analyzer.py +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/trace_analyzer.py @@ -336,17 +336,18 @@ async def run( rationale = rationale or Rationale(task_name=task.id, steps=[]) async with task.start_deps() as runtime: - overview = await self.get_overview(trace, trial) - analysis = await self.analyze_trajectory( - trace=trace, - trial=trial, - task=task, - overview=overview, - rationale=rationale, - insight=insight, - agent_path=agent_path, - runtime=runtime, - ) - diagnostic = await self.diagnose(trace, analysis, insight) - cache.store(self._experiment_dir, key, diagnostic) + with self.shell.use_dependency_runtime(runtime): + overview = await self.get_overview(trace, trial) + analysis = await self.analyze_trajectory( + trace=trace, + trial=trial, + task=task, + overview=overview, + rationale=rationale, + insight=insight, + agent_path=agent_path, + runtime=runtime, + ) + diagnostic = await self.diagnose(trace, analysis, insight) + cache.store(self._experiment_dir, key, diagnostic) return diagnostic diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/archives.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/archives.py new file mode 100644 index 0000000000..a99bbcc249 --- /dev/null +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/archives.py @@ -0,0 +1,116 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Archive transport that never follows links or accepts special files.""" + +from __future__ import annotations + +import shutil +import stat +import tarfile +from collections.abc import Iterator +from pathlib import Path, PurePosixPath + +DEFAULT_MAX_ARCHIVE_BYTES = 512 * 1024 * 1024 +DEFAULT_MAX_ARCHIVE_FILES = 20_000 +_IGNORED_PARTS = frozenset({".git", ".venv", "__pycache__"}) + + +def _archive_paths(root: Path) -> Iterator[Path]: + for path in sorted(root.rglob("*")): + relative = path.relative_to(root) + if any(part in _IGNORED_PARTS for part in relative.parts): + continue + yield path + + +def _validate_source_path(path: Path, relative: Path) -> None: + mode = path.lstat().st_mode + if stat.S_ISLNK(mode): + raise ValueError(f"Archive source contains a symbolic link: {relative}") + if stat.S_ISREG(mode) and path.stat().st_nlink > 1: + raise ValueError(f"Archive source contains a hard-linked file: {relative}") + if not stat.S_ISDIR(mode) and not stat.S_ISREG(mode): + raise ValueError(f"Archive source contains a special file: {relative}") + + +def create_directory_archive( + root: Path, + destination: Path, + *, + max_bytes: int = DEFAULT_MAX_ARCHIVE_BYTES, + max_files: int = DEFAULT_MAX_ARCHIVE_FILES, +) -> None: + """Create a deterministic-safe tar archive without following links.""" + source = root.expanduser().resolve() + if not source.is_dir(): + raise FileNotFoundError(f"Archive source directory not found: {source}") + + destination.parent.mkdir(parents=True, exist_ok=True) + total_bytes = 0 + entry_count = 0 + with tarfile.open(destination, mode="w:gz", dereference=False) as archive: + for path in _archive_paths(source): + relative = path.relative_to(source) + _validate_source_path(path, relative) + entry_count += 1 + if entry_count > max_files: + raise ValueError(f"Archive source exceeds {max_files} entries") + if path.is_file(): + total_bytes += path.stat().st_size + if total_bytes > max_bytes: + raise ValueError(f"Archive source exceeds {max_bytes} uncompressed bytes") + archive.add(path, arcname=relative.as_posix(), recursive=False) + + +def _validated_member_path(member: tarfile.TarInfo) -> PurePosixPath: + path = PurePosixPath(member.name) + if path.is_absolute() or not path.parts or any(part in ("", ".", "..") for part in path.parts): + raise ValueError(f"Unsafe archive member path: {member.name!r}") + if member.issym() or member.islnk() or member.isdev(): + raise ValueError(f"Unsupported archive member type: {member.name!r}") + if not member.isdir() and not member.isfile(): + raise ValueError(f"Unsupported archive member type: {member.name!r}") + return path + + +def extract_directory_archive( + archive_path: Path, + destination: Path, + *, + max_bytes: int = DEFAULT_MAX_ARCHIVE_BYTES, + max_files: int = DEFAULT_MAX_ARCHIVE_FILES, +) -> None: + """Extract a gzip tar after validating every path, type, count, and size.""" + if destination.exists(): + raise FileExistsError(f"Archive destination already exists: {destination}") + destination.mkdir(parents=True) + try: + with tarfile.open(archive_path, mode="r:gz") as archive: + members = archive.getmembers() + if len(members) > max_files: + raise ValueError(f"Archive exceeds {max_files} entries") + normalized_names = [_validated_member_path(member).as_posix() for member in members] + if len(set(normalized_names)) != len(normalized_names): + raise ValueError("Archive contains duplicate member paths") + total_bytes = sum(member.size for member in members if member.isfile()) + if total_bytes > max_bytes: + raise ValueError(f"Archive exceeds {max_bytes} uncompressed bytes") + + for member in members: + relative = _validated_member_path(member) + target = destination.joinpath(*relative.parts) + if member.isdir(): + target.mkdir(parents=True, exist_ok=True) + target.chmod(member.mode & 0o777) + continue + target.parent.mkdir(parents=True, exist_ok=True) + source = archive.extractfile(member) + if source is None: + raise ValueError(f"Archive file has no readable content: {member.name!r}") + with source, target.open("xb") as output: + shutil.copyfileobj(source, output) + target.chmod(member.mode & 0o777) + except BaseException: + shutil.rmtree(destination, ignore_errors=True) + raise diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/contracts.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/contracts.py new file mode 100644 index 0000000000..84aae8ff50 --- /dev/null +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/contracts.py @@ -0,0 +1,154 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Strict wire contracts for the bounded Experimentalist Harbor bridge.""" + +from __future__ import annotations + +from enum import StrEnum +from typing import Annotated, Literal + +from nemo_experimentalist_plugin.experimentalist.components.evaluator.models import EvaluationResult +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator + +IDENTIFIER_MAX_LENGTH = 256 +Identifier = Annotated[ + str, + Field(min_length=1, max_length=IDENTIFIER_MAX_LENGTH, pattern=r"^[A-Za-z0-9][A-Za-z0-9_.-]*$"), +] +Sha256Digest = Annotated[str, Field(pattern=r"^sha256:[0-9a-f]{64}$")] + + +class StrictModel(BaseModel): + """Base model that rejects authority hidden in unknown request fields.""" + + model_config = ConfigDict(extra="forbid") + + +class RunProfile(StrictModel): + """Host-owned Harbor resource and timeout policy.""" + + model_config = ConfigDict(extra="forbid", frozen=True) + + attempts: int + concurrency: int + retries: int + agent_timeout_multiplier: float + verifier_timeout_multiplier: float + setup_timeout_multiplier: float + build_timeout_multiplier: float + + +class EnvelopeTask(StrictModel): + """One generated task mapped to a trusted host-owned base task.""" + + task_id: Identifier + base_task_id: Identifier + + +class EvaluationEnvelope(StrictModel): + """Content-addressed task envelope selected by a request.""" + + id: Identifier + digest: Sha256Digest + tasks: list[EnvelopeTask] = Field(min_length=1, max_length=1024) + + @field_validator("tasks") + @classmethod + def unique_task_ids(cls, value: list[EnvelopeTask]) -> list[EnvelopeTask]: + """Reject ambiguous output directories.""" + task_ids = [task.task_id for task in value] + if len(set(task_ids)) != len(task_ids): + raise ValueError("envelope tasks must have unique task_id values") + return value + + +class ArchiveReference(StrictModel): + """Digest of one multipart archive after safe extraction.""" + + digest: Sha256Digest + + +class EvaluationSubmission(StrictModel): + """Only caller-controlled data accepted for a Harbor evaluation.""" + + schema_version: Literal[1] = 1 + request_id: Identifier + envelope: EvaluationEnvelope + candidate: ArchiveReference + overlay: ArchiveReference | None = None + run_profile: Literal["smoke", "standard"] = "standard" + + +class EvaluationState(StrEnum): + """Lifecycle exposed to the sandbox.""" + + PENDING = "pending" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + CANCELLED = "cancelled" + + +class EvaluationAccepted(StrictModel): + """Submission acknowledgement.""" + + job_id: Identifier + state: Literal[EvaluationState.PENDING] = EvaluationState.PENDING + + +class EvaluationStatus(StrictModel): + """Small polling response with sanitized failures.""" + + job_id: Identifier + state: EvaluationState + result: EvaluationResult | None = None + error: str | None = Field(default=None, max_length=2048) + + @model_validator(mode="after") + def validate_terminal_payload(self) -> EvaluationStatus: + """Keep result and error fields consistent with state.""" + if self.state == EvaluationState.COMPLETED and self.result is None: + raise ValueError("completed evaluation status requires result") + if self.state != EvaluationState.COMPLETED and self.result is not None: + raise ValueError("only completed evaluation status may include result") + if self.state == EvaluationState.FAILED and not self.error: + raise ValueError("failed evaluation status requires error") + if self.state != EvaluationState.FAILED and self.error is not None: + raise ValueError("only failed evaluation status may include error") + return self + + +class DependencyStartRequest(StrictModel): + """Start one trusted task environment for dependency analysis.""" + + schema_version: Literal[1] = 1 + request_id: Identifier + envelope_id: Identifier + envelope_digest: Sha256Digest + task_id: Identifier + base_task_id: Identifier + overlay_digest: Sha256Digest | None = None + + +class DependencySession(StrictModel): + """Opaque capability for one bridge-owned task environment.""" + + session_id: Identifier + capability_token: str = Field(min_length=32, max_length=256) + + +class DependencyExecRequest(StrictModel): + """A bounded command inside an existing bridge-owned environment.""" + + command: str = Field(min_length=1, max_length=65_536) + stdin: str | None = Field(default=None, max_length=1_048_576) + timeout_sec: int = Field(default=30, ge=1, le=3600) + + +class DependencyExecResponse(StrictModel): + """Capped command output.""" + + stdout: str = Field(default="", max_length=30_064) + stderr: str = Field(default="", max_length=30_064) + returncode: int diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/dependencies.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/dependencies.py new file mode 100644 index 0000000000..fc60d5b7f9 --- /dev/null +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/dependencies.py @@ -0,0 +1,111 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Bridge-owned Harbor environments for bounded dependency analysis.""" + +from __future__ import annotations + +import asyncio +from dataclasses import dataclass, field +from pathlib import Path +from uuid import uuid4 + +from nemo_experimentalist_plugin.experimentalist.components.evaluator.harbor import ( + HarborDependencyContext, + HarborDependencyRuntime, +) +from nemo_experimentalist_plugin.experimentalist.components.evaluator.models import ResourceRef +from nemo_experimentalist_plugin.harbor_bridge.contracts import ( + IDENTIFIER_MAX_LENGTH, + DependencyExecRequest, + DependencyExecResponse, + DependencyStartRequest, +) + +_OUTPUT_LIMIT = 30_000 +_SESSION_SUFFIX_LENGTH = 16 + + +def _truncate(value: str, stream: str) -> str: + if len(value) <= _OUTPUT_LIMIT: + return value + return value[:_OUTPUT_LIMIT] + f"\n... ({stream} truncated)" + + +@dataclass +class _Session: + context: HarborDependencyContext + lock: asyncio.Lock = field(default_factory=asyncio.Lock) + + +class HarborDependencySessionManager: + """Own active environments and expose no Docker configuration.""" + + def __init__(self, *, max_concurrent_sessions: int = 2) -> None: + self._max = max_concurrent_sessions + self._sessions: dict[str, _Session] = {} + self._starting = 0 + self._lock = asyncio.Lock() + + async def start(self, request: DependencyStartRequest, *, task_dir: Path, work_dir: Path) -> str: + async with self._lock: + if len(self._sessions) + self._starting >= self._max: + raise RuntimeError("Dependency session capacity reached") + self._starting += 1 + runtime = HarborDependencyRuntime( + task_path=ResourceRef(uri=task_dir.resolve().as_uri()), + force_build=True, + delete=True, + run_healthcheck=True, + ) + context = HarborDependencyContext(runtime, temp_root=work_dir / "runtime") + entered = False + try: + await context.__aenter__() + entered = True + request_prefix = request.request_id[: IDENTIFIER_MAX_LENGTH - _SESSION_SUFFIX_LENGTH - 1] + session_id = f"{request_prefix}-{uuid4().hex[:_SESSION_SUFFIX_LENGTH]}" + async with self._lock: + self._sessions[session_id] = _Session(context) + entered = False + except BaseException: + if entered: + await context.__aexit__(None, None, None) + raise + finally: + async with self._lock: + self._starting -= 1 + return session_id + + async def execute(self, session_id: str, request: DependencyExecRequest) -> DependencyExecResponse: + async with self._lock: + session = self._sessions.get(session_id) + if session is None: + raise KeyError(session_id) + async with session.lock: + result = await session.context.execute( + request.command, + stdin=request.stdin, + timeout=request.timeout_sec, + ) + return DependencyExecResponse( + stdout=_truncate(result.stdout, "stdout"), + stderr=_truncate(result.stderr, "stderr"), + returncode=result.returncode, + ) + + async def stop(self, session_id: str) -> None: + async with self._lock: + session = self._sessions.pop(session_id, None) + if session is None: + raise KeyError(session_id) + async with session.lock: + await session.context.__aexit__(None, None, None) + + async def close(self) -> None: + async with self._lock: + session_ids = list(self._sessions) + results = await asyncio.gather(*(self.stop(session_id) for session_id in session_ids), return_exceptions=True) + errors = [result for result in results if isinstance(result, Exception)] + if errors: + raise ExceptionGroup("Failed to stop Harbor dependency sessions", errors) diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/envelopes.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/envelopes.py new file mode 100644 index 0000000000..761a4c3f68 --- /dev/null +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/envelopes.py @@ -0,0 +1,508 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Content-addressed, host-owned Harbor task envelopes.""" + +from __future__ import annotations + +import hashlib +import json +import os +import re +import shutil +import stat +import tomllib +from collections.abc import Iterable +from pathlib import Path, PurePosixPath +from typing import Annotated, Literal + +import tomlkit +from nemo_experimentalist_plugin.experimentalist.components.evaluator.harbor import HarborDataset +from nemo_experimentalist_plugin.experimentalist.components.evaluator.models import DatasetRef, local_path_from_uri +from nemo_experimentalist_plugin.harbor_bridge.contracts import EnvelopeTask, Sha256Digest +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator + +ENVELOPE_DESCRIPTOR_FILENAME = ".nemo-trusted-harbor-envelope.json" +ENVELOPE_POLICY_FILENAME = "nemo-task-envelope.json" +ENVELOPE_MANIFEST_FILENAME = "manifest.json" +ENVELOPE_SCHEMA_VERSION = 1 +_TRANSPORT_IGNORED_PARTS = frozenset({".git", ".venv", "__pycache__"}) +_FORBIDDEN_OVERLAY_NAMES = frozenset( + { + ".dockerignore", + "Containerfile", + "Dockerfile", + ENVELOPE_DESCRIPTOR_FILENAME, + ENVELOPE_POLICY_FILENAME, + "compose.yaml", + "compose.yml", + "docker-compose.yaml", + "docker-compose.yml", + "task.toml", + } +) + +RelativePath = Annotated[str, Field(min_length=1, max_length=1024)] + + +def _validated_relative_path(value: str) -> str: + path = PurePosixPath(value) + if path.is_absolute() or not path.parts or any(part in ("", ".", "..") for part in path.parts): + raise ValueError(f"path must be a safe relative POSIX path: {value!r}") + return path.as_posix() + + +class TaskDataSlot(BaseModel): + """One exact mutable data file.""" + + model_config = ConfigDict(extra="forbid") + + path: RelativePath + media_type: Literal["application/json", "text/plain"] + max_bytes: int = Field(ge=1, le=16 * 1024 * 1024) + + @field_validator("path") + @classmethod + def validate_path(cls, value: str) -> str: + return _validated_relative_path(value) + + +class TaskEnvelopePolicy(BaseModel): + """Mutable files intentionally exposed by a trusted task template.""" + + model_config = ConfigDict(extra="forbid") + + schema_version: Literal[1] = ENVELOPE_SCHEMA_VERSION + task_data: list[TaskDataSlot] = Field(default_factory=list, max_length=128) + verifier_paths: list[RelativePath] = Field(default_factory=list, max_length=128) + + @field_validator("verifier_paths") + @classmethod + def validate_verifier_paths(cls, value: list[str]) -> list[str]: + return [_validated_relative_path(path) for path in value] + + @model_validator(mode="after") + def validate_unique_paths(self) -> TaskEnvelopePolicy: + paths = [slot.path for slot in self.task_data] + if len(set(paths)) != len(paths): + raise ValueError("task_data paths must be unique") + overlap = set(paths) & set(self.verifier_paths) + if overlap: + raise ValueError(f"paths cannot be task data and verifier overlays: {', '.join(sorted(overlap))}") + for path in [*paths, *self.verifier_paths]: + if PurePosixPath(path).name in _FORBIDDEN_OVERLAY_NAMES: + raise ValueError(f"runtime-control path cannot be mutable: {path}") + return self + + @property + def task_data_paths(self) -> list[str]: + """Return exact mutable data paths.""" + return [slot.path for slot in self.task_data] + + +class TrustedTaskManifest(BaseModel): + """One immutable task in an envelope.""" + + model_config = ConfigDict(extra="forbid") + + task_id: str = Field(min_length=1, max_length=256, pattern=r"^[A-Za-z0-9][A-Za-z0-9_.-]*$") + path: Literal["."] | RelativePath + content_digest: Sha256Digest + policy: TaskEnvelopePolicy = Field(default_factory=TaskEnvelopePolicy) + + @field_validator("path") + @classmethod + def validate_path(cls, value: str) -> str: + return value if value == "." else _validated_relative_path(value) + + +class TrustedEnvelopeManifest(BaseModel): + """Host-only catalog manifest.""" + + model_config = ConfigDict(extra="forbid") + + schema_version: Literal[1] = ENVELOPE_SCHEMA_VERSION + envelope_id: str = Field(min_length=1, max_length=256, pattern=r"^[A-Za-z0-9][A-Za-z0-9_.-]*$") + envelope_digest: Sha256Digest + source: str + tasks: list[TrustedTaskManifest] = Field(min_length=1) + + +class TrustedEnvelopeDescriptor(BaseModel): + """Non-secret descriptor copied into the OpenShell workspace.""" + + model_config = ConfigDict(extra="forbid") + + schema_version: Literal[1] = ENVELOPE_SCHEMA_VERSION + envelope_id: str + envelope_digest: Sha256Digest + tasks: list[TrustedTaskManifest] + + +class RegisteredEnvelope(BaseModel): + """Registration result.""" + + model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) + + manifest: TrustedEnvelopeManifest + dataset_path: Path + + +class ResolvedEnvelopeTask(BaseModel): + """Sandbox-side binding for one task working copy.""" + + model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) + + envelope_id: str + envelope_digest: Sha256Digest + task_id: str + base_task_id: str + task_path: Path + policy: TaskEnvelopePolicy + + +def _sha256_bytes(value: bytes) -> str: + return f"sha256:{hashlib.sha256(value).hexdigest()}" + + +def _canonical_digest(value: object) -> str: + payload = json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode() + return _sha256_bytes(payload) + + +def _tree_entries( + root: Path, + *, + exclude: set[str] | None = None, + ignored_parts: frozenset[str] = frozenset(), +) -> list[dict[str, str]]: + excluded = exclude or set() + entries: list[dict[str, str]] = [] + for path in sorted(root.rglob("*")): + relative = path.relative_to(root).as_posix() + if relative in excluded or any(part in ignored_parts for part in PurePosixPath(relative).parts): + continue + info = path.lstat() + mode = info.st_mode + if stat.S_ISLNK(mode): + raise ValueError(f"Envelope tree contains a symbolic link: {relative}") + if stat.S_ISDIR(mode): + entries.append({"path": relative, "type": "directory", "mode": oct(stat.S_IMODE(mode))}) + continue + if not stat.S_ISREG(mode): + raise ValueError(f"Envelope tree contains a special file: {relative}") + if info.st_nlink > 1: + raise ValueError(f"Envelope tree contains a hard-linked file: {relative}") + entries.append( + { + "path": relative, + "type": "file", + "digest": _sha256_bytes(path.read_bytes()), + "mode": oct(stat.S_IMODE(mode)), + } + ) + return entries + + +def tree_digest(root: Path, *, exclude: set[str] | None = None) -> str: + """Hash paths, contents, types, and modes without following links.""" + source = root.expanduser().resolve() + if not source.is_dir(): + raise FileNotFoundError(f"Envelope tree not found: {source}") + return _canonical_digest(_tree_entries(source, exclude=exclude)) + + +def transport_tree_digest(root: Path) -> str: + """Hash exactly the files eligible for archive transport.""" + source = root.expanduser().resolve() + if not source.is_dir(): + raise FileNotFoundError(f"Transport tree not found: {source}") + return _canonical_digest(_tree_entries(source, ignored_parts=_TRANSPORT_IGNORED_PARTS)) + + +def _safe_copy_tree(source: Path, destination: Path) -> None: + _tree_entries(source) + shutil.copytree(source, destination, copy_function=shutil.copy2) + + +def _policy_for_task(task_dir: Path) -> TaskEnvelopePolicy: + policy_path = task_dir / ENVELOPE_POLICY_FILENAME + if not policy_path.is_file(): + return TaskEnvelopePolicy() + try: + policy = TaskEnvelopePolicy.model_validate_json(policy_path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, ValueError) as exc: + raise ValueError(f"Invalid task envelope policy {policy_path}: {exc}") from exc + for slot in policy.task_data: + path = task_dir / slot.path + if path.exists() and not path.is_file(): + raise ValueError(f"Trusted task-data slot must name a file: {path}") + return policy + + +def _slug(value: str) -> str: + normalized = re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("._-") + return normalized[:72] or "dataset" + + +def register_dataset_envelope( + source: Path, + *, + catalog_root: Path, + name: str, + provenance: str | None = None, +) -> RegisteredEnvelope: + """Copy a resolved dataset into a content-addressed host catalog.""" + source_path = source.expanduser().resolve() + if not source_path.is_dir(): + raise FileNotFoundError(f"Trusted Harbor dataset not found: {source_path}") + _tree_entries(source_path) + dataset = HarborDataset.from_ref(DatasetRef(uri=source_path.as_uri(), metadata={"id": name})) + + task_sources: list[tuple[str, Path, str, TaskEnvelopePolicy]] = [] + for task in dataset.tasks: + task_path = local_path_from_uri(task.uri, context="Trusted Harbor task").resolve() + relative = task_path.relative_to(source_path).as_posix() or "." + task_sources.append((task.id, task_path, relative, _policy_for_task(task_path))) + + source_digest = tree_digest(source_path, exclude={ENVELOPE_DESCRIPTOR_FILENAME}) + envelope_id = f"{_slug(name)}-{source_digest.removeprefix('sha256:')[:16]}" + root = catalog_root.expanduser().resolve() / "envelopes" / envelope_id + dataset_path = root / "dataset" + manifest_path = root / ENVELOPE_MANIFEST_FILENAME + if root.exists(): + manifest = TrustedEnvelopeManifest.model_validate_json(manifest_path.read_text(encoding="utf-8")) + if manifest.envelope_digest != source_digest: + raise ValueError(f"Trusted envelope id collision: {envelope_id}") + return RegisteredEnvelope(manifest=manifest, dataset_path=dataset_path) + + root.parent.mkdir(parents=True, exist_ok=True) + partial = root.with_name(f".{root.name}.{os.getpid()}.partial") + if partial.exists(): + shutil.rmtree(partial) + try: + _safe_copy_tree(source_path, partial / "dataset") + tasks = [ + TrustedTaskManifest( + task_id=task_id, + path=relative, + content_digest=tree_digest( + partial / "dataset" if relative == "." else partial / "dataset" / relative, + exclude={ENVELOPE_DESCRIPTOR_FILENAME}, + ), + policy=policy, + ) + for task_id, _task_path, relative, policy in task_sources + ] + manifest = TrustedEnvelopeManifest( + envelope_id=envelope_id, + envelope_digest=source_digest, + source=provenance or str(source_path), + tasks=tasks, + ) + descriptor = TrustedEnvelopeDescriptor( + envelope_id=envelope_id, + envelope_digest=source_digest, + tasks=tasks, + ) + (partial / ENVELOPE_MANIFEST_FILENAME).write_text( + manifest.model_dump_json(indent=2) + "\n", + encoding="utf-8", + ) + (partial / "dataset" / ENVELOPE_DESCRIPTOR_FILENAME).write_text( + descriptor.model_dump_json(indent=2) + "\n", + encoding="utf-8", + ) + os.replace(partial, root) + except BaseException: + shutil.rmtree(partial, ignore_errors=True) + raise + return RegisteredEnvelope(manifest=manifest, dataset_path=dataset_path) + + +def _descriptor_at(dataset_path: Path, task_path: Path) -> tuple[TrustedEnvelopeDescriptor, Path]: + """Find a descriptor on a dataset or a copied one-task template.""" + for descriptor_path in ( + task_path / ENVELOPE_DESCRIPTOR_FILENAME, + dataset_path / ENVELOPE_DESCRIPTOR_FILENAME, + ): + if not descriptor_path.is_file(): + continue + try: + descriptor = TrustedEnvelopeDescriptor.model_validate_json(descriptor_path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, ValueError) as exc: + raise ValueError(f"Invalid trusted envelope descriptor {descriptor_path}: {exc}") from exc + return descriptor, descriptor_path.parent + raise ValueError( + f"Harbor task is not bound to a trusted host envelope: {task_path}. " + "Run it through the Experimentalist host launcher." + ) + + +def resolve_envelope_task(dataset_path: Path, task_path: Path, *, task_id: str) -> ResolvedEnvelopeTask: + """Bind a sandbox task to its host-generated descriptor.""" + descriptor, descriptor_root = _descriptor_at(dataset_path.resolve(), task_path.resolve()) + relative = task_path.resolve().relative_to(descriptor_root.resolve()).as_posix() or "." + matches = [task for task in descriptor.tasks if task.path == relative] + if not matches and len(descriptor.tasks) == 1 and descriptor.tasks[0].path == ".": + # Eval Author copies one registered template into each generated task. + # The copied descriptor still binds the derivative to the same base. + matches = descriptor.tasks + if len(matches) != 1: + raise ValueError(f"Trusted envelope descriptor does not bind task path {task_path}") + base = matches[0] + return ResolvedEnvelopeTask( + envelope_id=descriptor.envelope_id, + envelope_digest=descriptor.envelope_digest, + task_id=task_id, + base_task_id=base.task_id, + task_path=task_path.resolve(), + policy=base.policy, + ) + + +def _contains(parent: PurePosixPath, child: PurePosixPath) -> bool: + return child == parent or parent in child.parents + + +def path_is_allowed(relative: str, allowed: Iterable[str]) -> bool: + path = PurePosixPath(_validated_relative_path(relative)) + return any(_contains(PurePosixPath(value), path) for value in allowed) + + +def create_overlay_directory(bindings: list[ResolvedEnvelopeTask], destination: Path) -> str | None: + """Copy only declared mutable files from task working copies.""" + copied = False + destination.mkdir(parents=True, exist_ok=False) + for binding in bindings: + for declared in [*binding.policy.task_data_paths, *binding.policy.verifier_paths]: + source = binding.task_path / declared + if not source.exists(): + continue + target = destination / binding.task_id / declared + if source.is_dir(): + _safe_copy_tree(source, target) + elif source.is_file() and not source.is_symlink(): + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, target) + else: + raise ValueError(f"Task overlay contains a link or special file: {source}") + copied = True + if not copied: + shutil.rmtree(destination) + return None + return transport_tree_digest(destination) + + +def _set_materialized_identity(task_dir: Path, task_id: str, base_task_id: str) -> None: + if task_id == base_task_id: + return + path = task_dir / "task.toml" + document = tomlkit.parse(path.read_text(encoding="utf-8")) + task_table = document.get("task") + if task_table is None or not isinstance(task_table.get("name"), str): + raise ValueError(f"Trusted Harbor task has no [task].name: {path}") + raw_name = task_table["name"] + organization, separator, short_name = raw_name.rpartition("/") + if not separator: + raise ValueError(f"Trusted Harbor task name must use org/name format: {raw_name!r}") + task_table["name"] = f"{organization}/{short_name.split('__', 1)[0]}__{task_id}" + path.write_text(tomlkit.dumps(document), encoding="utf-8") + + +def _validate_task_data_file(path: Path, slot: TaskDataSlot) -> None: + if path.stat().st_size > slot.max_bytes: + raise ValueError(f"Task-data overlay exceeds {slot.max_bytes} bytes: {slot.path}") + try: + text = path.read_text(encoding="utf-8") + except UnicodeError as exc: + raise ValueError(f"Task-data overlay must be UTF-8 {slot.media_type}: {slot.path}") from exc + if slot.media_type == "application/json": + try: + json.loads(text) + except json.JSONDecodeError as exc: + raise ValueError(f"Task-data overlay is not valid JSON: {slot.path}: {exc}") from exc + + +class TrustedEnvelopeCatalog: + """Read-only catalog used only by the host bridge.""" + + def __init__(self, root: Path) -> None: + self.root = root.expanduser().resolve() + if not self.root.is_dir(): + raise FileNotFoundError(f"Trusted Harbor envelope catalog not found: {self.root}") + + def load(self, envelope_id: str, envelope_digest: str) -> tuple[TrustedEnvelopeManifest, Path]: + envelope_root = self.root / "envelopes" / envelope_id + try: + envelope_root.resolve().relative_to((self.root / "envelopes").resolve()) + except ValueError as exc: + raise ValueError(f"Trusted envelope id escapes the catalog: {envelope_id!r}") from exc + manifest_path = envelope_root / ENVELOPE_MANIFEST_FILENAME + if not manifest_path.is_file(): + raise KeyError(envelope_id) + manifest = TrustedEnvelopeManifest.model_validate_json(manifest_path.read_text(encoding="utf-8")) + if manifest.envelope_id != envelope_id or manifest.envelope_digest != envelope_digest: + raise ValueError(f"Trusted envelope identity mismatch: {envelope_id}") + dataset_root = envelope_root / "dataset" + if tree_digest(dataset_root, exclude={ENVELOPE_DESCRIPTOR_FILENAME}) != manifest.envelope_digest: + raise ValueError(f"Trusted envelope content changed after registration: {envelope_id}") + return manifest, dataset_root + + def materialize( + self, + *, + envelope_id: str, + envelope_digest: str, + selections: list[EnvelopeTask], + destination: Path, + overlay_dir: Path | None = None, + ) -> Path: + """Copy selected trusted tasks and apply only declared overlay files.""" + manifest, dataset_root = self.load(envelope_id, envelope_digest) + tasks = {task.task_id: task for task in manifest.tasks} + destination.mkdir(parents=True, exist_ok=False) + selected_ids = {selection.task_id for selection in selections} + if len(selected_ids) != len(selections): + raise ValueError("Materialized task ids must be unique") + if overlay_dir is not None: + unexpected = {path.name for path in overlay_dir.iterdir()} - selected_ids + if unexpected: + raise ValueError(f"Task overlay contains unknown task ids: {', '.join(sorted(unexpected))}") + + for selection in selections: + base = tasks.get(selection.base_task_id) + if base is None: + raise ValueError(f"Trusted envelope has no task {selection.base_task_id!r}") + source = dataset_root if base.path == "." else dataset_root / base.path + if tree_digest(source, exclude={ENVELOPE_DESCRIPTOR_FILENAME}) != base.content_digest: + raise ValueError(f"Trusted task content changed after registration: {base.task_id}") + target = destination / selection.task_id + _safe_copy_tree(source, target) + (target / ENVELOPE_DESCRIPTOR_FILENAME).unlink(missing_ok=True) + + task_overlay = overlay_dir / selection.task_id if overlay_dir is not None else None + if task_overlay is not None and task_overlay.exists(): + for path in sorted(task_overlay.rglob("*")): + relative = path.relative_to(task_overlay).as_posix() + if path.is_dir(): + continue + if path.name in _FORBIDDEN_OVERLAY_NAMES: + raise ValueError(f"Task overlay may not modify runtime-control file {relative}") + slot = next((item for item in base.policy.task_data if item.path == relative), None) + if slot is None and not path_is_allowed(relative, base.policy.verifier_paths): + raise ValueError(f"Task overlay may not modify undeclared path {relative}") + if path.is_symlink() or not path.is_file() or path.stat().st_nlink > 1: + raise ValueError(f"Task overlay contains a link or special file: {relative}") + if slot is not None: + _validate_task_data_file(path, slot) + output = target / relative + output.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, output) + _set_materialized_identity(target, selection.task_id, selection.base_task_id) + return destination + + +def task_config(task_dir: Path) -> dict[str, object]: + """Read trusted task configuration for assertions without executing it.""" + return tomllib.loads((task_dir / "task.toml").read_text(encoding="utf-8")) diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/runner.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/runner.py new file mode 100644 index 0000000000..58986d7645 --- /dev/null +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/runner.py @@ -0,0 +1,86 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Trusted server-side Harbor evaluation runner.""" + +from __future__ import annotations + +from pathlib import Path + +from harbor.models.job.config import RetryConfig +from nemo_experimentalist_plugin.experimentalist.components.evaluator.harbor import ( + HarborDataset, + HarborEvaluator, + HarborEvaluatorConfig, +) +from nemo_experimentalist_plugin.experimentalist.components.evaluator.models import ( + DatasetRef, + EvaluationResult, +) +from nemo_experimentalist_plugin.harbor_bridge.contracts import EvaluationSubmission, RunProfile +from nemo_experimentalist_plugin.harbor_bridge.trusted_agent import candidate_agent_import +from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, SecretStr + + +class TrustedInferenceConfig(BaseModel): + """Host-provided candidate inference configuration.""" + + model_config = ConfigDict(extra="forbid", frozen=True) + + api_key: SecretStr + api_base: AnyHttpUrl + model_name: str = Field(min_length=1, max_length=256) + + +class HarborBridgeRunner: + """Run one fixed trusted adapter over catalog-materialized tasks.""" + + def __init__(self, inference: TrustedInferenceConfig) -> None: + self.inference = inference + + async def run( + self, + *, + submission: EvaluationSubmission, + profile: RunProfile, + candidate_dir: Path, + dataset_dir: Path, + work_dir: Path, + ) -> EvaluationResult: + dataset = HarborDataset.from_ref( + DatasetRef( + uri=dataset_dir.resolve().as_uri(), + metadata={ + "id": submission.request_id, + "task_ids": [task.task_id for task in submission.envelope.tasks], + }, + ) + ) + await dataset.validate() + + with candidate_agent_import(candidate_dir) as trusted_import_path: + evaluator = HarborEvaluator( + HarborEvaluatorConfig( + job_name=submission.request_id, + jobs_dir=Path("results"), + n_attempts=profile.attempts, + n_concurrent_trials=profile.concurrency, + retry=RetryConfig(max_retries=profile.retries), + quiet=True, + verifier_timeout_multiplier=profile.verifier_timeout_multiplier, + agent_timeout_multiplier=profile.agent_timeout_multiplier, + agent_setup_timeout_multiplier=profile.setup_timeout_multiplier, + environment_build_timeout_multiplier=profile.build_timeout_multiplier, + import_path=trusted_import_path, + scope_import_path=False, + agent_model_name=self.inference.model_name, + agent_env={ + "INFERENCE_API_KEY": self.inference.api_key.get_secret_value(), + "INFERENCE_API_BASE": str(self.inference.api_base), + "AUT_MODEL_NAME": self.inference.model_name, + }, + trace_dir="/app/traces", + ), + experiment_dir=work_dir, + ) + return await evaluator.run(candidate_dir, dataset) diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/service.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/service.py new file mode 100644 index 0000000000..2ddcf6f163 --- /dev/null +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/service.py @@ -0,0 +1,661 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Ephemeral, authenticated, job-shaped Harbor bridge.""" + +from __future__ import annotations + +import argparse +import asyncio +import hmac +import logging +import os +import shutil +import stat +import tarfile +from collections.abc import Iterator +from contextlib import asynccontextmanager +from dataclasses import dataclass +from pathlib import Path +from typing import Annotated, Literal, Protocol +from urllib.parse import unquote, urlparse +from uuid import uuid4 + +import uvicorn +from fastapi import Depends, FastAPI, Header, HTTPException, Request, Response +from nemo_experimentalist_plugin.experimentalist.components.evaluator.models import ( + DataValue, + EvaluationResult, + ResourceRef, +) +from nemo_experimentalist_plugin.harbor_bridge.archives import ( + DEFAULT_MAX_ARCHIVE_BYTES, + DEFAULT_MAX_ARCHIVE_FILES, + create_directory_archive, + extract_directory_archive, +) +from nemo_experimentalist_plugin.harbor_bridge.contracts import ( + DependencyExecRequest, + DependencyExecResponse, + DependencySession, + DependencyStartRequest, + EnvelopeTask, + EvaluationAccepted, + EvaluationState, + EvaluationStatus, + EvaluationSubmission, + RunProfile, +) +from nemo_experimentalist_plugin.harbor_bridge.dependencies import HarborDependencySessionManager +from nemo_experimentalist_plugin.harbor_bridge.envelopes import ( + TrustedEnvelopeCatalog, + transport_tree_digest, +) +from pydantic import BaseModel, ConfigDict, Field, ValidationError +from starlette.datastructures import FormData, UploadFile +from starlette.responses import FileResponse + +logger = logging.getLogger(__name__) + + +RUN_PROFILES: dict[Literal["smoke", "standard"], RunProfile] = { + "smoke": RunProfile( + attempts=1, + concurrency=1, + retries=0, + agent_timeout_multiplier=0.25, + verifier_timeout_multiplier=0.25, + setup_timeout_multiplier=0.5, + build_timeout_multiplier=0.5, + ), + "standard": RunProfile( + attempts=3, + concurrency=4, + retries=1, + agent_timeout_multiplier=1.0, + verifier_timeout_multiplier=1.0, + setup_timeout_multiplier=1.0, + build_timeout_multiplier=1.0, + ), +} + + +class HarborBridgeSettings(BaseModel): + """Trusted bridge startup settings unavailable to OpenShell.""" + + model_config = ConfigDict(extra="forbid") + + storage_root: Path + catalog_root: Path + token: str = Field(min_length=16) + max_archive_bytes: int = Field(default=DEFAULT_MAX_ARCHIVE_BYTES, ge=1, le=2 * 1024 * 1024 * 1024) + max_archive_files: int = Field(default=DEFAULT_MAX_ARCHIVE_FILES, ge=1, le=100_000) + max_concurrent_evaluations: int = Field(default=1, ge=1, le=8) + max_concurrent_dependency_sessions: int = Field(default=2, ge=1, le=8) + sensitive_values: tuple[str, ...] = () + + +class EvaluationRunner(Protocol): + """Trusted adapter from validated bridge state to Harbor.""" + + async def run( + self, + *, + submission: EvaluationSubmission, + profile: RunProfile, + candidate_dir: Path, + dataset_dir: Path, + work_dir: Path, + ) -> EvaluationResult: + """Execute a fully server-constructed Harbor job.""" + ... + + +class UnconfiguredRunner: + """Fail loudly until the trusted Harbor runner is wired.""" + + async def run( + self, + *, + submission: EvaluationSubmission, + profile: RunProfile, + candidate_dir: Path, + dataset_dir: Path, + work_dir: Path, + ) -> EvaluationResult: + del submission, profile, candidate_dir, dataset_dir, work_dir + raise RuntimeError("Trusted Harbor runner is not configured") + + +@dataclass +class _Job: + job_id: str + work_dir: Path + state: EvaluationState = EvaluationState.PENDING + result: EvaluationResult | None = None + error: str | None = None + artifact_archive: Path | None = None + artifact_digest: str | None = None + task: asyncio.Task[None] | None = None + + def status(self) -> EvaluationStatus: + return EvaluationStatus( + job_id=self.job_id, + state=self.state, + result=self.result, + error=self.error, + ) + + +async def _require_form_parts( + request: Request, + *, + required: set[str], + optional: set[str], +) -> FormData: + form = await request.form() + keys = [key for key, _value in form.multi_items()] + allowed = required | optional + unexpected = sorted(set(keys) - allowed) + missing = sorted(required - set(keys)) + duplicated = sorted(key for key in allowed if keys.count(key) > 1) + if unexpected or missing or duplicated: + raise HTTPException(status_code=422, detail="Invalid multipart evaluation request") + return form + + +async def _save_upload(upload: UploadFile, destination: Path, *, max_bytes: int) -> None: + total = 0 + destination.parent.mkdir(parents=True, exist_ok=True) + try: + with destination.open("xb") as output: + while chunk := await upload.read(1024 * 1024): + total += len(chunk) + if total > max_bytes: + raise ValueError("Compressed archive exceeds bridge limit") + output.write(chunk) + finally: + await upload.close() + + +def _resource_refs(result: EvaluationResult) -> Iterator[ResourceRef]: + for trial in result.trials: + if trial.trace is not None: + yield trial.trace + yield from trial.resources.values() + for output in trial.outputs.values(): + if isinstance(output, ResourceRef): + yield output + for metric in trial.metrics.values(): + if metric.spec is not None and metric.spec.ref is not None: + yield metric.spec.ref + + +def _redact_value(value: DataValue, sensitive: tuple[str, ...]) -> DataValue: + if isinstance(value, str): + redacted = value + for secret in sensitive: + if secret: + redacted = redacted.replace(secret, "[REDACTED]") + if redacted.startswith("/") or urlparse(redacted).scheme == "file": + return "[HOST_PATH_REDACTED]" + return redacted + if isinstance(value, list): + return [_redact_value(item, sensitive) for item in value] + if isinstance(value, dict): + return {key: _redact_value(item, sensitive) for key, item in value.items()} + return value + + +def _constant_time_equal(value: str, expected: str) -> bool: + return hmac.compare_digest(value.encode("utf-8"), expected.encode("utf-8")) + + +def _copy_result_resource( + source: Path, + destination: Path, + *, + scratch: Path, + max_bytes: int, + max_files: int, +) -> Path: + mode = source.lstat().st_mode + if stat.S_ISLNK(mode): + raise ValueError("Harbor result resource must not be a symbolic link") + if stat.S_ISREG(mode): + if source.stat().st_nlink > 1: + raise ValueError("Harbor result resource must not be hard linked") + if source.stat().st_size > max_bytes: + raise ValueError("Harbor result resource exceeds the bridge artifact limit") + target = destination / source.name + destination.mkdir(parents=True) + shutil.copy2(source, target) + return target + if not stat.S_ISDIR(mode): + raise ValueError("Harbor result resource must be a regular file or directory") + archive = scratch / f"{destination.name}.tar.gz" + create_directory_archive(source, archive, max_bytes=max_bytes, max_files=max_files) + extract_directory_archive(archive, destination, max_bytes=max_bytes, max_files=max_files) + archive.unlink() + return destination + + +def _export_result( + result: EvaluationResult, + *, + job_id: str, + work_dir: Path, + sensitive: tuple[str, ...], + max_bytes: int, + max_files: int, +) -> tuple[EvaluationResult, Path, str]: + sanitized = EvaluationResult.model_validate_json(result.model_dump_json()) + sanitized.metadata = {key: _redact_value(value, sensitive) for key, value in sanitized.metadata.items()} + export_root = work_dir / "artifacts-export" + scratch = work_dir / "artifacts-scratch" + export_root.mkdir() + scratch.mkdir() + for index, resource in enumerate(_resource_refs(sanitized)): + parsed = urlparse(resource.uri) + raw_source = Path(unquote(parsed.path)) if parsed.scheme in ("", "file") else None + source = raw_source.resolve() if raw_source is not None and not raw_source.is_symlink() else None + if ( + source is not None + and parsed.netloc in ("", "localhost") + and source != work_dir + and source.is_relative_to(work_dir) + and source.exists() + ): + copied = _copy_result_resource( + source, + export_root / str(index), + scratch=scratch, + max_bytes=max_bytes, + max_files=max_files, + ) + relative = copied.relative_to(export_root).as_posix() + resource.uri = f"nemo-harbor-bridge:///artifacts/{relative}" + else: + resource.uri = f"nemo-harbor-bridge:///unavailable/{index}" + resource.metadata = {key: _redact_value(value, sensitive) for key, value in resource.metadata.items()} + for trial in sanitized.trials: + trial.metadata = {key: _redact_value(value, sensitive) for key, value in trial.metadata.items()} + trial.outputs = { + key: value if isinstance(value, ResourceRef) else _redact_value(value, sensitive) + for key, value in trial.outputs.items() + } + if trial.error is not None: + trial.error = {"type": "trial_failed", "message": "See retained Harbor trial logs"} + for metric in trial.metrics.values(): + metric.metadata = {key: _redact_value(value, sensitive) for key, value in metric.metadata.items()} + shutil.rmtree(scratch) + artifact_archive = work_dir / "artifacts.tar.gz" + create_directory_archive( + export_root, + artifact_archive, + max_bytes=max_bytes, + max_files=max_files, + ) + return sanitized, artifact_archive, transport_tree_digest(export_root) + + +def _validation_detail(exc: ValidationError) -> str: + locations = sorted({".".join(str(part) for part in error["loc"]) for error in exc.errors(include_input=False)}) + return f"Invalid evaluation metadata at: {', '.join(locations[:12])}" + + +def create_app( + *, + settings: HarborBridgeSettings, + runner: EvaluationRunner | None = None, + catalog: TrustedEnvelopeCatalog | None = None, + dependency_sessions: HarborDependencySessionManager | None = None, +) -> FastAPI: + """Create one run-scoped bridge application.""" + service_runner = runner or UnconfiguredRunner() + trusted_catalog = catalog or TrustedEnvelopeCatalog(settings.catalog_root) + storage_root = settings.storage_root.expanduser().resolve() + storage_root.mkdir(parents=True, exist_ok=True) + jobs: dict[str, _Job] = {} + session_manager = dependency_sessions or HarborDependencySessionManager( + max_concurrent_sessions=settings.max_concurrent_dependency_sessions + ) + dependency_capabilities: dict[str, str] = {} + dependency_work_dirs: dict[str, Path] = {} + semaphore = asyncio.Semaphore(settings.max_concurrent_evaluations) + + async def require_auth(authorization: Annotated[str | None, Header()] = None) -> None: + expected = f"Bearer {settings.token}" + if authorization is None or not _constant_time_equal(authorization, expected): + raise HTTPException(status_code=401, detail="Unauthorized") + + async def execute(job: _Job, submission: EvaluationSubmission) -> None: + try: + async with semaphore: + if job.state == EvaluationState.CANCELLED: + return + job.state = EvaluationState.RUNNING + result = await service_runner.run( + submission=submission, + profile=RUN_PROFILES[submission.run_profile], + candidate_dir=job.work_dir / "candidate", + dataset_dir=job.work_dir / "dataset", + work_dir=job.work_dir, + ) + job.result, job.artifact_archive, job.artifact_digest = _export_result( + result, + job_id=job.job_id, + work_dir=job.work_dir, + sensitive=settings.sensitive_values, + max_bytes=settings.max_archive_bytes, + max_files=settings.max_archive_files, + ) + job.state = EvaluationState.COMPLETED + except asyncio.CancelledError: + job.state = EvaluationState.CANCELLED + raise + except BaseException as exc: + logger.exception("Harbor bridge job %s failed", job.job_id) + job.error = f"{type(exc).__name__}: evaluation failed" + job.state = EvaluationState.FAILED + + @asynccontextmanager + async def lifespan(_app: FastAPI): + try: + yield + finally: + running = [job.task for job in jobs.values() if job.task is not None and not job.task.done()] + for task in running: + task.cancel() + if running: + await asyncio.gather(*running, return_exceptions=True) + await session_manager.close() + for work_dir in dependency_work_dirs.values(): + shutil.rmtree(work_dir, ignore_errors=True) + + app = FastAPI( + title="NeMo Experimentalist Harbor Bridge", + docs_url=None, + redoc_url=None, + openapi_url=None, + lifespan=lifespan, + ) + + @app.get("/health/ready") + async def health_ready() -> dict[str, str]: + return {"status": "ready"} + + @app.post( + "/v1/evaluations", + response_model=EvaluationAccepted, + status_code=202, + dependencies=[Depends(require_auth)], + ) + async def submit_evaluation(request: Request) -> EvaluationAccepted: + form = await _require_form_parts( + request, + required={"metadata", "candidate"}, + optional={"overlay"}, + ) + raw_metadata = form["metadata"] + candidate_upload = form["candidate"] + overlay_upload = form.get("overlay") + if not isinstance(raw_metadata, str) or not isinstance(candidate_upload, UploadFile): + raise HTTPException(status_code=422, detail="Invalid multipart evaluation request") + if overlay_upload is not None and not isinstance(overlay_upload, UploadFile): + raise HTTPException(status_code=422, detail="Invalid multipart evaluation request") + try: + submission = EvaluationSubmission.model_validate_json(raw_metadata) + except ValidationError as exc: + raise HTTPException(status_code=422, detail=_validation_detail(exc)) from None + if (submission.overlay is None) != (overlay_upload is None): + raise HTTPException(status_code=422, detail="Overlay metadata and archive must be provided together") + + job_id = f"eval-{uuid4().hex}" + work_dir = storage_root / job_id + try: + work_dir.mkdir(exist_ok=False) + candidate_archive = work_dir / "candidate.tar.gz" + await _save_upload(candidate_upload, candidate_archive, max_bytes=settings.max_archive_bytes) + candidate_dir = work_dir / "candidate" + extract_directory_archive( + candidate_archive, + candidate_dir, + max_bytes=settings.max_archive_bytes, + max_files=settings.max_archive_files, + ) + if transport_tree_digest(candidate_dir) != submission.candidate.digest: + raise ValueError("Candidate archive digest mismatch") + + overlay_dir = None + if overlay_upload is not None and submission.overlay is not None: + overlay_archive = work_dir / "overlay.tar.gz" + await _save_upload(overlay_upload, overlay_archive, max_bytes=settings.max_archive_bytes) + overlay_dir = work_dir / "overlay" + extract_directory_archive( + overlay_archive, + overlay_dir, + max_bytes=settings.max_archive_bytes, + max_files=settings.max_archive_files, + ) + if transport_tree_digest(overlay_dir) != submission.overlay.digest: + raise ValueError("Overlay archive digest mismatch") + + trusted_catalog.materialize( + envelope_id=submission.envelope.id, + envelope_digest=submission.envelope.digest, + selections=submission.envelope.tasks, + destination=work_dir / "dataset", + overlay_dir=overlay_dir, + ) + except (OSError, KeyError, ValueError, tarfile.TarError): + logger.exception("Rejected Harbor bridge submission %s", job_id) + shutil.rmtree(work_dir, ignore_errors=True) + raise HTTPException(status_code=422, detail="Evaluation request failed trusted validation") from None + + job = _Job(job_id=job_id, work_dir=work_dir) + jobs[job_id] = job + job.task = asyncio.create_task(execute(job, submission), name=f"harbor-bridge-{job_id}") + return EvaluationAccepted(job_id=job_id) + + @app.get( + "/v1/evaluations/{job_id}", + response_model=EvaluationStatus, + dependencies=[Depends(require_auth)], + ) + async def get_evaluation(job_id: str) -> EvaluationStatus: + job = jobs.get(job_id) + if job is None: + raise HTTPException(status_code=404, detail="Evaluation not found") + return job.status() + + @app.get( + "/v1/evaluations/{job_id}/artifacts", + dependencies=[Depends(require_auth)], + ) + async def get_evaluation_artifacts(job_id: str) -> FileResponse: + job = jobs.get(job_id) + if job is None: + raise HTTPException(status_code=404, detail="Evaluation not found") + if job.state != EvaluationState.COMPLETED or job.artifact_archive is None or job.artifact_digest is None: + raise HTTPException(status_code=409, detail="Evaluation artifacts are not ready") + return FileResponse( + job.artifact_archive, + media_type="application/gzip", + headers={"X-Nemo-Artifact-Digest": job.artifact_digest}, + ) + + @app.delete( + "/v1/evaluations/{job_id}", + status_code=204, + dependencies=[Depends(require_auth)], + ) + async def cancel_evaluation(job_id: str) -> Response: + job = jobs.get(job_id) + if job is None: + raise HTTPException(status_code=404, detail="Evaluation not found") + if job.task is not None and not job.task.done(): + job.task.cancel() + await asyncio.gather(job.task, return_exceptions=True) + if job.state in (EvaluationState.PENDING, EvaluationState.RUNNING): + job.state = EvaluationState.CANCELLED + return Response(status_code=204) + + @app.post( + "/v1/dependencies", + response_model=DependencySession, + status_code=201, + dependencies=[Depends(require_auth)], + ) + async def start_dependency(request: Request) -> DependencySession: + form = await _require_form_parts(request, required={"metadata"}, optional={"overlay"}) + raw_metadata = form["metadata"] + overlay_upload = form.get("overlay") + if not isinstance(raw_metadata, str): + raise HTTPException(status_code=422, detail="Invalid dependency request") + if overlay_upload is not None and not isinstance(overlay_upload, UploadFile): + raise HTTPException(status_code=422, detail="Invalid dependency request") + try: + metadata = DependencyStartRequest.model_validate_json(raw_metadata) + except ValidationError as exc: + raise HTTPException(status_code=422, detail=_validation_detail(exc)) from None + if (metadata.overlay_digest is None) != (overlay_upload is None): + raise HTTPException(status_code=422, detail="Overlay metadata and archive must be provided together") + + work_dir = storage_root / f"dependency-{uuid4().hex}" + try: + work_dir.mkdir() + overlay_dir = None + if overlay_upload is not None and metadata.overlay_digest is not None: + archive = work_dir / "overlay.tar.gz" + await _save_upload(overlay_upload, archive, max_bytes=settings.max_archive_bytes) + overlay_dir = work_dir / "overlay" + extract_directory_archive( + archive, + overlay_dir, + max_bytes=settings.max_archive_bytes, + max_files=settings.max_archive_files, + ) + if transport_tree_digest(overlay_dir) != metadata.overlay_digest: + raise ValueError("Overlay digest mismatch") + dataset_dir = trusted_catalog.materialize( + envelope_id=metadata.envelope_id, + envelope_digest=metadata.envelope_digest, + selections=[EnvelopeTask(task_id=metadata.task_id, base_task_id=metadata.base_task_id)], + destination=work_dir / "dataset", + overlay_dir=overlay_dir, + ) + session_id = await session_manager.start( + metadata, + task_dir=dataset_dir / metadata.task_id, + work_dir=work_dir, + ) + except (OSError, KeyError, ValueError, RuntimeError, tarfile.TarError): + logger.exception("Rejected dependency session request") + shutil.rmtree(work_dir, ignore_errors=True) + raise HTTPException(status_code=422, detail="Dependency request failed trusted validation") from None + capability = uuid4().hex + uuid4().hex + dependency_capabilities[session_id] = capability + dependency_work_dirs[session_id] = work_dir + return DependencySession(session_id=session_id, capability_token=capability) + + def require_capability(session_id: str, capability: str | None) -> None: + expected = dependency_capabilities.get(session_id) + if expected is None: + raise HTTPException(status_code=404, detail="Dependency session not found") + if capability is None or not _constant_time_equal(capability, expected): + raise HTTPException(status_code=403, detail="Invalid dependency capability") + + @app.post( + "/v1/dependencies/{session_id}/exec", + response_model=DependencyExecResponse, + dependencies=[Depends(require_auth)], + ) + async def execute_dependency( + session_id: str, + command: DependencyExecRequest, + capability: Annotated[str | None, Header(alias="X-Nemo-Dependency-Capability")] = None, + ) -> DependencyExecResponse: + require_capability(session_id, capability) + try: + return await session_manager.execute(session_id, command) + except KeyError: + raise HTTPException(status_code=404, detail="Dependency session not found") from None + + @app.delete( + "/v1/dependencies/{session_id}", + status_code=204, + dependencies=[Depends(require_auth)], + ) + async def stop_dependency( + session_id: str, + capability: Annotated[str | None, Header(alias="X-Nemo-Dependency-Capability")] = None, + ) -> Response: + require_capability(session_id, capability) + try: + await session_manager.stop(session_id) + except KeyError: + raise HTTPException(status_code=404, detail="Dependency session not found") from None + dependency_capabilities.pop(session_id, None) + work_dir = dependency_work_dirs.pop(session_id, None) + if work_dir is not None: + shutil.rmtree(work_dir, ignore_errors=True) + return Response(status_code=204) + + return app + + +def main() -> None: + """Run a bridge from host-provided environment settings.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--port", type=int, required=True) + parser.add_argument("--storage-root", type=Path, required=True) + parser.add_argument("--catalog-root", type=Path, required=True) + args = parser.parse_args() + token = os.environ.get("NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_TOKEN") + if token is None: + raise SystemExit("NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_TOKEN is required") + inference_api_key = os.environ.get("INFERENCE_API_KEY") + inference_api_base = os.environ.get("INFERENCE_API_BASE") + aut_model_name = os.environ.get("AUT_MODEL_NAME") + missing = [ + name + for name, value in ( + ("INFERENCE_API_KEY", inference_api_key), + ("INFERENCE_API_BASE", inference_api_base), + ("AUT_MODEL_NAME", aut_model_name), + ) + if not value + ] + if missing: + raise SystemExit(f"Trusted bridge startup environment is missing: {', '.join(missing)}") + assert inference_api_key is not None + assert inference_api_base is not None + assert aut_model_name is not None + from nemo_experimentalist_plugin.harbor_bridge.runner import ( # noqa: PLC0415 + HarborBridgeRunner, + TrustedInferenceConfig, + ) + + inference = TrustedInferenceConfig.model_validate( + { + "api_key": inference_api_key, + "api_base": inference_api_base, + "model_name": aut_model_name, + } + ) + app = create_app( + settings=HarborBridgeSettings( + storage_root=args.storage_root, + catalog_root=args.catalog_root, + token=token, + sensitive_values=(inference_api_key,), + ), + runner=HarborBridgeRunner(inference), + ) + uvicorn.run(app, host=args.host, port=args.port, log_level="info") + + +if __name__ == "__main__": + main() diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/trusted_agent.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/trusted_agent.py new file mode 100644 index 0000000000..9b6a571a16 --- /dev/null +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/harbor_bridge/trusted_agent.py @@ -0,0 +1,311 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Trusted Harbor adapter that treats an Experimentalist candidate as data.""" + +from __future__ import annotations + +import contextlib +import hashlib +import json +import os +import shlex +import shutil +import stat +import sys +import tarfile +import urllib.request +import uuid +from collections.abc import Iterator +from pathlib import Path +from types import ModuleType +from typing import ClassVar, override + +from harbor.agents.installed.base import BaseInstalledAgent, with_prompt_template +from harbor.environments.base import BaseEnvironment +from harbor.models.agent.context import AgentContext + +REMOTE_ROOT = "/installed-agent" +REMOTE_PROJECT = f"{REMOTE_ROOT}/project" +REMOTE_UV = f"{REMOTE_ROOT}/bin/uv" +REMOTE_VENV = f"{REMOTE_ROOT}/venv" +REMOTE_PYTHON_INSTALLS = f"{REMOTE_ROOT}/python" +REMOTE_UV_CACHE = f"{REMOTE_ROOT}/uv-cache" +UV_VERSION = "0.9.5" +CANDIDATE_RUN_TIMEOUT_SEC = 3600 +UV_ASSETS: dict[str, tuple[str, str]] = { + "aarch64": ( + "uv-aarch64-unknown-linux-musl.tar.gz", + "42b9b83933a289fe9c0e48f4973dee49ce0dfb95e19ea0b525ca0dbca3bce71f", + ), + "x86_64": ( + "uv-x86_64-unknown-linux-musl.tar.gz", + "3665ffb6c429c31ad6c778ac0489b7746e691acf025cf530b3510b2f9b1660ff", + ), +} +_IGNORED_PARTS = frozenset({".git", ".venv", "__pycache__", "tmp"}) + + +def _normalize_architecture(raw: str) -> str: + aliases = { + "amd64": "x86_64", + "arm64": "aarch64", + "aarch64": "aarch64", + "x86_64": "x86_64", + } + normalized = aliases.get(raw.strip().lower()) + if normalized is None: + supported = ", ".join(sorted(UV_ASSETS)) + raise RuntimeError(f"Unsupported task-container architecture {raw!r}; supported: {supported}") + return normalized + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as binary_file: + for chunk in iter(lambda: binary_file.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _download_archive(url: str, destination: Path, expected_sha256: str) -> None: + destination.parent.mkdir(parents=True, exist_ok=True) + partial = destination.with_name(f"{destination.name}.{uuid.uuid4().hex}.partial") + try: + request = urllib.request.Request(url, headers={"User-Agent": "nemo-experimentalist-harbor-bridge"}) + with urllib.request.urlopen(request, timeout=300) as response, partial.open("wb") as output: # noqa: S310 + shutil.copyfileobj(response, output) + actual_sha256 = _sha256(partial) + if actual_sha256 != expected_sha256: + raise RuntimeError( + f"Downloaded uv archive checksum mismatch: expected {expected_sha256}, got {actual_sha256}" + ) + os.replace(partial, destination) + finally: + partial.unlink(missing_ok=True) + + +def _cached_uv_binary(architecture: str) -> Path: + asset_name, expected_sha256 = UV_ASSETS[architecture] + default_cache = Path.cwd() / "tmp" / "runtime-cache" + cache_root = Path(os.environ.get("NEMO_EXPERIMENTALIST_RUNTIME_CACHE", default_cache)) + release_dir = cache_root / f"uv-{UV_VERSION}" + archive = release_dir / asset_name + if not archive.is_file() or _sha256(archive) != expected_sha256: + archive.unlink(missing_ok=True) + url = f"https://github.com/astral-sh/uv/releases/download/{UV_VERSION}/{asset_name}" + _download_archive(url, archive, expected_sha256) + + binary = release_dir / architecture / "uv" + binary.parent.mkdir(parents=True, exist_ok=True) + temporary_binary = binary.with_name(f"uv.{uuid.uuid4().hex}.partial") + member_name = f"{asset_name.removesuffix('.tar.gz')}/uv" + try: + with tarfile.open(archive, mode="r:gz") as bundle: + member = bundle.getmember(member_name) + source = bundle.extractfile(member) + if source is None: + raise RuntimeError(f"Pinned uv archive does not contain {member_name}") + with source, temporary_binary.open("wb") as output: + shutil.copyfileobj(source, output) + temporary_binary.chmod(0o755) + os.replace(temporary_binary, binary) + finally: + temporary_binary.unlink(missing_ok=True) + return binary + + +def _candidate_files(root: Path) -> list[Path]: + required = ("main.py", "pyproject.toml", "uv.lock") + missing = [name for name in required if not (root / name).is_file()] + if missing: + raise RuntimeError(f"Candidate bundle is incomplete; missing: {', '.join(missing)}") + + files: list[Path] = [] + for path in sorted(root.rglob("*")): + relative = path.relative_to(root) + if any(part in _IGNORED_PARTS for part in relative.parts): + continue + info = path.lstat() + if stat.S_ISLNK(info.st_mode): + raise RuntimeError(f"Candidate bundle contains a symbolic link: {relative}") + if stat.S_ISREG(info.st_mode): + if info.st_nlink > 1: + raise RuntimeError(f"Candidate bundle contains a hard-linked file: {relative}") + files.append(path) + elif not stat.S_ISDIR(info.st_mode): + raise RuntimeError(f"Candidate bundle contains a special file: {relative}") + return files + + +class TrustedCandidateAgent(BaseInstalledAgent): + """Fixed adapter for uv-managed ``python -m main`` candidates.""" + + candidate_dir: ClassVar[Path | None] = None + + @staticmethod + @override + def name() -> str: + return "nemo-experimentalist-candidate" + + @override + def version(self) -> str | None: + return "0.1.0" + + @classmethod + def _candidate_root(cls) -> Path: + if cls.candidate_dir is None: + raise RuntimeError("Trusted candidate adapter has no candidate bundle") + return cls.candidate_dir + + @staticmethod + async def _target_architecture(environment: BaseEnvironment) -> str: + result = await environment.exec("uname -s && uname -m") + if result.return_code != 0: + raise RuntimeError(f"Could not detect task-container platform: {result.stderr or result.stdout}") + lines = (result.stdout or "").splitlines() + if len(lines) != 2 or lines[0].strip() != "Linux": + raise RuntimeError(f"Experimentalist candidate requires a Linux task container, got {lines!r}") + return _normalize_architecture(lines[1]) + + @staticmethod + def _runtime_env() -> dict[str, str]: + return { + "UV_CACHE_DIR": REMOTE_UV_CACHE, + "UV_HTTP_TIMEOUT": "300", + "UV_NO_PROGRESS": "1", + "UV_PROJECT_ENVIRONMENT": REMOTE_VENV, + "UV_PYTHON_INSTALL_DIR": REMOTE_PYTHON_INSTALLS, + "UV_PYTHON_PREFERENCE": "only-managed", + } + + @override + async def install(self, environment: BaseEnvironment) -> None: + """Upload inert candidate files and install them inside the task container.""" + architecture = await self._target_architecture(environment) + uv_binary = _cached_uv_binary(architecture) + candidate_root = self._candidate_root() + candidate_files = _candidate_files(candidate_root) + + await self.exec_as_root( + environment, + command=( + f"mkdir -p {REMOTE_ROOT}/bin {REMOTE_PROJECT} {REMOTE_VENV} " + f"{REMOTE_PYTHON_INSTALLS} {REMOTE_UV_CACHE} && chmod -R a+rwx {REMOTE_ROOT}" + ), + ) + await environment.upload_file(uv_binary, REMOTE_UV) + for source in candidate_files: + relative = source.relative_to(candidate_root).as_posix() + target = f"{REMOTE_PROJECT}/{relative}" + await self.exec_as_root(environment, command=f"mkdir -p {shlex.quote(str(Path(target).parent))}") + await environment.upload_file(source, target) + await self.exec_as_root( + environment, + command=f"chmod 0755 {REMOTE_UV} && chmod -R a+rX {REMOTE_PROJECT}", + ) + await self.exec_as_agent( + environment, + command=( + f"{REMOTE_UV} python install 3.12 && " + f"{REMOTE_UV} sync --project {REMOTE_PROJECT} --frozen --no-dev && " + f"{REMOTE_VENV}/bin/python -m py_compile {REMOTE_PROJECT}/main.py" + ), + env=self._runtime_env(), + timeout_sec=600, + ) + + def _apply_summary(self, context: AgentContext) -> None: + summary_path = self.logs_dir / "summary.json" + try: + summary = json.loads(summary_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return + if not isinstance(summary, dict): + return + answer = summary.get("answer") + if isinstance(answer, str): + context.metadata = {**(context.metadata or {}), "answer": answer} + usage = summary.get("usage") + if not isinstance(usage, dict): + return + if isinstance(usage.get("input_tokens"), int): + context.n_input_tokens = usage["input_tokens"] + if isinstance(usage.get("output_tokens"), int): + context.n_output_tokens = usage["output_tokens"] + if isinstance(usage.get("cache_read_tokens"), int): + context.n_cache_tokens = usage["cache_read_tokens"] + + @with_prompt_template + @override + async def run( + self, + instruction: str, + environment: BaseEnvironment, + context: AgentContext, + ) -> None: + """Execute only the fixed entrypoint with process-scoped inference credentials.""" + api_key = self._get_env("INFERENCE_API_KEY") + if not api_key: + raise RuntimeError("INFERENCE_API_KEY is required for the Experimentalist candidate") + + prompt_path = self.logs_dir / "instruction.txt" + prompt_path.write_text(instruction, encoding="utf-8") + remote_prompt = f"{REMOTE_ROOT}/instruction.txt" + await environment.upload_file(prompt_path, remote_prompt) + + env = { + "INFERENCE_API_KEY": api_key, + "INFERENCE_API_BASE": self._get_env("INFERENCE_API_BASE") or "https://inference-api.nvidia.com/v1", + "PYTHONPATH": REMOTE_PROJECT, + } + model_name = self.model_name or self._get_env("AUT_MODEL_NAME") + if model_name: + env["AUT_MODEL_NAME"] = model_name + + command = ( + f"{REMOTE_VENV}/bin/python -m main " + f"--prompt-file {shlex.quote(remote_prompt)} " + "--trace-path /app/traces/trace.jsonl " + "--summary-path /logs/agent/summary.json " + "> /logs/agent/experimentalist-candidate.txt 2>&1; " + "status=$?; " + "cat /logs/agent/experimentalist-candidate.txt; " + "mkdir -p /logs/artifacts/traces && " + "cp -r /app/traces/. /logs/artifacts/traces/ 2>/dev/null || true; " + "exit $status" + ) + try: + result = await self.exec_as_agent( + environment, + command=command, + env=env, + cwd="/app", + timeout_sec=CANDIDATE_RUN_TIMEOUT_SEC, + ) + context.metadata = { + **(context.metadata or {}), + "returncode": result.return_code, + } + finally: + self._apply_summary(context) + + +@contextlib.contextmanager +def candidate_agent_import(candidate_dir: Path) -> Iterator[str]: + """Register a request-scoped bridge-owned class without importing candidate Python.""" + candidate_root = candidate_dir.expanduser().resolve() + _candidate_files(candidate_root) + module_name = f"{__package__}._candidate_{uuid.uuid4().hex}" + module = ModuleType(module_name) + adapter = type( + f"RequestCandidateAgent_{uuid.uuid4().hex}", + (TrustedCandidateAgent,), + {"candidate_dir": candidate_root}, + ) + setattr(module, "RequestCandidateAgent", adapter) + sys.modules[module_name] = module + try: + yield f"{module_name}:RequestCandidateAgent" + finally: + sys.modules.pop(module_name, None) diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/configure-providers.sh b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/configure-providers.sh new file mode 100755 index 0000000000..0403e40c14 --- /dev/null +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/configure-providers.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +if ! command -v openshell >/dev/null 2>&1; then + echo "openshell CLI is required" >&2 + exit 127 +fi +if [[ -z "${NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_TOKEN:-}" ]]; then + echo "NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_TOKEN is required on the host" >&2 + exit 2 +fi + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +profile_source="$script_dir/provider-profiles/nemo-experimentalist-harbor-bridge.yaml" +profile_dir="${NEMO_EXPERIMENTALIST_PROVIDER_PROFILE_DIR:?provider profile directory is required}" +bridge_provider="${NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_PROVIDER:-nemo-experimentalist-harbor-bridge}" +bridge_provider_type="nemo-experimentalist-harbor-bridge" +inference_provider="${NEMO_EXPERIMENTALIST_INFERENCE_PROVIDER:-nemo-experimentalist-inference}" +inference_provider_type="${NEMO_EXPERIMENTALIST_INFERENCE_PROVIDER_TYPE:-nvidia}" +inference_model="${NEMO_EXPERIMENTALIST_INFERENCE_MODEL:-}" +if [[ -z "$inference_model" && -n "${EXPERIMENTALIST_SMART_MODEL_NAME:-}" ]]; then + inference_model="${EXPERIMENTALIST_SMART_MODEL_NAME#openai/}" +fi + +mkdir -p "$profile_dir" +cp "$profile_source" "$profile_dir/" + +openshell settings set --global --key providers_v2_enabled --value true --yes + +bridge_provider_created=0 +cleanup_failed_setup() { + status=$? + if [[ "$status" -ne 0 && "$bridge_provider_created" == "1" ]]; then + openshell provider delete "$bridge_provider" >/dev/null 2>&1 || \ + echo "WARNING: could not delete partially configured provider $bridge_provider" >&2 + fi + exit "$status" +} +trap cleanup_failed_setup EXIT + +if openshell provider get "$bridge_provider" >/dev/null 2>&1; then + openshell provider delete "$bridge_provider" +fi +if openshell provider profile export "$bridge_provider_type" -o yaml >/dev/null 2>&1; then + openshell provider profile delete "$bridge_provider_type" +fi +openshell provider profile lint --from "$profile_dir" +openshell provider profile import --from "$profile_dir" + +bridge_provider_created=1 +openshell provider create \ + --name "$bridge_provider" \ + --type "$bridge_provider_type" \ + --credential NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_TOKEN + +if [[ -n "$inference_model" ]]; then + inference_set_args=(inference set --provider "$inference_provider" --model "$inference_model") + if openshell provider get "$inference_provider" >/dev/null 2>&1; then + openshell provider delete "$inference_provider" + fi + if [[ "$inference_provider_type" == "nvidia" ]]; then + openshell provider create \ + --name "$inference_provider" \ + --type "$inference_provider_type" \ + --credential NVIDIA_API_KEY \ + --config NVIDIA_BASE_URL=https://inference-api.nvidia.com/v1 + # OpenShell 0.0.92's generic probe is rejected by the Inference Hub GPT-5 + # proxy even though normal routed requests are supported. This skips only + # that setup probe; OpenShell still pins and enforces the runtime route. + inference_set_args+=(--no-verify) + else + openshell provider create \ + --name "$inference_provider" \ + --type "$inference_provider_type" \ + --from-existing + fi + openshell "${inference_set_args[@]}" +fi + +trap - EXIT diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/inner.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/inner.py new file mode 100644 index 0000000000..5d16b1af96 --- /dev/null +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/inner.py @@ -0,0 +1,80 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Internal OpenShell entrypoint; never used by the host CLI directly.""" + +from __future__ import annotations + +import argparse +import asyncio +import os +from pathlib import Path + +from nemo_experimentalist_plugin.client import make_client +from nemo_experimentalist_plugin.experimentalist.components.evaluator.models import DatasetRef +from nemo_experimentalist_plugin.openshell.preparation import SandboxRunManifest + +_RUNTIME_MARKER_ENV = "NEMO_EXPERIMENTALIST_OPEN_SHELL_RUNTIME" +_BRIDGE_URL_ENV = "NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_URL" + + +def _contained_path(root: Path, relative: str) -> Path: + value = (root / relative).resolve() + try: + value.relative_to(root) + except ValueError as exc: + raise ValueError(f"Prepared sandbox path escapes its input root: {relative!r}") from exc + return value + + +async def run_prepared_manifest(manifest_path: Path, *, output_dir: Path) -> str: + """Execute only a host-prepared, credential-free manifest.""" + if os.environ.get(_RUNTIME_MARKER_ENV) != "1": + raise RuntimeError("The Experimentalist inner runner requires an OpenShell runtime") + if not os.environ.get(_BRIDGE_URL_ENV): + raise RuntimeError(f"{_BRIDGE_URL_ENV} is required in the OpenShell runtime") + manifest_file = manifest_path.expanduser().resolve() + manifest = SandboxRunManifest.model_validate_json(manifest_file.read_text(encoding="utf-8")) + root = manifest_file.parent + agent = _contained_path(root, manifest.agent) + train = _contained_path(root, manifest.train_dataset) + validation = _contained_path(root, manifest.validation_dataset) + template = _contained_path(root, manifest.task_template) if manifest.task_template is not None else None + insight = _contained_path(root, manifest.insight) if manifest.insight is not None else None + agent_spec = _contained_path(root, manifest.agent_spec) if manifest.agent_spec is not None else None + skills = [_contained_path(root, value) for value in manifest.framework_skills_dirs] + + from nemo_experimentalist_plugin.experimentalist.run import run_experimentalist # noqa: PLC0415 + + client = make_client(os.environ.get("NMP_BASE_URL", "http://host.openshell.internal:8080")) + try: + return await run_experimentalist( + agent=str(agent), + agent_spec=str(agent_spec) if agent_spec is not None else None, + insight=insight, + train_dataset=DatasetRef(uri=train.as_uri(), metadata={"id": "train"}), + validation_dataset=DatasetRef(uri=validation.as_uri(), metadata={"id": "validation"}), + task_template=( + DatasetRef(uri=template.as_uri(), metadata={"id": "task-template"}) if template is not None else None + ), + experiment_dir=output_dir, + workspace=manifest.workspace, + client=client, + config=manifest.config, + mode="local", + framework_skills_dirs=skills, + ) + finally: + await client.close() + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + print(asyncio.run(run_prepared_manifest(args.manifest, output_dir=args.output))) + + +if __name__ == "__main__": + main() diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/launcher.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/launcher.py new file mode 100644 index 0000000000..24716b8aed --- /dev/null +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/launcher.py @@ -0,0 +1,354 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Host-side fail-closed launcher for one prepared Experimentalist run.""" + +from __future__ import annotations + +import os +import platform +import secrets +import shutil +import subprocess +import sys +import time +from collections.abc import Mapping +from dataclasses import dataclass +from pathlib import Path +from typing import BinaryIO +from urllib.parse import urlsplit, urlunsplit + +import httpx +from nemo_experimentalist_plugin.openshell.preparation import PreparedOpenShellRun + +DEFAULT_IMAGE = "local/nmp-experimentalist:local" +IMAGE_ENV = "NEMO_EXPERIMENTALIST_IMAGE" +PLATFORM_ENV = "NEMO_EXPERIMENTALIST_PLATFORM" +RUNTIME_IMAGE_LABEL = "com.nvidia.nemo.experimentalist.openshell-runtime" +RUNTIME_IMAGE_API = "1" +BRIDGE_TOKEN_ENV = "NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_TOKEN" +BRIDGE_URL_ENV = "NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_URL" +BRIDGE_PROVIDER_ENV = "NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_PROVIDER" +BRIDGE_BIND_ENV = "NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_BIND" +DEFAULT_BRIDGE_HOST_URL = "http://127.0.0.1:8765" +DEFAULT_BRIDGE_SANDBOX_URL = "http://host.openshell.internal:8765" +DEFAULT_SMART_MODEL = "openai/openai/openai/gpt-5.5" + + +class OpenShellLaunchError(RuntimeError): + """A required runtime component was unavailable before optimization.""" + + +@dataclass +class _ManagedBridge: + process: subprocess.Popen[bytes] + log_path: Path + log_handle: BinaryIO + + def stop(self) -> None: + if self.process.poll() is None: + self.process.terminate() + try: + self.process.wait(timeout=10) + except subprocess.TimeoutExpired: + self.process.kill() + self.process.wait(timeout=5) + self.log_handle.close() + + +def _find_repo_root(*starts: Path) -> Path | None: + for start in starts: + for candidate in (start, *start.parents): + if (candidate / "docker-bake.hcl").is_file() and ( + candidate / "plugins" / "nemo-experimentalist" / "Dockerfile" + ).is_file(): + return candidate + return None + + +def _host_platform(machine: str | None = None) -> str: + normalized = (machine or platform.machine()).strip().lower() + if normalized in {"aarch64", "arm64"}: + return "linux/arm64" + if normalized in {"amd64", "x86_64"}: + return "linux/amd64" + raise OpenShellLaunchError( + f"Unsupported host architecture {normalized!r}; set {PLATFORM_ENV} to linux/arm64 or linux/amd64" + ) + + +def _sandbox_platform_url(value: str) -> str: + try: + parsed = urlsplit(value) + port = parsed.port + except ValueError as exc: + raise OpenShellLaunchError(f"Invalid NeMo Platform URL: {value}") from exc + if parsed.scheme != "http" or parsed.path not in ("", "/") or parsed.query or parsed.fragment: + raise OpenShellLaunchError("The prototype OpenShell policy requires the root HTTP NeMo Platform URL") + if parsed.username is not None or parsed.password is not None: + raise OpenShellLaunchError("NeMo Platform URL must not contain user information") + if parsed.hostname not in {"localhost", "127.0.0.1", "::1"}: + raise OpenShellLaunchError("The prototype OpenShell policy supports only a host-local NeMo Platform URL") + if port not in (None, 8080): + raise OpenShellLaunchError("The prototype OpenShell policy supports NeMo Platform only on port 8080") + return urlunsplit((parsed.scheme, "host.openshell.internal:8080", parsed.path, parsed.query, parsed.fragment)) + + +def _acquire_default_image( + *, + docker: str, + image: str, + prepared: PreparedOpenShellRun, + runtime_env: dict[str, str], +) -> None: + inspected = subprocess.run( # noqa: S603 + [ + docker, + "image", + "inspect", + "--format", + f'{{{{ index .Config.Labels "{RUNTIME_IMAGE_LABEL}" }}}}', + image, + ], + check=False, + stdout=subprocess.PIPE, + text=True, + stderr=subprocess.DEVNULL, + ) + if inspected.returncode == 0 and inspected.stdout.strip() == RUNTIME_IMAGE_API: + return + repo_root = _find_repo_root(prepared.root, Path(__file__).resolve()) + if repo_root is None: + raise OpenShellLaunchError( + f"A compatible Experimentalist image {image!r} is not available. " + f"Set {IMAGE_ENV} to a published image or run from a nemo-platform checkout." + ) + selected_platform = runtime_env.get(PLATFORM_ENV, "").strip() or _host_platform() + if selected_platform not in {"linux/arm64", "linux/amd64"}: + raise OpenShellLaunchError(f"{PLATFORM_ENV} must be linux/arm64 or linux/amd64") + build_env = { + **runtime_env, + "IMAGE_REGISTRY": "local", + "BAKE_TAG": "local", + "BUILD_ARCH": selected_platform, + } + built = subprocess.run( # noqa: S603 + [ + docker, + "buildx", + "bake", + "-f", + "docker-bake.hcl", + "-f", + "plugins/nemo-experimentalist/docker-bake.hcl", + "nmp-experimentalist-docker", + "--load", + ], + cwd=repo_root, + env=build_env, + check=False, + ) + if built.returncode != 0: + raise OpenShellLaunchError(f"Could not build {image!r} for {selected_platform}") + + +def _bridge_ready(url: str) -> bool: + try: + return httpx.get(f"{url.rstrip('/')}/health/ready", timeout=1.0).status_code == 200 + except httpx.HTTPError: + return False + + +def _host_bridge_url(value: str) -> str: + parsed = urlsplit(value) + if parsed.hostname != "host.openshell.internal": + return value + port_suffix = f":{parsed.port}" if parsed.port is not None else "" + return urlunsplit((parsed.scheme, f"127.0.0.1{port_suffix}", parsed.path, parsed.query, parsed.fragment)) + + +def _bridge_probe_url(bind_host: str) -> str: + if bind_host == "0.0.0.0": + return DEFAULT_BRIDGE_HOST_URL + if bind_host == "::": + return "http://[::1]:8765" + authority = f"[{bind_host}]" if ":" in bind_host and not bind_host.startswith("[") else bind_host + return f"http://{authority}:8765" + + +def _start_bridge( + *, + prepared: PreparedOpenShellRun, + runtime_env: dict[str, str], +) -> _ManagedBridge | None: + configured_url = runtime_env.get(BRIDGE_URL_ENV, "").strip() + token = runtime_env.get(BRIDGE_TOKEN_ENV, "").strip() + bind_host = runtime_env.get(BRIDGE_BIND_ENV, "").strip() or "0.0.0.0" + host_url = _host_bridge_url(configured_url) if configured_url else _bridge_probe_url(bind_host) + if configured_url: + if not token: + raise OpenShellLaunchError(f"{BRIDGE_TOKEN_ENV} is required with an explicit {BRIDGE_URL_ENV}") + if not _bridge_ready(host_url): + raise OpenShellLaunchError(f"Configured Harbor bridge is not ready: {host_url}") + return None + if _bridge_ready(host_url): + raise OpenShellLaunchError( + f"Port 8765 already has a Harbor bridge; configure {BRIDGE_URL_ENV} and {BRIDGE_TOKEN_ENV} " + "explicitly to reuse it" + ) + + token = token or secrets.token_urlsafe(32) + runtime_env[BRIDGE_TOKEN_ENV] = token + runtime_env[BRIDGE_URL_ENV] = DEFAULT_BRIDGE_SANDBOX_URL + bridge_root = prepared.root / "host" / "bridge" + storage_root = bridge_root / "jobs" + bridge_root.mkdir(parents=True, exist_ok=True) + log_path = bridge_root / "bridge.log" + log_handle = log_path.open("ab") + process = subprocess.Popen( # noqa: S603 + [ + sys.executable, + "-m", + "nemo_experimentalist_plugin.harbor_bridge.service", + "--host", + bind_host, + "--port", + "8765", + "--storage-root", + str(storage_root), + "--catalog-root", + str(prepared.catalog_root), + ], + cwd=prepared.root, + env=runtime_env, + stdout=log_handle, + stderr=subprocess.STDOUT, + ) + managed = _ManagedBridge(process=process, log_path=log_path, log_handle=log_handle) + deadline = time.monotonic() + 20 + while time.monotonic() < deadline: + if process.poll() is not None: + managed.stop() + detail = log_path.read_text(encoding="utf-8", errors="replace").strip() + raise OpenShellLaunchError( + "The Experimentalist Harbor bridge exited before becoming ready" + (f": {detail}" if detail else "") + ) + if _bridge_ready(host_url): + return managed + time.sleep(0.2) + managed.stop() + raise OpenShellLaunchError(f"The Experimentalist Harbor bridge did not become ready; see {log_path}") + + +def _apply_runtime_defaults(runtime_env: dict[str, str]) -> None: + runtime_env.setdefault("EXPERIMENTALIST_SMART_MODEL_NAME", DEFAULT_SMART_MODEL) + if not runtime_env.get("INFERENCE_API_KEY") and runtime_env.get("NVIDIA_API_KEY"): + runtime_env["INFERENCE_API_KEY"] = runtime_env["NVIDIA_API_KEY"] + if not runtime_env.get("EXPERIMENTALIST_API_KEY"): + optimizer_key = runtime_env.get("INFERENCE_API_KEY") or runtime_env.get("NVIDIA_API_KEY") + if optimizer_key: + runtime_env["EXPERIMENTALIST_API_KEY"] = optimizer_key + runtime_env.setdefault("INFERENCE_API_BASE", "https://inference-api.nvidia.com/v1") + if not runtime_env.get("AUT_MODEL_NAME"): + configured_model = runtime_env.get("NEMO_EXPERIMENTALIST_AUT_MODEL_NAME") + if configured_model: + runtime_env["AUT_MODEL_NAME"] = configured_model + missing = [ + name + for name in ("EXPERIMENTALIST_API_KEY", "INFERENCE_API_KEY", "INFERENCE_API_BASE", "AUT_MODEL_NAME") + if not runtime_env.get(name) + ] + if missing: + raise OpenShellLaunchError("Missing trusted inference settings: " + ", ".join(missing)) + if not runtime_env.get("NVIDIA_API_KEY"): + runtime_env["NVIDIA_API_KEY"] = runtime_env["INFERENCE_API_KEY"] + if platform.system() == "Darwin": + runtime_env.setdefault("NEMO_EXPERIMENTALIST_POLICY_MODE", "docker-desktop") + + +def _configure_providers(prepared: PreparedOpenShellRun, runtime_env: dict[str, str]) -> None: + script_path = Path(__file__).with_name("configure-providers.sh") + if not script_path.is_file(): + raise OpenShellLaunchError(f"Packaged provider setup is missing: {script_path}") + provider_env = dict(runtime_env) + # OpenShell's NVIDIA profile resolves a credential named NVIDIA_API_KEY. + # Supply the already-selected optimizer key under that lookup name without + # exposing its value in argv or replacing the candidate's host environment. + provider_env["NVIDIA_API_KEY"] = runtime_env["EXPERIMENTALIST_API_KEY"] + runtime_env["NEMO_EXPERIMENTALIST_PROVIDER_PROFILE_DIR"] = str(prepared.root / "host" / "provider-profiles") + provider_env["NEMO_EXPERIMENTALIST_PROVIDER_PROFILE_DIR"] = runtime_env["NEMO_EXPERIMENTALIST_PROVIDER_PROFILE_DIR"] + completed = subprocess.run([str(script_path)], env=provider_env, check=False) # noqa: S603 + if completed.returncode != 0: + raise OpenShellLaunchError("Could not configure the Experimentalist OpenShell providers") + + +def _delete_bridge_provider(openshell: str, runtime_env: Mapping[str, str]) -> bool: + provider = runtime_env[BRIDGE_PROVIDER_ENV] + completed = subprocess.run( # noqa: S603 + [openshell, "provider", "delete", provider], + env=runtime_env, + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + return completed.returncode == 0 + + +def launch_openshell_run( + prepared: PreparedOpenShellRun, + *, + experiment_dir: Path, + platform_url: str, + env: Mapping[str, str] | None = None, +) -> str: + """Run the prepared manifest in OpenShell; never call Experimentalist locally.""" + runtime_env = dict(os.environ if env is None else env) + openshell = shutil.which("openshell", path=runtime_env.get("PATH")) + if openshell is None: + raise OpenShellLaunchError("OpenShell is required for `nemo experimentalist run`, but its CLI is not on PATH") + image = runtime_env.get(IMAGE_ENV, "").strip() or DEFAULT_IMAGE + runtime_env[IMAGE_ENV] = image + if image == DEFAULT_IMAGE: + docker = shutil.which("docker", path=runtime_env.get("PATH")) + if docker is None: + raise OpenShellLaunchError( + f"Default image {image!r} requires Docker for local discovery/build; set {IMAGE_ENV} " + "to an image available to the OpenShell gateway" + ) + _acquire_default_image( + docker=docker, + image=image, + prepared=prepared, + runtime_env=runtime_env, + ) + runtime_env["NMP_BASE_URL"] = _sandbox_platform_url(platform_url) + runtime_env["NEMO_EXPERIMENTALIST_OUTPUT_DIR"] = str(experiment_dir.expanduser().resolve()) + runtime_env.setdefault(BRIDGE_PROVIDER_ENV, f"nemo-exp-bridge-{secrets.token_hex(4)}") + _apply_runtime_defaults(runtime_env) + managed_bridge = _start_bridge(prepared=prepared, runtime_env=runtime_env) + providers_attempted = False + run_completed = False + try: + providers_attempted = True + _configure_providers(prepared, runtime_env) + script_path = Path(__file__).with_name("run.sh") + completed = subprocess.run( # noqa: S603 + [str(script_path), str(prepared.sandbox_input), str(experiment_dir.expanduser().resolve())], + env=runtime_env, + check=False, + text=True, + stdout=subprocess.PIPE, + ) + if completed.returncode != 0: + raise OpenShellLaunchError(f"OpenShell Experimentalist exited with status {completed.returncode}") + run_completed = True + return completed.stdout.strip() + finally: + provider_deleted = not providers_attempted or _delete_bridge_provider(openshell, runtime_env) + if managed_bridge is not None: + managed_bridge.stop() + if not provider_deleted: + message = f"Could not delete ephemeral OpenShell provider {runtime_env[BRIDGE_PROVIDER_ENV]!r}" + if run_completed: + raise OpenShellLaunchError(message) + print(f"WARNING: {message}", file=sys.stderr) diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/policy.docker-desktop.yaml b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/policy.docker-desktop.yaml new file mode 100644 index 0000000000..74b32f750d --- /dev/null +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/policy.docker-desktop.yaml @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +version: 1 + +filesystem_policy: + include_workdir: true + read_only: + - /app + - /bin + - /dev/urandom + - /etc + - /lib + - /opt + - /proc + - /usr + - /var/log + read_write: + - /dev/null + - /dev/shm + - /home/sandbox + - /sandbox + - /tmp + +landlock: + compatibility: best_effort + +process: + run_as_user: sandbox + run_as_group: sandbox + +network_policies: + nemo_platform: + name: nemo-platform + endpoints: + - host: host.openshell.internal + port: 8080 + protocol: rest + enforcement: enforce + rules: + - allow: + method: GET + path: /health/ready + - allow: + method: GET + path: /apis/intake/v2/workspaces/** + - allow: + method: POST + path: /apis/intake/v2/workspaces/** + - allow: + method: PUT + path: /apis/intake/v2/workspaces/** + - allow: + method: GET + path: /apis/insights/v2/workspaces/** + binaries: + - path: /app/.venv/bin/python + - path: /app/.venv/bin/python3.13 + - path: /usr/local/bin/python3.13 diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/policy.yaml b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/policy.yaml new file mode 100644 index 0000000000..6f5a924ac2 --- /dev/null +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/policy.yaml @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +version: 1 + +filesystem_policy: + include_workdir: true + read_only: + - /app + - /bin + - /dev/urandom + - /etc + - /lib + - /opt + - /proc + - /usr + - /var/log + read_write: + - /dev/null + - /dev/shm + - /home/sandbox + - /sandbox + - /tmp + +landlock: + compatibility: hard_requirement + +process: + run_as_user: sandbox + run_as_group: sandbox + +network_policies: + nemo_platform: + name: nemo-platform + endpoints: + - host: host.openshell.internal + port: 8080 + protocol: rest + enforcement: enforce + rules: + - allow: + method: GET + path: /health/ready + - allow: + method: GET + path: /apis/intake/v2/workspaces/** + - allow: + method: POST + path: /apis/intake/v2/workspaces/** + - allow: + method: PUT + path: /apis/intake/v2/workspaces/** + - allow: + method: GET + path: /apis/insights/v2/workspaces/** + binaries: + - path: /app/.venv/bin/python + - path: /app/.venv/bin/python3.13 + - path: /usr/local/bin/python3.13 diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/preparation.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/preparation.py new file mode 100644 index 0000000000..570e18f548 --- /dev/null +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/preparation.py @@ -0,0 +1,243 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Deterministically resolve and copy one run before OpenShell starts.""" + +from __future__ import annotations + +import asyncio +import json +import shutil +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Literal +from uuid import uuid4 + +from nemo_experimentalist_plugin.experimentalist.components.evaluator.models import local_path_from_uri +from nemo_experimentalist_plugin.experimentalist.components.repository import ( + clone_agent_repo, + looks_like_git, +) +from nemo_experimentalist_plugin.harbor_bridge.archives import ( + create_directory_archive, + extract_directory_archive, +) +from nemo_experimentalist_plugin.harbor_bridge.envelopes import register_dataset_envelope +from nemo_experimentalist_plugin.resolve import ( + EvolutionaryOptimizerConfig, + ResolvedExperimentInputs, +) +from pydantic import BaseModel, ConfigDict + +MANIFEST_FILENAME = "run.json" + + +class SandboxRunManifest(BaseModel): + """Credential-free inputs consumed by the internal sandbox entrypoint.""" + + model_config = ConfigDict(extra="forbid") + + schema_version: Literal[1] = 1 + agent: str + agent_spec: str | None = None + insight: str | None = None + train_dataset: str + validation_dataset: str + task_template: str | None = None + workspace: str + config: EvolutionaryOptimizerConfig + framework_skills_dirs: list[str] + + +@dataclass(frozen=True, slots=True) +class PreparedOpenShellRun: + """Host catalog plus the only directory uploaded into OpenShell.""" + + root: Path + catalog_root: Path + sandbox_input: Path + manifest_path: Path + + +def _safe_copy_directory(source: Path, destination: Path, *, scratch_root: Path) -> None: + archive = scratch_root / f"copy-{uuid4().hex}.tar.gz" + try: + create_directory_archive(source, archive) + extract_directory_archive(archive, destination) + finally: + archive.unlink(missing_ok=True) + + +def _copy_file(source: Path, destination: Path) -> None: + if not source.is_file(): + raise FileNotFoundError(f"Prepared input file not found: {source}") + if source.is_symlink() or source.stat().st_nlink > 1: + raise ValueError(f"Prepared input file must not be linked: {source}") + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, destination) + + +async def _resolved_insight( + insight: str | None, + *, + destination: Path, + client: Any, + workspace: str, +) -> tuple[str | None, dict[str, Any] | None]: + if insight is None: + return None, None + path = Path(insight).expanduser() + if path.is_file(): + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError) as exc: + raise ValueError(f"Resolved insight is not valid JSON: {path}: {exc}") from exc + else: + if client is None: + raise ValueError(f"Platform insight {insight!r} requires a Platform client during host preparation") + value = await client.insights.insights.get(workspace=workspace, insight_id=insight) + if hasattr(value, "model_dump"): + payload = value.model_dump(mode="json") + elif isinstance(value, dict): + payload = value + else: + raise ValueError(f"Platform returned an unsupported insight representation for {insight!r}") + if not isinstance(payload, dict): + raise ValueError("Resolved insight must be a JSON object") + destination.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") + return destination.name, payload + + +async def _materialize_agent( + source: str, + *, + destination: Path, + host_work: Path, + scratch_root: Path, + clone_depth: int | None, +) -> None: + if looks_like_git(source): + checkout = host_work / "agent-checkout" + provenance = await asyncio.to_thread(clone_agent_repo, source, checkout, clone_depth=clone_depth) + resolved = checkout if provenance.agent_path == "." else checkout / provenance.agent_path + else: + resolved = Path(source).expanduser().resolve() + if not resolved.is_dir(): + raise FileNotFoundError(f"Resolved agent directory not found: {resolved}") + _safe_copy_directory(resolved, destination, scratch_root=scratch_root) + + +async def prepare_openshell_run( + inputs: ResolvedExperimentInputs, + *, + experiment_dir: Path, + client: Any, +) -> PreparedOpenShellRun: + """Create a host-only catalog and credential-free sandbox input snapshot.""" + if inputs.config.storage.archive_candidates or inputs.config.storage.publish_winner: + raise ValueError( + "OpenShell execution does not support source-control archival or winner publishing; " + "disable storage.archive_candidates and storage.publish_winner" + ) + + root = experiment_dir.expanduser().resolve() / "openshell-runtime" + if root.exists(): + raise FileExistsError(f"OpenShell runtime directory already exists: {root}") + catalog_root = root / "host" / "catalog" + host_work = root / "host" / "work" + scratch_root = root / "host" / "scratch" + sandbox_input = root / "input" + scratch_root.mkdir(parents=True) + sandbox_input.mkdir(parents=True) + + try: + dataset_paths: dict[str, str] = {} + for name, reference in ( + ("train", inputs.train_dataset), + ("validation", inputs.validation_dataset), + ): + source = local_path_from_uri(reference.uri, context=f"{name} dataset").resolve() + registered = register_dataset_envelope( + source, + catalog_root=catalog_root, + name=name, + provenance=reference.uri, + ) + target = sandbox_input / "datasets" / name + target.parent.mkdir(parents=True, exist_ok=True) + _safe_copy_directory(registered.dataset_path, target, scratch_root=scratch_root) + dataset_paths[name] = target.relative_to(sandbox_input).as_posix() + + template_relative = None + if inputs.task_template is not None: + template_source = local_path_from_uri( + inputs.task_template.uri, + context="task template", + ).resolve() + template = register_dataset_envelope( + template_source, + catalog_root=catalog_root, + name="task-template", + provenance=inputs.task_template.uri, + ) + target = sandbox_input / "task-template" + _safe_copy_directory(template.dataset_path, target, scratch_root=scratch_root) + template_relative = target.relative_to(sandbox_input).as_posix() + + insight_relative, insight_payload = await _resolved_insight( + inputs.insight, + destination=sandbox_input / "insight.json", + client=client, + workspace=inputs.workspace, + ) + agent_source = inputs.agent + if agent_source is None and insight_payload is not None: + raw_agent = insight_payload.get("agent") + agent_source = raw_agent if isinstance(raw_agent, str) and raw_agent else None + if agent_source is None: + raise ValueError("Host preparation could not resolve an agent from flags, profile, or insight") + agent_target = sandbox_input / "agent" + await _materialize_agent( + agent_source, + destination=agent_target, + host_work=host_work, + scratch_root=scratch_root, + clone_depth=inputs.config.source.clone_depth, + ) + + agent_spec_relative = None + if inputs.agent_spec is not None: + target = sandbox_input / "AGENT-SPEC.md" + _copy_file(Path(inputs.agent_spec).expanduser(), target) + agent_spec_relative = target.relative_to(sandbox_input).as_posix() + + skills_relatives: list[str] = [] + for index, source in enumerate(inputs.framework_skills_dirs, start=1): + target = sandbox_input / "framework-skills" / f"{index:03d}-{source.name}" + target.parent.mkdir(parents=True, exist_ok=True) + _safe_copy_directory(source.expanduser().resolve(), target, scratch_root=scratch_root) + skills_relatives.append(target.relative_to(sandbox_input).as_posix()) + + manifest = SandboxRunManifest( + agent=agent_target.relative_to(sandbox_input).as_posix(), + agent_spec=agent_spec_relative, + insight=insight_relative, + train_dataset=dataset_paths["train"], + validation_dataset=dataset_paths["validation"], + task_template=template_relative, + workspace=inputs.workspace, + config=inputs.config, + framework_skills_dirs=skills_relatives, + ) + manifest_path = sandbox_input / MANIFEST_FILENAME + manifest_path.write_text(manifest.model_dump_json(indent=2) + "\n", encoding="utf-8") + except BaseException: + shutil.rmtree(root, ignore_errors=True) + raise + + return PreparedOpenShellRun( + root=root, + catalog_root=catalog_root, + sandbox_input=sandbox_input, + manifest_path=manifest_path, + ) diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/provider-profiles/nemo-experimentalist-harbor-bridge.yaml b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/provider-profiles/nemo-experimentalist-harbor-bridge.yaml new file mode 100644 index 0000000000..ffab10bf27 --- /dev/null +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/provider-profiles/nemo-experimentalist-harbor-bridge.yaml @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +id: nemo-experimentalist-harbor-bridge +display_name: NeMo Experimentalist Harbor bridge +description: Authenticated bounded Harbor evaluation and dependency sessions +category: other +credentials: + - name: bridge_token + description: Ephemeral bearer token owned by the host launcher + env_vars: [NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_TOKEN] + required: true + auth_style: bearer + header_name: authorization +discovery: + credentials: [bridge_token] +endpoints: + - host: host.openshell.internal + port: 8765 + protocol: rest + enforcement: enforce + rules: + - allow: + method: GET + path: /health/ready + - allow: + method: POST + path: /v1/evaluations + - allow: + method: GET + path: /v1/evaluations/* + - allow: + method: GET + path: /v1/evaluations/*/artifacts + - allow: + method: DELETE + path: /v1/evaluations/* + - allow: + method: POST + path: /v1/dependencies + - allow: + method: POST + path: /v1/dependencies/*/exec + - allow: + method: DELETE + path: /v1/dependencies/* +binaries: + - /app/.venv/bin/python + - /app/.venv/bin/python3.13 + - /usr/local/bin/python3.13 diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/run.sh b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/run.sh new file mode 100755 index 0000000000..9593b230cc --- /dev/null +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/openshell/run.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "usage: $0 PREPARED_INPUT_DIR OUTPUT_DIR" >&2 + exit 2 +fi +if ! command -v openshell >/dev/null 2>&1; then + echo "openshell CLI is required" >&2 + exit 127 +fi + +input_dir="$(cd "$1" && pwd)" +remote_input="/sandbox/input/$(basename "$input_dir")" +output_dir="$2" +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +image="${NEMO_EXPERIMENTALIST_IMAGE:-local/nmp-experimentalist:local}" +sandbox_name="${NEMO_EXPERIMENTALIST_SANDBOX_NAME:-nemo-exp-$$}" +platform_url="${NMP_BASE_URL:-http://host.openshell.internal:8080}" +bridge_url="${NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_URL:-http://host.openshell.internal:8765}" +bridge_provider="${NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_PROVIDER:-nemo-experimentalist-harbor-bridge}" +policy_mode="${NEMO_EXPERIMENTALIST_POLICY_MODE:-strict}" + +if (( ${#sandbox_name} > 19 )); then + echo "OpenShell sandbox names are limited to 19 characters: $sandbox_name" >&2 + exit 2 +fi +case "$policy_mode" in + strict) + policy_path="$script_dir/policy.yaml" + ;; + docker-desktop) + policy_path="$script_dir/policy.docker-desktop.yaml" + echo "WARNING: Docker Desktop mode continues without required Landlock enforcement." >&2 + ;; + *) + echo "NEMO_EXPERIMENTALIST_POLICY_MODE must be strict or docker-desktop" >&2 + exit 2 + ;; +esac + +cleanup() { + if [[ "${NEMO_EXPERIMENTALIST_KEEP_SANDBOX:-0}" != "1" ]]; then + openshell sandbox delete "$sandbox_name" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT + +create_args=( + sandbox create + --name "$sandbox_name" + --from "$image" + --policy "$policy_path" + --no-auto-providers + --provider "$bridge_provider" + --upload "$input_dir:/sandbox/input" + --env "NMP_BASE_URL=$platform_url" + --env "NEMO_EXPERIMENTALIST_OPEN_SHELL_RUNTIME=1" + --env "NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_URL=$bridge_url" + --env "EXPERIMENTALIST_API_BASE=https://inference.local/v1" + --env "EXPERIMENTALIST_API_KEY=openshell-managed" +) +for name in \ + EXPERIMENTALIST_SMART_MODEL_NAME \ + EXPERIMENTALIST_MID_MODEL_NAME \ + EXPERIMENTALIST_FAST_MODEL_NAME; do + if [[ -n "${!name:-}" ]]; then + create_args+=(--env "$name=${!name}") + fi +done + +openshell "${create_args[@]}" -- /bin/true + +openshell sandbox exec --name "$sandbox_name" -- \ + /bin/bash -c ' + set -euo pipefail + test "$NEMO_EXPERIMENTALIST_OPEN_SHELL_RUNTIME" = 1 + if command -v docker >/dev/null 2>&1; then + echo "Experimentalist runtime unexpectedly contains Docker" >&2 + exit 1 + fi + if [[ -e /var/run/docker.sock || -e /run/docker.sock ]]; then + echo "Experimentalist runtime unexpectedly exposes a Docker socket" >&2 + exit 1 + fi + if [[ -z "${NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_TOKEN:-}" ]]; then + echo "OpenShell did not inject the Harbor bridge credential placeholder" >&2 + exit 1 + fi + ' + +openshell sandbox exec --name "$sandbox_name" --workdir "$remote_input" -- \ + /app/.venv/bin/python -m nemo_experimentalist_plugin.openshell.inner \ + --manifest "$remote_input/run.json" \ + --output /sandbox/output + +mkdir -p "$output_dir" +openshell sandbox download "$sandbox_name" /sandbox/output "$output_dir" diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/preflight.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/preflight.py index d47b23fbec..b9210ee749 100644 --- a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/preflight.py +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/preflight.py @@ -115,12 +115,13 @@ def check_environment( insight_id: str | None = None, base_url: str, enforce_insight_agent: bool = True, + openshell: bool = False, probes: Probes | None = None, ) -> list[CheckResult]: """Environment checks for Experiment and Doctor. - Checks the experimentalist credentials and model endpoint, platform - reachability, an optional insight file, Docker, and Harbor. + Checks OpenShell or direct-run credentials, platform reachability, an + optional insight file, Docker, and Harbor. ``enforce_insight_agent=False`` skips the local-insight agent match (an explicit ``--agent`` overrides the insight's agent). @@ -128,25 +129,40 @@ def check_environment( p = probes or Probes() results: list[CheckResult] = [] profile_dir = profile.profile_dir if profile is not None else None - results += _check_env( - p, "credentials-experiment", ("EXPERIMENTALIST_API_BASE", "EXPERIMENTALIST_API_KEY"), profile_dir - ) - base = p.env.get("EXPERIMENTALIST_API_BASE") - if base: - model_url = f"{base.rstrip('/')}/models" - ok = p.http_ok(model_url) - display_model_url = f"{_redact_url(base).rstrip('/')}/models" + if openshell: + code, _ = p.run_cmd(["openshell", "--version"]) results.append( make_check_result( - "model-endpoint", - "credentials-experiment", - ok, - "advisory", - f"{display_model_url} reachable", - "model endpoint unreachable", - hint="check EXPERIMENTALIST_API_BASE and network", + "openshell", + "runtime", + code == 0, + "required", + "OpenShell CLI available", + "OpenShell CLI unavailable", + hint="install and configure OpenShell before running Experimentalist", ) ) + results += _check_env(p, "credentials-candidate", ("INFERENCE_API_KEY", "AUT_MODEL_NAME"), profile_dir) + else: + results += _check_env( + p, "credentials-experiment", ("EXPERIMENTALIST_API_BASE", "EXPERIMENTALIST_API_KEY"), profile_dir + ) + base = p.env.get("EXPERIMENTALIST_API_BASE") + if base: + model_url = f"{base.rstrip('/')}/models" + ok = p.http_ok(model_url) + display_model_url = f"{_redact_url(base).rstrip('/')}/models" + results.append( + make_check_result( + "model-endpoint", + "credentials-experiment", + ok, + "advisory", + f"{display_model_url} reachable", + "model endpoint unreachable", + hint="check EXPERIMENTALIST_API_BASE and network", + ) + ) ok = p.http_ok(f"{base_url.rstrip('/')}/health/ready") display_base_url = _redact_url(base_url) results.append( diff --git a/plugins/nemo-experimentalist/tests/experimentalist/test_dataset_staging_loop.py b/plugins/nemo-experimentalist/tests/experimentalist/test_dataset_staging_loop.py index a03990f009..472af53d45 100644 --- a/plugins/nemo-experimentalist/tests/experimentalist/test_dataset_staging_loop.py +++ b/plugins/nemo-experimentalist/tests/experimentalist/test_dataset_staging_loop.py @@ -64,7 +64,9 @@ async def run( monkeypatch.setattr( loop_module, "EvaluatorFactory", - lambda: SimpleNamespace(build_evaluator=lambda *args, **kwargs: object()), + lambda: SimpleNamespace( + build_evaluator=lambda *args, **kwargs: SimpleNamespace(prepare_dataset=lambda dataset: dataset) + ), ) monkeypatch.setattr(loop_module, "DatasetFactory", RecordingDatasetFactory) monkeypatch.setattr(loop_module, "EvalAuthor", MutatingEvalAuthor) diff --git a/plugins/nemo-experimentalist/tests/experimentalist/test_loop_failure.py b/plugins/nemo-experimentalist/tests/experimentalist/test_loop_failure.py index b5f17225eb..2274f68ce2 100644 --- a/plugins/nemo-experimentalist/tests/experimentalist/test_loop_failure.py +++ b/plugins/nemo-experimentalist/tests/experimentalist/test_loop_failure.py @@ -35,7 +35,9 @@ async def test_baseline_failure_marks_run_failed(monkeypatch, tmp_path, failure_ monkeypatch.setattr( loop_module, "EvaluatorFactory", - lambda: SimpleNamespace(build_evaluator=lambda *args, **kwargs: object()), + lambda: SimpleNamespace( + build_evaluator=lambda *args, **kwargs: SimpleNamespace(prepare_dataset=lambda dataset: dataset) + ), ) monkeypatch.setattr( loop_module, diff --git a/plugins/nemo-experimentalist/tests/experimentalist/test_loop_insight_suite.py b/plugins/nemo-experimentalist/tests/experimentalist/test_loop_insight_suite.py index e959de65be..020a59deef 100644 --- a/plugins/nemo-experimentalist/tests/experimentalist/test_loop_insight_suite.py +++ b/plugins/nemo-experimentalist/tests/experimentalist/test_loop_insight_suite.py @@ -173,7 +173,9 @@ async def run(self, **kwargs: object) -> SimpleNamespace: monkeypatch.setattr( loop_module, "EvaluatorFactory", - lambda: SimpleNamespace(build_evaluator=lambda *args, **kwargs: object()), + lambda: SimpleNamespace( + build_evaluator=lambda *args, **kwargs: SimpleNamespace(prepare_dataset=lambda dataset: dataset) + ), ) monkeypatch.setattr(loop_module, "EvalAuthor", ReturningEvalAuthor) monkeypatch.setattr( diff --git a/plugins/nemo-experimentalist/tests/test_cli_profile.py b/plugins/nemo-experimentalist/tests/test_cli_profile.py index 5f25fc7b78..fc7b5d425f 100644 --- a/plugins/nemo-experimentalist/tests/test_cli_profile.py +++ b/plugins/nemo-experimentalist/tests/test_cli_profile.py @@ -26,6 +26,7 @@ def quiet_probes(env: dict | None = None) -> Probes: "EXPERIMENTALIST_API_BASE": "http://llm", "EXPERIMENTALIST_API_KEY": "k", "INFERENCE_API_KEY": "k", + "AUT_MODEL_NAME": "fixture-model", }, ) @@ -35,10 +36,13 @@ def write_task_toml(profile_tree: Path) -> None: @pytest.fixture(autouse=True) -def _quiet_preflight(monkeypatch): +def _quiet_preflight(monkeypatch, tmp_path: Path): """Deterministic probes for every test; tests override with their own monkeypatch.setattr when they exercise specific probe behavior.""" monkeypatch.setattr(cli, "_PREFLIGHT_PROBES", quiet_probes()) + marker = tmp_path / "nemo-experimentalist-container" + marker.touch() + monkeypatch.setattr(cli, "_CONTAINER_MARKER", marker) @dataclass diff --git a/plugins/nemo-experimentalist/tests/test_experiment_cli.py b/plugins/nemo-experimentalist/tests/test_experiment_cli.py index f4460ae4a9..91c46ba924 100644 --- a/plugins/nemo-experimentalist/tests/test_experiment_cli.py +++ b/plugins/nemo-experimentalist/tests/test_experiment_cli.py @@ -16,7 +16,7 @@ @pytest.fixture(autouse=True) -def quiet_preflight(monkeypatch: pytest.MonkeyPatch) -> None: +def quiet_preflight(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: """Auto-preflight runs inside the experiment flow; make it pass deterministically. These tests pin the CLI→runner contract, not preflight behavior (that lives @@ -28,9 +28,17 @@ def quiet_preflight(monkeypatch: pytest.MonkeyPatch) -> None: Probes( run_cmd=lambda argv: (0, "ok"), http_ok=lambda url: True, - env={"EXPERIMENTALIST_API_BASE": "http://llm", "EXPERIMENTALIST_API_KEY": "k"}, + env={ + "EXPERIMENTALIST_API_BASE": "http://llm", + "EXPERIMENTALIST_API_KEY": "k", + "INFERENCE_API_KEY": "k", + "AUT_MODEL_NAME": "fixture-model", + }, ), ) + marker = tmp_path / "nemo-experimentalist-container" + marker.touch() + monkeypatch.setattr(cli, "_CONTAINER_MARKER", marker) @pytest.fixture(autouse=True) @@ -328,3 +336,68 @@ def test_experiment_cli_forwards_insight_platform_id_verbatim( assert result.exit_code == 0 assert runner.captured is not None assert runner.captured.insight == "insight-remote-123" + + +def test_public_run_uses_openshell_without_local_fallback( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + paths = _make_paths(tmp_path) + prepared = object() + captured: dict[str, object] = {} + + async def prepare(inputs, **kwargs): + captured["inputs"] = inputs + captured["prepare_kwargs"] = kwargs + return prepared + + def launch(value, **kwargs): + captured["prepared"] = value + captured["launch_kwargs"] = kwargs + return "sandbox-summary" + + async def local_runner(**kwargs): + raise AssertionError(f"host must not run Experimentalist: {kwargs}") + + monkeypatch.setattr(cli, "_CONTAINER_MARKER", tmp_path / "outside-container") + monkeypatch.setattr(cli, "_OPEN_SHELL_PREPARER", prepare) + monkeypatch.setattr(cli, "_OPEN_SHELL_LAUNCHER", launch) + monkeypatch.setattr(cli, "run_experimentalist", local_runner) + + result = _run_experiment(paths.args()) + + assert result.exit_code == 0, result.output + assert result.output.strip() == "sandbox-summary" + assert captured["prepared"] is prepared + assert captured["launch_kwargs"] == { + "experiment_dir": paths.experiment, + "platform_url": "http://localhost:8080", + } + + +def test_public_run_fails_when_openshell_is_unavailable_without_local_fallback( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + paths = _make_paths(tmp_path) + + async def prepare(*args, **kwargs): + del args, kwargs + return object() + + def fail_launch(*args, **kwargs): + del args, kwargs + raise cli.OpenShellLaunchError("OpenShell is unavailable") + + async def local_runner(**kwargs): + raise AssertionError(f"host must not run Experimentalist: {kwargs}") + + monkeypatch.setattr(cli, "_CONTAINER_MARKER", tmp_path / "outside-container") + monkeypatch.setattr(cli, "_OPEN_SHELL_PREPARER", prepare) + monkeypatch.setattr(cli, "_OPEN_SHELL_LAUNCHER", fail_launch) + monkeypatch.setattr(cli, "run_experimentalist", local_runner) + + result = _run_experiment(paths.args()) + + assert result.exit_code == 1 + assert "OpenShell is unavailable" in result.output diff --git a/plugins/nemo-experimentalist/tests/test_harbor_bridge_contracts.py b/plugins/nemo-experimentalist/tests/test_harbor_bridge_contracts.py new file mode 100644 index 0000000000..18ff5daa64 --- /dev/null +++ b/plugins/nemo-experimentalist/tests/test_harbor_bridge_contracts.py @@ -0,0 +1,234 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Security contract tests that do not require Docker or OpenShell.""" + +from __future__ import annotations + +import io +import os +import shutil +import tarfile +from pathlib import Path +from typing import cast + +import pytest +from nemo_experimentalist_plugin.harbor_bridge.archives import ( + create_directory_archive, + extract_directory_archive, +) +from nemo_experimentalist_plugin.harbor_bridge.contracts import ( + EnvelopeTask, + EvaluationSubmission, +) +from nemo_experimentalist_plugin.harbor_bridge.envelopes import ( + ENVELOPE_DESCRIPTOR_FILENAME, + TrustedEnvelopeCatalog, + create_overlay_directory, + register_dataset_envelope, + resolve_envelope_task, + tree_digest, +) +from pydantic import ValidationError + +_DIGEST = f"sha256:{'0' * 64}" + + +def _metadata() -> dict[str, object]: + return { + "schema_version": 1, + "request_id": "candidate-004-validation", + "envelope": { + "id": "fixture-0123456789abcdef", + "digest": _DIGEST, + "tasks": [{"task_id": "generated-trace-004", "base_task_id": "base-task"}], + }, + "candidate": {"digest": _DIGEST}, + "overlay": {"digest": _DIGEST}, + "run_profile": "smoke", + } + + +@pytest.mark.parametrize( + "field,value", + [ + ("image", "attacker/image:latest"), + ("mounts", ["/Users/ryan:/host"]), + ("env", {"NVIDIA_API_KEY": "something-else"}), + ("agent_import_path", "candidate.module:Agent"), + ("verifier_mode", "shared"), + ("docker", {"privileged": True}), + ], +) +def test_submission_rejects_unknown_authority(field: str, value: object) -> None: + payload = _metadata() + payload[field] = value + with pytest.raises(ValidationError, match="Extra inputs are not permitted"): + EvaluationSubmission.model_validate(payload) + + +def test_submission_rejects_nested_unknown_fields() -> None: + payload = _metadata() + envelope = cast(dict[str, object], payload["envelope"]) + envelope["image"] = "attacker/image:latest" + with pytest.raises(ValidationError, match="Extra inputs are not permitted"): + EvaluationSubmission.model_validate(payload) + + +def _write_tar(path: Path, members: list[tarfile.TarInfo]) -> None: + with tarfile.open(path, "w:gz") as archive: + for member in members: + content = io.BytesIO(b"x") + if member.isfile(): + member.size = 1 + archive.addfile(member, content) + else: + archive.addfile(member) + + +@pytest.mark.parametrize( + "member", + [ + tarfile.TarInfo("../escape"), + tarfile.TarInfo("/absolute"), + tarfile.TarInfo("safe/../escape"), + tarfile.TarInfo("safe-link"), + tarfile.TarInfo("hard-link"), + ], +) +def test_archive_rejects_traversal_and_links(tmp_path: Path, member: tarfile.TarInfo) -> None: + if member.name == "safe-link": + member.type = tarfile.SYMTYPE + member.linkname = "target" + elif member.name == "hard-link": + member.type = tarfile.LNKTYPE + member.linkname = "target" + archive = tmp_path / "input.tar.gz" + _write_tar(archive, [member]) + with pytest.raises(ValueError, match="Unsafe archive|Unsupported archive"): + extract_directory_archive(archive, tmp_path / "output") + assert not (tmp_path / "output").exists() + + +def test_archive_rejects_duplicate_paths(tmp_path: Path) -> None: + archive = tmp_path / "input.tar.gz" + _write_tar(archive, [tarfile.TarInfo("same"), tarfile.TarInfo("same")]) + with pytest.raises(ValueError, match="duplicate"): + extract_directory_archive(archive, tmp_path / "output") + + +def test_archive_source_rejects_hard_links(tmp_path: Path) -> None: + source = tmp_path / "source" + source.mkdir() + first = source / "first" + first.write_text("content", encoding="utf-8") + os.link(first, source / "second") + with pytest.raises(ValueError, match="hard-linked"): + create_directory_archive(source, tmp_path / "output.tar.gz") + + +def _task_dataset(tmp_path: Path) -> Path: + dataset = tmp_path / "source" + task = dataset / "base-task" + (task / "environment").mkdir(parents=True) + (task / "tests").mkdir() + (task / "task.toml").write_text( + """ +[task] +name = "fixture/base-task" + +[environment] +type = "docker" + +[verifier] +""".lstrip(), + encoding="utf-8", + ) + (task / "environment" / "Dockerfile").write_text("FROM python:3.12-slim\n", encoding="utf-8") + (task / "instruction.md").write_text("trusted instruction\n", encoding="utf-8") + (task / "data.json").write_text("{}\n", encoding="utf-8") + (task / "tests" / "test.sh").write_text("#!/bin/sh\n", encoding="utf-8") + (task / "nemo-task-envelope.json").write_text( + """ +{ + "schema_version": 1, + "task_data": [ + {"path": "instruction.md", "media_type": "text/plain", "max_bytes": 65536}, + {"path": "data.json", "media_type": "application/json", "max_bytes": 1024} + ], + "verifier_paths": ["tests"] +} +""".lstrip(), + encoding="utf-8", + ) + return dataset + + +def test_catalog_materializes_only_declared_overlays(tmp_path: Path) -> None: + source = _task_dataset(tmp_path) + registered = register_dataset_envelope(source, catalog_root=tmp_path / "catalog", name="fixture") + working = tmp_path / "working" + shutil.copytree(registered.dataset_path, working) + task = working / "base-task" + (task / "instruction.md").write_text("generated instruction\n", encoding="utf-8") + (task / "task.toml").write_text('[task]\nname = "attacker/replacement"\n', encoding="utf-8") + + binding = resolve_envelope_task(working, task, task_id="generated-task") + overlay = tmp_path / "overlay" + overlay_digest = create_overlay_directory([binding], overlay) + assert overlay_digest is not None + assert (overlay / "generated-task" / "instruction.md").read_text() == "generated instruction\n" + assert not (overlay / "generated-task" / "task.toml").exists() + + materialized = tmp_path / "materialized" + TrustedEnvelopeCatalog(tmp_path / "catalog").materialize( + envelope_id=registered.manifest.envelope_id, + envelope_digest=registered.manifest.envelope_digest, + selections=[EnvelopeTask(task_id="generated-task", base_task_id="base-task")], + destination=materialized, + overlay_dir=overlay, + ) + generated = materialized / "generated-task" + assert (generated / "instruction.md").read_text() == "generated instruction\n" + assert "fixture/base-task__generated-task" in (generated / "task.toml").read_text() + assert not (generated / ENVELOPE_DESCRIPTOR_FILENAME).exists() + + +@pytest.mark.parametrize("path", ["task.toml", "Dockerfile", ".dockerignore", "compose.yaml"]) +def test_catalog_rejects_runtime_control_overlays(tmp_path: Path, path: str) -> None: + source = _task_dataset(tmp_path) + registered = register_dataset_envelope(source, catalog_root=tmp_path / "catalog", name="fixture") + overlay = tmp_path / "overlay" / "generated-task" + overlay.mkdir(parents=True) + (overlay / path).write_text("attacker-controlled\n", encoding="utf-8") + + with pytest.raises(ValueError, match="runtime-control|undeclared"): + TrustedEnvelopeCatalog(tmp_path / "catalog").materialize( + envelope_id=registered.manifest.envelope_id, + envelope_digest=registered.manifest.envelope_digest, + selections=[EnvelopeTask(task_id="generated-task", base_task_id="base-task")], + destination=tmp_path / "materialized", + overlay_dir=tmp_path / "overlay", + ) + + +def test_catalog_detects_tampering_before_materialization(tmp_path: Path) -> None: + source = _task_dataset(tmp_path) + registered = register_dataset_envelope(source, catalog_root=tmp_path / "catalog", name="fixture") + (registered.dataset_path / "base-task" / "task.toml").write_text("[task]\nname='tampered/task'\n") + with pytest.raises(ValueError, match="changed after registration"): + TrustedEnvelopeCatalog(tmp_path / "catalog").load( + registered.manifest.envelope_id, + registered.manifest.envelope_digest, + ) + + +def test_digest_covers_file_modes(tmp_path: Path) -> None: + root = tmp_path / "root" + root.mkdir() + path = root / "script" + path.write_text("#!/bin/sh\n", encoding="utf-8") + path.chmod(0o644) + before = tree_digest(root) + path.chmod(0o755) + assert tree_digest(root) != before diff --git a/plugins/nemo-experimentalist/tests/test_harbor_bridge_runner.py b/plugins/nemo-experimentalist/tests/test_harbor_bridge_runner.py new file mode 100644 index 0000000000..3db5ade026 --- /dev/null +++ b/plugins/nemo-experimentalist/tests/test_harbor_bridge_runner.py @@ -0,0 +1,248 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Trusted adapter and Harbor job-construction tests.""" + +from __future__ import annotations + +import importlib +from pathlib import Path +from types import SimpleNamespace +from typing import Any + +import pytest +from harbor.job import JobConfig +from harbor.models.agent.context import AgentContext +from nemo_experimentalist_plugin.experimentalist.components.evaluator import harbor +from nemo_experimentalist_plugin.experimentalist.components.evaluator.harbor import ( + HarborDataset, + HarborEvaluator, + HarborEvaluatorConfig, +) +from nemo_experimentalist_plugin.experimentalist.components.evaluator.models import ( + DatasetRef, + EvaluationResult, +) +from nemo_experimentalist_plugin.harbor_bridge.contracts import ( + ArchiveReference, + EnvelopeTask, + EvaluationEnvelope, + EvaluationSubmission, + RunProfile, +) +from nemo_experimentalist_plugin.harbor_bridge.runner import ( + HarborBridgeRunner, + TrustedInferenceConfig, +) +from nemo_experimentalist_plugin.harbor_bridge.trusted_agent import ( + CANDIDATE_RUN_TIMEOUT_SEC, + TrustedCandidateAgent, + candidate_agent_import, +) + + +def _candidate(tmp_path: Path) -> Path: + candidate = tmp_path / "candidate" + candidate.mkdir() + (candidate / "main.py").write_text( + "raise AssertionError('candidate was imported on the host')\n", + encoding="utf-8", + ) + (candidate / "pyproject.toml").write_text( + '[project]\nname = "candidate"\nversion = "0.0.0"\nrequires-python = ">=3.12"\n', + encoding="utf-8", + ) + (candidate / "uv.lock").write_text('version = 1\nrevision = 3\nrequires-python = ">=3.12"\n') + return candidate + + +def _dataset(tmp_path: Path) -> Path: + dataset = tmp_path / "dataset" + task = dataset / "generated-task" + (task / "environment").mkdir(parents=True) + (task / "task.toml").write_text( + """ +[task] +name = "fixture/generated-task" + +[environment] +type = "docker" + +[verifier] +""".lstrip(), + encoding="utf-8", + ) + (task / "environment" / "Dockerfile").write_text("FROM python:3.12-slim\n", encoding="utf-8") + return dataset + + +def _submission() -> EvaluationSubmission: + digest = f"sha256:{'0' * 64}" + return EvaluationSubmission( + request_id="candidate-001", + envelope=EvaluationEnvelope( + id="fixture-envelope", + digest=digest, + tasks=[EnvelopeTask(task_id="generated-task", base_task_id="base-task")], + ), + candidate=ArchiveReference(digest=digest), + run_profile="smoke", + ) + + +def _profile() -> RunProfile: + return RunProfile( + attempts=1, + concurrency=1, + retries=0, + agent_timeout_multiplier=0.25, + verifier_timeout_multiplier=0.25, + setup_timeout_multiplier=0.5, + build_timeout_multiplier=0.5, + ) + + +def test_candidate_adapter_import_never_executes_candidate_python(tmp_path: Path) -> None: + candidate = _candidate(tmp_path) + with candidate_agent_import(candidate) as import_path: + module_name, attribute = import_path.split(":", 1) + module = importlib.import_module(module_name) + adapter = getattr(module, attribute) + assert issubclass(adapter, TrustedCandidateAgent) + assert adapter.candidate_dir == candidate + + +def test_candidate_adapter_rejects_links(tmp_path: Path) -> None: + candidate = _candidate(tmp_path) + (candidate / "link.py").symlink_to(candidate / "main.py") + with pytest.raises(RuntimeError, match="symbolic link"): + with candidate_agent_import(candidate): + pass + + +async def test_candidate_execution_has_finite_environment_timeout( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + logs_dir = tmp_path / "logs" + logs_dir.mkdir() + agent = TrustedCandidateAgent( + logs_dir=logs_dir, + extra_env={"INFERENCE_API_KEY": "dedicated-key"}, + ) + captured: dict[str, Any] = {} + + class Environment: + async def upload_file(self, source: Path, target: str) -> None: + captured["upload"] = (source, target) + + async def execute(_environment: object, **kwargs: Any) -> SimpleNamespace: + captured.update(kwargs) + return SimpleNamespace(return_code=0) + + monkeypatch.setattr(agent, "exec_as_agent", execute) + + await agent.run("trusted prompt", Environment(), AgentContext()) + + assert captured["timeout_sec"] == CANDIDATE_RUN_TIMEOUT_SEC + + +class _FakeEvaluator: + config: HarborEvaluatorConfig | None = None + + def __init__(self, options: HarborEvaluatorConfig, experiment_dir: Path) -> None: + self.__class__.config = options + self.experiment_dir = experiment_dir + + async def run(self, candidate_dir: Path, dataset: HarborDataset) -> EvaluationResult: + assert candidate_dir.is_dir() + assert dataset.get_task("generated-task") + return EvaluationResult(id="result") + + +async def test_runner_uses_only_trusted_adapter_and_preserves_verifier_mode( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + candidate = _candidate(tmp_path) + dataset = _dataset(tmp_path) + task_toml = dataset / "generated-task" / "task.toml" + before = task_toml.read_text(encoding="utf-8") + monkeypatch.setattr( + "nemo_experimentalist_plugin.harbor_bridge.runner.HarborEvaluator", + _FakeEvaluator, + ) + runner = HarborBridgeRunner( + TrustedInferenceConfig.model_validate( + { + "api_key": "dedicated-key", + "api_base": "https://inference.example.test/v1", + "model_name": "fixture-model", + } + ) + ) + result = await runner.run( + submission=_submission(), + profile=_profile(), + candidate_dir=candidate, + dataset_dir=dataset, + work_dir=tmp_path / "work", + ) + assert result.id == "result" + assert task_toml.read_text(encoding="utf-8") == before + config = _FakeEvaluator.config + assert config is not None + assert config.scope_import_path is False + assert config.import_path.startswith("nemo_experimentalist_plugin.harbor_bridge._candidate_") + assert config.agent_env == { + "INFERENCE_API_KEY": "dedicated-key", + "INFERENCE_API_BASE": "https://inference.example.test/v1", + "AUT_MODEL_NAME": "fixture-model", + } + + +class _FakeJob: + def __init__(self, config: JobConfig, job_dir: Path) -> None: + self.config = config + self.job_dir = job_dir + + async def run(self) -> None: + self.job_dir.mkdir(parents=True) + + +async def test_harbor_evaluator_does_not_scope_bridge_owned_import( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + candidate = _candidate(tmp_path) + dataset_path = _dataset(tmp_path) + dataset = HarborDataset.from_ref(DatasetRef(uri=dataset_path.as_uri())) + captured: dict[str, Any] = {} + + async def create_job(config: JobConfig) -> _FakeJob: + captured["config"] = config + return _FakeJob(config, tmp_path / "results") + + def fail_scoping(*_args: object, **_kwargs: object) -> None: + raise AssertionError("bridge-owned import path must not be scoped through candidate files") + + monkeypatch.setattr(harbor.Job, "create", create_job) + monkeypatch.setattr(harbor, "_scoped_import_path", fail_scoping) + evaluator = HarborEvaluator( + HarborEvaluatorConfig( + import_path="trusted.module:Adapter", + scope_import_path=False, + agent_model_name="fixture-model", + agent_env={"INFERENCE_API_KEY": "dedicated-key"}, + jobs_dir=Path("results"), + ), + experiment_dir=tmp_path, + ) + result = await evaluator.run(candidate, dataset) + assert result.trials == [] + config = captured["config"] + assert isinstance(config, JobConfig) + assert config.agents[0].import_path == "trusted.module:Adapter" + assert config.agents[0].model_name == "fixture-model" + assert config.agents[0].env == {"INFERENCE_API_KEY": "dedicated-key"} + assert config.environment.env == {} diff --git a/plugins/nemo-experimentalist/tests/test_harbor_bridge_service.py b/plugins/nemo-experimentalist/tests/test_harbor_bridge_service.py new file mode 100644 index 0000000000..d0e3b28ddb --- /dev/null +++ b/plugins/nemo-experimentalist/tests/test_harbor_bridge_service.py @@ -0,0 +1,340 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Job API tests with a deterministic runner and no Docker.""" + +from __future__ import annotations + +import subprocess +import sys +import time +from pathlib import Path +from typing import Any + +import pytest +from fastapi.testclient import TestClient +from nemo_experimentalist_plugin.experimentalist.components.evaluator.models import ( + EvaluationResult, + ResourceRef, + TrialResult, +) +from nemo_experimentalist_plugin.harbor_bridge.archives import ( + create_directory_archive, + extract_directory_archive, +) +from nemo_experimentalist_plugin.harbor_bridge.contracts import ( + ArchiveReference, + EnvelopeTask, + EvaluationEnvelope, + EvaluationSubmission, +) +from nemo_experimentalist_plugin.harbor_bridge.envelopes import ( + RegisteredEnvelope, + register_dataset_envelope, + transport_tree_digest, +) +from nemo_experimentalist_plugin.harbor_bridge.service import ( + HarborBridgeSettings, + RunProfile, + create_app, +) + +_TOKEN = "bridge-token-long-enough" + + +def test_bridge_module_invokes_cli_entrypoint() -> None: + result = subprocess.run( + [sys.executable, "-m", "nemo_experimentalist_plugin.harbor_bridge.service", "--help"], + check=False, + capture_output=True, + text=True, + ) + + assert result.returncode == 0 + assert "--storage-root" in result.stdout + assert "--catalog-root" in result.stdout + + +def _source_dataset(tmp_path: Path) -> RegisteredEnvelope: + dataset = tmp_path / "source" + task = dataset / "base-task" + (task / "environment").mkdir(parents=True) + (task / "task.toml").write_text( + '[task]\nname = "fixture/base-task"\n[environment]\ntype = "docker"\n[verifier]\n', + encoding="utf-8", + ) + (task / "environment" / "Dockerfile").write_text("FROM python:3.12-slim\n", encoding="utf-8") + return register_dataset_envelope(dataset, catalog_root=tmp_path / "catalog", name="fixture") + + +def _request_parts(tmp_path: Path, registered) -> tuple[dict[str, str], dict[str, tuple[str, bytes, str]]]: + candidate = tmp_path / "candidate" + candidate.mkdir() + (candidate / "main.py").write_text("print('answer')\n", encoding="utf-8") + archive = tmp_path / "candidate.tar.gz" + create_directory_archive(candidate, archive) + metadata = EvaluationSubmission( + request_id="candidate-001", + envelope=EvaluationEnvelope( + id=registered.manifest.envelope_id, + digest=registered.manifest.envelope_digest, + tasks=[EnvelopeTask(task_id="trial-task", base_task_id="base-task")], + ), + candidate=ArchiveReference(digest=transport_tree_digest(candidate)), + run_profile="smoke", + ) + return ( + {"metadata": metadata.model_dump_json()}, + {"candidate": ("candidate.tar.gz", archive.read_bytes(), "application/gzip")}, + ) + + +def _wait_terminal(client: TestClient, job_id: str) -> dict[str, Any]: + deadline = time.monotonic() + 3 + while time.monotonic() < deadline: + response = client.get( + f"/v1/evaluations/{job_id}", + headers={"Authorization": f"Bearer {_TOKEN}"}, + ) + payload = response.json() + if payload["state"] not in ("pending", "running"): + return payload + time.sleep(0.01) + raise AssertionError("bridge job did not finish") + + +class RecordingRunner: + def __init__(self, *, fail_with: str | None = None) -> None: + self.fail_with = fail_with + self.profile: RunProfile | None = None + self.candidate_dir: Path | None = None + self.dataset_dir: Path | None = None + + async def run(self, *, submission, profile, candidate_dir, dataset_dir, work_dir) -> EvaluationResult: + del submission + self.profile = profile + self.candidate_dir = candidate_dir + self.dataset_dir = dataset_dir + if self.fail_with is not None: + raise RuntimeError(self.fail_with) + return EvaluationResult( + id="result", + metadata={ + "work_dir": str(work_dir), + "token": _TOKEN, + }, + trials=[], + ) + + +class ArtifactRunner: + async def run(self, *, submission, profile, candidate_dir, dataset_dir, work_dir) -> EvaluationResult: + del submission, profile, candidate_dir, dataset_dir + trace = work_dir / "results" / "trace.jsonl" + trace.parent.mkdir() + trace.write_text('{"resourceSpans":[]}\n', encoding="utf-8") + return EvaluationResult( + id="artifact-result", + trials=[ + TrialResult( + id="trial-task__0", + task_id="trial-task", + attempt=0, + status="completed", + trace=ResourceRef(uri=trace.as_uri(), description="trace"), + outputs={ + "host_path": str(work_dir / "results"), + "token": _TOKEN, + }, + resources={"outside": ResourceRef(uri=Path("/etc/hosts").as_uri(), description="must not escape")}, + ) + ], + ) + + +def _client(tmp_path: Path, runner: RecordingRunner) -> TestClient: + app = create_app( + settings=HarborBridgeSettings( + storage_root=tmp_path / "jobs", + catalog_root=tmp_path / "catalog", + token=_TOKEN, + sensitive_values=(_TOKEN,), + ), + runner=runner, + ) + return TestClient(app) + + +def test_job_api_maps_profile_server_side_and_sanitizes_metadata(tmp_path: Path) -> None: + registered = _source_dataset(tmp_path) + data, files = _request_parts(tmp_path, registered) + runner = RecordingRunner() + with _client(tmp_path, runner) as client: + response = client.post( + "/v1/evaluations", + data=data, + files=files, + headers={"Authorization": f"Bearer {_TOKEN}"}, + ) + assert response.status_code == 202 + assert response.json()["state"] == "pending" + payload = _wait_terminal(client, response.json()["job_id"]) + + assert payload["state"] == "completed" + assert payload["result"]["metadata"] == { + "work_dir": "[HOST_PATH_REDACTED]", + "token": "[REDACTED]", + } + assert runner.profile is not None + assert runner.profile.attempts == 1 + assert runner.profile.concurrency == 1 + assert runner.candidate_dir is not None and (runner.candidate_dir / "main.py").is_file() + assert runner.dataset_dir is not None and (runner.dataset_dir / "trial-task" / "task.toml").is_file() + + +def test_job_api_exports_only_job_owned_artifacts(tmp_path: Path) -> None: + registered = _source_dataset(tmp_path) + data, files = _request_parts(tmp_path, registered) + app = create_app( + settings=HarborBridgeSettings( + storage_root=tmp_path / "jobs", + catalog_root=tmp_path / "catalog", + token=_TOKEN, + sensitive_values=(_TOKEN,), + ), + runner=ArtifactRunner(), + ) + auth = {"Authorization": f"Bearer {_TOKEN}"} + with TestClient(app) as client: + response = client.post("/v1/evaluations", data=data, files=files, headers=auth) + payload = _wait_terminal(client, response.json()["job_id"]) + artifact_response = client.get( + f"/v1/evaluations/{response.json()['job_id']}/artifacts", + headers=auth, + ) + + trial = payload["result"]["trials"][0] + assert trial["trace"]["uri"].startswith("nemo-harbor-bridge:///artifacts/") + assert trial["resources"]["outside"]["uri"].startswith("nemo-harbor-bridge:///unavailable/") + assert trial["outputs"] == { + "host_path": "[HOST_PATH_REDACTED]", + "token": "[REDACTED]", + } + assert str(tmp_path) not in str(payload) + assert artifact_response.status_code == 200 + assert artifact_response.headers["X-Nemo-Artifact-Digest"].startswith("sha256:") + archive = tmp_path / "downloaded-artifacts.tar.gz" + archive.write_bytes(artifact_response.content) + extracted = tmp_path / "downloaded-artifacts" + extract_directory_archive(archive, extracted) + assert next(extracted.rglob("trace.jsonl")).read_text(encoding="utf-8") == '{"resourceSpans":[]}\n' + + +@pytest.mark.parametrize( + "field,value", + [ + ("image", "attacker/image:latest"), + ("mounts", ["/Users/ryan:/host"]), + ("env", {"NVIDIA_API_KEY": "secret"}), + ("agent_import_path", "candidate.module:Agent"), + ("verifier_mode", "shared"), + ("docker", {"privileged": True}), + ], +) +def test_job_api_returns_422_for_unknown_authority(tmp_path: Path, field: str, value: object) -> None: + registered = _source_dataset(tmp_path) + data, files = _request_parts(tmp_path, registered) + metadata = EvaluationSubmission.model_validate_json(data["metadata"]).model_dump() + metadata[field] = value + with _client(tmp_path, RecordingRunner()) as client: + response = client.post( + "/v1/evaluations", + data={"metadata": __import__("json").dumps(metadata)}, + files=files, + headers={"Authorization": f"Bearer {_TOKEN}"}, + ) + assert response.status_code == 422 + + +def test_job_api_rejects_unexpected_multipart_authority(tmp_path: Path) -> None: + registered = _source_dataset(tmp_path) + data, files = _request_parts(tmp_path, registered) + files["docker"] = ("config.json", b"{}", "application/json") + with _client(tmp_path, RecordingRunner()) as client: + response = client.post( + "/v1/evaluations", + data=data, + files=files, + headers={"Authorization": f"Bearer {_TOKEN}"}, + ) + assert response.status_code == 422 + + +def test_job_api_rejects_digest_mismatch(tmp_path: Path) -> None: + registered = _source_dataset(tmp_path) + data, files = _request_parts(tmp_path, registered) + metadata = EvaluationSubmission.model_validate_json(data["metadata"]) + data["metadata"] = metadata.model_copy( + update={"candidate": ArchiveReference(digest=f"sha256:{'0' * 64}")} + ).model_dump_json() + with _client(tmp_path, RecordingRunner()) as client: + response = client.post( + "/v1/evaluations", + data=data, + files=files, + headers={"Authorization": f"Bearer {_TOKEN}"}, + ) + assert response.status_code == 422 + + +def test_job_api_rejects_malformed_archive_and_removes_work_dir(tmp_path: Path) -> None: + registered = _source_dataset(tmp_path) + data, files = _request_parts(tmp_path, registered) + files["candidate"] = ("candidate.tar.gz", b"not-a-tar-archive", "application/gzip") + with _client(tmp_path, RecordingRunner()) as client: + response = client.post( + "/v1/evaluations", + data=data, + files=files, + headers={"Authorization": f"Bearer {_TOKEN}"}, + ) + assert response.status_code == 422 + assert list((tmp_path / "jobs").iterdir()) == [] + + +def test_job_error_does_not_expose_secret_or_host_path(tmp_path: Path) -> None: + registered = _source_dataset(tmp_path) + data, files = _request_parts(tmp_path, registered) + sensitive = f"{_TOKEN} {tmp_path}" + with _client(tmp_path, RecordingRunner(fail_with=sensitive)) as client: + response = client.post( + "/v1/evaluations", + data=data, + files=files, + headers={"Authorization": f"Bearer {_TOKEN}"}, + ) + payload = _wait_terminal(client, response.json()["job_id"]) + assert payload["state"] == "failed" + assert _TOKEN not in payload["error"] + assert str(tmp_path) not in payload["error"] + + +def test_job_api_requires_authentication(tmp_path: Path) -> None: + _source_dataset(tmp_path) + app = create_app( + settings=HarborBridgeSettings( + storage_root=tmp_path / "jobs", + catalog_root=tmp_path / "catalog", + token=_TOKEN, + ), + runner=RecordingRunner(), + ) + with TestClient(app) as client: + assert client.get("/v1/evaluations/missing").status_code == 401 + assert ( + client.get( + "/v1/evaluations/missing", + headers=[(b"Authorization", "Bearer café".encode("latin-1"))], + ).status_code + == 401 + ) diff --git a/plugins/nemo-experimentalist/tests/test_openshell_runtime.py b/plugins/nemo-experimentalist/tests/test_openshell_runtime.py new file mode 100644 index 0000000000..7d3dfd83a3 --- /dev/null +++ b/plugins/nemo-experimentalist/tests/test_openshell_runtime.py @@ -0,0 +1,581 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""OpenShell preparation, launcher, entrypoint, and policy tests.""" + +from __future__ import annotations + +import json +import os +import subprocess +from pathlib import Path +from typing import Any, cast + +import pytest +import yaml +from nemo_experimentalist_plugin.experimentalist.components.evaluator.models import DatasetRef +from nemo_experimentalist_plugin.experimentalist.run import run_experimentalist +from nemo_experimentalist_plugin.openshell import inner, launcher +from nemo_experimentalist_plugin.openshell.preparation import ( + PreparedOpenShellRun, + SandboxRunManifest, + prepare_openshell_run, +) +from nemo_experimentalist_plugin.resolve import ( + EvolutionaryOptimizerConfig, + ResolvedExperimentInputs, +) + +PLUGIN_ROOT = Path(__file__).resolve().parents[1] +REPO_ROOT = PLUGIN_ROOT.parents[1] +OPEN_SHELL_ROOT = PLUGIN_ROOT / "src" / "nemo_experimentalist_plugin" / "openshell" + + +def _task_dataset(path: Path, name: str) -> Path: + task = path / name + (task / "environment").mkdir(parents=True) + (task / "tests").mkdir() + (task / "task.toml").write_text( + f'[task]\nname = "fixture/{name}"\n[environment]\ntype = "docker"\n[verifier]\n', + encoding="utf-8", + ) + (task / "instruction.md").write_text("trusted\n", encoding="utf-8") + (task / "environment" / "Dockerfile").write_text("FROM python:3.12-slim\n", encoding="utf-8") + (task / "tests" / "test.sh").write_text("#!/bin/sh\n", encoding="utf-8") + (task / "nemo-task-envelope.json").write_text( + json.dumps( + { + "schema_version": 1, + "task_data": [{"path": "instruction.md", "media_type": "text/plain", "max_bytes": 1000}], + "verifier_paths": ["tests/test.sh"], + } + ), + encoding="utf-8", + ) + return path + + +def _resolved_inputs(tmp_path: Path) -> ResolvedExperimentInputs: + agent = tmp_path / "agent" + agent.mkdir() + (agent / "main.py").write_text("print('agent')\n", encoding="utf-8") + train = _task_dataset(tmp_path / "train", "train-task") + validation = _task_dataset(tmp_path / "validation", "validation-task") + template = _task_dataset(tmp_path / "template", "template-task") + skills = tmp_path / "skills" + skills.mkdir() + (skills / "SKILL.md").write_text("# Fixture\n", encoding="utf-8") + return ResolvedExperimentInputs( + agent=str(agent), + agent_spec=None, + insight=None, + train_dataset=DatasetRef(uri=str(train), metadata={"id": "train"}), + validation_dataset=DatasetRef(uri=str(validation), metadata={"id": "validation"}), + task_template=DatasetRef(uri=str(template), metadata={"id": "task-template"}), + workspace="default", + config=EvolutionaryOptimizerConfig(max_rounds=1), + framework_skills_dirs=[skills], + ) + + +async def test_preparation_copies_only_credential_free_inputs_and_separates_catalog(tmp_path: Path) -> None: + inputs = _resolved_inputs(tmp_path) + secret = tmp_path / "host-secret.txt" + secret.write_text("do-not-copy", encoding="utf-8") + + prepared = await prepare_openshell_run( + inputs, + experiment_dir=tmp_path / "experiment", + client=None, + ) + + manifest = SandboxRunManifest.model_validate_json(prepared.manifest_path.read_text(encoding="utf-8")) + assert manifest.agent == "agent" + assert manifest.train_dataset == "datasets/train" + assert manifest.config.storage.publish_winner is False + assert not (prepared.sandbox_input / ".git").exists() + assert not (prepared.sandbox_input / secret.name).exists() + assert "do-not-copy" not in prepared.manifest_path.read_text(encoding="utf-8") + + sandbox_instruction = prepared.sandbox_input / "datasets" / "train" / "train-task" / "instruction.md" + sandbox_instruction.write_text("sandbox overlay\n", encoding="utf-8") + catalog_instruction = next(prepared.catalog_root.glob("envelopes/*/dataset/train-task/instruction.md")) + assert catalog_instruction.read_text(encoding="utf-8") == "trusted\n" + + +async def test_preparation_rejects_source_control_publishing(tmp_path: Path) -> None: + inputs = _resolved_inputs(tmp_path) + inputs.config.storage.publish_winner = True + + with pytest.raises(ValueError, match="does not support source-control"): + await prepare_openshell_run(inputs, experiment_dir=tmp_path / "experiment", client=None) + + +async def test_preparation_rejects_linked_agent_spec(tmp_path: Path) -> None: + inputs = _resolved_inputs(tmp_path) + source = tmp_path / "AGENT-SPEC-source.md" + source.write_text("# Agent\n", encoding="utf-8") + linked = tmp_path / "AGENT-SPEC.md" + linked.symlink_to(source) + inputs.agent_spec = str(linked) + + with pytest.raises(ValueError, match="must not be linked"): + await prepare_openshell_run(inputs, experiment_dir=tmp_path / "experiment", client=None) + + assert not (tmp_path / "experiment" / "openshell-runtime").exists() + + +async def test_inner_entrypoint_runs_only_with_marker_and_prepared_paths( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + prepared = await prepare_openshell_run( + _resolved_inputs(tmp_path), + experiment_dir=tmp_path / "experiment", + client=None, + ) + captured: dict[str, Any] = {} + + async def record_run(**kwargs: Any) -> str: + captured.update(kwargs) + return "inner-summary" + + class _Client: + async def close(self) -> None: + captured["closed"] = True + + monkeypatch.setattr( + "nemo_experimentalist_plugin.experimentalist.run.run_experimentalist", + record_run, + ) + monkeypatch.setattr(inner, "make_client", lambda value: _Client()) + monkeypatch.setenv("NEMO_EXPERIMENTALIST_OPEN_SHELL_RUNTIME", "1") + monkeypatch.setenv("NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_URL", "http://host.openshell.internal:8765") + monkeypatch.setenv( + "NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_TOKEN", + "provider-OPENSHELL-RESOLVE-ENV-NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_TOKEN", + ) + + result = await inner.run_prepared_manifest( + prepared.manifest_path, + output_dir=tmp_path / "sandbox-output", + ) + + assert result == "inner-summary" + assert Path(captured["agent"]).is_relative_to(prepared.sandbox_input) + assert cast(DatasetRef, captured["train_dataset"]).uri.startswith("file:") + assert captured["closed"] is True + + +def test_launcher_fails_closed_when_openshell_is_missing(tmp_path: Path) -> None: + prepared = PreparedOpenShellRun( + root=tmp_path, + catalog_root=tmp_path / "catalog", + sandbox_input=tmp_path / "input", + manifest_path=tmp_path / "input" / "run.json", + ) + with pytest.raises(launcher.OpenShellLaunchError, match="required"): + launcher.launch_openshell_run( + prepared, + experiment_dir=tmp_path / "output", + platform_url="http://localhost:8080", + env={"PATH": ""}, + ) + + +def test_runtime_defaults_keep_optimizer_and_candidate_credentials_separate() -> None: + runtime_env = { + "EXPERIMENTALIST_API_KEY": "optimizer-key", + "INFERENCE_API_KEY": "candidate-key", + "NVIDIA_API_KEY": "ambient-key", + "AUT_MODEL_NAME": "openai/model", + } + + launcher._apply_runtime_defaults(runtime_env) + + assert runtime_env["EXPERIMENTALIST_API_KEY"] == "optimizer-key" + assert runtime_env["INFERENCE_API_KEY"] == "candidate-key" + assert runtime_env["NVIDIA_API_KEY"] == "ambient-key" + + shared_key_env = { + "INFERENCE_API_KEY": "shared-key", + "AUT_MODEL_NAME": "openai/model", + } + launcher._apply_runtime_defaults(shared_key_env) + assert shared_key_env["EXPERIMENTALIST_API_KEY"] == "shared-key" + + +def test_configure_providers_uses_optimizer_key_for_nvidia_profile( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + prepared = PreparedOpenShellRun( + root=tmp_path, + catalog_root=tmp_path / "catalog", + sandbox_input=tmp_path / "input", + manifest_path=tmp_path / "input" / "run.json", + ) + captured: dict[str, Any] = {} + + def run(argv: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]: + captured["argv"] = argv + captured["env"] = kwargs["env"] + return subprocess.CompletedProcess(argv, 0) + + monkeypatch.setattr(launcher.subprocess, "run", run) + runtime_env = { + "EXPERIMENTALIST_API_KEY": "optimizer-key", + "NVIDIA_API_KEY": "ambient-key", + } + + launcher._configure_providers(prepared, runtime_env) + + provider_env = cast(dict[str, str], captured["env"]) + assert provider_env["NVIDIA_API_KEY"] == "optimizer-key" + assert runtime_env["NVIDIA_API_KEY"] == "ambient-key" + assert "optimizer-key" not in cast(list[str], captured["argv"]) + assert runtime_env["NEMO_EXPERIMENTALIST_PROVIDER_PROFILE_DIR"] == str(tmp_path / "host" / "provider-profiles") + + +@pytest.mark.parametrize( + "platform_url", + [ + "http://platform.example:8080", + "http://localhost:9090", + "http://user@localhost:8080", + "https://localhost:8080", + "http://localhost:8080/api", + ], +) +def test_launcher_rejects_platform_urls_outside_shipped_policy( + tmp_path: Path, + platform_url: str, + monkeypatch: pytest.MonkeyPatch, +) -> None: + prepared = PreparedOpenShellRun( + root=tmp_path, + catalog_root=tmp_path / "catalog", + sandbox_input=tmp_path / "input", + manifest_path=tmp_path / "input" / "run.json", + ) + monkeypatch.setattr(launcher.shutil, "which", lambda name, *, path=None: f"/bin/{name}") + + with pytest.raises(launcher.OpenShellLaunchError, match="policy|user information|root HTTP"): + launcher.launch_openshell_run( + prepared, + experiment_dir=tmp_path / "output", + platform_url=platform_url, + env={"PATH": "/bin", launcher.IMAGE_ENV: "registry.example/experimentalist:v1"}, + ) + + +def test_launcher_uses_custom_image_and_never_calls_local_runner( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + prepared = PreparedOpenShellRun( + root=tmp_path, + catalog_root=tmp_path / "catalog", + sandbox_input=tmp_path / "input", + manifest_path=tmp_path / "input" / "run.json", + ) + prepared.sandbox_input.mkdir() + calls: list[list[str]] = [] + + monkeypatch.setattr(launcher.shutil, "which", lambda name, *, path=None: f"/bin/{name}") + monkeypatch.setattr(launcher, "_apply_runtime_defaults", lambda env: None) + monkeypatch.setattr(launcher, "_start_bridge", lambda **kwargs: None) + monkeypatch.setattr(launcher, "_configure_providers", lambda *args: None) + monkeypatch.setattr(launcher, "_delete_bridge_provider", lambda *args: True) + + def run(argv: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]: + calls.append(argv) + return subprocess.CompletedProcess(argv, 0, stdout="sandbox-summary\n") + + monkeypatch.setattr(launcher.subprocess, "run", run) + + result = launcher.launch_openshell_run( + prepared, + experiment_dir=tmp_path / "output", + platform_url="http://localhost:8080", + env={"PATH": "/bin", launcher.IMAGE_ENV: "registry.example/experimentalist:v1"}, + ) + + assert result == "sandbox-summary" + assert calls == [ + [ + str(Path(launcher.__file__).with_name("run.sh")), + str(prepared.sandbox_input), + str((tmp_path / "output").resolve()), + ] + ] + + +def test_default_image_build_loads_plugin_bake_definition( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + prepared = PreparedOpenShellRun( + root=REPO_ROOT, + catalog_root=tmp_path / "catalog", + sandbox_input=tmp_path / "input", + manifest_path=tmp_path / "input" / "run.json", + ) + calls: list[list[str]] = [] + + def run(argv: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]: + del kwargs + calls.append(argv) + return subprocess.CompletedProcess(argv, 1 if argv[1:3] == ["image", "inspect"] else 0, stdout="") + + monkeypatch.setattr(launcher.subprocess, "run", run) + + launcher._acquire_default_image( + docker="/bin/docker", + image=launcher.DEFAULT_IMAGE, + prepared=prepared, + runtime_env={launcher.PLATFORM_ENV: "linux/arm64"}, + ) + + assert calls[1] == [ + "/bin/docker", + "buildx", + "bake", + "-f", + "docker-bake.hcl", + "-f", + "plugins/nemo-experimentalist/docker-bake.hcl", + "nmp-experimentalist-docker", + "--load", + ] + + +def test_launcher_deletes_provider_when_configuration_fails( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + prepared = PreparedOpenShellRun( + root=tmp_path, + catalog_root=tmp_path / "catalog", + sandbox_input=tmp_path / "input", + manifest_path=tmp_path / "input" / "run.json", + ) + prepared.sandbox_input.mkdir() + deleted: list[str] = [] + + monkeypatch.setattr(launcher.shutil, "which", lambda name, *, path=None: f"/bin/{name}") + monkeypatch.setattr(launcher, "_apply_runtime_defaults", lambda env: None) + monkeypatch.setattr(launcher, "_start_bridge", lambda **kwargs: None) + + def fail_configuration(*_args: object) -> None: + raise launcher.OpenShellLaunchError("configuration failed") + + def delete_provider(_openshell: str, env: dict[str, str]) -> bool: + deleted.append(env[launcher.BRIDGE_PROVIDER_ENV]) + return True + + monkeypatch.setattr(launcher, "_configure_providers", fail_configuration) + monkeypatch.setattr(launcher, "_delete_bridge_provider", delete_provider) + + with pytest.raises(launcher.OpenShellLaunchError, match="configuration failed"): + launcher.launch_openshell_run( + prepared, + experiment_dir=tmp_path / "output", + platform_url="http://localhost:8080", + env={"PATH": "/bin", launcher.IMAGE_ENV: "registry.example/experimentalist:v1"}, + ) + + assert len(deleted) == 1 + + +def test_bridge_listener_uses_configured_host( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + prepared = PreparedOpenShellRun( + root=tmp_path, + catalog_root=tmp_path / "catalog", + sandbox_input=tmp_path / "input", + manifest_path=tmp_path / "input" / "run.json", + ) + ready = iter((False, True)) + probed: list[str] = [] + captured: dict[str, Any] = {} + + class Process: + def __init__(self) -> None: + self.returncode: int | None = None + + def poll(self) -> int | None: + return self.returncode + + def terminate(self) -> None: + self.returncode = 0 + + def wait(self, timeout: int) -> int: + del timeout + return 0 + + def kill(self) -> None: + self.returncode = -9 + + def popen(argv: list[str], **kwargs: Any) -> Process: + captured["argv"] = argv + captured["kwargs"] = kwargs + return Process() + + def bridge_ready(url: str) -> bool: + probed.append(url) + return next(ready) + + monkeypatch.setattr(launcher, "_bridge_ready", bridge_ready) + monkeypatch.setattr(launcher.subprocess, "Popen", popen) + + managed = launcher._start_bridge( + prepared=prepared, + runtime_env={launcher.BRIDGE_BIND_ENV: "172.18.0.1"}, + ) + + assert managed is not None + argv = cast(list[str], captured["argv"]) + assert argv[argv.index("--host") + 1] == "172.18.0.1" + assert probed == ["http://172.18.0.1:8765", "http://172.18.0.1:8765"] + managed.stop() + + +def test_openshell_assets_expose_only_bounded_authority() -> None: + strict = yaml.safe_load((OPEN_SHELL_ROOT / "policy.yaml").read_text(encoding="utf-8")) + development = yaml.safe_load((OPEN_SHELL_ROOT / "policy.docker-desktop.yaml").read_text(encoding="utf-8")) + provider = yaml.safe_load( + (OPEN_SHELL_ROOT / "provider-profiles" / "nemo-experimentalist-harbor-bridge.yaml").read_text(encoding="utf-8") + ) + configure_script = (OPEN_SHELL_ROOT / "configure-providers.sh").read_text(encoding="utf-8") + run_script = (OPEN_SHELL_ROOT / "run.sh").read_text(encoding="utf-8") + dockerfile = (PLUGIN_ROOT / "Dockerfile").read_text(encoding="utf-8") + root_docker_bake = (REPO_ROOT / "docker-bake.hcl").read_text(encoding="utf-8") + docker_bake = (PLUGIN_ROOT / "docker-bake.hcl").read_text(encoding="utf-8") + + assert strict["landlock"]["compatibility"] == "hard_requirement" + assert development["landlock"]["compatibility"] == "best_effort" + assert strict["filesystem_policy"] == development["filesystem_policy"] + assert "docker.sock" not in json.dumps(strict).lower() + assert [rule["allow"] for rule in provider["endpoints"][0]["rules"]] == [ + {"method": "GET", "path": "/health/ready"}, + {"method": "POST", "path": "/v1/evaluations"}, + {"method": "GET", "path": "/v1/evaluations/*"}, + {"method": "GET", "path": "/v1/evaluations/*/artifacts"}, + {"method": "DELETE", "path": "/v1/evaluations/*"}, + {"method": "POST", "path": "/v1/dependencies"}, + {"method": "POST", "path": "/v1/dependencies/*/exec"}, + {"method": "DELETE", "path": "/v1/dependencies/*"}, + ] + assert '--upload "$input_dir:/sandbox/input"' in run_script + assert "NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_TOKEN=" not in run_script + assert configure_script.index("trap cleanup_failed_setup EXIT") < configure_script.index( + 'openshell provider create \\\n --name "$bridge_provider"' + ) + assert 'openshell provider get "$bridge_provider"' in configure_script + assert "command -v docker" in run_script + assert "docker.sock" not in dockerfile + assert "USER sandbox" in dockerfile + assert "ENTRYPOINT" not in dockerfile + dependency_sync = dockerfile.index("--no-install-workspace") + source_copy = dockerfile.index("COPY --from=nmp-workspace . .") + assert dependency_sync < source_copy + assert "COPY --from=dependencies /app/.venv /app/.venv" in dockerfile + assert "COPY --from=builder /wheels /wheels" in dockerfile + assert "COPY --from=builder /app/.venv" not in dockerfile + assert 'dockerfile = "plugins/nemo-experimentalist/Dockerfile"' in docker_bake + assert 'target "nmp-experimentalist-docker"' not in root_docker_bake + + +def test_openshell_shell_assets_parse() -> None: + for name in ("run.sh", "configure-providers.sh"): + result = subprocess.run( + ["bash", "-n", str(OPEN_SHELL_ROOT / name)], + check=False, + capture_output=True, + text=True, + env=os.environ, + ) + assert result.returncode == 0, result.stderr + + +def test_provider_setup_uses_env_optimizer_key_and_upstream_model_id(tmp_path: Path) -> None: + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + openshell = fake_bin / "openshell" + openshell.write_text( + r"""#!/usr/bin/env bash +printf '%s\n' "$*" >> "$OPENSHELL_TEST_LOG" +if [[ "$*" == provider\ profile\ export* && "${OPENSHELL_TEST_PROFILE_EXISTS:-1}" == "0" ]]; then + exit 1 +fi +""", + encoding="utf-8", + ) + openshell.chmod(0o755) + command_log = tmp_path / "openshell.log" + profile_dir = tmp_path / "profiles" + env = { + **os.environ, + "PATH": f"{fake_bin}:{os.environ['PATH']}", + "OPENSHELL_TEST_LOG": str(command_log), + "NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_TOKEN": "bridge-token", + "NEMO_EXPERIMENTALIST_PROVIDER_PROFILE_DIR": str(profile_dir), + "EXPERIMENTALIST_SMART_MODEL_NAME": "openai/openai/openai/gpt-5-mini", + "NVIDIA_API_KEY": "optimizer-secret", + } + + result = subprocess.run( + [str(OPEN_SHELL_ROOT / "configure-providers.sh")], + check=False, + capture_output=True, + text=True, + env=env, + ) + + assert result.returncode == 0, result.stderr + commands = command_log.read_text(encoding="utf-8").splitlines() + bridge_provider_delete = commands.index("provider delete nemo-experimentalist-harbor-bridge") + profile_delete = commands.index("provider profile delete nemo-experimentalist-harbor-bridge") + profile_lint = commands.index(f"provider profile lint --from {profile_dir}") + profile_import = commands.index(f"provider profile import --from {profile_dir}") + assert bridge_provider_delete < profile_delete < profile_lint < profile_import + inference_provider = next( + command for command in commands if "provider create --name nemo-experimentalist-inference" in command + ) + assert "--credential NVIDIA_API_KEY" in inference_provider + assert "--from-existing" not in inference_provider + assert "optimizer-secret" not in "\n".join(commands) + assert ( + "inference set --provider nemo-experimentalist-inference --model openai/openai/gpt-5-mini --no-verify" + in commands + ) + + missing_profile_log = tmp_path / "missing-profile.log" + missing_profile_env = { + **env, + "OPENSHELL_TEST_LOG": str(missing_profile_log), + "OPENSHELL_TEST_PROFILE_EXISTS": "0", + } + missing_profile_result = subprocess.run( + [str(OPEN_SHELL_ROOT / "configure-providers.sh")], + check=False, + capture_output=True, + text=True, + env=missing_profile_env, + ) + + assert missing_profile_result.returncode == 0, missing_profile_result.stderr + missing_profile_commands = missing_profile_log.read_text(encoding="utf-8").splitlines() + profile_commands = [command for command in missing_profile_commands if command.startswith("provider profile")] + assert profile_commands == [ + "provider profile export nemo-experimentalist-harbor-bridge -o yaml", + f"provider profile lint --from {profile_dir}", + f"provider profile import --from {profile_dir}", + ] + + +def test_test_module_import_does_not_replace_real_runner() -> None: + assert callable(run_experimentalist) diff --git a/plugins/nemo-experimentalist/tests/test_preflight.py b/plugins/nemo-experimentalist/tests/test_preflight.py index 8b3b8b5c37..e6212b004b 100644 --- a/plugins/nemo-experimentalist/tests/test_preflight.py +++ b/plugins/nemo-experimentalist/tests/test_preflight.py @@ -162,6 +162,47 @@ def test_missing_experiment_credentials_are_required(tmp_path: Path) -> None: assert not any(r.name == "INFERENCE_API_KEY" for r in results) +def test_openshell_environment_checks_runtime_and_candidate_credentials(tmp_path: Path) -> None: + results = check_environment( + profile=full_profile(tmp_path), + insight=None, + base_url="http://localhost:8080", + openshell=True, + probes=make_probes( + env={ + "INFERENCE_API_KEY": "dedicated-key", + "AUT_MODEL_NAME": "fixture-model", + } + ), + ) + + assert all(result.status == "pass" for result in results) + assert any(result.name == "openshell" for result in results) + assert not any(result.name == "openshell-inference-credential" for result in results) + assert not any(result.name == "EXPERIMENTALIST_API_BASE" for result in results) + + +def test_missing_openshell_is_a_required_failure(tmp_path: Path) -> None: + def runtime_probe(argv: list[str]) -> tuple[int, str]: + return (127, "missing") if argv == ["openshell", "--version"] else (0, "ok") + + results = check_environment( + profile=full_profile(tmp_path), + insight=None, + base_url="http://localhost:8080", + openshell=True, + probes=Probes( + run_cmd=runtime_probe, + http_ok=lambda url: True, + env={"INFERENCE_API_KEY": "dedicated-key", "AUT_MODEL_NAME": "fixture-model"}, + ), + ) + + openshell = next(result for result in results if result.name == "openshell") + assert openshell.status == "fail" + assert openshell.severity == "required" + + def test_git_source_probe_failure_is_advisory(tmp_path: Path) -> None: def remote_unreachable(argv: list[str]) -> tuple[int, str]: return (0, "git version 2") if argv == ["git", "--version"] else (1, "remote unavailable") diff --git a/plugins/nemo-experimentalist/tests/test_remote_harbor.py b/plugins/nemo-experimentalist/tests/test_remote_harbor.py new file mode 100644 index 0000000000..c5e46c3e5f --- /dev/null +++ b/plugins/nemo-experimentalist/tests/test_remote_harbor.py @@ -0,0 +1,417 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Remote evaluator and dependency-session boundary tests.""" + +from __future__ import annotations + +import json +import shutil +from pathlib import Path +from typing import Any, cast + +import httpx +import nemo_experimentalist_plugin.harbor_bridge.dependencies as dependency_module +import pytest +from fastapi.testclient import TestClient +from nemo_experimentalist_plugin.experimentalist.components.evaluator.factory import EvaluatorFactory +from nemo_experimentalist_plugin.experimentalist.components.evaluator.harbor import HarborDataset +from nemo_experimentalist_plugin.experimentalist.components.evaluator.models import ( + DatasetRef, + DependencyRuntimeError, + EvaluationResult, + MetricResult, + ResourceRef, + TrialResult, +) +from nemo_experimentalist_plugin.experimentalist.components.evaluator.remote_harbor import ( + BRIDGE_TOKEN_ENV, + BRIDGE_URL_ENV, + OPEN_SHELL_RUNTIME_ENV, + RemoteHarborDependencyContext, + RemoteHarborDependencyRuntime, + RemoteHarborEvaluator, + RemoteHarborEvaluatorConfig, + _bridge_headers, +) +from nemo_experimentalist_plugin.harbor_bridge.contracts import ( + IDENTIFIER_MAX_LENGTH, + DependencyExecResponse, + DependencyStartRequest, +) +from nemo_experimentalist_plugin.harbor_bridge.dependencies import HarborDependencySessionManager +from nemo_experimentalist_plugin.harbor_bridge.envelopes import ( + RegisteredEnvelope, + register_dataset_envelope, +) +from nemo_experimentalist_plugin.harbor_bridge.service import HarborBridgeSettings, create_app + +_TOKEN = "remote-bridge-token-long-enough" + + +def _registered_dataset(tmp_path: Path, *, single_task_root: bool = False) -> RegisteredEnvelope: + dataset = tmp_path / "source" + task = dataset if single_task_root else dataset / "base-task" + (task / "environment").mkdir(parents=True) + (task / "tests").mkdir() + (task / "task.toml").write_text( + '[task]\nname = "fixture/base-task"\n[environment]\ntype = "docker"\n[verifier]\n', + encoding="utf-8", + ) + (task / "instruction.md").write_text("trusted instruction\n", encoding="utf-8") + (task / "environment" / "Dockerfile").write_text("FROM python:3.12-slim\n", encoding="utf-8") + (task / "tests" / "test.sh").write_text("#!/bin/sh\n", encoding="utf-8") + (task / "nemo-task-envelope.json").write_text( + json.dumps( + { + "schema_version": 1, + "task_data": [ + { + "path": "instruction.md", + "media_type": "text/plain", + "max_bytes": 65536, + } + ], + "verifier_paths": ["tests/test.sh"], + } + ), + encoding="utf-8", + ) + return register_dataset_envelope(dataset, catalog_root=tmp_path / "catalog", name="fixture") + + +class _RecordingRunner: + calls = 0 + + async def run(self, **kwargs: Any) -> EvaluationResult: + self.calls += 1 + dataset_dir = cast(Path, kwargs["dataset_dir"]) + candidate_dir = cast(Path, kwargs["candidate_dir"]) + work_dir = cast(Path, kwargs["work_dir"]) + assert (candidate_dir / "main.py").is_file() + assert (dataset_dir / "base-task" / "instruction.md").read_text(encoding="utf-8") == "changed\n" + trace = work_dir / "results" / "trace.jsonl" + trace.parent.mkdir() + trace.write_text('{"resourceSpans":[]}\n', encoding="utf-8") + return EvaluationResult( + id="bridge-result", + trials=[ + TrialResult( + id="base-task__0", + task_id="base-task", + attempt=0, + status="completed", + trace=ResourceRef(uri=trace.as_uri(), description="trace"), + metrics={"reward": MetricResult(name="reward", value=1.0)}, + ) + ], + ) + + +async def test_remote_evaluator_submits_and_translates_trials( + tmp_path: Path, + monkeypatch, +) -> None: + monkeypatch.chdir(tmp_path) + registered = _registered_dataset(tmp_path) + sandbox_dataset = tmp_path / "sandbox-dataset" + shutil.copytree(registered.dataset_path, sandbox_dataset) + (sandbox_dataset / "base-task" / "instruction.md").write_text("changed\n", encoding="utf-8") + candidates = [tmp_path / "baseline", tmp_path / "candidate"] + for candidate in candidates: + candidate.mkdir() + (candidate / "main.py").write_text("raise AssertionError('host import')\n", encoding="utf-8") + runner = _RecordingRunner() + app = create_app( + settings=HarborBridgeSettings( + storage_root=tmp_path / "jobs", + catalog_root=tmp_path / "catalog", + token=_TOKEN, + ), + runner=runner, + ) + monkeypatch.setenv(BRIDGE_TOKEN_ENV, _TOKEN) + evaluator = RemoteHarborEvaluator( + RemoteHarborEvaluatorConfig( + bridge_url="http://bridge.test", + run_profile="smoke", + poll_interval_sec=0.01, + max_archive_bytes=4096, + ), + experiment_dir=Path("experiment"), + transport=httpx.ASGITransport(app=app), + ) + dataset = HarborDataset.from_ref(DatasetRef(uri=sandbox_dataset.as_uri())) + evaluator.prepare_dataset(dataset) + + baseline_result = await evaluator.run(candidates[0], dataset) + result = await evaluator.run(candidates[1], dataset) + + assert runner.calls == 2 + assert baseline_result.aggregate_metrics == {"reward": 1.0} + assert result.aggregate_metrics == {"reward": 1.0} + assert result.trials[0].task_id == "base-task" + assert result.trials[0].trace is not None + assert Path(result.trials[0].trace.uri.removeprefix("file://")).read_text(encoding="utf-8") == ( + '{"resourceSpans":[]}\n' + ) + runtime = dataset.tasks[0].dependencies + assert isinstance(runtime, RemoteHarborDependencyRuntime) + assert runtime.max_archive_bytes == 4096 + + +async def test_dependency_command_extends_client_timeout( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured_timeout: dict[str, float] = {} + + def respond(request: httpx.Request) -> httpx.Response: + captured_timeout.update(request.extensions["timeout"]) + return httpx.Response(200, json={"stdout": "", "stderr": "", "returncode": 0}) + + monkeypatch.setenv(BRIDGE_TOKEN_ENV, _TOKEN) + registered = _registered_dataset(tmp_path) + dataset = HarborDataset.from_ref(DatasetRef(uri=registered.dataset_path.as_uri())) + evaluator = RemoteHarborEvaluator( + RemoteHarborEvaluatorConfig( + bridge_url="http://bridge.test", + request_timeout_sec=5, + ), + transport=httpx.MockTransport(respond), + ) + evaluator.prepare_dataset(dataset) + runtime = dataset.tasks[0].dependencies + assert isinstance(runtime, RemoteHarborDependencyRuntime) + runtime._session_id = "dependency-session" + runtime._capability = "capability" + + await runtime.execute("pwd", timeout=120) + + assert captured_timeout["read"] == 130 + + +def test_copied_template_automatically_uses_remote_dependency_runtime( + tmp_path: Path, + monkeypatch, +) -> None: + registered = _registered_dataset(tmp_path, single_task_root=True) + generated_suite = tmp_path / "generated" + shutil.copytree(registered.dataset_path, generated_suite / "trace-task") + monkeypatch.setenv(OPEN_SHELL_RUNTIME_ENV, "1") + monkeypatch.setenv(BRIDGE_URL_ENV, "http://bridge.test") + + dataset = HarborDataset.from_path(generated_suite) + + runtime = dataset.tasks[0].dependencies + assert isinstance(runtime, RemoteHarborDependencyRuntime) + assert runtime.base_task_id == "source" + assert runtime.task_id == "trace-task" + + +def test_factory_selects_remote_evaluator_without_local_fallback( + monkeypatch, +) -> None: + monkeypatch.setenv(OPEN_SHELL_RUNTIME_ENV, "1") + monkeypatch.setenv(BRIDGE_URL_ENV, "http://bridge.test") + + evaluator = EvaluatorFactory().build_evaluator("harbor", {}) + + assert isinstance(evaluator, RemoteHarborEvaluator) + + +def test_factory_fails_closed_when_bridge_url_is_missing(monkeypatch) -> None: + monkeypatch.setenv(OPEN_SHELL_RUNTIME_ENV, "1") + monkeypatch.delenv(BRIDGE_URL_ENV, raising=False) + + with pytest.raises(RuntimeError, match=BRIDGE_URL_ENV): + EvaluatorFactory().build_evaluator("harbor", {}) + + +def test_openshell_bridge_request_uses_provider_placeholder(monkeypatch) -> None: + placeholder = "openshell:resolve:env:NEMO_EXPERIMENTALIST_HARBOR_BRIDGE_TOKEN" + monkeypatch.setenv(OPEN_SHELL_RUNTIME_ENV, "1") + monkeypatch.setenv(BRIDGE_TOKEN_ENV, placeholder) + + assert _bridge_headers(BRIDGE_TOKEN_ENV) == {"Authorization": f"Bearer {placeholder}"} + + +def test_openshell_bridge_request_fails_without_provider_placeholder(monkeypatch) -> None: + monkeypatch.setenv(OPEN_SHELL_RUNTIME_ENV, "1") + monkeypatch.delenv(BRIDGE_TOKEN_ENV, raising=False) + + with pytest.raises(DependencyRuntimeError, match=BRIDGE_TOKEN_ENV): + _bridge_headers(BRIDGE_TOKEN_ENV) + + +async def test_dependency_shutdown_preserves_body_error_and_clears_state( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv(BRIDGE_TOKEN_ENV, _TOKEN) + registered = _registered_dataset(tmp_path) + dataset = HarborDataset.from_ref(DatasetRef(uri=registered.dataset_path.as_uri())) + evaluator = RemoteHarborEvaluator( + RemoteHarborEvaluatorConfig(bridge_url="http://bridge.test"), + transport=httpx.MockTransport(lambda _request: httpx.Response(500)), + ) + evaluator.prepare_dataset(dataset) + runtime = dataset.tasks[0].dependencies + assert isinstance(runtime, RemoteHarborDependencyRuntime) + context = RemoteHarborDependencyContext(runtime) + + runtime._session_id = "dependency-session" + runtime._capability = "capability" + body_error = RuntimeError("body failed") + assert await context.__aexit__(RuntimeError, body_error, None) is False + assert runtime._session_id is None + assert runtime._capability is None + + runtime._session_id = "dependency-session" + runtime._capability = "capability" + with pytest.raises(DependencyRuntimeError, match="shutdown failed"): + await context.__aexit__(None, None, None) + assert runtime._session_id is None + assert runtime._capability is None + + +async def test_dependency_session_id_is_bounded_and_runtime_is_stopped( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + contexts: list[Any] = [] + + class FakeContext: + def __init__(self, runtime: Any, *, temp_root: Path) -> None: + del runtime, temp_root + self.stopped = False + contexts.append(self) + + async def __aenter__(self) -> FakeContext: + return self + + async def __aexit__(self, *args: object) -> bool: + del args + self.stopped = True + return False + + monkeypatch.setattr(dependency_module, "HarborDependencyContext", FakeContext) + manager = HarborDependencySessionManager() + request = DependencyStartRequest( + request_id="r" * IDENTIFIER_MAX_LENGTH, + envelope_id="envelope", + envelope_digest=f"sha256:{'0' * 64}", + task_id="task", + base_task_id="base-task", + ) + + session_id = await manager.start( + request, + task_dir=tmp_path / "task", + work_dir=tmp_path / "work", + ) + + assert len(session_id) == IDENTIFIER_MAX_LENGTH + assert session_id.startswith("r") + await manager.stop(session_id) + assert contexts[0].stopped is True + + +class _FakeDependencySessions: + def __init__(self) -> None: + self.started: DependencyStartRequest | None = None + self.stopped: str | None = None + + async def start( + self, + request: DependencyStartRequest, + *, + task_dir: Path, + work_dir: Path, + ) -> str: + assert task_dir.is_dir() + assert work_dir.is_dir() + self.started = request + return "dependency-session" + + async def execute(self, session_id: str, request) -> DependencyExecResponse: + assert session_id == "dependency-session" + return DependencyExecResponse(stdout=request.command, stderr="", returncode=0) + + async def stop(self, session_id: str) -> None: + self.stopped = session_id + + async def close(self) -> None: + return None + + +def test_dependency_api_uses_opaque_capability_and_rejects_authority(tmp_path: Path) -> None: + registered = _registered_dataset(tmp_path) + sessions = _FakeDependencySessions() + app = create_app( + settings=HarborBridgeSettings( + storage_root=tmp_path / "jobs", + catalog_root=tmp_path / "catalog", + token=_TOKEN, + ), + dependency_sessions=cast(Any, sessions), + ) + metadata = DependencyStartRequest( + request_id="dependency-test", + envelope_id=registered.manifest.envelope_id, + envelope_digest=registered.manifest.envelope_digest, + task_id="generated-task", + base_task_id="base-task", + ) + auth = {"Authorization": f"Bearer {_TOKEN}"} + with TestClient(app) as client: + rejected = metadata.model_dump() + rejected["image"] = "attacker/image:latest" + assert ( + client.post( + "/v1/dependencies", + data={"metadata": json.dumps(rejected)}, + headers=auth, + ).status_code + == 422 + ) + + started = client.post( + "/v1/dependencies", + data={"metadata": metadata.model_dump_json()}, + headers=auth, + ) + assert started.status_code == 201 + capability = started.json()["capability_token"] + assert ( + client.post( + "/v1/dependencies/dependency-session/exec", + json={"command": "pwd"}, + headers=auth, + ).status_code + == 403 + ) + assert ( + client.post( + "/v1/dependencies/dependency-session/exec", + json={"command": "pwd"}, + headers=[ + (b"Authorization", f"Bearer {_TOKEN}".encode()), + (b"X-Nemo-Dependency-Capability", "café".encode("latin-1")), + ], + ).status_code + == 403 + ) + executed = client.post( + "/v1/dependencies/dependency-session/exec", + json={"command": "pwd"}, + headers={**auth, "X-Nemo-Dependency-Capability": capability}, + ) + assert executed.json() == {"stdout": "pwd", "stderr": "", "returncode": 0} + stopped = client.delete( + "/v1/dependencies/dependency-session", + headers={**auth, "X-Nemo-Dependency-Capability": capability}, + ) + assert stopped.status_code == 204 + assert sessions.started == metadata + assert sessions.stopped == "dependency-session" diff --git a/plugins/nemo-insights/testbed/README.md b/plugins/nemo-insights/testbed/README.md index a547fbc625..e0b6f876aa 100644 --- a/plugins/nemo-insights/testbed/README.md +++ b/plugins/nemo-insights/testbed/README.md @@ -13,6 +13,7 @@ uv run python -m testbed analyze all # refresh every pinned ben uv run python -m testbed list uv run python -m testbed doctor # fresh clone? run this first uv run python -m testbed run tau2-airline # produce: tau2 -> ingest -> record the run (expensive, once) +uv run python -m testbed run tau3-airline-harbor # produce: Harbor -> ingest -> record the run uv run python -m testbed analyze tau2-airline --live # analyze the recorded run's live traces (no restore) uv run python -m testbed analyze glamr --live # intake: analyze existing live traces uv run python -m testbed snapshot tau2-airline # export the subject's workspaces (read API) into a portable bundle @@ -198,16 +199,38 @@ exactly what to install/set (`✓ ready` or `✗ needs: …`). Subjects live in `testbeds.toml` — one table per subject, keyed by `type`: - `type = "intake"` — analyze an agent's existing Intake traces (config: `agent`, `workspace`, `base_url`, optional `since`). - `type = "benchmark"` — run a benchmark to produce traces, ingest them into Intake, then analyze (config: `domain`, `base_url`, `workspace`, `agent_llm`, `user_llm`, `task_split_name`, `num_trials`, `max_concurrency`, `seed`, optional `num_tasks`/`timeout`/`include_rewards`). +- `type = "harbor"` — run a Harbor dataset against an importable agent wrapper, enrich and ingest its OTLP traces and verifier rewards, then analyze the recorded evaluation. Use exactly one of `dataset`, `dataset_ref`, or `dataset_id`; optionally select tasks with `task_names` or `num_tasks`. `--since` (analyze `--live`, snapshot) accepts `Nd`/`Nh`/`Nm` (days/hours/minutes) or an ISO date; `--since ''` means no lower bound (the epoch). Insights are written to `testbed/tmp/insights_.yaml`. +## Harbor benchmark (Tau3 Airline example) + +The checked-in subject runs `sierra-research/tau3-bench@1` from Harbor Hub +against the Experimentalist's Tau3 NOOA example agent. Harbor and its container +runtime are required. + +```bash +export NMP_BASE_URL=http://localhost:8080 +export INFERENCE_API_KEY=sk-... +export OPENAI_API_KEY="$INFERENCE_API_KEY" +export OPENAI_BASE_URL=https://inference-api.nvidia.com/v1 + +uv run python -m testbed run tau3-airline-harbor --base "$NMP_BASE_URL" +uv run python -m testbed analyze tau3-airline-harbor --live +``` + +The run creates an Evaluation in `canonical-tau3-airline`, associates every +Harbor trial through `nemo.experiment.id`, adds `nemo.test_case.id`, and posts +each numeric verifier reward as an evaluator result. + ## Config split: secrets in `.env`, everything else in `testbeds.toml` On startup the CLI auto-loads `testbed/.env` (gitignored) as `KEY=VALUE` lines. Keep **only secrets/endpoints** there — `INFERENCE_API_KEY` (analyst) and `OPENAI_API_KEY`/`OPENAI_API_BASE` (the proxy litellm uses for the benchmark sim LLMs). +The Harbor example agent uses `OPENAI_API_KEY`/`OPENAI_BASE_URL` instead. GLAMR live analysis additionally reads `GLAMR_INTAKE_USER` and `GLAMR_INTAKE_PASSWORD` from `.env`; `testbeds.toml` stores only those environment-variable names, never their credential values. diff --git a/plugins/nemo-insights/testbed/adapters.py b/plugins/nemo-insights/testbed/adapters.py index 54a0d7c108..4f34312270 100644 --- a/plugins/nemo-insights/testbed/adapters.py +++ b/plugins/nemo-insights/testbed/adapters.py @@ -2,18 +2,23 @@ # SPDX-License-Identifier: Apache-2.0 """Per-type testbed adapters that turn a subject into analyst Insights.""" +import base64 +import json import os import shutil import sys import time +from collections.abc import Iterator, Mapping from datetime import datetime, timezone from pathlib import Path -from typing import Protocol +from typing import Any, Protocol import httpx +from google.protobuf.json_format import ParseDict from nemo_insights_plugin.analyst.run import run_analyst from nemo_insights_plugin.client import make_client from nemo_platform import AsyncNeMoPlatform +from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ExportTraceServiceRequest from testbed.ingest import ( create_experiment, ensure_experiment_group, @@ -23,7 +28,7 @@ ) from testbed.intake_client import build_basic_auth_intake_client from testbed.otlp_build import session_id_for, sim_to_spans -from testbed.otlp_ingest import export_spans, post_evaluator_results, trace_id_for +from testbed.otlp_ingest import export_spans, export_trace_request, post_evaluator_results, trace_id_for from testbed.registry import Subject from testbed.tau2run import load_tasks, policy_version, read_policy, resolve_paths, run_tau2 @@ -54,7 +59,7 @@ def __init__(self, subject: Subject) -> None: def check(self) -> list[str]: """Unmet prerequisites for this subject (empty list = ready to run).""" cfg = self.subject.config - missing = [f"config key '{k}'" for k in ("agent", "workspace", "base_url") if not cfg.get(k)] + missing: list[str] = [f"config key '{k}'" for k in ("agent", "workspace", "base_url") if not cfg.get(k)] if cfg.get("auth") == "basic": missing.extend(self._missing_basic_auth()) return missing @@ -307,9 +312,453 @@ async def analyze( ) -_ADAPTERS: dict[str, type[IntakeAdapter] | type[BenchmarkAdapter]] = { +def _export_harbor_trace_files( + base_url: str, + workspace: str, + trace_dir: Path, + agent_name: str, + *, + evaluation_id: str, + agent_version: str | None = None, + client: httpx.Client | None = None, +) -> tuple[int, int, set[str]]: + """Enrich Harbor OTLP-JSONL traces and export them to Intake.""" + owns_client = client is None + active_client = client or httpx.Client(timeout=30.0) + sent = errors = 0 + session_ids: set[str] = set() + try: + for path in sorted(trace_dir.rglob("*.jsonl")): + parsed: list[tuple[int, dict[str, Any]]] = [] + for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + if not line.strip(): + continue + try: + body = json.loads(line) + except json.JSONDecodeError as exc: + print(f"Harbor trace {path.name} line {line_number}: JSON error — {exc}", file=sys.stderr) + errors += 1 + continue + if not isinstance(body, dict): + print( + f"Harbor trace {path.name} line {line_number}: expected a JSON object", + file=sys.stderr, + ) + errors += 1 + continue + parsed.append((line_number, body)) + + test_case_id, input_value, output_value, rewards = _harbor_trace_context(path, trace_dir, parsed) + evaluator_posted = False + for line_number, body in parsed: + resource_spans = body.get("resourceSpans", []) + if not isinstance(resource_spans, list): + errors += 1 + continue + session_id = _find_session_id(resource_spans) or path.stem + session_ids.add(session_id) + _inject_resource_attributes( + resource_spans, + { + "gen_ai.agent.name": agent_name, + "gen_ai.agent.id": agent_name, + "gen_ai.agent.version": agent_version, + "session.id": session_id, + "gen_ai.conversation.id": session_id, + "nemo.experiment.id": evaluation_id, + "nemo.optimizer.workspace": workspace, + }, + ) + _inject_harbor_root_attributes( + resource_spans, + { + "nemo.test_case.id": test_case_id, + "input.value": input_value, + "input.mime_type": "text/plain" if input_value else None, + "output.value": output_value, + "output.mime_type": "text/markdown" if output_value else None, + }, + ) + root_span_id = _find_harbor_root_span_id(resource_spans) + try: + request = ParseDict(body, ExportTraceServiceRequest()) + export_trace_request( + base_url, + workspace, + request, + client=active_client, + ) + if root_span_id is not None and not evaluator_posted: + for reward_name, reward_value in rewards.items(): + post_evaluator_results( + base_url, + workspace, + span_id=root_span_id, + session_id=session_id, + score=reward_value, + name=reward_name, + client=active_client, + ) + evaluator_posted = True + except Exception as exc: + print(f"Harbor trace {path.name} line {line_number}: export error — {exc}", file=sys.stderr) + errors += 1 + else: + sent += 1 + finally: + if owns_client: + active_client.close() + return sent, errors, session_ids + + +def _harbor_trace_context( + path: Path, + trace_dir: Path, + parsed: list[tuple[int, dict[str, Any]]], +) -> tuple[str | None, str | None, str | None, dict[str, float]]: + """Resolve task, input, output, and verifier rewards for one Harbor trial.""" + relative = path.relative_to(trace_dir) + trial_dir = trace_dir / relative.parts[0] if len(relative.parts) > 1 else None + test_case_id = None + output_value = None + rewards: dict[str, float] = {} + if trial_dir is not None: + result_path = trial_dir / "result.json" + if result_path.is_file(): + try: + result = json.loads(result_path.read_text(encoding="utf-8")) + test_case_id = str(result["task_name"]) + raw_rewards = ( + {} if result.get("exception_info") else (result.get("verifier_result") or {}).get("rewards") or {} + ) + rewards = { + str(name): float(value) + for name, value in raw_rewards.items() + if isinstance(value, int | float) and not isinstance(value, bool) + } + except (json.JSONDecodeError, KeyError, TypeError, ValueError): + pass + artifact_root = trial_dir / "artifacts" / "logs" / "artifacts" + outputs = sorted(artifact_root.glob("output.*")) if artifact_root.is_dir() else [] + if outputs: + output_value = outputs[0].read_text(encoding="utf-8", errors="replace") + + input_value, trace_output_value = _root_values_from_trace(parsed) + if trace_output_value is not None: + output_value = trace_output_value + return test_case_id, input_value, output_value, rewards + + +def _root_values_from_trace(parsed: list[tuple[int, dict[str, Any]]]) -> tuple[str | None, str | None]: + """Return input and output values from root AGENT spans.""" + input_value = None + output_value = None + for _, body in parsed: + root = _find_harbor_root_span(body.get("resourceSpans", [])) + if root is None: + continue + attributes = {item.get("key"): item.get("value", {}) for item in root.get("attributes", [])} + if input_value is None: + input_value = attributes.get("input.value", {}).get("stringValue") + if output_value is None: + output_value = attributes.get("output.value", {}).get("stringValue") + if input_value is not None and output_value is not None: + break + return input_value, output_value + + +def _inject_harbor_root_attributes(resource_spans: list[dict[str, Any]], extra: Mapping[str, str | None]) -> None: + for span in _harbor_root_spans(resource_spans): + attributes = span.setdefault("attributes", []) + existing = {item["key"] for item in attributes} + for key, value in extra.items(): + if value is not None and key not in existing: + attributes.append({"key": key, "value": {"stringValue": value}}) + + +def _find_harbor_root_span_id(resource_spans: list[dict[str, Any]]) -> str | None: + """Return root AGENT span ID in Intake's hexadecimal form.""" + root = _find_harbor_root_span(resource_spans) + if root is None: + return None + return base64.b64decode(root["spanId"]).hex() + + +def _find_harbor_root_span(resource_spans: list[dict[str, Any]]) -> dict[str, Any] | None: + return next(_harbor_root_spans(resource_spans), None) + + +def _harbor_root_spans(resource_spans: list[dict[str, Any]]) -> Iterator[dict[str, Any]]: + """Yield root AGENT spans from OTLP JSON.""" + for resource_span in resource_spans: + for scope_spans in resource_span.get("scopeSpans", []): + for span in scope_spans.get("spans", []): + kind = next( + ( + item.get("value", {}).get("stringValue") + for item in span.get("attributes", []) + if item.get("key") == "openinference.span.kind" + ), + None, + ) + if not span.get("parentSpanId") and kind == "AGENT": + yield span + + +def _inject_resource_attributes(resource_spans: list[dict[str, Any]], extra: Mapping[str, str | None]) -> None: + for resource_span in resource_spans: + attributes = resource_span.setdefault("resource", {}).setdefault("attributes", []) + existing = {attribute["key"] for attribute in attributes} + for key, value in extra.items(): + if value is not None and key not in existing: + attributes.append({"key": key, "value": {"stringValue": value}}) + + +def _find_session_id(resource_spans: list[dict[str, Any]]) -> str | None: + for resource_span in resource_spans: + for scope_spans in resource_span.get("scopeSpans", []): + for span in scope_spans.get("spans", []): + for attribute in span.get("attributes", []): + if attribute.get("key") == "session.id": + return attribute.get("value", {}).get("stringValue") + return None + + +class HarborAdapter: + """Run Harbor, ingest collected OTLP traces, then analyze that evaluation.""" + + def __init__(self, subject: Subject) -> None: + self.subject = subject + + @staticmethod + def _repo_path(value: object, *, repo_root: Path) -> Path: + path = Path(str(value)) + return (repo_root / path).resolve() if not path.is_absolute() else path.resolve() + + @classmethod + def _build_dataset_config(cls, cfg: Mapping[str, object], *, repo_root: Path): + from harbor.models.job.config import DatasetConfig # noqa: PLC0415 + + dataset_path = cfg.get("dataset") + dataset_ref = cfg.get("dataset_ref") + dataset_id = cfg.get("dataset_id") + selected = [value for value in (dataset_path, dataset_ref, dataset_id) if value] + if len(selected) != 1: + raise ValueError("exactly one of config keys 'dataset', 'dataset_ref', or 'dataset_id' is required") + if cfg.get("registry_path") and cfg.get("registry_url"): + raise ValueError("config keys 'registry_path' and 'registry_url' are mutually exclusive") + + n_tasks = int(str(cfg["num_tasks"])) if cfg.get("num_tasks") is not None else None + raw_task_names = cfg.get("task_names") + if raw_task_names is not None and ( + not isinstance(raw_task_names, list) + or not all(isinstance(task_name, str) and task_name for task_name in raw_task_names) + ): + raise ValueError("config key 'task_names' must be a list of non-empty strings") + task_names = ( + [task_name for task_name in raw_task_names if isinstance(task_name, str)] + if isinstance(raw_task_names, list) + else None + ) + if dataset_path: + return DatasetConfig( + path=cls._repo_path(dataset_path, repo_root=repo_root), + n_tasks=n_tasks, + task_names=task_names, + ) + + registry_path = cfg.get("registry_path") + registry_url = str(cfg["registry_url"]) if cfg.get("registry_url") else None + resolved_registry_path = cls._repo_path(registry_path, repo_root=repo_root) if registry_path else None + if dataset_ref: + dataset_name, separator, dataset_version = str(dataset_ref).rpartition("@") + if not separator: + dataset_name = str(dataset_ref) + dataset_version = "" + return DatasetConfig( + name=dataset_name, + version=dataset_version or None, + registry_url=registry_url, + registry_path=resolved_registry_path, + n_tasks=n_tasks, + task_names=task_names, + ) + return DatasetConfig( + name=str(dataset_id), + version=str(cfg["dataset_version"]) if cfg.get("dataset_version") else None, + registry_url=registry_url, + registry_path=resolved_registry_path, + n_tasks=n_tasks, + task_names=task_names, + ) + + @classmethod + def _build_job_config(cls, cfg: Mapping[str, object], *, run_id: str, repo_root: Path): + from harbor.models.job.config import AgentConfig, EnvironmentConfig, JobConfig, VerifierConfig # noqa: PLC0415 + + environment_env: dict[str, str] = {} + if user_llm := cfg.get("user_llm"): + environment_env["TAU2_USER_MODEL"] = str(user_llm) + for config_key, env_key in ( + ("user_reasoning_effort", "TAU2_USER_REASONING_EFFORT"), + ("user_temperature", "TAU2_USER_TEMPERATURE"), + ("user_llm_args_json", "TAU2_USER_LLM_ARGS_JSON"), + ): + if (value := cfg.get(config_key)) is not None: + environment_env[env_key] = str(value) + + verifier_env: dict[str, str] = {} + if verifier_llm := cfg.get("verifier_llm"): + verifier_env["TAU2_NL_ASSERTIONS_MODEL"] = str(verifier_llm) + timeout = float(str(cfg["timeout"])) if cfg.get("timeout") is not None else None + return JobConfig( + job_name=run_id, + jobs_dir=cls._repo_path(cfg.get("jobs_dir", "testbed/tmp/jobs"), repo_root=repo_root), + n_attempts=int(str(cfg.get("num_trials", 1))), + datasets=[cls._build_dataset_config(cfg, repo_root=repo_root)], + agents=[ + AgentConfig( + import_path=str(cfg.get("agent_import_path", "harbor_wrapper:WrappedAgent")), + model_name=str(cfg["agent_llm"]) if cfg.get("agent_llm") else None, + override_timeout_sec=timeout, + ) + ], + environment=EnvironmentConfig(env=environment_env), + verifier=VerifierConfig(env=verifier_env), + n_concurrent_trials=int(str(cfg.get("max_concurrency", 2))), + ) + + def check(self) -> list[str]: + cfg = self.subject.config + missing: list[str] = [f"config key '{key}'" for key in ("base_url", "workspace") if not cfg.get(key)] + if not (agent_dir_value := cfg.get("agent_dir")): + missing.append("config key 'agent_dir'") + elif not self._repo_path(agent_dir_value, repo_root=REPO_ROOT).is_dir(): + missing.append(f"agent_dir '{self._repo_path(agent_dir_value, repo_root=REPO_ROOT)}' is not a directory") + if not (os.environ.get("OPENAI_API_KEY") or os.environ.get("INFERENCE_API_KEY")): + missing.append("env OPENAI_API_KEY or INFERENCE_API_KEY") + if not (os.environ.get("OPENAI_BASE_URL") or os.environ.get("INFERENCE_API_BASE")): + missing.append("env OPENAI_BASE_URL or INFERENCE_API_BASE") + try: + self._build_job_config(cfg, run_id="preflight", repo_root=REPO_ROOT) + except ModuleNotFoundError: + missing.append("Harbor is not installed; sync the experimentalist dependency group") + except (TypeError, ValueError) as exc: + missing.append(str(exc)) + return missing + + async def produce(self) -> dict[str, object]: + cfg = self.subject.config + if missing := self.check(): + raise SystemExit(f"harbor testbed '{self.subject.name}' is missing: " + "; ".join(missing)) + + # The checked-in Harbor wrapper and Tau3 sidecars use OpenAI-compatible + # variable names. Accept the Platform-wide inference names as the source + # of truth so one credential setup works for both testbed and Analyst. + model_api_key = os.environ.get("OPENAI_API_KEY") or os.environ["INFERENCE_API_KEY"] + model_api_base = os.environ.get("OPENAI_BASE_URL") or os.environ["INFERENCE_API_BASE"] + os.environ.setdefault("OPENAI_API_KEY", model_api_key) + os.environ.setdefault("OPENAI_BASE_URL", model_api_base) + + agent_dir = self._repo_path(cfg["agent_dir"], repo_root=REPO_ROOT) + base_url = str(cfg["base_url"]) + workspace = str(cfg["workspace"]) + agent_name = str(cfg.get("agent_name", "agent-0")) + run_id = mint_agent_id(workspace) + created_at = datetime.now(timezone.utc).isoformat() + raw_dataset = str(cfg.get("dataset_ref") or cfg.get("dataset_id") or Path(str(cfg["dataset"])).name) + dataset_name, separator, ref_version = raw_dataset.rpartition("@") + if not separator: + dataset_name = raw_dataset + dataset_version = str(cfg.get("dataset_version") or ref_version or "unversioned") + + ensure_workspace(base_url, workspace) + group_id = ensure_experiment_group(base_url, workspace, workspace) + create_experiment( + base_url, + workspace, + name=run_id, + experiment_group_id=group_id, + dataset_name=dataset_name, + dataset_version=dataset_version, + metadata={"agent": agent_name, "model": str(cfg.get("agent_llm", "")), "producer": "harbor"}, + ) + + agent_dir_string = str(agent_dir) + if agent_dir_string not in sys.path: + sys.path.insert(0, agent_dir_string) + + from harbor.job import Job # noqa: PLC0415 + + job_config = self._build_job_config(cfg, run_id=run_id, repo_root=REPO_ROOT) + job = await Job.create(job_config) + await job.run() + + trace_dir = job_config.jobs_dir / run_id + sent, errors, session_ids = _export_harbor_trace_files( + base_url, + workspace, + trace_dir, + agent_name, + evaluation_id=run_id, + agent_version=str(cfg.get("agent_version", "")) or None, + ) + if sent == 0: + raise SystemExit(f"harbor testbed '{self.subject.name}': no OTLP traces found under {trace_dir}") + if errors: + print(f"warning: {errors} Harbor trace upload error(s) for '{self.subject.name}'.", file=sys.stderr) + if len(session_ids) < 3: + print(f"warning: only {len(session_ids)} session(s) ingested; the analyst needs 3+.", file=sys.stderr) + if session_ids: + visible = poll_visible(base_url, workspace, session_ids) + if len(visible) < len(session_ids): + print( + f"warning: only {len(visible)}/{len(session_ids)} session(s) visible in Intake.", + file=sys.stderr, + ) + + return { + "agent": agent_name, + "workspace": workspace, + "base_url": base_url, + "run_id": run_id, + "experiment_id": run_id, + "experiment_group": workspace, + "dataset_name": dataset_name, + "dataset_version": dataset_version, + "created_at": created_at, + } + + async def analyze( + self, + *, + record: dict[str, object] | None, + since: datetime | None, + verbose: bool, + out_path: Path, + ) -> str: + if record is None: + raise SystemExit( + f"no recorded run for '{self.subject.name}' — run " + f"`uv run python -m testbed run {self.subject.name}` first" + ) + return await run_analyst( + agent=str(record["agent"]), + agent_spec=None, + workspace=str(record["workspace"]), + base_url=str(record["base_url"]), + client=make_client(str(record["base_url"])), + insights_output=str(out_path), + verbose=verbose, + since=since, + evaluation_id=str(record["experiment_id"]), + ) + + +_ADAPTERS: dict[str, type[IntakeAdapter] | type[BenchmarkAdapter] | type[HarborAdapter]] = { "intake": IntakeAdapter, "benchmark": BenchmarkAdapter, + "harbor": HarborAdapter, } diff --git a/plugins/nemo-insights/testbed/cli.py b/plugins/nemo-insights/testbed/cli.py index d781155dfc..47e648d7f7 100644 --- a/plugins/nemo-insights/testbed/cli.py +++ b/plugins/nemo-insights/testbed/cli.py @@ -106,7 +106,7 @@ def _doctor(subjects: dict[str, Subject], name: str | None) -> None: unmet: list[str] = [] if not os.environ.get("INFERENCE_API_KEY"): unmet.append("env INFERENCE_API_KEY (analyst key, in testbed/.env)") - if shutil.which("gh") is None: + if subject.type in ("benchmark", "intake") and shutil.which("gh") is None: unmet.append("gh CLI (needed for pinned/--state analyze; https://cli.github.com)") unmet += build_adapter(subject).check() if unmet: @@ -114,7 +114,10 @@ def _doctor(subjects: dict[str, Subject], name: str | None) -> None: for item in unmet: print(f" - {item}") else: - print(f"✓ {subject_name} ({subject.type}) — ready: uv run python -m testbed analyze {subject_name} --live") + action = ( + f"run {subject_name}" if subject.type in ("benchmark", "harbor") else f"analyze {subject_name} --live" + ) + print(f"✓ {subject_name} ({subject.type}) — ready: uv run python -m testbed {action}") def _atomic_write_text(path: Path, contents: str) -> None: @@ -865,7 +868,7 @@ def main() -> None: p_doc.add_argument("name", nargs="?", help="Subject name; omit to check every subject.") p_run = sub.add_parser( "run", - help="Produce traces for a subject (benchmark: run tau2 + ingest), then record the run.", + help="Produce and ingest traces for a benchmark or Harbor subject, then record the run.", ) p_run.add_argument("name", help="Subject name from testbeds.toml.") p_run.add_argument( @@ -903,9 +906,14 @@ def main() -> None: record = asyncio.run(build_adapter(subject).produce()) TMP.mkdir(parents=True, exist_ok=True) save_run(TMP / f"{args.name}.run.json", record) - ws_line = f"realistic ws '{record['realistic_workspace']}'" - if record.get("oracle_workspace"): - ws_line += f" + oracle ws '{record['oracle_workspace']}'" + if realistic_workspace := record.get("realistic_workspace"): + ws_line = f"realistic ws '{realistic_workspace}'" + if record.get("oracle_workspace"): + ws_line += f" + oracle ws '{record['oracle_workspace']}'" + elif workspace := record.get("workspace"): + ws_line = f"ws '{workspace}'" + else: + ws_line = "workspace not recorded" print( f"✓ recorded run '{record['agent']}' ({ws_line}) — analyze with: " f"uv run python -m testbed analyze {args.name} --live" diff --git a/plugins/nemo-insights/testbed/otlp_ingest.py b/plugins/nemo-insights/testbed/otlp_ingest.py index ea1b618eed..4b7d9a97dc 100644 --- a/plugins/nemo-insights/testbed/otlp_ingest.py +++ b/plugins/nemo-insights/testbed/otlp_ingest.py @@ -6,8 +6,8 @@ ``opentelemetry.proto`` ``ExportTraceServiceRequest`` and POSTs it to Intake's permissive OTLP route. OTLP ingest does **not** auto-create the queryable ``evaluator_results`` rows the Analyst reads, so :func:`post_evaluator_results` -separately POSTs the reward (mirroring exactly the row the ATIF importer used to -create: ``name="reward"``, ``NUMERIC``, targeting the EVALUATOR span). +separately POSTs verifier rewards as ``NUMERIC`` evaluator rows targeting the +relevant root span. The protobuf build mirrors nemo-platform's own ``services/intake/tests/integration/spans/conftest.py::make_otlp_request`` helper, @@ -85,19 +85,20 @@ def post_evaluator_results( span_id: str, session_id: str, score: float, + name: str = "reward", client: httpx.Client | None = None, ) -> None: """POST the reward row the OTLP path doesn't auto-create. - Reproduces the ATIF importer's row exactly: ``name="reward"``, ``NUMERIC``, - targeting the EVALUATOR span — so the Analyst reads the reward unchanged. + ``name`` defaults to ``reward`` for Tau2 compatibility; Harbor adapters pass + each verifier criterion's name. The Analyst reads every value unchanged. Raises ``RuntimeError`` on any non-2xx. """ url = f"{base_url.rstrip('/')}/apis/intake/v2/workspaces/{workspace}/evaluator-results" body = { "span_id": span_id, "session_id": session_id, - "name": "reward", + "name": name, "value": score, "data_type": "NUMERIC", } diff --git a/plugins/nemo-insights/testbed/testbeds.toml b/plugins/nemo-insights/testbed/testbeds.toml index ed684fb102..3ac5f819e3 100644 --- a/plugins/nemo-insights/testbed/testbeds.toml +++ b/plugins/nemo-insights/testbed/testbeds.toml @@ -30,6 +30,30 @@ agent = "nemo-oo-airline" workspace = "tau2-airline-20260710-152942-4754" base_url = "http://localhost:8080" +[tau3-airline-harbor] +type = "harbor" +agent_dir = "../nemo-experimentalist/examples/tau3-nooa-agent" +base_url = "http://localhost:8080" +workspace = "canonical-tau3-airline" +agent_name = "nemo-experimentalist-tau3-nooa" +agent_version = "1.0.0" +agent_llm = "openai/openai/openai/gpt-5-mini" +user_llm = "openai/openai/openai/gpt-5-mini" +verifier_llm = "openai/openai/openai/gpt-5-mini" +dataset_ref = "sierra-research/tau3-bench@1" +registry_url = "https://hub.harborframework.com" +task_names = [ + "sierra-research/tau3-bench__tau3-airline-0", + "sierra-research/tau3-bench__tau3-airline-1", + "sierra-research/tau3-bench__tau3-airline-4", + "sierra-research/tau3-bench__tau3-airline-5", + "sierra-research/tau3-bench__tau3-airline-9", + "sierra-research/tau3-bench__tau3-airline-10", +] +num_trials = 1 +max_concurrency = 6 +timeout = 3600 + [tau2-airline] type = "benchmark" domain = "airline" diff --git a/plugins/nemo-insights/tests/testbed/test_adapters.py b/plugins/nemo-insights/tests/testbed/test_adapters.py index 04645bdf17..0cbd796352 100644 --- a/plugins/nemo-insights/tests/testbed/test_adapters.py +++ b/plugins/nemo-insights/tests/testbed/test_adapters.py @@ -5,7 +5,15 @@ from typing import Any import pytest -from testbed.adapters import BenchmarkAdapter, IntakeAdapter, build_adapter +from testbed.adapters import ( + BenchmarkAdapter, + HarborAdapter, + IntakeAdapter, + _export_harbor_trace_files, + _harbor_trace_context, + _root_values_from_trace, + build_adapter, +) from testbed.registry import Subject _CFG = { @@ -37,6 +45,148 @@ }, ] +_HARBOR_OTLP_JSON = ( + '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"harbor"}}]},' + '"scopeSpans":[{"spans":[{"traceId":"AAAAAAAAAAAAAAAAAAAAAQ==","spanId":"AAAAAAAAAAE=",' + '"name":"agent","startTimeUnixNano":"1","endTimeUnixNano":"2",' + '"attributes":[{"key":"session.id","value":{"stringValue":"sess-1"}}]}]}]}]}' +) + + +def test_harbor_trace_conversion_enriches_and_exports(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + (tmp_path / "fallback-session.jsonl").write_text(f"{_HARBOR_OTLP_JSON}\n", encoding="utf-8") + calls: list[dict[str, object]] = [] + + def capture_export(base_url, workspace, request, *, client=None, headers=None): + calls.append({"base_url": base_url, "workspace": workspace, "request": request, "headers": headers}) + + monkeypatch.setattr("testbed.adapters.export_trace_request", capture_export) + + result = _export_harbor_trace_files( + "http://x", + "ws", + tmp_path, + "agent-0", + evaluation_id="evaluation-1", + ) + + assert result == (1, 0, {"sess-1"}) + assert calls[0]["headers"] is None + request = calls[0]["request"] + resource_attrs = { + item.key: getattr(item.value, item.value.WhichOneof("value")) + for item in request.resource_spans[0].resource.attributes + } + assert resource_attrs == { + "service.name": "harbor", + "gen_ai.agent.name": "agent-0", + "gen_ai.agent.id": "agent-0", + "session.id": "sess-1", + "gen_ai.conversation.id": "sess-1", + "nemo.experiment.id": "evaluation-1", + "nemo.optimizer.workspace": "ws", + } + + +def test_harbor_trace_conversion_adds_task_fields_and_rewards( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + trial_dir = tmp_path / "task-1__trial" + trace_dir = trial_dir / "artifacts" / "logs" / "artifacts" / "traces" + trace_dir.mkdir(parents=True) + (trial_dir / "result.json").write_text( + json.dumps({"task_name": "task-1", "verifier_result": {"rewards": {"score": 0.75, "reward": 1.0}}}), + encoding="utf-8", + ) + output_path = trial_dir / "artifacts" / "logs" / "artifacts" / "output.md" + output_path.write_text("# Final answer", encoding="utf-8") + body = json.loads(_HARBOR_OTLP_JSON) + root = body["resourceSpans"][0]["scopeSpans"][0]["spans"][0] + root["name"] = "method.solve" + root["attributes"].extend( + [ + {"key": "openinference.span.kind", "value": {"stringValue": "AGENT"}}, + {"key": "input.value", "value": {"stringValue": "Research this topic"}}, + ] + ) + (trace_dir / "trace.jsonl").write_text(json.dumps(body), encoding="utf-8") + exports: list[object] = [] + rewards: list[dict[str, object]] = [] + monkeypatch.setattr( + "testbed.adapters.export_trace_request", + lambda base_url, workspace, request, *, client=None, headers=None: exports.append(request), + ) + monkeypatch.setattr( + "testbed.adapters.post_evaluator_results", + lambda base_url, workspace, **kwargs: rewards.append( + {key: value for key, value in kwargs.items() if key != "client"} + ), + ) + + assert _export_harbor_trace_files("http://x", "ws", tmp_path, "agent-0", evaluation_id="evaluation-1") == ( + 1, + 0, + {"sess-1"}, + ) + exported_root = exports[0].resource_spans[0].scope_spans[0].spans[0] + attributes = {item.key: getattr(item.value, item.value.WhichOneof("value")) for item in exported_root.attributes} + assert attributes["nemo.test_case.id"] == "task-1" + assert attributes["input.value"] == "Research this topic" + assert attributes["output.value"] == "# Final answer" + assert rewards == [ + {"span_id": exported_root.span_id.hex(), "session_id": "sess-1", "score": 0.75, "name": "score"}, + {"span_id": exported_root.span_id.hex(), "session_id": "sess-1", "score": 1.0, "name": "reward"}, + ] + + +def test_harbor_trace_context_drops_rewards_for_failed_trial(tmp_path: Path) -> None: + trial_dir = tmp_path / "task-1__trial" + trace_dir = trial_dir / "artifacts" / "logs" / "artifacts" / "traces" + trace_dir.mkdir(parents=True) + path = trace_dir / "trace.jsonl" + path.write_text("", encoding="utf-8") + (trial_dir / "result.json").write_text( + json.dumps( + { + "task_name": "task-1", + "exception_info": {"exception_type": "RuntimeError"}, + "verifier_result": {"rewards": {"reward": 1.0}}, + } + ), + encoding="utf-8", + ) + + test_case_id, _, _, rewards = _harbor_trace_context(path, tmp_path, []) + + assert test_case_id == "task-1" + assert rewards == {} + + +def test_root_values_from_trace_reads_only_root_agent_span() -> None: + body = json.loads(_HARBOR_OTLP_JSON) + spans = body["resourceSpans"][0]["scopeSpans"][0]["spans"] + spans[0]["attributes"].extend( + [ + {"key": "openinference.span.kind", "value": {"stringValue": "AGENT"}}, + {"key": "input.value", "value": {"stringValue": "root input"}}, + {"key": "output.value", "value": {"stringValue": "root output"}}, + ] + ) + spans.append( + { + "traceId": "AAAAAAAAAAAAAAAAAAAAAQ==", + "spanId": "AAAAAAAAAAI=", + "parentSpanId": "AAAAAAAAAAE=", + "name": "acompletion", + "startTimeUnixNano": "1", + "endTimeUnixNano": "2", + "attributes": [{"key": "input.value", "value": {"stringValue": "child input"}}], + } + ) + + assert _root_values_from_trace([(1, body)]) == ("root input", "root output") + def _intake_subject(**overrides) -> Subject: config = {"agent": "a", "workspace": "w", "base_url": "u", **overrides} @@ -169,6 +319,92 @@ def test_build_adapter_dispatches_benchmark(): assert isinstance(adapter, BenchmarkAdapter) +def test_build_adapter_dispatches_harbor(): + adapter = build_adapter(Subject("tau3-airline-harbor", "harbor", {})) + assert isinstance(adapter, HarborAdapter) + + +def test_harbor_dataset_config_accepts_hub_ref(): + config = HarborAdapter._build_dataset_config( + { + "dataset_ref": "sierra-research/tau3-bench@1", + "registry_url": "https://hub.harborframework.com", + "task_names": [ + "sierra-research/tau3-bench__tau3-airline-0", + "sierra-research/tau3-bench__tau3-airline-1", + ], + }, + repo_root=Path("/repo"), + ) + + assert config.name == "sierra-research/tau3-bench" + assert config.version == "1" + assert config.registry_url == "https://hub.harborframework.com" + assert config.task_names == [ + "sierra-research/tau3-bench__tau3-airline-0", + "sierra-research/tau3-bench__tau3-airline-1", + ] + + +def test_harbor_dataset_config_rejects_ambiguous_source(): + with pytest.raises(ValueError, match="exactly one"): + HarborAdapter._build_dataset_config( + {"dataset": "local", "dataset_ref": "org/data@1"}, + repo_root=Path("/repo"), + ) + + +def test_harbor_check_accepts_platform_inference_environment( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.delenv("OPENAI_BASE_URL", raising=False) + monkeypatch.setenv("INFERENCE_API_KEY", "secret") + monkeypatch.setenv("INFERENCE_API_BASE", "https://inference.example/v1") + subject = Subject( + "tau3-airline-harbor", + "harbor", + { + "base_url": "http://localhost:8080", + "workspace": "canonical-tau3-airline", + "agent_dir": str(tmp_path), + "dataset_ref": "sierra-research/tau3-bench@1", + }, + ) + + assert HarborAdapter(subject).check() == [] + + +async def test_harbor_analyze_uses_record(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + seen: dict[str, object] = {} + + async def fake_run_analyst(**kwargs: object) -> str: + seen.update(kwargs) + return "REPORT" + + monkeypatch.setattr("testbed.adapters.run_analyst", fake_run_analyst) + monkeypatch.setattr("testbed.adapters.make_client", lambda base_url: object()) + record = { + "agent": "nemo-experimentalist-tau3-nooa", + "workspace": "canonical-tau3-airline", + "base_url": "http://localhost:8080", + "experiment_id": "canonical-tau3-airline-20260731-120000-abcd", + } + + report = await HarborAdapter(Subject("tau3-airline-harbor", "harbor", {})).analyze( + record=record, + since=None, + verbose=True, + out_path=tmp_path / "insights.yaml", + ) + + assert report == "REPORT" + assert seen["agent"] == "nemo-experimentalist-tau3-nooa" + assert seen["workspace"] == "canonical-tau3-airline" + assert seen["evaluation_id"] == record["experiment_id"] + + async def test_benchmark_preflight_lists_missing(monkeypatch): monkeypatch.delenv("OPENAI_API_KEY", raising=False) monkeypatch.delenv("OPENAI_API_BASE", raising=False) diff --git a/plugins/nemo-insights/tests/testbed/test_cli.py b/plugins/nemo-insights/tests/testbed/test_cli.py index 7a982c2d59..9c932ce318 100644 --- a/plugins/nemo-insights/tests/testbed/test_cli.py +++ b/plugins/nemo-insights/tests/testbed/test_cli.py @@ -899,6 +899,20 @@ def test_doctor_flags_missing_gh(monkeypatch, capsys): assert "gh CLI (needed for pinned/--state analyze; https://cli.github.com)" in out +def test_doctor_harbor_does_not_require_gh(monkeypatch, capsys): + monkeypatch.setattr(cli, "_load_dotenv", lambda *a, **k: None) + monkeypatch.setenv("INFERENCE_API_KEY", "sk") + monkeypatch.setattr("testbed.adapters.HarborAdapter.check", lambda self: []) + monkeypatch.setattr(cli.shutil, "which", lambda _name: None) + monkeypatch.setattr(sys, "argv", ["testbed", "doctor", "tau3-airline-harbor"]) + + cli.main() + + out = capsys.readouterr().out + assert "✓ tau3-airline-harbor (harbor) — ready: uv run python -m testbed run tau3-airline-harbor" in out + assert "gh CLI" not in out + + def test_doctor_lists_unmet(monkeypatch, capsys): monkeypatch.setattr(cli, "_load_dotenv", lambda *a, **k: None) # don't let .env supply the key monkeypatch.delenv("INFERENCE_API_KEY", raising=False) @@ -934,6 +948,30 @@ async def fake_produce(self): assert load_run(tmp_path / "tau2-airline.run.json")["agent"] == "tau2-airline-xyz" +def test_run_harbor_records_and_prints_workspace(monkeypatch, tmp_path, capsys): + monkeypatch.setattr(cli, "TMP", tmp_path) + + async def fake_produce(self): + return { + "agent": "nemo-experimentalist-tau3-nooa", + "workspace": "canonical-tau3-airline", + "base_url": "http://localhost:8080", + "experiment_id": "eval-1", + } + + monkeypatch.setattr("testbed.adapters.HarborAdapter.produce", fake_produce) + monkeypatch.setattr(sys, "argv", ["testbed", "run", "tau3-airline-harbor"]) + + cli.main() + + out = capsys.readouterr().out + assert "ws 'canonical-tau3-airline'" in out + assert "analyze tau3-airline-harbor --live" in out + from testbed.runstore import load_run + + assert load_run(tmp_path / "tau3-airline-harbor.run.json")["experiment_id"] == "eval-1" + + def test_analyze_live_passes_record_to_analyze(monkeypatch, tmp_path): from testbed.runstore import save_run diff --git a/plugins/nemo-insights/tests/testbed/test_otlp_ingest.py b/plugins/nemo-insights/tests/testbed/test_otlp_ingest.py index 03e27b1db6..a758d46420 100644 --- a/plugins/nemo-insights/tests/testbed/test_otlp_ingest.py +++ b/plugins/nemo-insights/tests/testbed/test_otlp_ingest.py @@ -189,6 +189,22 @@ def test_post_evaluator_results_route_and_body(): } +def test_post_evaluator_results_accepts_named_criterion(): + stub = _JsonStub(status=201) + + post_evaluator_results( + "http://x", + "ws", + span_id="sp1", + session_id="sess-1", + score=0.75, + name="policy_compliance", + client=stub, + ) + + assert stub.calls[0][1]["name"] == "policy_compliance" + + def test_post_evaluator_results_raises_on_error(): with pytest.raises(RuntimeError): post_evaluator_results("http://x", "ws", span_id="s", session_id="z", score=0.0, client=_JsonStub(status=500)) diff --git a/plugins/nemo-insights/tests/testbed/test_registry.py b/plugins/nemo-insights/tests/testbed/test_registry.py index dd7b286aa3..bfd1b8bf4b 100644 --- a/plugins/nemo-insights/tests/testbed/test_registry.py +++ b/plugins/nemo-insights/tests/testbed/test_registry.py @@ -35,8 +35,9 @@ def test_registry_contains_only_expected_analyzable_subjects() -> None: "tau2-airline", "tau2-retail", "tau2-telecom", + "tau3-airline-harbor", } - assert all(subject.type in ("benchmark", "intake") for subject in subjects.values()) + assert all(subject.type in ("benchmark", "harbor", "intake") for subject in subjects.values()) assert subjects["nvq"].config["agent"] == "content-dedup" @@ -64,6 +65,22 @@ def test_tau2_telecom_uses_small_split() -> None: assert telecom.config["task_split_name"] == "small" +def test_tau3_airline_harbor_uses_checked_in_agent_and_hub_dataset() -> None: + subject = load_registry(cli.REGISTRY_PATH)["tau3-airline-harbor"] + + assert subject.type == "harbor" + assert subject.config["agent_dir"] == "../nemo-experimentalist/examples/tau3-nooa-agent" + assert subject.config["dataset_ref"] == "sierra-research/tau3-bench@1" + assert subject.config["task_names"] == [ + "sierra-research/tau3-bench__tau3-airline-0", + "sierra-research/tau3-bench__tau3-airline-1", + "sierra-research/tau3-bench__tau3-airline-4", + "sierra-research/tau3-bench__tau3-airline-5", + "sierra-research/tau3-bench__tau3-airline-9", + "sierra-research/tau3-bench__tau3-airline-10", + ] + + def test_every_analyzable_subject_has_expected_state_pin() -> None: expected = { "glamr": "state-v8", @@ -74,6 +91,7 @@ def test_every_analyzable_subject_has_expected_state_pin() -> None: "tau2-telecom": "state-v10", } - assert { - name: release.lock_ref(cli.HERE / "state.lock", name) for name in sorted(load_registry(cli.REGISTRY_PATH)) - } == expected + subjects = load_registry(cli.REGISTRY_PATH) + reproducible = sorted(name for name, subject in subjects.items() if subject.type in ("benchmark", "intake")) + + assert {name: release.lock_ref(cli.HERE / "state.lock", name) for name in reproducible} == expected diff --git a/uv.lock b/uv.lock index 1ebcda6cd5..fab3e9bfa8 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" }, ] @@ -721,15 +705,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" }, @@ -761,29 +741,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" }, ] @@ -859,21 +823,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" }, ] @@ -922,29 +878,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" }, ] @@ -962,11 +906,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" }, @@ -974,11 +915,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" }, @@ -1623,23 +1561,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" }, ] @@ -1810,37 +1738,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" }, ] @@ -1998,15 +1908,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" }, @@ -2030,13 +1936,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" }, @@ -2133,19 +2037,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" }, ] @@ -2560,17 +2458,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" }, @@ -2646,21 +2538,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" }, ] @@ -3243,27 +3127,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" }, ] @@ -3326,23 +3198,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" }, ] @@ -3566,11 +3432,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" }, @@ -3579,11 +3441,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" }, ] @@ -3650,38 +3508,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" }, ] @@ -4339,6 +4179,7 @@ name = "nemo-experimentalist-plugin" version = "0.1.0" source = { editable = "plugins/nemo-experimentalist" } dependencies = [ + { name = "fastapi", 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 = "harbor", 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 = "nemo-eval-author-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')" }, @@ -4349,12 +4190,15 @@ dependencies = [ { name = "opentelemetry-proto", 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')" }, { 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 = "python-multipart", 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')" }, { name = "tomlkit", 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 = "uvicorn", 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.metadata] requires-dist = [ + { name = "fastapi", specifier = ">=0.115.4" }, { name = "harbor", specifier = ">=0.16" }, { name = "httpx" }, { name = "nemo-eval-author-plugin", editable = "plugins/nemo-eval-author" }, @@ -4365,8 +4209,10 @@ requires-dist = [ { name = "opentelemetry-proto", specifier = ">=1.42.1" }, { name = "protobuf", specifier = ">=6.0.0" }, { name = "pydantic", specifier = ">=2" }, + { name = "python-multipart", specifier = ">=0.0.20" }, { name = "pyyaml", specifier = ">=6.0.3" }, { name = "tomlkit", specifier = ">=0.13.3" }, + { name = "uvicorn", specifier = ">=0.34.0" }, ] [[package]] @@ -8649,22 +8495,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" }, ] @@ -8676,17 +8514,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" }, ] @@ -8978,35 +8812,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" }, ] @@ -9019,7 +8838,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" }, ] @@ -9048,21 +8866,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" }, ] @@ -9117,19 +8927,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" }, ] @@ -9266,21 +9072,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" }, @@ -9326,17 +9124,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" }, @@ -9699,13 +9491,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" }, @@ -9719,13 +9509,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" }, @@ -9818,38 +9606,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" }, ] @@ -9965,21 +9735,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" }, ] @@ -10012,29 +9774,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" }, ] @@ -10054,16 +9804,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" }, ] @@ -10104,13 +9848,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" }, ] @@ -10826,12 +10565,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" }, ] @@ -11058,16 +10793,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" }, ] @@ -11364,11 +11093,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" }, ] @@ -11501,25 +11227,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" }, @@ -11661,30 +11378,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" }, ] @@ -11715,44 +11420,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" }, ] @@ -11774,24 +11455,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" }, ]