From 61c12994d9ca3f97a572e0d9b394867f46fb5a6a Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Wed, 3 Jun 2026 11:19:37 -0600 Subject: [PATCH 01/26] feat: plugin-contributed authorization policy merge Add AuthzContribution discovery from nemo.authz entry points, NemoService classes, and customization contributors; merge into OPA bundle at runtime and via auth-tools sync-plugins. Pass Authorization and other SDK default headers through submit_remote so protected routes receive credentials. Extend test_authz with a contributor example and coverage for authz discovery plus authenticated submit. Signed-off-by: Sam Oluwalana --- .../src/nemo_platform_plugin/discovery.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py index 9a75ffc909..3ea7f1f187 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py @@ -216,8 +216,12 @@ def discover_manifests() -> dict[str, PluginManifest]: # ``dist.metadata`` is ``email.message.Message``-compatible # and supports ``.get`` at runtime; ty's stub for # ``importlib.metadata.PackageMetadata`` doesn't expose it. - version = dist.metadata.get("Version", "") if dist is not None else "" # ty: ignore[unresolved-attribute] - description = dist.metadata.get("Summary", "") if dist is not None else "" # ty: ignore[unresolved-attribute] + version = ( + dist.metadata.get("Version", "") if dist is not None else "" + ) # ty: ignore[unresolved-attribute] + description = ( + dist.metadata.get("Summary", "") if dist is not None else "" + ) # ty: ignore[unresolved-attribute] except Exception: version = "" description = "" From 6397f68b019fa4736b1ca7ee230eae26df6b8ccb Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Thu, 21 May 2026 12:50:46 -0600 Subject: [PATCH 02/26] feat: add customizer as a plugin - AIRCORE-350 Respect the NMP_PLATFORM_SEED_AUTH_ENABLED=false env var Auth is required for local seed, update the default for local env fix bug with submit missing credentials Add functionality to allow plugins to update authz routes. uv run python services/core/auth/scripts/auth-tools.py sync-plugins Add skill for customization - add sizing guidance to the SKILL Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Sam Oluwalana --- .cursor/rules/nemo-platform.mdc | 3 +- AGENTS.md | 3 +- CLAUDE.md | 3 +- docker-bake.automodel.hcl | 203 ++++ docs/agents/plugins.md | 2 +- docs/set-up/config-reference.md | 2 + .../skills/nemo-fine-tune/SKILL.md | 43 - .../skills/nemo-fine-tune/tests.json | 65 -- .../skills/nemo-skill-selection/SKILL.md | 8 +- .../nemo_platform_ext/tests/cli/test_app.py | 3 +- .../src/nemo_platform_plugin/commands.py | 16 + .../customization_contributor.py | 36 + .../src/nemo_platform_plugin/discovery.py | 78 +- .../tests/test_commands.py | 29 + .../tests/test_discovery.py | 56 + packages/nmp_platform/README.md | 8 + packages/nmp_platform/config/local.env | 31 +- packages/nmp_platform/config/local.yaml | 28 +- .../src/nmp/platform_runner/config/local.yaml | 9 +- .../src/nmp/platform_runner/registry.py | 1 + .../tests/test_registry.py | 20 + plugins/nemo-automodel/README.md | 30 + plugins/nemo-automodel/SCOPE.md | 967 ++++++++++++++++++ plugins/nemo-automodel/pyproject.toml | 51 + .../src/nemo_automodel_plugin/__init__.py | 4 + .../src/nemo_automodel_plugin/cli/__init__.py | 9 + .../src/nemo_automodel_plugin/cli/inputs.py | 98 ++ .../src/nemo_automodel_plugin/cli/main.py | 20 + .../src/nemo_automodel_plugin/config.py | 29 + .../src/nemo_automodel_plugin/contributor.py | 88 ++ .../nemo_automodel_plugin/jobs/__init__.py | 2 + .../src/nemo_automodel_plugin/jobs/jobs.py | 95 ++ .../src/nemo_automodel_plugin/schema.py | 240 +++++ .../src/nemo_automodel_plugin/sdk/__init__.py | 18 + .../nemo_automodel_plugin/sdk/http_utils.py | 64 ++ .../sdk/job_resources.py | 86 ++ .../nemo_automodel_plugin/sdk/resources.py | 164 +++ .../src/nemo_automodel_plugin/transform.py | 99 ++ .../tests/fixtures/minimal_sft_lora.json | 30 + .../tests/fixtures/qwen3_0.6b_sft_lora.json | 30 + plugins/nemo-automodel/tests/test_api.py | 53 + plugins/nemo-automodel/tests/test_cli.py | 124 +++ .../nemo-automodel/tests/test_contributor.py | 28 + plugins/nemo-automodel/tests/test_schema.py | 28 + plugins/nemo-customizer/README.md | 7 + plugins/nemo-customizer/docs/CUSTOMIZATION.md | 23 + plugins/nemo-customizer/pyproject.toml | 53 + .../src/nemo_customizer/__init__.py | 12 + .../src/nemo_customizer/cli.py | 42 + .../src/nemo_customizer/contributor.py | 8 + .../src/nemo_customizer/discovery.py | 16 + .../src/nemo_customizer/router.py | 96 ++ .../src/nemo_customizer/sdk/__init__.py | 16 + .../src/nemo_customizer/sdk/resources.py | 74 ++ .../src/nemo_customizer/skills.py | 14 + .../skills/nemo-customizer/SKILL.md | 287 ++++++ .../references/dataset-formats.md | 16 + .../references/hf-conversion.md | 54 + .../references/hyperparameters.md | 313 ++++++ .../references/troubleshooting.md | 69 ++ .../scripts/poll_automodel_job.sh | 34 + .../skills/nemo-customizer/tests.json | 65 ++ .../test_customization_discovery_reexport.py | 21 + plugins/nemo-customizer/tests/test_router.py | 103 ++ plugins/nemo-customizer/tests/test_sdk.py | 37 + plugins/nemo-customizer/tests/test_skills.py | 21 + pyproject.toml | 8 + .../skills/nemo-fine-tune/SKILL.md | 43 - .../skills/nemo-skill-selection/SKILL.md | 8 +- services/automodel/README.md | 3 + .../automodel/docker/Dockerfile.mamba-wheel | 245 +++++ .../docker/Dockerfile.nmp-automodel-base | 92 ++ .../docker/Dockerfile.nmp-automodel-tasks | 49 + .../docker/Dockerfile.nmp-automodel-training | 54 + .../docker/Dockerfile.platform-workspace | 20 + services/automodel/docker/README.md | 98 ++ services/automodel/docker/docker-bake.hcl | 4 + services/automodel/docker/locks/README.md | 11 + .../mamba-wheel-build-py311/pyproject.toml | 27 + .../locks/mamba-wheel-build-py311/uv.lock | 355 +++++++ .../mamba-wheel-build-py312/pyproject.toml | 27 + .../locks/mamba-wheel-build-py312/uv.lock | 356 +++++++ .../docker/no_override_requirements.txt | 10 + .../automodel/docker/pyproject.workspace.toml | 31 + services/automodel/pyproject.toml | 40 + .../automodel/src/nmp/automodel/__init__.py | 4 + .../automodel/src/nmp/automodel/adapter.py | 132 +++ .../src/nmp/automodel/api/__init__.py | 0 .../src/nmp/automodel/api/v2/__init__.py | 0 .../src/nmp/automodel/api/v2/jobs/__init__.py | 0 .../src/nmp/automodel/api/v2/jobs/schemas.py | 639 ++++++++++++ .../src/nmp/automodel/app/__init__.py | 4 + .../src/nmp/automodel/app/constants.py | 173 ++++ .../src/nmp/automodel/app/jobs/__init__.py | 2 + .../src/nmp/automodel/app/jobs/compiler.py | 501 +++++++++ .../src/nmp/automodel/app/jobs/context.py | 82 ++ .../nmp/automodel/app/jobs/file_io/schemas.py | 182 ++++ .../app/jobs/model_entity/__init__.py | 11 + .../app/jobs/model_entity/schemas.py | 106 ++ .../automodel/app/jobs/training/compiler.py | 399 ++++++++ .../automodel/app/jobs/training/schemas.py | 293 ++++++ .../automodel/src/nmp/automodel/compile.py | 34 + .../automodel/src/nmp/automodel/config.py | 49 + .../src/nmp/automodel/entities/__init__.py | 29 + .../src/nmp/automodel/entities/validators.py | 47 + .../src/nmp/automodel/entities/values.py | 96 ++ .../automodel/src/nmp/automodel/images.py | 47 + .../src/nmp/automodel/platform_client.py | 39 + .../src/nmp/automodel/tasks/__init__.py | 4 + .../src/nmp/automodel/tasks/__main__.py | 48 + .../src/nmp/automodel/tasks/docker/README.md | 74 ++ .../tasks/docker/docker-compose.yaml | 52 + .../nmp/automodel/tasks/file_io/__init__.py | 8 + .../nmp/automodel/tasks/file_io/__main__.py | 9 + .../nmp/automodel/tasks/file_io/callbacks.py | 783 ++++++++++++++ .../tasks/file_io/progress_reporter.py | 93 ++ .../src/nmp/automodel/tasks/file_io/run.py | 560 ++++++++++ .../src/nmp/automodel/tasks/file_io/utils.py | 184 ++++ .../automodel/tasks/model_entity/__init__.py | 8 + .../automodel/tasks/model_entity/__main__.py | 15 + .../nmp/automodel/tasks/model_entity/run.py | 436 ++++++++ .../nmp/automodel/tasks/progress_reporter.py | 12 + .../nmp/automodel/tasks/training/__init__.py | 0 .../nmp/automodel/tasks/training/__main__.py | 40 + .../tasks/training/backends/__init__.py | 20 + .../tasks/training/backends/backend.py | 187 ++++ .../tasks/training/backends/callbacks.py | 94 ++ .../tasks/training/backends/checkpoints.py | 518 ++++++++++ .../tasks/training/backends/config.py | 848 +++++++++++++++ .../tasks/training/backends/finetune.py | 260 +++++ .../tasks/training/backends/requirements.txt | 1 + .../tasks/training/chat_templates.py | 196 ++++ .../tasks/training/datasets/preparation.py | 509 +++++++++ .../tasks/training/datasets/schemas.py | 430 ++++++++ .../tasks/training/datasets/validation.py | 297 ++++++ .../automodel/tasks/training/distributed.py | 245 +++++ .../tasks/training/errors/converter.py | 119 +++ .../tasks/training/errors/error_rules.yaml | 643 ++++++++++++ .../tasks/training/errors/exceptions.py | 431 ++++++++ .../automodel/tasks/training/errors/parser.py | 255 +++++ .../automodel/tasks/training/integrations.py | 168 +++ .../tasks/training/model_utils/constants.py | 4 + .../tasks/training/model_utils/file_utils.py | 172 ++++ .../nmp/automodel/tasks/training/progress.py | 173 ++++ .../nmp/automodel/tasks/training/protocol.py | 14 + .../nmp/automodel/tasks/training/runner.py | 190 ++++ .../nmp/automodel/tasks/training/schemas.py | 42 + .../tasks/training/sequence_packing.py | 349 +++++++ .../templates/llama-3.1-instruct.jinja | 61 ++ .../templates/llama-3.2-instruct.jinja | 61 ++ .../templates/llama-3.3-instruct.jinja | 61 ++ .../training/templates/nemotron-3.1.jinja | 51 + .../training/templates/nemotron-3.3.jinja | 21 + .../templates/nemotron-super-3.3.jinja | 82 ++ .../tasks/training/templates/phi-4.jinja | 15 + .../src/nmp/automodel/tasks/training/utils.py | 93 ++ services/automodel/tests/test_adapter.py | 35 + services/automodel/tests/test_compiler.py | 151 +++ .../automodel/tests/test_contract_configs.py | 78 ++ services/automodel/tests/test_images.py | 46 + services/automodel/tests/test_job_context.py | 73 ++ .../automodel/tests/test_platform_client.py | 14 + .../automodel/tests/test_progress_reporter.py | 40 + services/automodel/tests/test_validators.py | 22 + services/core/entities/config/local.env | 9 +- .../core/jobs/src/nmp/core/jobs/config.py | 16 +- .../core/jobs/controllers/backends/config.py | 12 +- services/core/models/pyproject.toml | 4 +- .../src/nmp/core/models/api/v2/models.py | 5 +- .../generate_configs.py | 15 +- tests/smoke_gpu/conftest.py | 6 + tests/smoke_gpu/test_nemo_automodel.py | 43 + third_party/licenses.jsonl | 2 + third_party/osv-licenses.json | 354 ++++++- third_party/requirements-main.txt | 503 ++------- uv.lock | 173 ++++ 176 files changed, 18613 insertions(+), 659 deletions(-) create mode 100644 docker-bake.automodel.hcl delete mode 100644 packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-fine-tune/SKILL.md delete mode 100644 packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-fine-tune/tests.json create mode 100644 packages/nemo_platform_plugin/src/nemo_platform_plugin/customization_contributor.py create mode 100644 plugins/nemo-automodel/README.md create mode 100644 plugins/nemo-automodel/SCOPE.md create mode 100644 plugins/nemo-automodel/pyproject.toml create mode 100644 plugins/nemo-automodel/src/nemo_automodel_plugin/__init__.py create mode 100644 plugins/nemo-automodel/src/nemo_automodel_plugin/cli/__init__.py create mode 100644 plugins/nemo-automodel/src/nemo_automodel_plugin/cli/inputs.py create mode 100644 plugins/nemo-automodel/src/nemo_automodel_plugin/cli/main.py create mode 100644 plugins/nemo-automodel/src/nemo_automodel_plugin/config.py create mode 100644 plugins/nemo-automodel/src/nemo_automodel_plugin/contributor.py create mode 100644 plugins/nemo-automodel/src/nemo_automodel_plugin/jobs/__init__.py create mode 100644 plugins/nemo-automodel/src/nemo_automodel_plugin/jobs/jobs.py create mode 100644 plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py create mode 100644 plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/__init__.py create mode 100644 plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/http_utils.py create mode 100644 plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/job_resources.py create mode 100644 plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/resources.py create mode 100644 plugins/nemo-automodel/src/nemo_automodel_plugin/transform.py create mode 100644 plugins/nemo-automodel/tests/fixtures/minimal_sft_lora.json create mode 100644 plugins/nemo-automodel/tests/fixtures/qwen3_0.6b_sft_lora.json create mode 100644 plugins/nemo-automodel/tests/test_api.py create mode 100644 plugins/nemo-automodel/tests/test_cli.py create mode 100644 plugins/nemo-automodel/tests/test_contributor.py create mode 100644 plugins/nemo-automodel/tests/test_schema.py create mode 100644 plugins/nemo-customizer/README.md create mode 100644 plugins/nemo-customizer/docs/CUSTOMIZATION.md create mode 100644 plugins/nemo-customizer/pyproject.toml create mode 100644 plugins/nemo-customizer/src/nemo_customizer/__init__.py create mode 100644 plugins/nemo-customizer/src/nemo_customizer/cli.py create mode 100644 plugins/nemo-customizer/src/nemo_customizer/contributor.py create mode 100644 plugins/nemo-customizer/src/nemo_customizer/discovery.py create mode 100644 plugins/nemo-customizer/src/nemo_customizer/router.py create mode 100644 plugins/nemo-customizer/src/nemo_customizer/sdk/__init__.py create mode 100644 plugins/nemo-customizer/src/nemo_customizer/sdk/resources.py create mode 100644 plugins/nemo-customizer/src/nemo_customizer/skills.py create mode 100644 plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/SKILL.md create mode 100644 plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/dataset-formats.md create mode 100644 plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hf-conversion.md create mode 100644 plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md create mode 100644 plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/troubleshooting.md create mode 100755 plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/scripts/poll_automodel_job.sh create mode 100644 plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/tests.json create mode 100644 plugins/nemo-customizer/tests/test_customization_discovery_reexport.py create mode 100644 plugins/nemo-customizer/tests/test_router.py create mode 100644 plugins/nemo-customizer/tests/test_sdk.py create mode 100644 plugins/nemo-customizer/tests/test_skills.py delete mode 100644 sdk/python/nemo-platform/src/nemo_platform/skills/nemo-fine-tune/SKILL.md create mode 100644 services/automodel/README.md create mode 100644 services/automodel/docker/Dockerfile.mamba-wheel create mode 100644 services/automodel/docker/Dockerfile.nmp-automodel-base create mode 100644 services/automodel/docker/Dockerfile.nmp-automodel-tasks create mode 100644 services/automodel/docker/Dockerfile.nmp-automodel-training create mode 100644 services/automodel/docker/Dockerfile.platform-workspace create mode 100644 services/automodel/docker/README.md create mode 100644 services/automodel/docker/docker-bake.hcl create mode 100644 services/automodel/docker/locks/README.md create mode 100644 services/automodel/docker/locks/mamba-wheel-build-py311/pyproject.toml create mode 100644 services/automodel/docker/locks/mamba-wheel-build-py311/uv.lock create mode 100644 services/automodel/docker/locks/mamba-wheel-build-py312/pyproject.toml create mode 100644 services/automodel/docker/locks/mamba-wheel-build-py312/uv.lock create mode 100644 services/automodel/docker/no_override_requirements.txt create mode 100644 services/automodel/docker/pyproject.workspace.toml create mode 100644 services/automodel/pyproject.toml create mode 100644 services/automodel/src/nmp/automodel/__init__.py create mode 100644 services/automodel/src/nmp/automodel/adapter.py create mode 100644 services/automodel/src/nmp/automodel/api/__init__.py create mode 100644 services/automodel/src/nmp/automodel/api/v2/__init__.py create mode 100644 services/automodel/src/nmp/automodel/api/v2/jobs/__init__.py create mode 100644 services/automodel/src/nmp/automodel/api/v2/jobs/schemas.py create mode 100644 services/automodel/src/nmp/automodel/app/__init__.py create mode 100644 services/automodel/src/nmp/automodel/app/constants.py create mode 100644 services/automodel/src/nmp/automodel/app/jobs/__init__.py create mode 100644 services/automodel/src/nmp/automodel/app/jobs/compiler.py create mode 100644 services/automodel/src/nmp/automodel/app/jobs/context.py create mode 100644 services/automodel/src/nmp/automodel/app/jobs/file_io/schemas.py create mode 100644 services/automodel/src/nmp/automodel/app/jobs/model_entity/__init__.py create mode 100644 services/automodel/src/nmp/automodel/app/jobs/model_entity/schemas.py create mode 100644 services/automodel/src/nmp/automodel/app/jobs/training/compiler.py create mode 100644 services/automodel/src/nmp/automodel/app/jobs/training/schemas.py create mode 100644 services/automodel/src/nmp/automodel/compile.py create mode 100644 services/automodel/src/nmp/automodel/config.py create mode 100644 services/automodel/src/nmp/automodel/entities/__init__.py create mode 100644 services/automodel/src/nmp/automodel/entities/validators.py create mode 100644 services/automodel/src/nmp/automodel/entities/values.py create mode 100644 services/automodel/src/nmp/automodel/images.py create mode 100644 services/automodel/src/nmp/automodel/platform_client.py create mode 100644 services/automodel/src/nmp/automodel/tasks/__init__.py create mode 100644 services/automodel/src/nmp/automodel/tasks/__main__.py create mode 100644 services/automodel/src/nmp/automodel/tasks/docker/README.md create mode 100644 services/automodel/src/nmp/automodel/tasks/docker/docker-compose.yaml create mode 100644 services/automodel/src/nmp/automodel/tasks/file_io/__init__.py create mode 100644 services/automodel/src/nmp/automodel/tasks/file_io/__main__.py create mode 100644 services/automodel/src/nmp/automodel/tasks/file_io/callbacks.py create mode 100644 services/automodel/src/nmp/automodel/tasks/file_io/progress_reporter.py create mode 100644 services/automodel/src/nmp/automodel/tasks/file_io/run.py create mode 100644 services/automodel/src/nmp/automodel/tasks/file_io/utils.py create mode 100644 services/automodel/src/nmp/automodel/tasks/model_entity/__init__.py create mode 100644 services/automodel/src/nmp/automodel/tasks/model_entity/__main__.py create mode 100644 services/automodel/src/nmp/automodel/tasks/model_entity/run.py create mode 100644 services/automodel/src/nmp/automodel/tasks/progress_reporter.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/__init__.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/__main__.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/backends/__init__.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/backends/backend.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/backends/callbacks.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/backends/checkpoints.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/backends/config.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/backends/finetune.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/backends/requirements.txt create mode 100644 services/automodel/src/nmp/automodel/tasks/training/chat_templates.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/datasets/preparation.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/datasets/schemas.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/datasets/validation.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/distributed.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/errors/converter.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/errors/error_rules.yaml create mode 100644 services/automodel/src/nmp/automodel/tasks/training/errors/exceptions.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/errors/parser.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/integrations.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/model_utils/constants.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/model_utils/file_utils.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/progress.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/protocol.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/runner.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/schemas.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/sequence_packing.py create mode 100644 services/automodel/src/nmp/automodel/tasks/training/templates/llama-3.1-instruct.jinja create mode 100644 services/automodel/src/nmp/automodel/tasks/training/templates/llama-3.2-instruct.jinja create mode 100644 services/automodel/src/nmp/automodel/tasks/training/templates/llama-3.3-instruct.jinja create mode 100644 services/automodel/src/nmp/automodel/tasks/training/templates/nemotron-3.1.jinja create mode 100644 services/automodel/src/nmp/automodel/tasks/training/templates/nemotron-3.3.jinja create mode 100644 services/automodel/src/nmp/automodel/tasks/training/templates/nemotron-super-3.3.jinja create mode 100644 services/automodel/src/nmp/automodel/tasks/training/templates/phi-4.jinja create mode 100644 services/automodel/src/nmp/automodel/tasks/training/utils.py create mode 100644 services/automodel/tests/test_adapter.py create mode 100644 services/automodel/tests/test_compiler.py create mode 100644 services/automodel/tests/test_contract_configs.py create mode 100644 services/automodel/tests/test_images.py create mode 100644 services/automodel/tests/test_job_context.py create mode 100644 services/automodel/tests/test_platform_client.py create mode 100644 services/automodel/tests/test_progress_reporter.py create mode 100644 services/automodel/tests/test_validators.py create mode 100644 tests/smoke_gpu/test_nemo_automodel.py diff --git a/.cursor/rules/nemo-platform.mdc b/.cursor/rules/nemo-platform.mdc index a892fe5900..ee5b9d7235 100644 --- a/.cursor/rules/nemo-platform.mdc +++ b/.cursor/rules/nemo-platform.mdc @@ -37,9 +37,8 @@ User-facing skills in `packages/nemo_platform_ext/src/nemo_platform_ext/skills/` - `nemo-try-agent`: test a deployed agent or chat with a model. - `nemo-status`: read-only health dashboard. Run this before assuming the platform is up. - `nemo-teardown`: guided shutdown with confirmation. -- `nemo-fine-tune`: fine-tuning. Not yet available; the skill tells the user this honestly instead of letting you improvise. -Plugin-owned skills under `plugins/*/src/*/skills/` handle their own routing for guardrails, evaluations, optimization, data designer, anonymizer, and auditor. +Plugin-owned skills under `plugins/*/src/*/skills/` handle their own routing for customization, guardrails, evaluations, optimization, data designer, anonymizer, and auditor. ## Sandboxed environments diff --git a/AGENTS.md b/AGENTS.md index 22d0abc557..0bd8af5ff6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,9 +33,8 @@ User-facing skills in `packages/nemo_platform_ext/src/nemo_platform_ext/skills/` - `nemo-try-agent`: test a deployed agent or chat with a model. - `nemo-status`: read-only health dashboard. - `nemo-teardown`: guided shutdown with confirmation. -- `nemo-fine-tune`: fine-tuning. Not yet available; the skill tells the user it's not shipped instead of improvising with another training library. -Plugin-owned skills under `plugins/*/src/*/skills/` handle guardrails, evaluations, optimization, data designer, anonymizer, and auditor. +Plugin-owned skills under `plugins/*/src/*/skills/` handle their own routing for customization, guardrails, evaluations, optimization, data designer, anonymizer, and auditor. ### Working in a sandboxed environment diff --git a/CLAUDE.md b/CLAUDE.md index 7f65eed89d..8c01b4535b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,9 +33,8 @@ User-facing skills in `packages/nemo_platform_ext/src/nemo_platform_ext/skills/` - `nemo-try-agent`: test a deployed agent or chat with a model. - `nemo-status`: read-only health dashboard. Run this before assuming the platform is up. - `nemo-teardown`: guided shutdown with confirmation. -- `nemo-fine-tune`: fine-tuning. Not yet available; the skill tells the user it's not shipped instead of letting the agent improvise with another training library. -Plugin-owned skills live under `plugins/*/src/*/skills/` and handle their own routing for guardrails, evaluations, optimization, data designer, anonymizer, and auditor. +Plugin-owned skills live under `plugins/*/src/*/skills/` and handle their own routing for customization, guardrails, evaluations, optimization, data designer, anonymizer, and auditor. ### Working in a sandboxed coding-agent environment diff --git a/docker-bake.automodel.hcl b/docker-bake.automodel.hcl new file mode 100644 index 0000000000..d51f2157dc --- /dev/null +++ b/docker-bake.automodel.hcl @@ -0,0 +1,203 @@ +# nmp-automodel image bake - run from Platform repo root (context = "."). +# +# Inspect targets (no build; finishes in ~0s): +# docker buildx bake --print -f docker-bake.automodel.hcl nmp-automodel-gpu-wheels +# +# Build wheels (override registry/tag via env, not --set): +# export WHEELS_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev +# export WHEELS_TAG=$(git rev-parse --short HEAD) +# docker buildx bake -f docker-bake.automodel.hcl nmp-automodel-gpu-wheels --push +# +# Build automodel images: +# docker buildx bake -f docker-bake.automodel.hcl nmp-automodel-base-builder +# +# Published tags: nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-{base,tasks,training}: +# NVCR allows only one repo segment after the registry prefix (no nmp/automodel-base nesting). + +variable "IMAGE_REGISTRY" { + default = "nvcr.io/0921617854601259/nemo-platform-dev" +} + +variable "BASE_REGISTRY" { + default = "nvcr.io/0921617854601259/nemo-platform-dev" +} + +variable "WHEELS_REGISTRY" { + default = "nvcr.io/0921617854601259/nemo-platform-dev" +} + +variable "BAKE_TAG" { + default = "local" +} + +variable "BASE_TAG_AUTOMODEL" { + default = "local" +} + +variable "WHEELS_TAG" { + default = "3fd6986ff173b598446ffac06d9be3f84b482495" +} + +variable "CUDA_VERSION" { + default = "12.8.1" +} + +variable "MAMBA_22_COMMIT" { + default = "6b32be06d026e170b3fdaf3ae6282c5a6ff57b06" +} + +variable "MAMBA_23_COMMIT" { + default = "v2.3.0" +} + +variable "CAUSAL_CONV1D_VERSION" { + default = "v1.5.3" +} + +# For local builds: --set "*.platform=linux/amd64" +variable "BUILD_PLATFORMS" { + default = ["linux/arm64"] +} + +function "wheel_tags" { + params = [name] + result = ["${WHEELS_REGISTRY}/${name}:${WHEELS_TAG}"] +} + +function "get_causal_conv1d_wheel_image" { + params = [] + result = "${WHEELS_REGISTRY}/causal-conv1d-wheel:${WHEELS_TAG}" +} + +function "get_mamba_ssm_wheel_image" { + params = [] + result = "${WHEELS_REGISTRY}/mamba-ssm-wheel:${WHEELS_TAG}" +} + +group "nmp-automodel-gpu-wheels" { + targets = [ + "causal-conv1d-wheel", + "mamba-ssm-wheel", + ] +} + +group "nmp-automodel" { + targets = [ + "nmp-automodel-base-builder", + "nmp-automodel-tasks-docker", + "nmp-automodel-training-docker", + "nmp-automodel-tasks-smoke-test", + "nmp-automodel-training-smoke-test", + ] +} + +# Pre-built mamba-ssm / causal-conv1d wheels (cp311, cp312, cu13.1.1). Pushed to WHEELS_REGISTRY. +target "causal-conv1d-wheel" { + target = "causal-conv1d-wheel" + context = "." + dockerfile = "services/automodel/docker/Dockerfile.mamba-wheel" + tags = wheel_tags("causal-conv1d-wheel") + args = { + CUDA_VERSION = CUDA_VERSION + CAUSAL_CONV1D_VERSION = CAUSAL_CONV1D_VERSION + } + platforms = BUILD_PLATFORMS +} + +target "mamba-ssm-wheel" { + target = "mamba-ssm-wheel" + context = "." + dockerfile = "services/automodel/docker/Dockerfile.mamba-wheel" + tags = wheel_tags("mamba-ssm-wheel") + args = { + CUDA_VERSION = CUDA_VERSION + MAMBA_22_COMMIT = MAMBA_22_COMMIT + MAMBA_23_COMMIT = MAMBA_23_COMMIT + } + platforms = BUILD_PLATFORMS +} + +target "platform-workspace" { + target = "platform-workspace" + context = "." + dockerfile = "services/automodel/docker/Dockerfile.platform-workspace" +} + +target "nmp-automodel-base-builder" { + target = "nmp-automodel-base-builder" + context = "." + dockerfile = "services/automodel/docker/Dockerfile.nmp-automodel-base" + no-cache-filter = ["automodel-clone"] + tags = ["${IMAGE_REGISTRY}/nmp-automodel-base:${BAKE_TAG}"] + args = { + CAUSAL_CONV1D_WHEEL_IMAGE = get_causal_conv1d_wheel_image() + MAMBA_SSM_WHEEL_IMAGE = get_mamba_ssm_wheel_image() + } + platforms = BUILD_PLATFORMS +} + +target "nmp-automodel-tasks-docker" { + target = "runtime" + context = "." + dockerfile = "services/automodel/docker/Dockerfile.nmp-automodel-tasks" + contexts = { + platform-workspace = "target:platform-workspace" + nmp-automodel-base = "target:nmp-automodel-base-builder" + } + tags = ["${IMAGE_REGISTRY}/nmp-automodel-tasks:${BAKE_TAG}"] + args = { + BASE_REGISTRY = BASE_REGISTRY + BASE_TAG_AUTOMODEL = BASE_TAG_AUTOMODEL + } + platforms = BUILD_PLATFORMS +} + +target "nmp-automodel-training-docker" { + target = "runtime" + context = "." + dockerfile = "services/automodel/docker/Dockerfile.nmp-automodel-training" + contexts = { + platform-workspace = "target:platform-workspace" + nmp-automodel-base = "target:nmp-automodel-base-builder" + } + tags = ["${IMAGE_REGISTRY}/nmp-automodel-training:${BAKE_TAG}"] + args = { + BASE_REGISTRY = BASE_REGISTRY + BASE_TAG_AUTOMODEL = BASE_TAG_AUTOMODEL + } + platforms = BUILD_PLATFORMS +} + +target "nmp-automodel-tasks-smoke-test" { + target = "smoke-test" + context = "." + dockerfile = "services/automodel/docker/Dockerfile.nmp-automodel-tasks" + contexts = { + platform-workspace = "target:platform-workspace" + nmp-automodel-base = "target:nmp-automodel-base-builder" + } + args = { + BASE_REGISTRY = BASE_REGISTRY + BASE_TAG_AUTOMODEL = BASE_TAG_AUTOMODEL + SMOKE_MARKER = "smoke_nmp_automodel_tasks" + } + output = ["type=cacheonly"] + platforms = BUILD_PLATFORMS +} + +target "nmp-automodel-training-smoke-test" { + target = "smoke-test" + context = "." + dockerfile = "services/automodel/docker/Dockerfile.nmp-automodel-training" + contexts = { + platform-workspace = "target:platform-workspace" + nmp-automodel-base = "target:nmp-automodel-base-builder" + } + args = { + BASE_REGISTRY = BASE_REGISTRY + BASE_TAG_AUTOMODEL = BASE_TAG_AUTOMODEL + SMOKE_MARKER = "smoke_nmp_automodel_training" + } + output = ["type=cacheonly"] + platforms = BUILD_PLATFORMS +} diff --git a/docs/agents/plugins.md b/docs/agents/plugins.md index 373a2206bf..23a49604af 100644 --- a/docs/agents/plugins.md +++ b/docs/agents/plugins.md @@ -68,7 +68,7 @@ The skills that drive the agent lifecycle are: | `agents-optimize` | Selects a deployed agent, establishes an evaluation baseline, and suggests Switchyard routing, model swaps, skill optimization, prompt tuning, and new-model evaluations. See [Optimize Agents](optimization.md). | | `agents-secure` | Selects a deployed agent, checks guardrail coverage, and scans recent telemetry for sensitive data. See [Secure Agents](security.md). | -Plugin-owned skills cover guardrails, evaluations, optimization, data +Plugin-owned skills cover customization, guardrails, evaluations, optimization, data designer, anonymizer, and auditor. They are installed with their plugin and appear in `nemo skills list` once the platform restarts. diff --git a/docs/set-up/config-reference.md b/docs/set-up/config-reference.md index e9e415a307..bee7db3d9f 100644 --- a/docs/set-up/config-reference.md +++ b/docs/set-up/config-reference.md @@ -409,6 +409,8 @@ jobs: reconcile_interval_seconds: 2 # Interval in seconds for the job scheduler to run | default: 5 schedule_interval_seconds: 5 + # Register the subprocess/default execution profile. When unset, defaults to true for docker/none runtimes and false for kubernetes. + enable_subprocess_executor: ``` ### `models` diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-fine-tune/SKILL.md b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-fine-tune/SKILL.md deleted file mode 100644 index 690118a7a8..0000000000 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-fine-tune/SKILL.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -name: nemo-fine-tune -description: Fine-tune a model on NeMo Platform. Not yet available; this skill describes the path for when fine-tuning ships. Use for any "train a model," "fine-tune," "customize a model," or "finetune" intent so the agent tells the user the feature isn't shipped and does not go off and implement training with some other library. -triggers: - - fine-tune - - fine tune - - finetune - - train a model - - train on my data - - customize a model - - sft a model - - model customization - - model fine-tuning -not-for: - - nemo-build-agent (use for agent scaffolding and deployment, not model training) - - nemo-explore (use for agent design conversation) - - nemo-skill-selection (use to disambiguate user intent) -compatibility: NeMo Platform any version. No prerequisites today since fine-tuning is not yet shipped. When fine-tuning lands, this skill will document Customizer plugin requirements (host-gpu mode, training data format, supported base models). -maturity: beta -license: Apache-2.0 -user-invocable: true -allowed-tools: [Read] ---- - -# Fine-tuning on NeMo Platform - -**Fine-tuning is not yet available on NeMo Platform.** Tell the user this directly. Do not run any `nemo customization` CLI commands or scaffold a fine-tuning job; the underlying functionality is not shipped. - -When fine-tuning lands, it will be delivered through a Customizer plugin that wraps NVIDIA's training stack (AutoModel, Megatron-Bridge, and related). This skill will be filled in at that point. - -## What to tell the user today - -- Fine-tuning is on the NeMo Platform roadmap and is not currently functional. Any CLI surface that looks like it should work (`nemo customization jobs ...`) is not connected to a working training backend. -- Other NeMo Platform capabilities they can use today: harden an agent (`nemo-skill-selection` → guardrails / auditor / anonymizer), evaluate an agent (`nemo-skill-selection` → evaluator), tune an agent's prompts and routing (`nemo-skill-selection` → optimization). -- If they need fine-tuning urgently, point them at upstream NVIDIA training tools (NeMo Framework, NeMo-RL, Megatron-LM) and tell them this skill will be wired up once the Customizer plugin lands. - -## Verification - -There is nothing to verify. Do not claim a fine-tuning task succeeded. If the user asks the agent to run fine-tuning anyway, refuse and explain why. - -## When fine-tuning ships - -This skill will gain pre-flight checks, a training-data preparation walkthrough, job submission, progress monitoring, and result download. Track the Customizer plugin in the NeMo Platform roadmap; this skill updates when that ships. diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-fine-tune/tests.json b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-fine-tune/tests.json deleted file mode 100644 index 385b0bff34..0000000000 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-fine-tune/tests.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "skill": "nemo-fine-tune", - "tests": [ - { - "type": "explicit", - "prompt": "Use nemo-fine-tune to start a job on my training data.", - "expected_skill": "nemo-fine-tune" - }, - { - "type": "explicit", - "prompt": "Run the fine-tune skill against my dataset at data/train.jsonl.", - "expected_skill": "nemo-fine-tune" - }, - { - "type": "explicit", - "prompt": "Open the nemo fine-tune skill and walk me through it.", - "expected_skill": "nemo-fine-tune" - }, - { - "type": "implicit", - "prompt": "I want to fine-tune a model on my own data.", - "expected_skill": "nemo-fine-tune" - }, - { - "type": "implicit", - "prompt": "Can NeMo Platform train a model for me?", - "expected_skill": "nemo-fine-tune" - }, - { - "type": "implicit", - "prompt": "I need to customize a model with SFT.", - "expected_skill": "nemo-fine-tune" - }, - { - "type": "contextual", - "prompt": "I want to optimize my agent's prompt for better accuracy.", - "expected_skill_not": "nemo-fine-tune" - }, - { - "type": "contextual", - "prompt": "Build me an agent that uses a smaller model for cheap tasks.", - "expected_skill_not": "nemo-fine-tune" - }, - { - "type": "contextual", - "prompt": "Evaluate my agent against a benchmark dataset.", - "expected_skill_not": "nemo-fine-tune" - }, - { - "type": "negative-control", - "prompt": "What's the weather in San Francisco today?", - "expected_skill_not": "nemo-fine-tune" - }, - { - "type": "negative-control", - "prompt": "Help me set up a new Postgres database on this machine.", - "expected_skill_not": "nemo-fine-tune" - }, - { - "type": "negative-control", - "prompt": "Show me the latest news about NVIDIA stock.", - "expected_skill_not": "nemo-fine-tune" - } - ] -} diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-skill-selection/SKILL.md b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-skill-selection/SKILL.md index 3148c879ad..b059793280 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-skill-selection/SKILL.md +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-skill-selection/SKILL.md @@ -1,6 +1,6 @@ --- name: nemo-skill-selection -description: Top-level skill selector for any task involving NeMo Platform (NVIDIA's agent platform). Picks the right downstream skill (setup, explore, spec, build, try, status, teardown, fine-tune) from natural-language intent. Use over generic brainstorming, planning, or onboarding skills for any NeMo Platform task. +description: Top-level skill selector for any task involving NeMo Platform (NVIDIA's agent platform). Picks the right downstream skill (setup, explore, spec, build, try, status, teardown, customization training) from natural-language intent. Use over generic brainstorming, planning, or onboarding skills for any NeMo Platform task. triggers: - build an agent - create an agent @@ -48,7 +48,7 @@ Match the user's intent to one downstream skill. Pick exactly one. | "ask my agent", "try the agent", "test it" | `nemo-try-agent` | Send a query to a deployed agent or fall back to model chat | | "status", "what is running", "platform health", "is the platform up", "what's deployed", "show me what's running" | `nemo-status` | Read-only dashboard: platform, agents, providers, models | | "shut down", "stop NeMo", "tear down", "clean up" | `nemo-teardown` | Stop the cluster (keep data, delete platform data, or full cleanup) | -| "fine-tune", "customize the model", "train on my data" | `nemo-fine-tune` | Fine-tuning is not yet available on NeMo Platform. Pick this so the agent tells the user it's not shipped instead of going off to implement training with some other library. | +| "fine-tune", "customize the model", "train on my data", "SFT", "LoRA" | `nemo-customizer` | Model customization via installed customization contributor plugins (`nemo-customizer-plugin`). Requires plugin skills to be installed (`nemo skills install` / enabled-plugins). | | "optimize my agent", "make it cheaper", "reduce latency", "smaller model", "switchyard", "routing split", "compare against a newer model" | `agents-optimize` (plugin-owned, in `plugins/nemo-agents`) | Cost / latency / quality optimization for a **deployed** agent. Routing splits, skill tuning, prompt tuning, new-model scans. | | "secure my agent", "harden my agent", "check for PII", "leaked secrets", "guardrail coverage" | `agents-secure` (plugin-owned, in `plugins/nemo-agents`) | Safety and security audit for a **deployed** agent. Guardrails, PII, secrets scan. | | "evaluate my agent", "run a benchmark", "eval suite" | `nemo-evaluator` (plugin-owned, in `plugins/nemo-evaluator`) | Evaluation metrics, LLM-judge, benchmark jobs against a deployed agent or model. | @@ -104,12 +104,12 @@ NeMo Platform skills I can route to: nemo-try-agent query a deployed agent or chat with a model nemo-status read-only platform health dashboard nemo-teardown guided shutdown - nemo-fine-tune fine-tuning (not yet shipped; reports that honestly) Plugin-owned skills: agents-optimize cost / latency / quality optimization for a deployed agent agents-secure safety and security audit for a deployed agent nemo-evaluator evaluation metrics, LLM-judge, benchmark jobs + nemo-customizer fine-tuning of models guardrails content-safety middleware via virtual models auditor red-team vulnerability scanning (garak) data-designer synthetic dataset generation @@ -142,5 +142,5 @@ Do not proactively suggest Studio as the path for anything a skill already cover - **One skill at a time.** Do not load more than one downstream skill in the same turn. Each downstream skill is a full procedure with its own context budget. - **Install must happen before any skill can do useful work.** Build, try, and status all assume the platform is up. If the user has not run the CLI install (`make bootstrap` + `nemo setup`), the skills cannot work around that; hand them to `setup` for instructions. - **NeMo Platform is the product name.** Capital N, e, M, o, P. Not "nemo" or "Nemo." NAT on first mention is "NVIDIA NeMo Agent Toolkit (NAT)." -- **Fine-tuning is not yet available.** When the user asks to fine-tune, train, or customize a model, pick `nemo-fine-tune` so the agent tells the user it's not shipped instead of trying to wire up training with some other library. Do not run `nemo customization` CLI commands; the backend is not connected. +- **Model customization** goes to the `nemo-customizer` plugin skill when `nemo-customizer-plugin` (and a training backend) are installed. If that skill is not available, tell the user to enable customization plugins and install skills — do not improvise training with an external library. - **Framework honesty.** If the user describes an agent in CrewAI, AutoGen, plain LangChain, or Pydantic AI, tell them up front that NeMo Platform's optimization and evaluation surfaces operate on NAT-wrapped LangGraph agents. They will need to wrap their agent before the build path produces value. diff --git a/packages/nemo_platform_ext/tests/cli/test_app.py b/packages/nemo_platform_ext/tests/cli/test_app.py index 48338f585f..7e1dfe70f9 100644 --- a/packages/nemo_platform_ext/tests/cli/test_app.py +++ b/packages/nemo_platform_ext/tests/cli/test_app.py @@ -51,7 +51,8 @@ def test_help_includes_getting_started(): assert "Getting started:" in result.stdout assert "nemo docs --list" in result.stdout assert "nemo services run --help" in result.stdout - assert "Set up NeMo Platform: start services, configure a provider, install skills." in result.stdout + # Help panel truncates long command descriptions; match the visible prefix. + assert "Set up NeMo Platform: start services" in result.stdout assert "--help, -h" in result.stdout assert "nemo auth login --base-url" not in result.stdout assert "nemo quickstart configure" not in result.stdout diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/commands.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/commands.py index 0e9550e1e7..548c572a50 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/commands.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/commands.py @@ -1094,6 +1094,22 @@ def _resolve_submit_auth_headers(typer_ctx: typer.Context) -> dict[str, str]: return {} +def _resolve_submit_auth_headers(typer_ctx: typer.Context) -> dict[str, str]: + """Bearer (and other) default headers from the active CLI context.""" + state = typer_ctx.obj + if state is None or not hasattr(state, "get_sdk_context"): + return {} + try: + ctx = state.get_sdk_context() + client_config = ctx.user.get_client_config() + headers = client_config.get("default_headers") + if isinstance(headers, dict): + return {str(k): str(v) for k, v in headers.items()} + except Exception: + return {} + return {} + + # ---- submit ------------------------------------------------------ # diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/customization_contributor.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/customization_contributor.py new file mode 100644 index 0000000000..71092596ab --- /dev/null +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/customization_contributor.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Contributor protocol for customization training backends.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, ClassVar, Protocol, runtime_checkable + +import typer + +if TYPE_CHECKING: + from nemo_platform_plugin.authz import AuthzContribution + from nemo_platform_plugin.service import RouterSpec + + +@runtime_checkable +class CustomizationContributor(Protocol): + """One training backend mounted under ``/apis/customization``.""" + + name: ClassVar[str] + dependencies: ClassVar[list[str]] + + def get_routers(self) -> list[RouterSpec]: + """HTTP routes for this backend (workspace-scoped prefix per backend).""" + + def get_cli(self) -> typer.Typer | None: + """CLI subgroup mounted at ``nemo customization ``.""" + + def get_authz_contribution(self) -> AuthzContribution | None: + """Optional authorization policy (endpoints + permissions) for this contributor. + + Implement to return :class:`~nemo_platform_plugin.authz.AuthzContribution`, or + register a ``nemo.authz`` entry point instead. + """ + ... diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py index 3ea7f1f187..83d8a965aa 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py @@ -22,6 +22,7 @@ ``nemo.docs`` → :func:`discover_docs` — ``() -> Path | dict`` callable ``nemo.executors`` → :func:`discover_executors` — ``Executor`` class ``nemo.inference_middleware`` → :func:`discover_inference_middleware` — :class:`~nemo_platform_plugin.inference_middleware.NemoInferenceMiddleware` subclass (typed, IGW instantiates) +``nemo.customization.contributors`` → :func:`discover_customization_contributors` — :class:`~nemo_platform_plugin.customization_contributor.CustomizationContributor` instance (typed, customization router instantiates) ``nemo.seed`` → :func:`discover_seed_jobs` — :class:`~nemo_platform_plugin.seed.NemoSeedJob` subclass (typed, platform instantiates) ``nemo.authz`` → :func:`~nemo_platform_plugin.authz_discovery.discover_authz_contributions` — policy endpoints/permissions (merged at runtime and via ``auth-tools sync-plugins``) @@ -51,6 +52,7 @@ if TYPE_CHECKING: from nemo_platform_plugin.cli import NemoCLI from nemo_platform_plugin.controller import NemoController + from nemo_platform_plugin.customization_contributor import CustomizationContributor from nemo_platform_plugin.function import NemoFunction from nemo_platform_plugin.inference_middleware import NemoInferenceMiddleware from nemo_platform_plugin.job import NemoJob @@ -74,6 +76,7 @@ "nemo.docs", "nemo.executors", "nemo.inference_middleware", + "nemo.customization.contributors", "nemo.seed", "nemo.authz", ) @@ -97,10 +100,13 @@ "nemo.docs": "NEMO_PLUGIN_DOCS_ALLOWLIST", "nemo.executors": "NEMO_PLUGIN_EXECUTORS_ALLOWLIST", "nemo.inference_middleware": "NEMO_PLUGIN_INFERENCE_MIDDLEWARE_ALLOWLIST", + "nemo.customization.contributors": "NEMO_PLUGIN_CUSTOMIZATION_CONTRIBUTORS_ALLOWLIST", "nemo.seed": "NEMO_PLUGIN_SEED_ALLOWLIST", "nemo.authz": "NEMO_PLUGIN_AUTHZ_ALLOWLIST", } +CUSTOMIZATION_CONTRIBUTORS_GROUP = "nemo.customization.contributors" + def _manifest_plugin_name(group: str, entry_point_name: str) -> str: if group in _DOT_SCOPED_GROUPS: @@ -216,12 +222,8 @@ def discover_manifests() -> dict[str, PluginManifest]: # ``dist.metadata`` is ``email.message.Message``-compatible # and supports ``.get`` at runtime; ty's stub for # ``importlib.metadata.PackageMetadata`` doesn't expose it. - version = ( - dist.metadata.get("Version", "") if dist is not None else "" - ) # ty: ignore[unresolved-attribute] - description = ( - dist.metadata.get("Summary", "") if dist is not None else "" - ) # ty: ignore[unresolved-attribute] + version = dist.metadata.get("Version", "") if dist is not None else "" # ty: ignore[unresolved-attribute] + description = dist.metadata.get("Summary", "") if dist is not None else "" # ty: ignore[unresolved-attribute] except Exception: version = "" description = "" @@ -475,6 +477,70 @@ def discover_executors() -> dict[str, Any]: return discover("nemo.executors") +def _instantiate_customization_contributor(loaded: object) -> CustomizationContributor: + from nemo_platform_plugin.customization_contributor import CustomizationContributor + + if isinstance(loaded, type): + instance = loaded() + else: + instance = loaded + if not isinstance(instance, CustomizationContributor): + raise TypeError( + f"Expected CustomizationContributor instance, got {type(instance)!r}", + ) + return instance + + +@cache +def discover_customization_contributors() -> dict[str, CustomizationContributor]: + """Typed wrapper: discover ``nemo.customization.contributors`` entry-points. + + Returns a dict keyed by entry-point key (e.g. ``"automodel"``) mapping to a + :class:`~nemo_platform_plugin.customization_contributor.CustomizationContributor` + instance. Entry points may register a class (instantiated here) or a pre-built + instance. Broken contributors are skipped with a warning (same fault isolation as + :func:`discover`). + """ + + result: dict[str, CustomizationContributor] = {} + + for ep in discover_entry_points(CUSTOMIZATION_CONTRIBUTORS_GROUP).values(): + try: + loaded = ep.load() + contributor = _instantiate_customization_contributor(loaded) + key = getattr(type(contributor), "name", None) or ep.name + if key != ep.name: + logger.warning( + "Contributor entry-point key %r differs from class name %r; using entry-point key", + ep.name, + key, + ) + result[ep.name] = contributor + logger.debug( + "Loaded customization contributor %r from %s", + ep.name, + ep.value, + ) + except Exception: + logger.warning( + "Failed to load customization contributor %r (%s) — skipping", + ep.name, + ep.value, + exc_info=True, + ) + + return result + + +def discover_customization_contributor_classes() -> dict[str, type]: + """Return contributor entry-point name → loaded class (for tests).""" + result: dict[str, type] = {} + for key, loaded in discover(CUSTOMIZATION_CONTRIBUTORS_GROUP).items(): + if isinstance(loaded, type): + result[key] = loaded + return result + + def discover_inference_middleware() -> dict[str, type[NemoInferenceMiddleware]]: """Typed wrapper: discover ``nemo.inference_middleware`` entry-points. diff --git a/packages/nemo_platform_plugin/tests/test_commands.py b/packages/nemo_platform_plugin/tests/test_commands.py index 6e64b363b2..9e0e45852c 100644 --- a/packages/nemo_platform_plugin/tests/test_commands.py +++ b/packages/nemo_platform_plugin/tests/test_commands.py @@ -319,6 +319,35 @@ def test_submit_accepts_profile_and_cluster_flags(self) -> None: assert "--profile" in output assert "--cluster" in output + def test_submit_passes_cli_auth_headers(self, monkeypatch) -> None: + captured: dict[str, object] = {} + + def _capture(_self, _job_cls, _spec, headers=None, **_kwargs) -> dict: + captured["headers"] = headers + return {"id": "job-123"} + + class _State: + def get_sdk_context(self) -> SimpleNamespace: + return SimpleNamespace( + user=SimpleNamespace( + get_client_config=lambda: { + "default_headers": {"Authorization": "Bearer test-token"}, + } + ) + ) + + monkeypatch.setattr("nemo_platform_plugin.scheduler.NemoJobScheduler.submit_remote", _capture) + + app = _app_with_jobs(_GreetJob) + result = runner.invoke( + app, + ["greet", "submit", "--base-url", "http://127.0.0.1:8080"], + obj=_State(), + ) + + assert result.exit_code == 0, result.output + assert captured["headers"] == {"Authorization": "Bearer test-token"} + # --------------------------------------------------------------------------- # explain verb — phase 1 MR 1.2c stubs diff --git a/packages/nemo_platform_plugin/tests/test_discovery.py b/packages/nemo_platform_plugin/tests/test_discovery.py index 3eda7da6de..ac629d00a3 100644 --- a/packages/nemo_platform_plugin/tests/test_discovery.py +++ b/packages/nemo_platform_plugin/tests/test_discovery.py @@ -13,8 +13,10 @@ from nemo_platform_plugin.cli import NemoCLI from nemo_platform_plugin.discovery import ( _ALL_SURFACE_GROUPS, + CUSTOMIZATION_CONTRIBUTORS_GROUP, discover, discover_cli, + discover_customization_contributors, discover_entry_points, discover_functions, discover_jobs, @@ -40,10 +42,12 @@ def clear_discovery_cache(): discover_entry_points.cache_clear() discover.cache_clear() discover_manifests.cache_clear() + discover_customization_contributors.cache_clear() yield discover_entry_points.cache_clear() discover.cache_clear() discover_manifests.cache_clear() + discover_customization_contributors.cache_clear() # --------------------------------------------------------------------------- @@ -563,3 +567,55 @@ def test_function_only_plugins_use_plugin_name_not_function_name(self) -> None: result = discover_manifests() assert list(result.keys()) == ["example"] assert result["example"].version == "1.2.3" + + +class TestDiscoverCustomizationContributors: + def test_group_in_all_surface_groups(self) -> None: + assert CUSTOMIZATION_CONTRIBUTORS_GROUP in _ALL_SURFACE_GROUPS + + def test_uses_customization_contributors_group(self) -> None: + with patch("nemo_platform_plugin.discovery.entry_points", return_value=[]) as mock_eps: + discover_customization_contributors() + mock_eps.assert_called_once_with(group=CUSTOMIZATION_CONTRIBUTORS_GROUP) + + def test_instantiates_contributor_class(self) -> None: + class _Contributor: + name = "fake" + dependencies = ["jobs"] + + def get_routers(self) -> list[RouterSpec]: + return [] + + def get_cli(self) -> None: + return None + + def get_authz_contribution(self): + return None + + ep = _make_ep("fake", _Contributor) + with patch("nemo_platform_plugin.discovery.entry_points", return_value=[ep]): + result = discover_customization_contributors() + assert isinstance(result["fake"], _Contributor) + + def test_failing_contributor_is_skipped(self) -> None: + bad = _make_ep("bad", None) + bad.load.side_effect = RuntimeError("broken") + + class _Contributor: + name = "good" + dependencies = ["jobs"] + + def get_routers(self) -> list[RouterSpec]: + return [] + + def get_cli(self) -> None: + return None + + def get_authz_contribution(self): + return None + + good = _make_ep("good", _Contributor) + with patch("nemo_platform_plugin.discovery.entry_points", return_value=[bad, good]): + result = discover_customization_contributors() + assert "bad" not in result + assert "good" in result diff --git a/packages/nmp_platform/README.md b/packages/nmp_platform/README.md index aa65b3c079..c819acd790 100644 --- a/packages/nmp_platform/README.md +++ b/packages/nmp_platform/README.md @@ -62,3 +62,11 @@ tests/test_main.py The `config/` files (`local.yaml`, `local.env`) are not Python — they are the default config consumed by `nemo services run` during local development and referenced from several Makefiles and run scripts in the repo. + +`local.env` sets SQLite for the entity store (`~/.local/share/nemo/nmp-platform.db`) +so no PostgreSQL is required. Source it before starting services: + +```bash +set -a && source packages/nmp_platform/config/local.env && set +a +uv run nemo services run --host 127.0.0.1 --port 8080 +``` diff --git a/packages/nmp_platform/config/local.env b/packages/nmp_platform/config/local.env index 19cd54438e..9e62549d1a 100644 --- a/packages/nmp_platform/config/local.env +++ b/packages/nmp_platform/config/local.env @@ -1,20 +1,27 @@ -# Environment variables for local development - NO external dependencies -# -# This env file runs the platform using SQLite and noop secrets. -# No PostgreSQL, OpenBao, or other external services required. +# Environment variables for local development — no external dependencies. # -# Usage: set -a && source packages/nmp_platform/config/local.env && set +a +# Entity store uses SQLite below. No PostgreSQL, OpenBao, or other services required. +# +# Usage (from repo root): +# set -a && source packages/nmp_platform/config/local.env && set +a +# uv run nemo services run --host 127.0.0.1 --port 8080 +# +# CLI against a remote platform (no local nemo services run): +# export NMP_BASE_URL=http://10.0.0.51:8080 +# nemo auth login --unsigned-token --email you@example.com +# +# Reset local DB + files: stop the platform, then rm -rf ~/.local/share/nemo + +# Platform API for `nemo` CLI (overrides ~/.config/nmp/config.yaml when this file is sourced) +NMP_BASE_URL=http://10.0.0.51:8080 +NEMO_BASE_URL=http://10.0.0.51:8080 # Config file NMP_CONFIG_FILE_PATH=packages/nmp_platform/config/local.yaml -# Database connections (postgres exposed at localhost:5432) -DATABASE_HOST=localhost -DATABASE_PORT=5432 -DATABASE_USER=nmp -DATABASE_PASSWORD=nmp -DATABASE_NAME=nmp -DATABASE_DIALECT=postgresql +# Entity store (SQLite; parent dir is created on first platform start) +DATABASE_DIALECT=sqlite +DATABASE_PATH="${HOME}/.local/share/nemo/nmp-platform.db" # Docker configuration for jobs (connect to host Docker socket directly) DOCKER_HOST=unix:///var/run/docker.sock diff --git a/packages/nmp_platform/config/local.yaml b/packages/nmp_platform/config/local.yaml index 1c51e33189..87940bde69 100644 --- a/packages/nmp_platform/config/local.yaml +++ b/packages/nmp_platform/config/local.yaml @@ -1,5 +1,12 @@ -# Local development configuration for running platform with quickstart infrastructure -# Usage: NMP_CONFIG_FILE_PATH=packages/nmp_platform/config/local.yaml uv run nemo services run +# Local development for nemo services run (SQLite entity store, embedded auth). +# +# set -a && source packages/nmp_platform/config/local.env && set +a +# export NMP_BASE_URL=http://127.0.0.1:8080 +# uv run nemo services run --host 127.0.0.1 --port 8080 +# +# Use default service set (omit --services) or --service-group all. Then: +# nemo auth login --unsigned-token +# uv run nemo-platform run task --task nmp.platform_seed platform: runtime: "docker" @@ -13,7 +20,8 @@ platform: service: {} auth: - enabled: false + enabled: true + allow_unsigned_jwt: true # local CLI: nemo auth login --unsigned-token policy_decision_point_provider: embedded policy_decision_point_base_url: "http://localhost:8080" # Low timeouts for fast test feedback (same as integration tests) @@ -34,11 +42,12 @@ auth: # default_scopes: "platform:read platform:write openid profile email offline_access" # scope_prefix: "api://nmp/" -# Entities service configuration (uses DATABASE_* env vars from local.env) +# Entities service configuration (SQLite via DATABASE_* in local.env) entities: {} # Jobs service configuration jobs: + # Explicitly register the subprocess executor at profile "default". This opts # the documented `cpu/default` plugin steps (Data Designer create, Evaluator # metrics, Anonymizer, hello-world, etc.) into the cpu→subprocess translation @@ -62,6 +71,17 @@ jobs: ttl_seconds_before_active: 60 ttl_seconds_active: 3600 ttl_seconds_after_finished: 300 + # Uncomment for using customizer + # - provider: cpu + # profile: gpu + # backend: docker + # config: + # launcher_tool_path: ./services/core/jobs/jobs-launcher/jobs-launcher + # - provider: gpu + # profile: gpu + # backend: docker + # config: + # launcher_tool_path: ./services/core/jobs/jobs-launcher/jobs-launcher # Local path to the jobs-launcher binary used by the Docker job backend executor_defaults: diff --git a/packages/nmp_platform_runner/src/nmp/platform_runner/config/local.yaml b/packages/nmp_platform_runner/src/nmp/platform_runner/config/local.yaml index da199f0791..b85998d442 100644 --- a/packages/nmp_platform_runner/src/nmp/platform_runner/config/local.yaml +++ b/packages/nmp_platform_runner/src/nmp/platform_runner/config/local.yaml @@ -1,13 +1,16 @@ +# Bundled fallback when NMP_CONFIG_FILE_PATH is unset. For local dev, prefer: +# source packages/nmp_platform/config/local.env (SQLite + paths) platform: runtime: "docker" - base_url: "http://0.0.0.0:8080" + base_url: "http://127.0.0.1:8080" service: {} auth: - enabled: false + enabled: true + allow_unsigned_jwt: true policy_decision_point_provider: embedded - policy_decision_point_base_url: "http://localhost:8080" + policy_decision_point_base_url: "http://127.0.0.1:8080" policy_data_refresh_interval: 2 bundle_cache_seconds: 0 admin_email: "admin@example.com" diff --git a/packages/nmp_platform_runner/src/nmp/platform_runner/registry.py b/packages/nmp_platform_runner/src/nmp/platform_runner/registry.py index 58200766f5..4128802286 100644 --- a/packages/nmp_platform_runner/src/nmp/platform_runner/registry.py +++ b/packages/nmp_platform_runner/src/nmp/platform_runner/registry.py @@ -63,6 +63,7 @@ OPENAPI_SERVICES = [ "auth", + "customization", "entities", "evaluation", "files", diff --git a/packages/nmp_platform_runner/tests/test_registry.py b/packages/nmp_platform_runner/tests/test_registry.py index 4eba56620a..c530368a98 100644 --- a/packages/nmp_platform_runner/tests/test_registry.py +++ b/packages/nmp_platform_runner/tests/test_registry.py @@ -90,6 +90,26 @@ def test_openapi_services_are_explicit_and_do_not_auto_include_plugins(monkeypat assert registry.get_openapi_service_names(available) == ["auth", "evaluation"] +def test_customization_in_openapi_when_plugin_service_available(monkeypatch): + clear_registry_caches() + + class CustomizationService(NemoService): + name = "customization" + + def get_routers(self) -> list[RouterSpec]: + return [RouterSpec(router=APIRouter())] + + monkeypatch.setattr( + registry, + "AVAILABLE_SERVICES", + {"auth": "nmp.core.auth.main:service"}, + ) + monkeypatch.setattr(registry, "discover_services", lambda: {"customization": CustomizationService}) + + available = registry.get_available_services() + assert "customization" in registry.get_openapi_service_names(available) + + def test_intake_is_registered_as_api_and_openapi_service(): clear_registry_caches() available = registry.get_available_services() diff --git a/plugins/nemo-automodel/README.md b/plugins/nemo-automodel/README.md new file mode 100644 index 0000000000..3c6df3a557 --- /dev/null +++ b/plugins/nemo-automodel/README.md @@ -0,0 +1,30 @@ +# nemo-automodel-plugin + +Automodel training contributor under `/apis/customization/v2/workspaces/{workspace}/automodel/`. + +Requires **`nemo-customizer-plugin`** at runtime (router + `client.customization` SDK) and **`nmp-automodel`** (compiler/tasks). The Automodel plugin does not declare a pyproject dependency on the customizer plugin — install both via root `enabled-plugins`: + +```bash +uv sync --group enabled-plugins +``` + +## CLI + +Verbs are mounted directly on the contributor (no `jobs` subgroup): + +```bash +nemo customization automodel explain +nemo customization automodel submit path/to/job.json +nemo customization automodel submit path/to/job.json -w acme-corp +nemo customization automodel submit path/to/job.json --cluster my-cluster +``` + +`run` is registered but **always fails** — Automodel training is submit-only (platform API / Docker GPU jobs), not local subprocess execution: + +```bash +nemo customization automodel run path/to/job.json # exits with error +``` + +Other customization backends may still use `nemo customization jobs submit ...`. + +Job JSON uses the simplified `AutomodelJobInput` schema (see `nemo_automodel_plugin/schema.py`). Submit posts to `/apis/customization/v2/workspaces/{workspace}/automodel/jobs`. diff --git a/plugins/nemo-automodel/SCOPE.md b/plugins/nemo-automodel/SCOPE.md new file mode 100644 index 0000000000..1d5f8c85ab --- /dev/null +++ b/plugins/nemo-automodel/SCOPE.md @@ -0,0 +1,967 @@ +# NeMo Automodel Plugin — Work Scope + +**Start here:** [Implementation order](#implementation-order) (sequence, checklists, success criteria). + +This document scopes the work to replace the legacy Customizer Automodel path with a first-party **NeMo Automodel plugin** (customization **contributor**), the **`nemo-customizer-plugin`** router at `/apis/customization`, and the **`nmp-automodel`** task/compiler package (no standalone HTTP server). Legacy `Platform/services/customizer/` is reference only. New work: `plugins/nemo-customizer/`, `plugins/nemo-automodel/`, `services/automodel/`. + +Training is powered by the upstream **`nemo_automodel`** library (repo: `Automodel/` at workspace root, NGC image `nvcr.io/nvidia/nemo-automodel:25.11.00`). + +--- + +## Implementation order + +Canonical sequencing for this scope. **Work breakdown** (below) and design sections add detail; checklists live here only. + +### Sequence overview + +| Step | Focus | Package / area | Blocks | +|------|--------|----------------|--------| +| **0** | Design lock + platform Jobs flag | cross-cutting | — | +| **1** | Customization router | `plugins/nemo-customizer` | Automodel HTTP (step 4) | +| **2** | Task/compiler library | `services/automodel` (`nmp-automodel`) | Images (step 3), contributor compile (step 4) | +| **3** | Container images | `nmp-automodel` Dockerfiles | E2E GPU runs | +| **4** | Automodel plugin + Docker gate | `plugins/nemo-automodel` | CLI submit (step 5), integration (step 6) | +| **5** | CLI submit path | `nemo-automodel` + router CLI | — | +| **6** | Tests & contracts | `Platform/tests/...` | — | +| **7** | SDK, OpenAPI, docs, deploy | platform + plugins | — | + +**Parallel OK:** Step 0 Jobs flag with step 1–2. Step 2 compiler port with step 1 router (after contributor protocol is sketched). + +```mermaid +flowchart LR + S0[Step 0 Design lock] + S1[Step 1 nemo-customizer] + S2[Step 2 nmp-automodel] + S3[Step 3 Images] + S4[Step 4 nemo-automodel plugin] + S5[Step 5 CLI] + S6[Step 6 Tests] + S7[Step 7 Docs deploy] + S0 --> S1 + S0 --> S2 + S1 --> S4 + S2 --> S3 + S2 --> S4 + S3 --> S6 + S4 --> S5 + S4 --> S6 + S5 --> S7 + S6 --> S7 +``` + +--- + +### Step 0 — Design lock & platform prerequisites + +Lock names, routes, schemas, and cross-cutting Jobs config before feature PRs. First implementation PR can be plugin + `nmp-automodel` without Studio migration. + +**Design lock checklist** + +- [x] **Name & routes:** Router `NemoService.name = customization`; Automodel contributor prefix `v2/workspaces/{workspace}/automodel` → `/apis/customization/v2/workspaces/{workspace}/automodel/...`; CLI `nemo customization automodel` — [URL routing](#url-routing-decided), [Customization router](#customization-router-in-scope--v1). +- [x] **Workspace contract:** Path `{workspace}` authoritative; spec uses workspace-relative names + optional `ws/name` qualifiers; dataset URI rules documented; **no** `workspace` key in JSON body — [Workspace scoping](#workspace-scoping-required). +- [x] **Simplified JSON schema:** Publish `AutomodelJobInput` (v1) for POST/CLI; `AutomodelJobOutput` for stored/GET; `extra="forbid"` — [Simplified JSON spec](#simplified-json-spec-draft--automodeljobinput-only). +- [x] **Schema validators (legacy parity):** Reject `output_model` with message to use `output` (legacy `CustomizationJobInput`); `model_config` / field validators for distillation-only fields when `training_type: sft`. +- [x] **Dataset shape:** `{ training, validation? }` fileset URIs; `to_spec()` runs `check_dataset_access` per ref (port `platform_client`) — legacy API used a single `dataset` string; mapping documented in migration guide. +- [x] **Integrations:** `wandb` / `mlflow` accept `api_key_secret` (`SecretRef`) plus enabled/project fields — not only `null` placeholders. +- [x] **v1 exclusions (locked):** `deployment_config` (post-train NIM deploy), embedding-model SFT, DPO/GRPO — see [Decisions](#decisions-resolved). +- [x] **Input vs canonical spec (Option A):** Two schemas + `to_spec()` — port `transform_input_to_output` — [Input vs canonical spec](#input-vs-canonical-spec--decided-option-a). +- [x] **Deprecation / Studio:** Legacy customizer not in default `AVAILABLE_SERVICES`; UI feature-flagged off — [Deprecation](#deprecation--platform-spin-up-and-studio-verified). + +**Workspace registration (do before first integration test):** + +- [x] Add `plugins/nemo-customizer`, `plugins/nemo-automodel`, `services/automodel` to root `Platform/pyproject.toml` workspace members. +- [x] Add `nemo-customizer-plugin` and `nemo-automodel-plugin` to `[dependency-groups] enabled-plugins` (pattern: `nemo-evaluator-plugin`). + +**Platform Jobs — `jobs.enable_subprocess_executor`** (cross-cutting, not Automodel-only; rationale in [Platform jobs: `runtime` vs step executors](#platform-jobs-runtime-vs-step-executors)): + +- [x] Add field to `JobsServiceConfig` (`Platform/services/core/jobs/src/nmp/core/jobs/config.py`). +- [x] Gate `SubprocessJobExecutionProfile` in `get_default_executor_profiles_for_runtime()` (K8s default `false`, docker local default `true`). +- [ ] Document in `Platform/services/core/jobs/README.md`. +- [ ] Expose in `packages/nmp_platform/config/local.yaml` and `nmp_platform_runner` local config. +- [ ] `GET /v2/execution-profiles` reflects the flag. + +--- + +### Step 1 — `nemo-customizer` (blocks Automodel HTTP) + +**Problem:** `discover_services()` maps each `nemo.services` key to one `/apis//` mount — only one owner for `customization`. Training backends (Automodel, RL, Megatron, Unsloth) must share one URL tree without a monolithic `nmp-customizer` or per-backend top-level services. + +**Solution:** New package `plugins/nemo-customizer/` (`nemo_customizer`) ships the sole `nemo.services` → `customization` registration. Backends register as **contributors** via `nemo.customization.contributors`. Full design: [Customization router](#customization-router-in-scope--v1). + +**Router behavior (implement in this step):** + +1. `discover_customization_contributors()` — fault-isolated; allowlist `NEMO_PLUGIN_CUSTOMIZATION_CONTRIBUTORS_ALLOWLIST` (or `NEMO_PLUGIN_ALLOWLIST`). +2. **Zero contributors** → fail startup with clear error. +3. `CustomizationRouterService.get_routers()` — merge `RouterSpec` lists; **`dependencies`** = union of contributor + platform deps. +4. `CustomizationCLI.get_cli()` — `typer.Typer(name="customization")` + mount contributor subgroups (`automodel`, …). +5. OpenAPI / SDK — single service name `customization` when router + ≥1 contributor enabled. +6. **Route collision guard** — distinct segment per contributor under `.../workspaces/{workspace}/`; legacy `.../jobs` unmounted in v1. + +**`nemo-customizer-plugin` pyproject.toml:** + +```toml +[project.entry-points."nemo.services"] +customization = "nemo_customizer.router:CustomizationRouterService" + +[project.entry-points."nemo.cli"] +customization = "nemo_customizer.cli:CustomizationCLI" +``` + +**Deliverables** + +- [x] `CustomizationContributor` protocol in `nemo_platform_plugin/customization_contributor.py`; `discover_customization_contributors()` in `nemo_platform_plugin/discovery.py` (fault-isolated via `discover_entry_points`; allowlist `NEMO_PLUGIN_CUSTOMIZATION_CONTRIBUTORS_ALLOWLIST` or `NEMO_PLUGIN_ALLOWLIST`). `nemo_customizer/discovery.py` re-exports for backward compatibility. +- [x] **Zero contributors:** fail router startup with a clear error (do not mount an empty `/apis/customization` tree silently). +- [x] `CustomizationRouterService` + `CustomizationCLI` (merge contributors); **`dependencies`** = union of all contributor `dependencies` plus platform deps (`entities`, `jobs`, `auth`, …). +- [x] Entry points: `nemo.services` + `nemo.cli` → key `customization`. +- [x] Unit tests: two fake contributors → merged routes; prefix collision detection; zero contributors → startup error. +- [x] `OPENAPI_SERVICES` / registry: include `customization` when router plugin enabled **and** ≥1 contributor discovered. +- [x] `docs/CUSTOMIZATION.md` — contributor author guide (RL / Megatron / Unsloth). +- [x] Workspace members + `enabled-plugins` — [Step 0 workspace registration](#step-0--design-lock--platform-prerequisites). + +**Out of scope:** Legacy `POST .../workspaces/{ws}/jobs` multi-backend path; Studio cutover. + +--- + +### Step 2 — `nmp-automodel` package core + +`Platform/services/automodel/` — Python package **`nmp-automodel`**: compilers, task entrypoints, Dockerfiles. **No HTTP server** (unlike legacy `customizer-server`). Reference port: `Platform/services/customizer/` (trim multi-backend paths only). + +**4-step `PlatformJobSpec` pipeline** (Automodel-only): + +1. `file_io` (CPU) — download model + datasets (`nmp/automodel-tasks` image). +2. Training (GPU) — `finetune.py` + `nemo_automodel` recipes (SFT + KD); `nmp/automodel-training` image. +3. `file_io` upload. +4. `model_entity` — register model in Models service (behavior unchanged from legacy). + +| Area | Source | Action | +|------|--------|--------| +| Automodel config compiler | `tasks/training/backends/automodel/config.py` | Move; drop non-automodel imports; SFT + `_configure_kd()` | +| Training runner/backend | `backend.py`, `finetune.py`, `callbacks.py`, `checkpoints.py` | Move; keep `JobsServiceProgressReporter` + `TrainingProgressCallback` (rank-0) | +| Training step compiler | `app/jobs/training/compiler.py` | **Strip** to automodel-only; fixed `nmp/automodel-training` image ref | +| Job compiler | `app/jobs/compiler.py` | **Strip** DPO/RL/`nemo_rl`/`megatron_bridge`; keep distillation (KD); 4-step only | +| File I/O tasks | `tasks/file_io/` | Ported: `run.py`, `callbacks.py`, `utils.py`, `progress_reporter.py` | +| Model entity task | `tasks/model_entity/` | Move unchanged behavior | +| Schemas | `api/v2/jobs/schemas.py` | `AutomodelJobInput` + `AutomodelJobOutput` (+ sub-models) for plugin `to_spec` / compiler | + +**Deliverables** + +- [x] `nmp-automodel` installable; task entry points via console scripts + `nemo-platform run task --task nmp.automodel.tasks.*`. +- [x] Unit tests: adapter + compiler (`services/automodel/tests/`); contract `generate_configs.py` imports `nmp.automodel`. +- [x] Prove `PlatformJobSpec` generation for SFT (4-step pipeline, `nmp.automodel.tasks.*` commands, `nmp/automodel-training` / `nmp/automodel-tasks` images). +- [x] `validate_for_training()` on legacy `CustomizationJobOutput` (compiler); plugin `AutomodelJobOutput` has parallel validator in `nemo_automodel_plugin.schema`. +- [x] `platform_client.py` — `fetch_model_entity`, `check_dataset_access`. +- [x] `_resolve_v4_compatible()` in training compiler. +- [x] Task modules `nmp.automodel.tasks.{file_io,training,model_entity}`; compiled steps use `nmp.automodel.tasks.*`. +- [x] `AutomodelConfig.default_training_execution_profile` (`NMP_AUTOMODEL_*`); adapter + compile wrapper apply request `profile`. + +**Internal Jobs callback path** (not a new public route — same contract as legacy customizer): + +- [x] `NMPJobContext` env vars for job id, step, workspace, task name. +- [x] `JobsServiceProgressReporter` / `TrainingProgressCallback` → `sdk.jobs.tasks.create_or_update` (rank-0 only). +- [ ] Document as internal; exclude from public OpenAPI if auxiliary routes are added. + +*Optional later:* webhook-style callbacks — out of initial scope. + +--- + +### Step 3 — Container images + +Two runtime images (`nmp/automodel-tasks`, `nmp/automodel-training`) built from `nmp/automodel-base` (PyTorch + `nemo_automodel` deps), published under `nvcr.io/0921617854601259/nemo-platform-dev/nmp/...` — not the upstream NGC `nvcr.io/nvidia/nemo-automodel` training container name and not full `nmp-customizer` / RL / Megatron stack. Do **not** reuse or extend `customizer-automodel` during transition. + +| Image key | Dockerfile | Used by | Contents | +|-----------|------------|---------|----------| +| `nmp/automodel-training` | `Dockerfile.nmp-automodel-training` | GPU training step | `nmp/automodel-base` + `nmp-automodel` finetune backend (SFT + KD recipes) | +| `nmp/automodel-tasks` | `Dockerfile.nmp-automodel-tasks` | CPU steps (`file_io`, `model_entity`) | Slim glue; task entrypoints without customizer API server / RL / Megatron | + +**Deliverables** + +- [ ] Wire both keys in plugin `get_qualified_image()` / `NMP_AUTOMODEL_*` env overrides. +- [ ] CI: smoke import on **training** image (pattern: `Platform/tests/smoke_gpu/test_customizer_automodel.py`); lighter smoke on **tasks** image. +- [ ] Plugin README: size/dependency audit vs `customizer-automodel`. +- [ ] Helm/assets: image refs (Studio cutover to new URLs still out of scope). + +--- + +### Step 4 — `nemo-automodel` plugin (contributor + job) + +Plugin HTTP only — merged by router at `/apis/customization/.../automodel/...`. Requires **step 1** (`nemo-customizer-plugin`) in workspace. **`compile()`** depends on **step 2** (`nmp-automodel`). + +**Automodel plugin `pyproject.toml` (contributor — not `nemo.services`):** + +```toml +[project.entry-points."nemo.customization.contributors"] +automodel = "nemo_automodel_plugin.contributor:AutomodelContributor" + +[project.entry-points."nemo.jobs"] +"customization.automodel.jobs" = "nemo_automodel_plugin.jobs.jobs:AutomodelJob" +``` + +**Deliverables** + +- [x] **pyproject.toml:** `nemo-platform-plugin`, `nmp-automodel` (no `nemo-customizer-plugin` wheel dep — router installed via `enabled-plugins` only). Entry points: + - `nemo.customization.contributors` → `AutomodelContributor` (`automodel`) + - `nemo.jobs` → `customization.automodel.jobs` → `AutomodelJob` + - optional `nemo.docs` (no `nemo.services` / no top-level `nemo.cli`) +- [x] **`AutomodelContributor.get_routers()`** — optional `.../automodel/healthz`; mount jobs via `add_job_routes` (see wiring below); prefix `v2/workspaces/{workspace}/automodel`; `job_collection_path = "/automodel/jobs"`. +- [x] **`add_job_routes` wiring (required):** + - `service_name="customization"` — Jobs `source`, list filters, and OpenAPI service segment (default `_derive_service_name()` → `nemo-automodel-plugin` is **wrong**). + - `generate_job_name=generate_automodel_id` — `automodel-{uuid.hex[:12]}` when body omits `name` (same pattern as legacy `generate_customization_id`). + - `route_options=[JobRouteOption.CORE]` — create/list/get/delete/status/cancel/results; **no** PAUSE_RESUME in v1 (legacy parity). + - `default_profile` from plugin config when spec omits `training.execution_profile`. + - Request-body `profile` on `BaseJobRequest` — **deferred** (platform `add_job_routes` still drops it); v1 uses **`training.execution_profile`** in JSON only. +- [x] **`AutomodelJob`:** `description` set; `input_spec_schema` / `spec_schema` / `to_spec()` (Option A); `compile()` on `AutomodelJobOutput` only; `dependencies`: `entities`, `auth`, `jobs`, `secrets`, `files`, `models`. +- [ ] **Job envelope:** `description`, `project`, `ownership`, `custom_fields` — inherited from `job_route_factory` (no Automodel-specific fields); document in README. +- [x] **`get_cli()`** — `automodel` Typer subgroup via `add_job_commands` (`jobs` → `run` / `submit` / `explain` to `.../automodel/jobs`). Data Designer–style `cli/inputs.py` simplified JSON is [Step 5](#step-5--cli-submit-path), not required for Step 4. +- [x] SDK: `nemo-customizer-plugin` owns `nemo.sdk` → `customization`; composes `client.customization.automodel` from `nemo-automodel-plugin`. `nemo.docs` if user docs ship with plugin. +- [x] Workspace members + `enabled-plugins` — [Step 0 workspace registration](#step-0--design-lock--platform-prerequisites). + +**Docker enforcement & GPU validation** (`nemo_platform_plugin.jobs.docker` today: `validate_gpu_available_for_docker` only when `runtime == DOCKER` and reserved GPU list is empty — **extend for all Automodel jobs**): + +- [x] At **compile** (plugin `compile()` or shared helper): require `NemoPlatformConfig.runtime == DOCKER`. +- [x] Require `validate_docker_available()` (daemon reachable). +- [x] Require GPU pool configured (reuse or extend `validate_gpu_available_for_docker`). +- [x] `PlatformJobCompilationError` → 422, e.g. *“Automodel training requires `platform.runtime: docker` with GPU-backed container execution (Docker daemon reachable and GPUs configured).”* +- [x] Do **not** silently downgrade `platform.runtime` to `NONE` for this plugin. +- [ ] Do **not** conflate with `jobs.enable_subprocess_executor` — Automodel never schedules `subprocess` training steps. + +--- + +### Step 5 — CLI submit path + +First-class CLI for simplified JSON jobs (pattern: Data Designer `[CONFIG_SOURCE]` → canonical spec in `plugins/nemo-data-designer/.../cli/inputs.py`). Commands hang under `nemo customization automodel` (router CLI + contributor subgroup). + +**Submit URL** (via `nemo_platform_plugin.commands` job submit helper): + +`/apis/customization/v2/workspaces/{workspace}/automodel/jobs` + +Custom wrappers must **forward** `--workspace` / `-w` to the framework callback (default `"default"` for local dev only). + +**Deliverables** + +- [x] `nemo customization automodel jobs submit --workspace ` — `cli/inputs.py` validates `AutomodelJobInput` and POSTs to `.../automodel/jobs` (`tests/test_cli.py`). +- [x] `jobs explain` — exposes `input_spec_schema` + `spec_schema` via framework `explain` (`tests/test_cli.py`). +- [x] CLI tests: `-w` / `--workspace` in submit URL (`submit_path_for` + mocked `submit_remote`). + +--- + +### Step 6 — Tests & contract continuity + +Relocate contract tests from legacy `customizer-automodel` path; validate router + contributor + compiler together. + +**Deliverables** + +- [x] Contract script import path fixed (`generate_configs.py` → `backends.config`); `services/automodel/tests/test_contract_configs.py` parses SFT/packing inputs + optional `--check` (embedding gated/skipped for v1). +- [x] Unit/API: Automodel + customization router routes under `/v2/workspaces/{workspace}/automodel/...` (`plugins/nemo-automodel/tests/test_api.py`). +- [x] Integration: compile-only via `services/automodel/tests/test_compiler.py` (contract fixture when present); CLI submit mocked in `test_cli.py`. +- [ ] Agentic smoke: adapt `Platform/tests/agentic-use/customizer-lora-job-cli` → `nemo customization automodel` CLI. +- [ ] E2E: job completes → **Model** entity exists → fileset populated → LoRA metadata when `finetuning_type=lora`. +- [x] Workspace isolation: routes scoped by `{workspace}` path segment (`test_api.py`); full cross-workspace API test deferred to Jobs service integration. + +--- + +### Step 7 — SDK, OpenAPI, docs & rollout + +**API & SDK polish** + +- [ ] OpenAPI tags: “Automodel Training Jobs”. +- [ ] List/get/delete/results routes via `add_job_routes` defaults under `/v2/workspaces/{workspace}/automodel/jobs`. +- [x] SDK hub: `client.customization.automodel.jobs.create(workspace=..., spec=...)` — paths under `/v2/workspaces/{workspace}/automodel/jobs`; **no** silent global namespace default (document `workspace="default"` for local dev). +- [ ] Error mapping: `PlatformJobCompilationError` / `validate_for_training` → 422; `check_dataset_access` / model entity auth failures → 403 or 422 with clear copy. +- [ ] Migration guide field table: legacy flat `training` + single `dataset` string → `AutomodelJobInput` sections; `output_model` → `output`. + +**Docs & deploy** + +- [ ] Automodel plugin README: install, enabled-plugins, CLI examples, sample `job.json`. +- [ ] Config reference: `NMP_AUTOMODEL_*` (training/tasks image overrides, resource defaults); link `NMP_JOBS_ENABLE_SUBPROCESS_EXECUTOR` / [Step 0](#step-0--design-lock--platform-prerequisites). +- [ ] Migration guide: `CustomizationJob` / `CustomizationJobInput` → `AutomodelJobInput` field mapping. +- [ ] Helm/assets: deploy `nmp/automodel-training` + `nmp/automodel-tasks` (replace `customizer-automodel` on product cutover — Studio migration still out of scope). + +--- + +### Success criteria (exit checks) + +- [ ] `nemo customization automodel jobs submit job.json -w acme-corp` → `/apis/customization/v2/workspaces/acme-corp/automodel/jobs`; fails fast without Docker/GPU. +- [ ] `POST` accepts `AutomodelJobInput`; GET returns enriched `AutomodelJobOutput` in `acme-corp`. +- [ ] Completed job: **Model** entity + fileset + adapter metadata in same workspace. +- [ ] Training progress on Jobs task `status_details.metrics`. +- [ ] Training image CI smoke passes. +- [ ] No legacy `platform_job_config_compiler` / multi-backend customizer dependency. +- [ ] Router test: second fake contributor merges without router code changes. + +--- + +## Goals (from requirements) + +| Requirement | Intent | +|-------------|--------| +| **First-class CLI** | Submit/run jobs from a **simplified JSON** job config (not the full CustomizationJob API surface). Pattern: Data Designer’s `[CONFIG_SOURCE]` → canonical spec (`plugins/nemo-data-designer/.../cli/inputs.py`). | +| **Fail if Docker disabled for jobs** | Automodel training is GPU + container-only. Reject compile/submit when `platform.runtime` is not `docker` or Docker daemon/GPUs are unavailable (stricter than today’s “warn and set runtime NONE”). Independent of `jobs.enable_subprocess_executor`. | +| **First-class API** | Workspace-scoped REST under `/apis/customization/v2/workspaces/{workspace}/automodel/...` — `{workspace}` is a **required path segment** on every job route (create, list, get, delete, results). Served via the **customization router** (single `/apis/customization` mount); Automodel is the first contributor. | +| **Customization router** | **`nemo-customizer-plugin`** owns `/apis/customization` and merges HTTP/CLI/SDK from contributors (Automodel v1; RL / Megatron / Unsloth later) — no monolithic `nmp-customizer`, no per-backend top-level `/apis/*` services. | +| **Automodel-only job path** | No NeMo RL, Megatron-Bridge, DPO, GRPO, or multi-backend dispatch. Single compiler → single training step image. | +| **Internal callback API** | Keep task-level progress updates to the Jobs service (`sdk.jobs.tasks.create_or_update`) from training subprocesses — not a public user API. | +| **Simplified training image** | New image derived from `nemo-automodel` NGC base with only platform task glue + `nemo_automodel`, not full `nmp-customizer` / RL / Megatron stack. | +| **Entity lifecycle** | Jobs still: download artifacts → train → upload checkpoint → **create/update Model entity** (and LoRA adapter metadata where applicable). | +| **Jobs API parity** | `service_name="customization"` on `add_job_routes`; auto `automodel-{id}` names; `training.execution_profile` in spec; CORE routes only. | + +--- + +## Platform jobs: `runtime` vs step executors + +Two layers are easy to conflate; this plugin only cares about the second for **training steps**, but operators need both clear in config and docs. + +| Layer | Config | Cardinality | Meaning | +|-------|--------|-------------|---------| +| **Platform deployment** | `platform.runtime` | **One value** per process (`docker` \| `kubernetes` \| `none`) | How the platform orchestrates container workloads (Docker daemon vs K8s vs neither). **Not** “how every job step runs.” | +| **Job step execution** | `platform_spec.steps[].executor` | **Per step** | Backend for that step: `cpu`/`gpu` + container → Docker or K8s; `subprocess` → host process (local dev / lightweight tasks). | + +Today, when `platform.runtime: docker`, the Jobs service **implicitly** also registers `subprocess/default` (host execution) alongside `cpu/default` and `gpu/default` (Docker). That coupling is what makes `runtime: docker` sound like “everything runs in Docker.” + +### Proposed: `jobs.enable_subprocess_executor` + +Make host subprocess execution an **explicit** platform choice instead of a side effect of `runtime: docker`. + +| Field | Type | Default | Behavior | +|-------|------|---------|----------| +| `jobs.enable_subprocess_executor` | `bool` | `true` when `platform.runtime == docker` (local dev); **`false` on Kubernetes** unless explicitly set `true` | When `true`, register `subprocess/default` and allow steps with `provider: subprocess`. When `false`, omit subprocess from default profiles; CPU/GPU container steps use Docker (or K8s) only. Dev clusters may opt in explicitly; production K8s should leave host execution disabled. | + +**Implementation:** [Step 0 — Platform Jobs flag](#step-0--design-lock--platform-prerequisites) (cross-cutting, not Automodel-only). + +**Automodel plugin implications:** + +- Training steps are **always** `cpu`/`gpu` + container → Docker; Automodel does **not** depend on `enable_subprocess_executor`. +- Compile gate ([Step 4](#step-4--nemo-automodel-plugin-contributor--job)): **`platform.runtime == docker`** + daemon + GPUs — not “subprocess enabled.” +- Optional `jobs run` ([Step 4](#step-4--nemo-automodel-plugin-contributor--job) 2b): subprocess only if the flag is enabled. +- Prefer error copy: *“Automodel training requires `platform.runtime: docker` with GPU-backed container execution”* — avoid *“Docker job runtime”* without qualification. + +**Example local config (explicit):** + +```yaml +platform: + runtime: docker + +jobs: + enable_subprocess_executor: true # host steps for dev; training still uses cpu/gpu + container + executor_defaults: + docker: + launcher_tool_path: ./services/core/jobs/jobs-launcher/jobs-launcher + subprocess: + working_directory: /tmp/nmp-subprocess-jobs +``` + +Production / GPU-only deployments can set `enable_subprocess_executor: false` to avoid registering host execution while keeping `runtime: docker` for Automodel and other container jobs. + +--- + +## Current state (reference) + +### Legacy Customizer (`Platform/services/customizer/`) + +- **API**: `CustomizationJobInput` / `CustomizationJobOutput` via `job_route_factory` (`api/v2/jobs/endpoints.py`). +- **Compiler**: `platform_job_config_compiler` builds a **4-step** `PlatformJobSpec`: + 1. `nmp.customizer.tasks.file_io` (CPU) — download model + datasets + 2. Training (GPU) — backend selected in training compiler (`automodel` \| `nemo_rl` \| `megatron_bridge`) + 3. `file_io` upload + 4. `nmp.customizer.tasks.model_entity` — register model in Models service +- **Automodel backend**: `tasks/training/backends/automodel/` — `compile_automodel_config()`, `AutomodelBackend`, `finetune.py` (wraps `nemo_automodel` recipes + `TrainingProgressCallback`). +- **Image**: `customizer-automodel` (see `nmp/docker/Dockerfile.nmp-customizer`); contract tests in `Platform/tests/customizer-automodel-contract/`. +- **Progress “callbacks”**: `JobsServiceProgressReporter` + `TrainingProgressCallback` call Jobs internal task API (rank-0 only). + +### Platform plugin patterns (`Platform/plugins/`) + +- Entry points: training plugins use `nemo.customization.contributors`; **`nemo-customizer-plugin`** uses `nemo.services` + `nemo.cli` key `customization`; jobs via `nemo.jobs` (`customization..`). +- Jobs: `NemoJob` + `add_job_routes()` (`nemo_platform_plugin.jobs.routes`). +- Reference plugins: `nemo-evaluator` (service + job scaffold), `nemo-data-designer` (CLI config file → spec), `nemo-agents` (service + multiple routers). + +### Simplified config shape (already validated) + +Contract input JSONs under `Platform/tests/customizer-automodel-contract/input_configs/` are a good starting point for the **CLI/API simplified spec** (e.g. `llama_3_2_1b_lora.json`): `model`, `dataset`, `training`, `schedule`, `batch`, `optimizer`, `parallelism`, `output_model`, optional `seed`. + +--- + +## Target architecture + +```mermaid +flowchart TB + subgraph surfaces [Plugin surfaces] + CLI["nemo customization automodel jobs submit -w WS job.json"] + API["POST .../v2/workspaces/WS/automodel/jobs"] + SDK["client...jobs.create(workspace=WS)"] + end + + subgraph router [nemo-customizer] + CUST["CustomizationRouterService"] + MERGE["merge contributors"] + end + + subgraph plugin [plugins/nemo-automodel] + CONTrib["AutomodelContributor"] + JOB["AutomodelJob\n(NemoJob.compile)"] + CLI_MOD["automodel CLI subgroup"] + end + + subgraph pkg [services/automodel — library only, no HTTP server] + CORE["compile_spec / validate"] + TASK_TRAIN["tasks/training\n(automodel only)"] + TASK_IO["tasks/file_io"] + TASK_ME["tasks/model_entity"] + end + + subgraph deploy [platform.runtime docker] + JOBS["Jobs service"] + DOCKER["cpu/gpu steps → Docker"] + MODELS["Models service"] + FILES["Files service"] + end + + CLI --> CUST + API --> CUST + SDK --> CUST + CUST --> MERGE --> CONTrib + CONTrib --> JOB + CONTrib --> CLI_MOD + CONTrib --> CORE + JOB -->|compile PlatformJobSpec| JOBS + JOBS --> DOCKER + DOCKER --> TASK_IO + DOCKER --> TASK_TRAIN + DOCKER --> TASK_ME + TASK_TRAIN -->|internal tasks API| JOBS + TASK_ME --> MODELS + TASK_IO --> FILES +``` + +### Package layout (proposed) + +``` +Platform/ + plugins/nemo-customizer/ # router + contributor protocol (v1) + pyproject.toml + src/nemo_customizer/ + router.py # CustomizationRouterService (nemo.services → customization) + cli.py # CustomizationCLI + contributor.py # re-export CustomizationContributor from nemo_platform_plugin + discovery.py # re-export discover_customization_contributors + docs/CUSTOMIZATION.md # contributor author guide + + plugins/nemo-automodel/ + SCOPE.md # this file + pyproject.toml + src/nemo_automodel_plugin/ + contributor.py # AutomodelContributor (routers + CLI subgroup) + cli.py + cli/inputs.py # JSON config → spec + config.py # NemoConfig (image names, defaults) + schema.py # AutomodelJobInput, AutomodelJobOutput, sub-models + jobs/ + jobs.py # AutomodelJob (compile + optional local run) + sdk/ # optional hub resources + docs/ + + services/automodel/ # Python package nmp-automodel — tasks/compiler only (no HTTP server) + pyproject.toml + src/nmp/automodel/ + config.py + platform_client.py # model entity fetch (from customizer) + app/jobs/ + compiler.py # Automodel-only PlatformJobSpec (4 steps, slim) + training/ + compiler.py # single GPU step + schemas.py + file_io/ # port or thin wrapper from customizer + model_entity/ # port from customizer + tasks/ + training/backends/automodel/ # port: config, backend, finetune, callbacks + file_io/ + model_entity/ + docker/ + Dockerfile.nmp-automodel-training # GPU: nmp-automodel-base + finetune + Dockerfile.nmp-automodel-tasks # CPU: file_io / model_entity glue (slimmer) + tests/ + +``` + +**Dependency rule**: + +| Package | Depends on | Provides | +|---------|------------|----------| +| **`nemo-customizer-plugin`** | `nemo-platform-plugin` | Router service/CLI; `CustomizationContributor` protocol and `discover_customization_contributors()` live in **`nemo_platform_plugin`** | +| **`nemo-automodel`** (plugin) | `nemo-platform-plugin`, `nmp-automodel` (+ `nemo-customizer-plugin` at runtime via `enabled-plugins`) | `AutomodelContributor`, schemas; Step 5 `cli/inputs.py` optional | +| **`nmp-automodel`** (service) | `nmp-common`, platform SDK types | Compilers, task entrypoints, Dockerfiles | + +Avoid pulling entire legacy `nmp-customizer`. **`nemo-platform-plugin`** holds the contributor protocol and discovery (IGW-aligned); **`nemo-customizer-plugin`** holds only the router service/CLI merge logic. + +### Customization router (in scope — v1) + +**Problem:** `discover_services()` maps `nemo.services` entry-point **keys** 1:1 to mounted apps (`/apis//...`). Only one plugin can own `customization`. A monolithic customizer is out; multiple training backends (Automodel, RL, Megatron, Unsloth) must share one URL tree without boxing future plugins into Automodel’s package. + +**Solution:** **`nemo-customizer-plugin`** ships **`CustomizationRouterService`** as the sole `nemo.services` registration for `customization`. Training plugins register as **contributors** via a new entry-point group; they do **not** register their own top-level `nemo.services` key. + +| Piece | Owner | Registration | +|-------|--------|----------------| +| `/apis/customization/...` mount | `nemo-customizer-plugin` | `nemo.services` → `customization` = `CustomizationRouterService` | +| Automodel routes | `nemo-automodel` plugin | `nemo.customization.contributors` → `automodel` = `AutomodelContributor` | +| Future RL / Megatron / Unsloth | Each backend’s plugin | Same group, distinct keys: `rl`, `megatron`, `unsloth`, … | +| Task/compiler library | `nmp-automodel` | No HTTP; imported by plugin + Jobs task images | + +**Contributor contract** (protocol in `nemo_customizer.contributor`): + +```python +class CustomizationContributor(Protocol): + """One training backend under /apis/customization.""" + + name: ClassVar[str] # must match entry-point key, e.g. "automodel" + + def get_routers(self) -> list[RouterSpec]: + """e.g. prefix v2/workspaces/{workspace}/automodel + job routes.""" + + def get_cli(self) -> typer.Typer | None: + """Subgroup mounted at `nemo customization `.""" +``` + +SDK: **`nemo-customizer-plugin`** registers `nemo.sdk` → `customization` and composes per-contributor SDK modules (e.g. `nemo_automodel_plugin.sdk.resources` → `client.customization.automodel`). +``` + +**Router behavior:** + +1. `discover_customization_contributors()` loads all `nemo.customization.contributors` entry points (fault-isolated, allowlist via `NEMO_PLUGIN_CUSTOMIZATION_CONTRIBUTORS_ALLOWLIST` or `NEMO_PLUGIN_ALLOWLIST`). +2. If **zero** contributors load, **fail startup** with a clear configuration error (router enabled but no backends). +3. `CustomizationRouterService.get_routers()` concatenates each contributor’s `RouterSpec` list (stable sort by `name`); `dependencies` = union of contributor + platform service deps (`merge_router_dependencies()` at router startup). +4. `CustomizationCLI.get_cli()` builds `typer.Typer(name="customization")` and mounts each contributor subgroup (`automodel`, …). +5. OpenAPI / SDK generation includes the merged tree under service name `customization` only. +6. **No route collision:** each contributor owns a distinct path segment after `.../workspaces/{workspace}/` (Automodel → `automodel`; legacy multi-backend `jobs` stays unmounted until a contributor revives it intentionally). + +**Automodel plugin wiring (v1):** + +| Surface | Entry point | Notes | +|---------|-------------|--------| +| HTTP | `nemo.customization.contributors.automodel` | **Not** `nemo.services` — router owns the mount | +| Jobs | `nemo.jobs` → `customization.automodel.jobs` | Unchanged | +| CLI | Via contributor `get_cli()` | `nemo customization automodel jobs ...` | +| SDK | `nemo-customizer-plugin` → `nemo.sdk:customization` composes contributor SDKs | `client.customization.automodel.jobs` | +| Tasks | `nmp-automodel` package | No server | + +**pyproject.toml (Automodel plugin):** + +```toml +[project.entry-points."nemo.customization.contributors"] +automodel = "nemo_automodel_plugin.contributor:AutomodelContributor" + +[project.entry-points."nemo.jobs"] +"customization.automodel.jobs" = "nemo_automodel_plugin.jobs.jobs:AutomodelJob" +``` + +**`nemo-customizer-plugin` pyproject.toml:** + +```toml +[project.entry-points."nemo.services"] +customization = "nemo_customizer.router:CustomizationRouterService" + +[project.entry-points."nemo.cli"] +customization = "nemo_customizer.cli:CustomizationCLI" + +[project.entry-points."nemo.sdk"] +customization = "nemo_customizer.sdk.resources:customization_sdk_resources" +``` + +Enable in platform workspace / `enabled-plugins` alongside `nemo-automodel`. + +**Implementation checklist:** [Step 1 — `nemo-customizer-plugin`](#step-1--nemo-customizer-blocks-automodel-http). + +### URL routing (decided) + +Platform mounts the router at `/apis/customization//...`. Automodel contributor prefix: + +| Piece | Value | +|-------|--------| +| Router `NemoService.name` | `customization` | +| Contributor key | `automodel` | +| Automodel `RouterSpec.prefix` | `v2/workspaces/{workspace}/automodel` | +| Example job create | `POST /apis/customization/v2/workspaces/{workspace}/automodel/jobs` | +| Legacy (deprecated) | `POST /apis/customization/v2/workspaces/{workspace}/jobs` — **not registered** in v1 | + +**No `/train/` segment:** Flat `/jobs` under `.../automodel/` (`NemoJob.job_collection_path = "/jobs"`). + +| Job wiring | Value | +|----------|--------| +| `NemoJob.job_collection_path` | `"/jobs"` | +| `NemoJob.name` | `"jobs"` (CLI/SDK subgroup suffix only) | +| `nemo.jobs` entry key | `customization.automodel.jobs` | +| `add_job_routes(..., service_name=)` | **`"customization"`** (required; sets Jobs `source` + filters) | +| `generate_job_name` | **`generate_automodel_id`** → `automodel-{hex}` | +| `route_options` | **`[JobRouteOption.CORE]`** (no pause/resume v1) | +| `training.execution_profile` | Spec field → GPU step profile; default from `NMP_AUTOMODEL_DEFAULT_TRAINING_EXECUTION_PROFILE` | +| Request `profile` body | **Deferred** — use spec field until `BaseJobRequest` plumbing lands | + +Do **not** register `nemo.services` = `automodel` (would split the product URL tree). + +**Contributor job mount (reference):** + +```python +from nmp.common.jobs.api_factory import JobRouteOption +from nemo_platform_plugin.jobs.routes import add_job_routes + +def get_routers(self) -> list[RouterSpec]: + return [ + RouterSpec( + prefix="v2/workspaces/{workspace}/automodel", + router=add_job_routes( + AutomodelJob, + service_name="customization", + generate_job_name=generate_automodel_id, + route_options=[JobRouteOption.CORE], + default_profile=plugin_config.default_training_execution_profile, + ), + ), + ] +``` + +**CLI:** `nemo customization automodel jobs submit job.json` — router CLI + Automodel contributor subgroup. + +### Workspace scoping (required) + +All Automodel resources are scoped to a **platform workspace** (tenant/project boundary). The workspace is carried on the URL path for HTTP, on CLI/SDK calls for clients, and in job/task runtime env — it is **not** a separate top-level field in the simplified job JSON body. + +#### API routes (full pattern) + +Mount prefix: `v2/workspaces/{workspace}/automodel` → base: + +`/apis/customization/v2/workspaces/{workspace}/automodel` + +| Operation | Method | Path (after base) | +|-----------|--------|-------------------| +| Create job | `POST` | `/jobs` | +| List jobs | `GET` | `/jobs` | +| Get job | `GET` | `/jobs/{job_name}` | +| Delete job | `DELETE` | `/jobs/{job_name}` | +| Job results | `GET` | `/jobs/{job_name}/results/...` | + +Example: + +```http +POST /apis/customization/v2/workspaces/acme-corp/automodel/jobs +Content-Type: application/json + +{ "model": "llama-3-8b-base", "dataset": { ... }, ... } +``` + +`acme-corp` is the scope for: authz checks, Jobs service record, Models/Filesets entities, and compiled fileset `workspace` fields. + +#### Workspace in the job spec (body vs path) + +| Source | Role | +|--------|------| +| **Path `{workspace}`** | Authoritative scope for the job and all entities created by it (output model, output fileset, Jobs record). | +| **Spec `model`** | Model entity **name** in the path workspace, or qualified `other-workspace/model-name` for cross-workspace reads (same as legacy `CustomizationJobInput.model`). | +| **Spec `dataset`** | `{ training: "name" }` or `{ training: "workspace/name" }` — bare names resolve in the path workspace (no `fileset://` prefix). | +| **Spec `output.name`** | New or updated `ModelEntity` **in the path workspace** only. | +| **Body `workspace` field** | **Do not add** — avoids conflicting with the path param. | + +`compile(workspace, spec, ...)` and `to_spec(..., workspace=...)` receive the path workspace from `add_job_routes` / `job_route_factory` (same contract as `nemo_platform_plugin.jobs.routes`). + +#### CLI + +Auto-generated `submit` / `run` include `--workspace` / `-w` (default `"default"`). Custom wrappers must **forward** it to the framework callback: + +```bash +nemo customization automodel jobs submit job.json --workspace acme-corp +nemo customization automodel jobs submit job.json -w acme-corp +# execution profile: set training.execution_profile in job.json (request --profile body deferred) +``` + +Submit URL (see `nemo_platform_plugin.commands` job submit helper): + +`/apis/customization/v2/workspaces/{workspace}/automodel/jobs` + +i.e. `/apis/{NemoService.name}/{RouterSpec.prefix}/...` with `name=customization` and prefix `v2/workspaces/{workspace}/automodel`. + +#### SDK + +Hub resources take `workspace` on every call (pattern: evaluator `client.evaluator...`): + +```python +job = client.customization.automodel.jobs.create( + workspace="acme-corp", + spec=AutomodelJobInput(...), +) +status = client.customization.automodel.jobs.retrieve( + workspace="acme-corp", + name=job.name, +) +``` + +SDK must not default silently to a global namespace; document `workspace="default"` for local dev only. + +#### Runtime (compiled job + tasks) + +| Stage | Workspace usage | +|-------|------------------| +| **Compile** | `fetch_model_entity(spec.model, workspace, sdk)`; output fileset refs use `workspace=None` in compile JSON and are resolved at runtime to the job workspace (legacy customizer pattern). | +| **Jobs service** | Job created in path workspace. | +| **Task containers** | `NEMO_JOB_WORKSPACE` (and `JobContext.workspace` / `get_workspace()`) set from job; `model_entity` task creates entities in that workspace. | +| **Progress callbacks** | `sdk.jobs.tasks.create_or_update(..., workspace=job_ctx.workspace, job=job_ctx.job_id, ...)`. | +| **List/filter** | API list endpoints return only jobs in the path workspace. | + +#### Tests + +→ [Step 5](#step-5--cli-submit-path) (CLI `-w`), [Step 6](#step-6--tests--contract-continuity) (API, integration, workspace isolation). + +--- + +## Work breakdown + +Phases map to [Implementation order](#implementation-order). **Checklists and step-level detail live in the steps above**; sections below add design reference (Option A wiring, Studio verification, JSON spec) without duplicating deliverables. + +| Phase | Implementation step(s) | Topic | +|-------|------------------------|--------| +| 0 | [Step 0](#step-0--design-lock--platform-prerequisites) | Design lock, Jobs flag, schemas | +| 1 | [Step 1](#step-1--nemo-customizer-blocks-automodel-http) | Customization router | +| 2 | [Step 2](#step-2--nmp-automodel-package-core) | `nmp-automodel` compiler/tasks | +| 3 | [Step 3](#step-3--container-images) | Docker images | +| 4 | [Step 4](#step-4--nemo-automodel-plugin-contributor--job) | Automodel plugin + Docker gate | +| 5 | [Step 5](#step-5--cli-submit-path) | CLI | +| 6 | [Step 6](#step-6--tests--contract-continuity) | Tests | +| 7 | [Step 7](#step-7--sdk-openapi-docs--rollout) | SDK / docs / deploy | +| — | [Step 2](#step-2--nmp-automodel-package-core) (callbacks) | Internal Jobs task API (not public) | + +### Phase 0 — Design lock + +→ [Step 0](#step-0--design-lock--platform-prerequisites). Router design: [Customization router](#customization-router-in-scope--v1). + +#### Input vs canonical spec — **decided: Option A** + +On job **create**, the platform always: + +1. Validates the POST body against **`AutomodelJobInput`** (`input_spec_schema`). +2. Runs **`AutomodelJob.to_spec()`** → **`AutomodelJobOutput`** stored on the Jobs record (`spec_schema`). +3. Runs **`compile()`** on the canonical output → `platform_spec` for execution. + +Enrichment (auto output name/fileset, adapter vs model type, dataset ACL, model entity fetch) happens in step 2 — the Jobs service persists that result, not a post-compile rewrite. Rejected alternatives: single-schema POST (manual output fields), enrich-only-in-`compile()` (broken persistence), renamed input fields (unnecessary vs legacy). + +**`AutomodelJob` wiring:** + +```python +class AutomodelJobInput(BaseModel): # POST body / CLI JSON + model: str # name or workspace/name + dataset: DatasetSpec # training + optional validation fileset URIs + training: TrainingSpec # includes training_type, execution_profile, ... + output: OutputRequest | None = None # optional name only + # @model_validator: reject "output_model" key with legacy error message + +class AutomodelJobOutput(BaseModel): # stored spec + GET response shape + output: OutputResponse # required: name, fileset, type (model | adapter) + # ... enriched fields from input ... + + def validate_for_training(self) -> None: + # Port MoE / parallelism rules from CustomizationJobOutput + +class AutomodelJob(NemoJob): + name = "jobs" + job_collection_path = "/jobs" + input_spec_schema = AutomodelJobInput + spec_schema = AutomodelJobOutput + dependencies = ["entities", "auth", "jobs", "secrets", "files", "models"] + + @classmethod + async def to_spec(cls, input_spec, *, workspace, entity_client, async_sdk, is_local): + # Port transform_input_to_output + check_dataset_access per fileset + + @classmethod + async def compile(cls, *, workspace, spec: AutomodelJobOutput, ...): + spec.validate_for_training() + # nmp.automodel.app.jobs.compiler → PlatformJobSpec (4 steps) +``` + +**Implementation notes:** + +- Port source: `Platform/services/customizer/src/nmp/customizer/utils.py` (`transform_input_to_output`). +- `to_spec()` generates `output.fileset`, infers `output.type`, runs `fetch_model_entity` + `check_dataset_access`. +- `compile()` receives **`AutomodelJobOutput` only**; calls `validate_for_training()` before building `PlatformJobSpec`. +- Mount via `add_job_routes(..., service_name="customization", generate_job_name=generate_automodel_id)` — [URL routing](#url-routing-decided). +- **CLI JSON** = `AutomodelJobInput`. **`jobs explain`** exposes both schemas. + +#### Deprecation — Platform spin-up and Studio (verified) + +**Platform `AVAILABLE_SERVICES`** (`packages/nmp_platform_runner/src/nmp/platform_runner/registry.py`) does **not** include `customization` / `customizer`: + +```18:33:packages/nmp_platform_runner/src/nmp/platform_runner/registry.py +AVAILABLE_SERVICES: dict[str, str] = { + "hello-world": "nmp.hello_world.main:service", + "studio": "nmp.studio.main:service", + ... + "inference-gateway": "nmp.core.inference_gateway.main:service", +} +``` + +`API_SERVICES` and `OPENAPI_SERVICES` likewise omit customization. Plugin services are merged at runtime via `discover_services()` (e.g. future `customization` from `nemo.services`), but the **legacy `nmp.customizer` microservice is not started** by default platform spin-up in this repo. + +**Note:** The older `nmp/` tree still lists `"customization": "nmp.customizer.main:service"` in its copy of the registry — do not treat that as Platform default behavior. + +**Studio today:** + +| Signal | Status | +|--------|--------| +| `VITE_FF_CUSTOMIZER_ENABLED` | Default **`false`** (`featureFlags.ts`) | +| Routes | Gated via `CUSTOMIZER_ENABLED` / `gateRoutes` — customization pages hidden when flag off | +| Live API | Vendored hooks target `/apis/customization/v2/.../jobs`; comment states service removed and UI must not call at runtime | +| Tests | MSW handlers in `mocks/handlers/customizer.ts`; `create-a-customization.spec.tsx` is **`describe.skip`** | + +```8:9:Platform/web/packages/sdk/vendored/customizer/api.ts +// Note: these hooks call /apis/customization/v2/... endpoints that won't exist while the customizer +// service is removed. The customizer UI is feature-flagged off, so they should never be invoked at runtime. +``` + +**First PR implication:** Safe to register **`CustomizationRouterService`** plus **`AutomodelContributor`** without legacy `nmp-customizer`. Studio/SDK migration **out of scope**. + +### Phase 1 — `nmp-automodel` package core + +→ [Step 2](#step-2--nmp-automodel-package-core). Port table and deliverables are defined there. + +### Phase 2 — Plugin surfaces + +→ [Step 4](#step-4--nemo-automodel-plugin-contributor--job) + [Step 5](#step-5--cli-submit-path). Requires [Step 1](#step-1--nemo-customizer-blocks-automodel-http). + +### Phase 3 — Docker enforcement & GPU validation + +→ [Step 4](#step-4--nemo-automodel-plugin-contributor--job) (compile-time checks). Today `validate_gpu_available_for_docker` only runs when `runtime == DOCKER` and reserved GPU list is empty — extend for all Automodel jobs. + +### Phase 4 — Container images + +→ [Step 3](#step-3--container-images). + +### Phase 5 — Internal Jobs callback path + +→ [Step 2](#step-2--nmp-automodel-package-core) (not a new public route). Optional later: webhooks. + +### Phase 6 — API & SDK polish + +→ [Step 7](#step-7--sdk-openapi-docs--rollout). + +### Phase 7 — Testing & contract continuity + +→ [Step 6](#step-6--tests--contract-continuity). + +### Phase 8 — Docs & rollout + +→ [Step 7](#step-7--sdk-openapi-docs--rollout). + +--- + +## Simplified JSON spec (draft) — `AutomodelJobInput` only + +POST body and CLI JSON file use **`AutomodelJobInput`** only. After create, GET returns **`AutomodelJobOutput`** with enriched `output` (fileset, type). Validated in the context of the path **`workspace`** (or CLI `-w`). Entity names below are relative to that workspace unless qualified as `other-ws/name`. + +```json +{ + "name": "optional-job-name", + "model": "llama-3-8b-base", + "dataset": { + "training": "my-sft-train", + "validation": "my-sft-val" + }, + "training": { + "training_type": "sft | distillation", + "finetuning_type": "lora | all_weights | lora_merged", + "lora": { "rank": 16, "alpha": 32, "merge": false, "target_modules": null }, + "max_seq_length": 2048, + "execution_profile": "gpu", + "teacher_model": "meta/llama-3.2-3b-instruct", + "distillation_ratio": 0.5, + "distillation_temperature": 1.0, + "teacher_precision": "bf16", + "offload_teacher": false + }, + "schedule": { "epochs": 1, "max_steps": 50, "val_check_interval": 25, "seed": 42 }, + "batch": { "global_batch_size": 8, "micro_batch_size": 1, "sequence_packing": false }, + "optimizer": { + "learning_rate": 5e-6, + "weight_decay": 0.01, + "warmup_steps": 0 + }, + "parallelism": { + "num_nodes": 1, + "num_gpus_per_node": 1, + "tensor_parallel_size": 1, + "pipeline_parallel_size": 1, + "context_parallel_size": 1 + }, + "output": { "name": "my-finetuned-model", "description": "optional" }, + "integrations": { + "wandb": { "enabled": true, "project": "my-project", "api_key_secret": "wandb-api-key" }, + "mlflow": null + } +} +``` + +**Validation rules:** + +- **`output_model` is rejected** at parse time (legacy: *"spec.output_model was removed. Use spec.output instead."*). +- `teacher_model`, `distillation_*`, and `offload_teacher` only when `training_type` is `distillation` (omit for `sft`). +- Optional `dataset.prompt_template` for non-chat prompt/completion data (chat datasets use tokenizer chat template — document in README). +- Compiler may accept additional optimizer/parallelism fields required by contract JSONs even if omitted from this minimal example (`adam_beta1`, `expert_parallel_size`, …). + +**Training types in v1:** + +| `training_type` | Automodel recipe | Notes | +|-----------------|------------------|-------| +| `sft` | `TrainFinetuneRecipeForNextTokenPrediction` | Default; LoRA / all_weights / lora_merged | +| `distillation` | `KnowledgeDistillationRecipeForNextTokenPrediction` | Requires `teacher_model`; maps to Automodel `teacher_model`, `kd_ratio`, `kd_loss_fn` ([`nemo_automodel/recipes/llm/kd.py`](https://github.com/NVIDIA-NeMo/Automodel/blob/main/nemo_automodel/recipes/llm/kd.py), example [`examples/llm_kd/llama3_2/llama3_2_1b_kd.yaml`](https://github.com/NVIDIA-NeMo/Automodel/blob/main/examples/llm_kd/llama3_2/llama3_2_1b_kd.yaml)) | + +**KD / distillation fields** (when `training_type: distillation`): mirror legacy Customizer API — `teacher_model` (entity ref in path workspace), `distillation_ratio` (→ `kd_ratio`, default `0.5`), `distillation_temperature` (→ `kd_loss_fn.temperature`, default `1.0`), `teacher_precision` (default `bf16`), optional `offload_teacher` (→ `offload_teacher_model`). Compiler port: `_configure_kd()` in legacy `automodel/config.py`. Validate tokenizer compatibility student/teacher before submit. + +**Explicitly out of scope (v1):** DPO, GRPO, `nemo_rl`, `megatron_bridge`, quantized LoRA, DoRA, **embedding-model SFT** (`embed_1b` / biencoder recipe), **`deployment_config`** (post-train NIM deploy), request-body **`profile`** on job create (use `training.execution_profile`). + +**Compiler responsibilities** (unchanged from legacy): + +1. Resolve `model` → `ModelEntity` in **path workspace** (or explicit `ws/name` ref). +2. Resolve dataset filesets in **path workspace** → local paths in download step. +3. `compile_automodel_config()` → YAML/JSON for `finetune.py`. +4. Generate output fileset + `ModelEntityTaskConfig` with `workspace` = path workspace (output model and fileset live in that workspace). + +--- + +## Risk & complexity notes + +| Topic | Note | +|-------|------| +| **Largest port** | `compile_automodel_config()` (~800 LOC) and `validate_for_training()` (MoE/parallelism). `deployment_config` and embedding SFT are **out of scope v1**. | +| **Shared code** | File I/O and model_entity tasks are backend-agnostic — candidate for `nmp-common` or small `nmp-training-tasks` lib later; v1 can duplicate to ship faster. | +| **Python version** | NGC automodel uses 3.12; platform pins 3.11 for API — task image runs 3.12 (existing customizer pattern). | +| **KD / distillation** | In v1 JSON as `training_type: distillation`; compiler maps to Automodel KD recipe (see simplified JSON section). | +| **Customizer service** | Remains in repo but unused; avoid dual registration in `NMP_SERVICES`. | +| **Studio cutover** | **Out of scope** — no feature flag or Studio migration in Automodel v1; `VITE_FF_CUSTOMIZER_ENABLED` stays off. | +| **Customization router** | v1 in scope: **`nemo-customizer-plugin`** (`CustomizationRouterService` + contributor protocol); Automodel first contributor; RL/Megatron/Unsloth add contributors later without new `/apis/*` services. | +| **`runtime` vs subprocess flag** | `platform.runtime: docker` enables Docker-backed job profiles; `jobs.enable_subprocess_executor` separately controls host subprocess. Automodel training requires the former, not the latter. | + +--- + +## Non-critical follow-ups (post-v1) + +Merged into [Implementation order](#implementation-order) and [Decisions](#decisions-resolved). Remaining items are not blocking the first PR: + +| Topic | Notes | +|-------|--------| +| **`nemo.customization.contributors` in `_ALL_SURFACE_GROUPS`** | **Done:** `nemo_platform_plugin.discovery` — manifests + `discover_customization_contributors()` (IGW-aligned). | +| **Request-body `profile` on job create** | Platform follow-up MR on `BaseJobRequest` + `add_job_routes`; until then CLI `--profile` may only map to `training.execution_profile` in JSON. | +| **`custom_fields` passthrough** | Factory already supports; document if customers rely on it. | +| **Full optimizer / MoE parallelism in public JSON** | Compiler + contracts may need fields beyond the minimal example; expand OpenAPI as contract port discovers gaps. | +| **Chat dataset contract tests** | Port `*_full_sft_chat.json` when `prompt_template` behavior is documented. | + +--- + +## Decisions (resolved) + +| # | Topic | Decision | +|---|--------|----------| +| 1 | **Service vs plugin-only** | **No standalone `nmp-automodel` HTTP server.** Automodel HTTP lives on **`AutomodelContributor`** merged by **`nemo-customizer-plugin`** (`CustomizationRouterService`) at `/apis/customization`. `nmp-automodel` is compiler + tasks only. | +| 2 | **KD / distillation** | **Include in v1** simplified JSON when `training_type: distillation`. Map to [Automodel KD recipe](https://github.com/NVIDIA-NeMo/Automodel/tree/main/nemo_automodel/recipes/llm/kd.py) (`teacher_model`, `kd_ratio`, `kd_loss_fn`, optional `offload_teacher_model`). Port legacy `_configure_kd()` / `DistillationConfig` from customizer automodel backend. | +| 3 | **Image naming** | **`nvcr.io/0921617854601259/nemo-platform-dev/nmp/automodel-training`** (GPU) and **`.../nmp/automodel-tasks`** (CPU). Do **not** reuse `customizer-automodel` or the upstream `nvcr.io/nvidia/nemo-automodel` image name. | +| 4 | **Workspace package name** | **`nmp-automodel`** (PyPI upstream library remains `nemo-automodel` / NGC image name unchanged). | +| 5 | **Studio cutover** | **Punted** — no Studio feature flag or migration to `.../automodel/...` in this scope. | +| 6 | **`customization` owner** | **In scope v1:** dedicated **`nemo-customizer-plugin`** owns `nemo.services` key `customization`; backends register via **`nemo.customization.contributors`** (Automodel first; RL / Megatron / Unsloth later). `nemo-automodel` must **not** register `nemo.services` directly. | +| 7 | **`enable_subprocess_executor` on K8s** | **Default `false` on Kubernetes**; explicit `true` only when dev clusters need host subprocess. Default `true` for `platform.runtime: docker` local dev. | +| 8 | **Jobs `source` / naming** | **`service_name="customization"`** on `add_job_routes` (never default `nemo-automodel-plugin`). Auto names: **`automodel-{hex}`** via `generate_automodel_id`. | +| 9 | **`execution_profile` v1** | In **`training.execution_profile`** on job spec; default from **`NMP_AUTOMODEL_DEFAULT_TRAINING_EXECUTION_PROFILE`**. Request-body `profile` on create — **deferred** (platform gap). | +| 10 | **Embedding SFT** | **Out of scope v1** (causal LM + KD only); `embed_1b` contracts gated in Step 6 until product expands. | +| 11 | **`deployment_config`** | **Out of scope v1** (post-train NIM deploy; Studio-adjacent). | +| 12 | **Router zero contributors** | **Fail startup** if customization plugin is enabled but no `nemo.customization.contributors` load. | + diff --git a/plugins/nemo-automodel/pyproject.toml b/plugins/nemo-automodel/pyproject.toml new file mode 100644 index 0000000000..126816f9df --- /dev/null +++ b/plugins/nemo-automodel/pyproject.toml @@ -0,0 +1,51 @@ +[project] +name = "nemo-automodel-plugin" +version = "0.1.0" +description = "NeMo Automodel customization contributor for NeMo Platform." +readme = "README.md" +requires-python = ">=3.11,<3.14" +dependencies = [ + "nemo-platform-plugin", + "nemo-platform", + "nmp-automodel", + "pydantic>=2.10.6", + "pydantic-settings>=2.6.1", + "typer>=0.12.5", +] + +[project.entry-points."nemo.customization.contributors"] +automodel = "nemo_automodel_plugin.contributor:AutomodelContributor" + +[project.entry-points."nemo.jobs"] +"customization.automodel.jobs" = "nemo_automodel_plugin.jobs.jobs:AutomodelJob" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/nemo_automodel_plugin"] + +[tool.uv.sources] +nemo-platform-plugin = { workspace = true } +nemo-platform = { workspace = true } +nmp-automodel = { workspace = true } +nemo-customizer-plugin = { workspace = true } + +[dependency-groups] +dev = [ + "pytest>=8.3.4", + "pytest-asyncio>=0.25.3", + "ruff>=0.11.8", + "fastapi>=0.115.0", + "httpx>=0.27.0", + "nemo-customizer-plugin", +] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +pythonpath = ["src"] +testpaths = ["tests"] + +[tool.pyright] +extraPaths = ["src"] diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/__init__.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/__init__.py new file mode 100644 index 0000000000..7cca7f911e --- /dev/null +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""NeMo Automodel customization contributor.""" diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/__init__.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/__init__.py new file mode 100644 index 0000000000..4b22fee8f6 --- /dev/null +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/__init__.py @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Automodel contributor CLI helpers.""" + +from nemo_automodel_plugin.cli.inputs import apply_automodel_job_cli_overrides, load_job_json +from nemo_automodel_plugin.cli.main import AutomodelContributorCLI + +__all__ = ["AutomodelContributorCLI", "apply_automodel_job_cli_overrides", "load_job_json"] diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/inputs.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/inputs.py new file mode 100644 index 0000000000..1dcd6e7ca5 --- /dev/null +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/inputs.py @@ -0,0 +1,98 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""CLI overrides: submit/run accept a job JSON file instead of ``--spec``.""" + +from __future__ import annotations + +import json +from collections.abc import Callable +from pathlib import Path + +import typer + +from nemo_automodel_plugin.schema import AutomodelJobInput + +_JOB_JSON_HELP = "Path to Automodel job JSON (AutomodelJobInput schema)." + + +def load_job_json(path: Path) -> str: + """Load and validate job JSON; return canonical JSON string for ``--spec``.""" + data = json.loads(path.read_text()) + validated = AutomodelJobInput.model_validate(data) + return validated.model_dump_json() + + +def apply_automodel_job_cli_overrides(group: typer.Typer) -> None: + """Flat ``automodel`` CLI: ``submit JOB.json``; ``run`` is disabled.""" + _replace_job_run_disabled(group) + _replace_job_submit(group) + + +def _pluck_callback(group: typer.Typer, verb: str) -> Callable[..., None]: + callback = next(c for c in group.registered_commands if c.name == verb).callback + if callback is None: + raise RuntimeError(f"missing {verb!r} callback to override") + return callback + + +def _drop_command(group: typer.Typer, name: str) -> None: + group.registered_commands = [c for c in group.registered_commands if c.name != name] + + +def _replace_job_run_disabled(group: typer.Typer) -> None: + _drop_command(group, "run") + + @group.command("run") + def run( + _typer_ctx: typer.Context, + _job_json: Path | None = typer.Argument( + None, + metavar="JOB_JSON", + help=_JOB_JSON_HELP, + ), + ) -> None: + typer.secho( + "Automodel does not support local run. Submit to the platform API instead:\n" + " nemo customization automodel submit -w ", + err=True, + fg=typer.colors.RED, + ) + raise typer.Exit(code=1) + + +def _replace_job_submit(group: typer.Typer) -> None: + original = _pluck_callback(group, "submit") + + @group.command("submit") + def submit( + typer_ctx: typer.Context, + job_json: Path = typer.Argument(..., metavar="JOB_JSON", help=_JOB_JSON_HELP), + workspace: str = typer.Option("default", "--workspace", "-w", help="Target workspace."), + profile: str | None = typer.Option(None, "--profile"), + cluster: str | None = typer.Option(None, "--cluster"), + base_url: str | None = typer.Option( + None, + "--base-url", + help=( + "Override platform API host. If omitted: --cluster, then CLI context, " + "then $NMP_BASE_URL, then http://localhost:8080." + ), + ), + options: list[str] = typer.Option([], "-o", help="Backend option override, 'backend.key=value'."), + options_file: Path | None = typer.Option(None, "--options-file"), + ) -> None: + spec_json = load_job_json(job_json) + original( + typer_ctx, + spec=spec_json, + spec_file=None, + options=options, + options_file=options_file, + profile=profile, + cluster=cluster, + base_url=base_url, + workspace=workspace, + config=None, + config_file=None, + ) diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/main.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/main.py new file mode 100644 index 0000000000..e0d2072d75 --- /dev/null +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/main.py @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""CLI hooks for the Automodel customization contributor.""" + +from __future__ import annotations + +import typer +from nemo_platform_plugin.job import NemoJob + +from nemo_automodel_plugin.cli.inputs import apply_automodel_job_cli_overrides +from nemo_automodel_plugin.jobs.jobs import AutomodelJob + + +class AutomodelContributorCLI: + """Passed to ``add_job_commands`` to override job submit/run with job-file args.""" + + def update_job_cli(self, job_cls: type[NemoJob], group: typer.Typer) -> None: + if job_cls is AutomodelJob: + apply_automodel_job_cli_overrides(group) diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/config.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/config.py new file mode 100644 index 0000000000..7ac7a09e58 --- /dev/null +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/config.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Plugin configuration for Automodel training.""" + +from __future__ import annotations + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class AutomodelPluginConfig(BaseSettings): + """Environment-driven Automodel plugin settings.""" + + model_config = SettingsConfigDict(env_prefix="NMP_AUTOMODEL_", extra="ignore") + + default_training_execution_profile: str = "gpu" + training_image: str = "nmp-automodel-training" + tasks_image: str = "nmp-automodel-tasks" + + +def get_config() -> AutomodelPluginConfig: + return AutomodelPluginConfig() + + +def generate_automodel_id() -> str: + """Generate a job name when the submitter omits ``name``.""" + import uuid + + return f"automodel-{uuid.uuid4().hex[:12]}" diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/contributor.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/contributor.py new file mode 100644 index 0000000000..c8e70a9b39 --- /dev/null +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/contributor.py @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Automodel customization contributor.""" + +from __future__ import annotations + +from typing import ClassVar + +import typer +from fastapi import APIRouter +from nemo_platform_plugin.authz import AuthzContribution, authz_for_workspace_job_collection +from nemo_platform_plugin.jobs.routes import add_job_routes +from nemo_platform_plugin.service import RouterSpec +from nmp.common.jobs.api_factory import JobRouteOption + +from nemo_automodel_plugin.config import generate_automodel_id, get_config +from nemo_automodel_plugin.jobs.jobs import AutomodelJob + + +class AutomodelContributor: + """Registers Automodel routes under the customization router.""" + + name: ClassVar[str] = "automodel" + dependencies: ClassVar[list[str]] = ["entities", "auth", "jobs", "secrets", "files", "models"] + + def get_routers(self) -> list[RouterSpec]: + config = get_config() + router = APIRouter() + + @router.get("/healthz") + async def healthz() -> dict[str, str]: + return {"backend": self.name, "status": "ok"} + + jobs_router = add_job_routes( + AutomodelJob, + service_name="customization", + generate_job_name=generate_automodel_id, + route_options=[JobRouteOption.CORE], + default_profile=config.default_training_execution_profile, + ) + + return [ + RouterSpec( + router=router, + prefix="/v2/workspaces/{workspace}/automodel", + tag="Automodel", + description="Automodel contributor health.", + ), + RouterSpec( + router=jobs_router, + prefix="/v2/workspaces/{workspace}", + tag="Automodel Jobs", + description="Automodel training jobs.", + ), + ] + + def get_cli(self) -> typer.Typer: + from nemo_platform_plugin.commands import ( + _add_explain_command, + _add_run_command, + _add_submit_command, + ) + from nemo_platform_plugin.scheduler import NemoJobScheduler + + from nemo_automodel_plugin.cli.inputs import apply_automodel_job_cli_overrides + + app = typer.Typer( + name=self.name, + help="Automodel training jobs (SFT, distillation).", + no_args_is_help=True, + ) + scheduler = NemoJobScheduler() + _add_run_command(app, AutomodelJob, scheduler) + _add_submit_command(app, AutomodelJob, scheduler) + _add_explain_command(app, AutomodelJob, scheduler) + apply_automodel_job_cli_overrides(app) + return app + + def get_authz_contribution(self) -> AuthzContribution: + """Register automodel job routes with the platform authorization policy.""" + return authz_for_workspace_job_collection( + api_area="customization", + collection_suffix="/automodel/jobs", + permission_prefix="customization.automodel.jobs", + include_healthz=True, + healthz_suffix="/automodel/healthz", + ) diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/jobs/__init__.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/jobs/__init__.py new file mode 100644 index 0000000000..e5725ea5a4 --- /dev/null +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/jobs/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/jobs/jobs.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/jobs/jobs.py new file mode 100644 index 0000000000..e23e7c4785 --- /dev/null +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/jobs/jobs.py @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Automodel training job (NemoJob).""" + +from __future__ import annotations + +from typing import ClassVar, cast + +from nemo_platform import AsyncNeMoPlatform +from nemo_platform_plugin.config import NemoPlatformConfig, Runtime +from nemo_platform_plugin.job import NemoJob +from nemo_platform_plugin.jobs.api_factory import PlatformJobSpec +from nemo_platform_plugin.jobs.docker import validate_gpu_available_for_docker +from nemo_platform_plugin.jobs.exceptions import PlatformJobCompilationError +from nmp.automodel.compile import platform_job_config_compiler +from pydantic import BaseModel + +from nemo_automodel_plugin.config import get_config +from nemo_automodel_plugin.schema import AutomodelJobInput, AutomodelJobOutput +from nemo_automodel_plugin.transform import transform_input_to_output + + +def _require_docker_runtime() -> None: + platform_config = NemoPlatformConfig.get() + if platform_config.runtime != Runtime.DOCKER: + raise PlatformJobCompilationError( + "Automodel training requires platform.runtime: docker with GPU-backed container execution.", + ) + from nemo_platform_plugin.config import validate_docker_available + + if not validate_docker_available(): + raise PlatformJobCompilationError( + "Automodel training requires a reachable Docker daemon (platform.runtime: docker).", + ) + + +class AutomodelJob(NemoJob): + """GPU Automodel fine-tuning job under the customization router.""" + + name: ClassVar[str] = "automodel.jobs" + description: ClassVar[str] = "Automodel SFT and knowledge-distillation training jobs." + job_collection_path: ClassVar[str | None] = "/automodel/jobs" + input_spec_schema: ClassVar[type[BaseModel] | None] = AutomodelJobInput + spec_schema: ClassVar[type[BaseModel] | None] = AutomodelJobOutput + dependencies: ClassVar[list[str]] = ["entities", "auth", "jobs", "secrets", "files", "models"] + + @classmethod + async def to_spec( + cls, + input_spec: BaseModel, + workspace: str, + entity_client: object, + async_sdk: object, + is_local: bool, + ) -> AutomodelJobOutput: + job_input = ( + input_spec + if isinstance(input_spec, AutomodelJobInput) + else AutomodelJobInput.model_validate(input_spec.model_dump()) + ) + return await transform_input_to_output(job_input, workspace, cast(AsyncNeMoPlatform, async_sdk)) + + @classmethod + async def compile( + cls, + workspace: str, + spec: BaseModel, + entity_client: object, + job_name: str | None, + async_sdk: object, + profile: str | None = None, + options: dict | None = None, + ) -> PlatformJobSpec: + _require_docker_runtime() + canonical = ( + spec if isinstance(spec, AutomodelJobOutput) else AutomodelJobOutput.model_validate(spec.model_dump()) + ) + canonical.validate_for_training() + + plugin_config = get_config() + execution_profile = ( + canonical.training.execution_profile or profile or plugin_config.default_training_execution_profile + ) + + platform_spec = await platform_job_config_compiler( + canonical, + workspace, + cast(AsyncNeMoPlatform, async_sdk), + job_name=job_name, + profile=execution_profile, + ) + + validate_gpu_available_for_docker(platform_spec) + return platform_spec diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py new file mode 100644 index 0000000000..30a9ef5870 --- /dev/null +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py @@ -0,0 +1,240 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Automodel job input/output schemas (simplified JSON v1).""" + +from __future__ import annotations + +from typing import Any, Literal, Self + +from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator + + +class LoRAParams(BaseModel): + model_config = ConfigDict(extra="forbid") + + rank: int = Field(default=16, gt=0) + alpha: int = Field(default=32, gt=0) + merge: bool = False + target_modules: list[str] | None = None + + +class DatasetSpec(BaseModel): + model_config = ConfigDict(extra="forbid") + + training: str = Field(description="Training fileset as 'name' or 'workspace/name'.") + validation: str | None = None + prompt_template: str | None = None + + +class TrainingSpec(BaseModel): + model_config = ConfigDict(extra="forbid") + + training_type: Literal["sft", "distillation"] = "sft" + finetuning_type: Literal["lora", "all_weights", "lora_merged"] = "lora" + lora: LoRAParams | None = None + max_seq_length: int = Field(default=2048, gt=0) + execution_profile: str | None = Field(default=None, min_length=1) + teacher_model: str | None = None + distillation_ratio: float = Field(default=0.5, ge=0.0, le=1.0) + distillation_temperature: float = Field(default=1.0, gt=0.0) + teacher_precision: Literal["bf16", "fp16", "fp32"] = "bf16" + offload_teacher: bool = False + + @model_validator(mode="after") + def _training_type_fields(self) -> Self: + if self.training_type == "distillation" and not self.teacher_model: + raise ValueError("teacher_model is required when training_type is distillation") + if self.finetuning_type.startswith("lora") and self.lora is None: + self.lora = LoRAParams() + return self + + +class ScheduleSpec(BaseModel): + model_config = ConfigDict(extra="forbid") + + epochs: int = Field(default=1, gt=0) + max_steps: int | None = Field(default=None, gt=0) + val_check_interval: float | None = None + seed: int | None = None + + +class BatchSpec(BaseModel): + model_config = ConfigDict(extra="forbid") + + global_batch_size: int = Field(default=8, gt=0) + micro_batch_size: int = Field(default=1, gt=0) + sequence_packing: bool = False + + +class OptimizerSpec(BaseModel): + model_config = ConfigDict(extra="forbid") + + learning_rate: float = Field(default=5e-6, gt=0.0) + weight_decay: float = Field(default=0.01, ge=0.0) + warmup_steps: int = Field(default=0, ge=0) + + +class ParallelismSpec(BaseModel): + model_config = ConfigDict(extra="forbid") + + num_nodes: int = Field(default=1, gt=0) + num_gpus_per_node: int = Field(default=1, gt=0) + tensor_parallel_size: int = Field(default=1, gt=0) + pipeline_parallel_size: int = Field(default=1, gt=0) + context_parallel_size: int = Field(default=1, gt=0) + expert_parallel_size: int | None = Field(default=None, gt=0) + + +class OutputRequest(BaseModel): + model_config = ConfigDict(extra="forbid") + + name: str + description: str | None = None + + +class OutputResponse(BaseModel): + model_config = ConfigDict(extra="forbid") + + name: str + type: Literal["model", "adapter"] + fileset: str + description: str | None = None + + +class WandbIntegration(BaseModel): + model_config = ConfigDict(extra="forbid") + + enabled: bool = True + project: str | None = None + api_key_secret: str | None = None + + +class IntegrationsSpec(BaseModel): + model_config = ConfigDict(extra="forbid") + + wandb: WandbIntegration | None = None + mlflow: dict[str, Any] | None = None + + +class AutomodelJobInput(BaseModel): + """POST body / CLI JSON.""" + + model_config = ConfigDict(extra="forbid") + + name: str | None = None + model: str + dataset: DatasetSpec + training: TrainingSpec + schedule: ScheduleSpec = Field(default_factory=ScheduleSpec) + batch: BatchSpec = Field(default_factory=BatchSpec) + optimizer: OptimizerSpec = Field(default_factory=OptimizerSpec) + parallelism: ParallelismSpec = Field(default_factory=ParallelismSpec) + output: OutputRequest | None = None + integrations: IntegrationsSpec | None = None + + @model_validator(mode="before") + @classmethod + def reject_legacy_fields(cls, data: object) -> object: + if isinstance(data, dict) and "output_model" in data: + raise ValueError("spec.output_model was removed. Use spec.output instead.") + return data + + +class AutomodelJobOutput(BaseModel): + """Stored canonical spec after ``to_spec()``.""" + + model_config = ConfigDict(extra="forbid") + + name: str | None = None + model: str + dataset: DatasetSpec + training: TrainingSpec + schedule: ScheduleSpec + batch: BatchSpec + optimizer: OptimizerSpec + parallelism: ParallelismSpec + output: OutputResponse + integrations: IntegrationsSpec | None = None + + def validate_for_training(self) -> None: + """MoE / parallelism constraints (ported from legacy CustomizationJobOutput).""" + p = self.parallelism + num_nodes = p.num_nodes + num_gpus_per_node = p.num_gpus_per_node + tp = p.tensor_parallel_size + pp = p.pipeline_parallel_size + cp = p.context_parallel_size + ep = p.expert_parallel_size + + total_gpus = num_gpus_per_node * num_nodes + model_parallel_size = tp * pp * cp + if total_gpus % model_parallel_size != 0: + raise ValidationError.from_exception_data( + "parallelism", + [ + { + "type": "value_error", + "loc": ("parallelism",), + "msg": ( + f"Total GPUs ({total_gpus}) must be divisible by " + f"tensor_parallel_size ({tp}) * pipeline_parallel_size ({pp}) * " + f"context_parallel_size ({cp}) = {model_parallel_size}" + ), + "input": p.model_dump(), + } + ], + ) + + derived_dp = total_gpus // model_parallel_size + gb = self.batch.global_batch_size + mb = self.batch.micro_batch_size + divisor = mb * derived_dp + if gb % divisor != 0: + raise ValidationError.from_exception_data( + "batch", + [ + { + "type": "value_error", + "loc": ("batch", "global_batch_size"), + "msg": ( + f"global_batch_size ({gb}) must be divisible by " + f"micro_batch_size ({mb}) * data_parallel_size ({derived_dp}) = {divisor}" + ), + "input": gb, + } + ], + ) + + if ep is not None: + dp_cp = derived_dp * cp + if dp_cp % ep != 0: + raise ValidationError.from_exception_data( + "parallelism", + [ + { + "type": "value_error", + "loc": ("parallelism", "expert_parallel_size"), + "msg": ( + f"(data_parallel_size * context_parallel_size) ({dp_cp}) " + f"must be divisible by expert_parallel_size ({ep})" + ), + "input": ep, + } + ], + ) + if ep > 1 and tp > 1 and total_gpus > 1: + raise ValidationError.from_exception_data( + "parallelism", + [ + { + "type": "value_error", + "loc": ("parallelism", "tensor_parallel_size"), + "msg": ( + f"Tensor parallelism (tensor_parallel_size={tp}) is not supported for MoE models " + f"when expert_parallel_size > 1 ({ep}); tensor_parallel_size must be 1." + ), + "input": tp, + } + ], + ) diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/__init__.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/__init__.py new file mode 100644 index 0000000000..0b9ecd8895 --- /dev/null +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/__init__.py @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Automodel contributor SDK (mounted under ``client.customization`` by nemo-customizer).""" + +from nemo_automodel_plugin.sdk.resources import ( + AsyncAutomodelCustomization, + AsyncAutomodelJobsResource, + AutomodelCustomization, + AutomodelJobsResource, +) + +__all__ = [ + "AsyncAutomodelCustomization", + "AsyncAutomodelJobsResource", + "AutomodelCustomization", + "AutomodelJobsResource", +] diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/http_utils.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/http_utils.py new file mode 100644 index 0000000000..adf4b7ffc9 --- /dev/null +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/http_utils.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Shared HTTP helpers for Automodel customization SDK resources.""" + +from __future__ import annotations + +from typing import Any +from urllib.parse import quote, urljoin + +from nemo_platform import AsyncNeMoPlatform, NeMoPlatform + +from nemo_automodel_plugin.schema import AutomodelJobInput + +PlatformClient = NeMoPlatform | AsyncNeMoPlatform + +_API_PREFIX = "/apis/customization" +_JOBS_COLLECTION = "v2/workspaces/{workspace}/automodel/jobs" + + +def base_url(source: str) -> str: + """Return the normalized base URL for a raw URL string.""" + return source.rstrip("/") + + +def resolve_workspace(platform: PlatformClient, workspace: str | None, strict: bool = False) -> str: + """Return the explicit, platform, or default workspace for customization routes.""" + resolved = workspace or platform.workspace + if resolved is None: + if strict: + raise ValueError("workspace must be provided when the client has no default workspace") + return "default" + return resolved + + +def url(platform: PlatformClient, path: str, workspace: str | None = None) -> str: + """Build a full customization plugin API URL for the provided route path.""" + resolved_path = path.format(workspace=quote(resolve_workspace(platform, workspace), safe="")) + return _join_url(str(platform.base_url), f"{_API_PREFIX}/{resolved_path}") + + +def jobs_collection_url(platform: PlatformClient, workspace: str | None = None) -> str: + """URL for the Automodel jobs collection in a workspace.""" + return url(platform, _JOBS_COLLECTION, workspace) + + +def job_url(platform: PlatformClient, job_name: str, workspace: str | None = None) -> str: + """URL for a single Automodel job.""" + return _join_url(jobs_collection_url(platform, workspace), quote(job_name, safe="")) + + +def platform_default_headers(platform: PlatformClient) -> dict[str, str]: + """Return string-valued default platform headers for direct HTTP calls.""" + return {str(key): value for key, value in platform.default_headers.items() if isinstance(value, str)} + + +def create_job_payload(spec: AutomodelJobInput) -> dict[str, dict[str, Any]]: + """Serialize an Automodel job creation request body.""" + return {"spec": spec.model_dump(mode="json")} + + +def _join_url(root: str, relative_path: str) -> str: + """Join a root URL and a relative path using URL parsing rules.""" + return urljoin(f"{base_url(root)}/", relative_path.lstrip("/")) diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/job_resources.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/job_resources.py new file mode 100644 index 0000000000..7832f87a86 --- /dev/null +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/job_resources.py @@ -0,0 +1,86 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Automodel job resources for status polling via the customization plugin API.""" + +from __future__ import annotations + +from typing import Any +from urllib.parse import quote + +from nemo_platform_plugin.jobs.schemas import PlatformJobStatusResponse +from pydantic import BaseModel + +from nemo_automodel_plugin.sdk import http_utils + + +class AutomodelJobRecord(BaseModel): + """Minimal job record returned by the customization Automodel jobs API.""" + + name: str + workspace: str + status: str | None = None + spec: dict[str, Any] | None = None + + +class AutomodelJobResource: + """Sync handle for one submitted Automodel job.""" + + def __init__( + self, + job: AutomodelJobRecord, + http_client: Any, + base_url: str, + workspace: str, + headers: dict[str, str], + ) -> None: + self.job = job + self._http_client = http_client + self._base_url = base_url + self._workspace = workspace + self._headers = headers + + def get_status(self) -> PlatformJobStatusResponse: + """Fetch current job status.""" + response = self._http_client.get( + _job_status_path(self._base_url, self._workspace, self.job.name), + headers=self._headers, + ) + response.raise_for_status() + return PlatformJobStatusResponse.model_validate(response.json()) + + +class AsyncAutomodelJobResource: + """Async handle for one submitted Automodel job.""" + + def __init__( + self, + job: AutomodelJobRecord, + http_client: Any, + base_url: str, + workspace: str, + headers: dict[str, str], + ) -> None: + self.job = job + self._http_client = http_client + self._base_url = base_url + self._workspace = workspace + self._headers = headers + + async def get_status(self) -> PlatformJobStatusResponse: + """Fetch current job status.""" + response = await self._http_client.get( + _job_status_path(self._base_url, self._workspace, self.job.name), + headers=self._headers, + ) + response.raise_for_status() + return PlatformJobStatusResponse.model_validate(response.json()) + + +def _job_status_path(base_url: str, workspace: str, job_name: str) -> str: + encoded_workspace = quote(workspace, safe="") + encoded_job = quote(job_name, safe="") + return ( + f"{http_utils.base_url(base_url)}/apis/customization/v2/workspaces/" + f"{encoded_workspace}/automodel/jobs/{encoded_job}" + ) diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/resources.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/resources.py new file mode 100644 index 0000000000..63e08a9266 --- /dev/null +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/resources.py @@ -0,0 +1,164 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Automodel contributor SDK resources (composed by ``nemo-customizer-plugin``).""" + +from __future__ import annotations + +from typing import Any + +from nemo_platform import AsyncNeMoPlatform, NeMoPlatform + +from nemo_automodel_plugin.schema import AutomodelJobInput +from nemo_automodel_plugin.sdk import http_utils +from nemo_automodel_plugin.sdk.job_resources import ( + AsyncAutomodelJobResource, + AutomodelJobRecord, + AutomodelJobResource, +) + + +class AutomodelJobsResource: + """Sync SDK namespace at ``client.customization.automodel.jobs``.""" + + def __init__(self, platform: NeMoPlatform) -> None: + self._platform = platform + self._http_client = platform._client + + def plugin_status(self) -> dict[str, object]: + """Return Automodel contributor health from the customization service.""" + response = self._http_client.get( + http_utils.url( + self._platform, + "v2/workspaces/{workspace}/automodel/healthz", + self._platform.workspace, + ), + headers=http_utils.platform_default_headers(self._platform), + ) + response.raise_for_status() + payload = response.json() + if not isinstance(payload, dict): + raise TypeError("Automodel health response must be a JSON object.") + return {str(key): value for key, value in payload.items()} + + def create( + self, + spec: AutomodelJobInput, + workspace: str | None = None, + name: str | None = None, + ) -> AutomodelJobResource: + """Submit an Automodel training job.""" + body: dict[str, Any] = http_utils.create_job_payload(spec) + if name is not None: + body["name"] = name + response = self._http_client.post( + http_utils.jobs_collection_url(self._platform, workspace), + json=body, + headers=http_utils.platform_default_headers(self._platform), + ) + response.raise_for_status() + record = AutomodelJobRecord.model_validate(response.json()) + resolved_ws = http_utils.resolve_workspace(self._platform, workspace) + return AutomodelJobResource( + job=record, + http_client=self._http_client, + base_url=http_utils.base_url(str(self._platform.base_url)), + workspace=resolved_ws, + headers=http_utils.platform_default_headers(self._platform), + ) + + def get_job_resource(self, job_name: str, workspace: str | None = None) -> AutomodelJobResource: + """Get a resource handle for an existing Automodel job.""" + resolved_ws = http_utils.resolve_workspace(self._platform, workspace) + response = self._http_client.get( + http_utils.job_url(self._platform, job_name, resolved_ws), + headers=http_utils.platform_default_headers(self._platform), + ) + response.raise_for_status() + return AutomodelJobResource( + job=AutomodelJobRecord.model_validate(response.json()), + http_client=self._http_client, + base_url=http_utils.base_url(str(self._platform.base_url)), + workspace=resolved_ws, + headers=http_utils.platform_default_headers(self._platform), + ) + + +class AsyncAutomodelJobsResource: + """Async SDK namespace at ``client.customization.automodel.jobs``.""" + + def __init__(self, platform: AsyncNeMoPlatform) -> None: + self._platform = platform + self._http_client = platform._client + + async def plugin_status(self) -> dict[str, object]: + """Return Automodel contributor health from the customization service.""" + response = await self._http_client.get( + http_utils.url( + self._platform, + "v2/workspaces/{workspace}/automodel/healthz", + self._platform.workspace, + ), + headers=http_utils.platform_default_headers(self._platform), + ) + response.raise_for_status() + payload = response.json() + if not isinstance(payload, dict): + raise TypeError("Automodel health response must be a JSON object.") + return {str(key): value for key, value in payload.items()} + + async def create( + self, + spec: AutomodelJobInput, + workspace: str | None = None, + name: str | None = None, + ) -> AsyncAutomodelJobResource: + """Submit an Automodel training job.""" + body: dict[str, Any] = http_utils.create_job_payload(spec) + if name is not None: + body["name"] = name + response = await self._http_client.post( + http_utils.jobs_collection_url(self._platform, workspace), + json=body, + headers=http_utils.platform_default_headers(self._platform), + ) + response.raise_for_status() + record = AutomodelJobRecord.model_validate(response.json()) + resolved_ws = http_utils.resolve_workspace(self._platform, workspace) + return AsyncAutomodelJobResource( + job=record, + http_client=self._http_client, + base_url=http_utils.base_url(str(self._platform.base_url)), + workspace=resolved_ws, + headers=http_utils.platform_default_headers(self._platform), + ) + + async def get_job_resource(self, job_name: str, workspace: str | None = None) -> AsyncAutomodelJobResource: + """Get a resource handle for an existing Automodel job.""" + resolved_ws = http_utils.resolve_workspace(self._platform, workspace) + response = await self._http_client.get( + http_utils.job_url(self._platform, job_name, resolved_ws), + headers=http_utils.platform_default_headers(self._platform), + ) + response.raise_for_status() + return AsyncAutomodelJobResource( + job=AutomodelJobRecord.model_validate(response.json()), + http_client=self._http_client, + base_url=http_utils.base_url(str(self._platform.base_url)), + workspace=resolved_ws, + headers=http_utils.platform_default_headers(self._platform), + ) + + +class AutomodelCustomization: + """Sync SDK namespace at ``client.customization.automodel``.""" + + def __init__(self, platform: NeMoPlatform) -> None: + self.jobs = AutomodelJobsResource(platform) + + +class AsyncAutomodelCustomization: + """Async SDK namespace at ``client.customization.automodel``.""" + + def __init__(self, platform: AsyncNeMoPlatform) -> None: + self.jobs = AsyncAutomodelJobsResource(platform) diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/transform.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/transform.py new file mode 100644 index 0000000000..290fa60613 --- /dev/null +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/transform.py @@ -0,0 +1,99 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Input → canonical spec transformation.""" + +from __future__ import annotations + +import uuid +from typing import TYPE_CHECKING + +from nmp.automodel.platform_client import check_dataset_access, fetch_model_entity +from nmp.common.entities.utils import parse_entity_ref + +from nemo_automodel_plugin.schema import ( + AutomodelJobInput, + AutomodelJobOutput, + OutputResponse, +) + +if TYPE_CHECKING: + from nemo_platform import AsyncNeMoPlatform + +_MAX_PREFIX_LEN = 50 +_HEX_LEN = 12 + + +def _random_suffix(prefix: str) -> str: + truncated = prefix[:_MAX_PREFIX_LEN].rstrip("-") + return f"{truncated}-{uuid.uuid4().hex[:_HEX_LEN]}" + + +def _entity_basename(model_ref: str, workspace: str) -> str: + return parse_entity_ref(model_ref, workspace).name + + +def _dataset_basename(uri: str) -> str: + normalized = uri + if normalized.startswith("fileset://"): + normalized = normalized[len("fileset://") :] + return parse_entity_ref(normalized, "default").name + + +def _infer_output_type(input_spec: AutomodelJobInput, is_embedding_model: bool) -> str: + if is_embedding_model: + return "model" + lora = input_spec.training.lora + if input_spec.training.finetuning_type == "lora" and lora is not None and not lora.merge: + return "adapter" + return "model" + + +async def transform_input_to_output( + input_spec: AutomodelJobInput, + workspace: str, + sdk: AsyncNeMoPlatform, +) -> AutomodelJobOutput: + """Enrich submitter input into canonical AutomodelJobOutput.""" + model_entity = await fetch_model_entity(input_spec.model, workspace, sdk) + await check_dataset_access(sdk, input_spec.dataset.training, workspace) + if input_spec.dataset.validation: + await check_dataset_access(sdk, input_spec.dataset.validation, workspace) + + is_embedding = bool(model_entity.spec and getattr(model_entity.spec, "is_embedding_model", False)) + if is_embedding: + raise ValueError( + "Embedding-model SFT is not supported in Automodel v1. " + "Use a causal LM checkpoint or wait for a future release." + ) + + entity_name = _entity_basename(input_spec.model, workspace) + dataset_name = _dataset_basename(input_spec.dataset.training) + output_type = _infer_output_type(input_spec, is_embedding) + + if input_spec.output is None: + out_name = _random_suffix(f"{entity_name}-{dataset_name}") + fileset = out_name + else: + out_name = input_spec.output.name + fileset = out_name + + output = OutputResponse( + name=out_name, + type=output_type, # type: ignore[arg-type] + fileset=fileset, + description=input_spec.output.description if input_spec.output else None, + ) + + return AutomodelJobOutput( + name=input_spec.name, + model=input_spec.model, + dataset=input_spec.dataset, + training=input_spec.training, + schedule=input_spec.schedule, + batch=input_spec.batch, + optimizer=input_spec.optimizer, + parallelism=input_spec.parallelism, + output=output, + integrations=input_spec.integrations, + ) diff --git a/plugins/nemo-automodel/tests/fixtures/minimal_sft_lora.json b/plugins/nemo-automodel/tests/fixtures/minimal_sft_lora.json new file mode 100644 index 0000000000..b8c0568485 --- /dev/null +++ b/plugins/nemo-automodel/tests/fixtures/minimal_sft_lora.json @@ -0,0 +1,30 @@ +{ + "model": "default/qwen3-1.7b", + "dataset": { + "training": "default/train-data" + }, + "training": { + "training_type": "sft", + "finetuning_type": "lora", + "max_seq_length": 2048 + }, + "schedule": { + "epochs": 1, + "max_steps": 10 + }, + "batch": { + "global_batch_size": 8, + "micro_batch_size": 1 + }, + "optimizer": { + "learning_rate": 5e-6 + }, + "parallelism": { + "num_nodes": 1, + "num_gpus_per_node": 1, + "tensor_parallel_size": 1 + }, + "output": { + "name": "test-out" + } +} diff --git a/plugins/nemo-automodel/tests/fixtures/qwen3_0.6b_sft_lora.json b/plugins/nemo-automodel/tests/fixtures/qwen3_0.6b_sft_lora.json new file mode 100644 index 0000000000..3958c20a73 --- /dev/null +++ b/plugins/nemo-automodel/tests/fixtures/qwen3_0.6b_sft_lora.json @@ -0,0 +1,30 @@ +{ + "model": "default/qwen3-0.6b", + "dataset": { + "training": "default/qwen3-0.6b-train" + }, + "training": { + "training_type": "sft", + "finetuning_type": "lora", + "max_seq_length": 2048 + }, + "schedule": { + "epochs": 1, + "max_steps": 50 + }, + "batch": { + "global_batch_size": 4, + "micro_batch_size": 1 + }, + "optimizer": { + "learning_rate": 5e-5 + }, + "parallelism": { + "num_nodes": 1, + "num_gpus_per_node": 1, + "tensor_parallel_size": 1 + }, + "output": { + "name": "qwen3-0.6b-lora-out" + } +} diff --git a/plugins/nemo-automodel/tests/test_api.py b/plugins/nemo-automodel/tests/test_api.py new file mode 100644 index 0000000000..b5ca3e3b71 --- /dev/null +++ b/plugins/nemo-automodel/tests/test_api.py @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from nemo_automodel_plugin.contributor import AutomodelContributor +from nemo_customizer.router import CustomizationRouterService + + +def _make_automodel_app() -> FastAPI: + app = FastAPI() + for spec in AutomodelContributor().get_routers(): + app.include_router(spec.router, prefix=spec.prefix, tags=[spec.tag] if spec.tag else None) + return app + + +def test_automodel_healthz_under_workspace() -> None: + client = TestClient(_make_automodel_app()) + response = client.get("/v2/workspaces/test-ws/automodel/healthz") + assert response.status_code == 200 + assert response.json() == {"backend": "automodel", "status": "ok"} + + +def test_automodel_jobs_collection_path() -> None: + paths = {route.path for route in _make_automodel_app().routes if hasattr(route, "path")} + assert "/v2/workspaces/{workspace}/automodel/jobs" in paths + + +def test_customization_router_merges_automodel(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + "nemo_customizer.router.discover_customization_contributors", + lambda: {"automodel": AutomodelContributor()}, + ) + service = CustomizationRouterService() + app = FastAPI() + for spec in service.get_routers(): + prefix = spec.prefix or "" + app.include_router(spec.router, prefix=prefix) + + client = TestClient(app) + assert client.get("/healthz").json()["contributors"] == ["automodel"] + assert client.get("/v2/workspaces/ws-a/automodel/healthz").status_code == 200 + + +def test_workspace_isolation_list_uses_path_segment() -> None: + """Job routes are under ``/v2/workspaces/{workspace}/automodel/jobs`` — distinct per workspace.""" + app = _make_automodel_app() + paths = {route.path for route in app.routes if hasattr(route, "path")} + assert "/v2/workspaces/{workspace}/automodel/jobs" in paths + assert "/v2/workspaces/{workspace}/automodel/healthz" in paths diff --git a/plugins/nemo-automodel/tests/test_cli.py b/plugins/nemo-automodel/tests/test_cli.py new file mode 100644 index 0000000000..549ad452c8 --- /dev/null +++ b/plugins/nemo-automodel/tests/test_cli.py @@ -0,0 +1,124 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import json +from pathlib import Path + +import httpx +import pytest +from nemo_automodel_plugin.cli.inputs import load_job_json +from nemo_automodel_plugin.contributor import AutomodelContributor +from nemo_automodel_plugin.jobs.jobs import AutomodelJob +from nemo_platform_plugin.scheduler import NemoJobScheduler, submit_path_for +from typer.testing import CliRunner + +FIXTURES = Path(__file__).parent / "fixtures" + + +def test_submit_path_includes_workspace() -> None: + path = submit_path_for(AutomodelJob, workspace="acme-corp") + assert path == "/apis/customization/v2/workspaces/acme-corp/automodel/jobs" + + +def test_load_job_json_validates_fixture() -> None: + job_path = FIXTURES / "minimal_sft_lora.json" + spec = json.loads(load_job_json(job_path)) + assert spec["training"]["training_type"] == "sft" + assert spec["dataset"]["training"] == "default/train-data" + + +def test_jobs_submit_posts_to_automodel_collection(monkeypatch: pytest.MonkeyPatch) -> None: + capture: dict = {} + + def handler(request: httpx.Request) -> httpx.Response: + capture["method"] = request.method + capture["url"] = str(request.url) + capture["body"] = json.loads(request.content) + return httpx.Response(200, json={"id": "job-1", "status": "queued"}) + + monkeypatch.setattr( + "nemo_platform_plugin.discovery.discover_jobs", + lambda: {"customization.automodel.jobs": AutomodelJob}, + ) + scheduler = NemoJobScheduler() + scheduler.submit_remote( + AutomodelJob, + json.loads(load_job_json(FIXTURES / "minimal_sft_lora.json")), + base_url="https://nmp.test", + workspace="ws-a", + http_client=httpx.Client(transport=httpx.MockTransport(handler)), + ) + + assert capture["method"] == "POST" + assert capture["url"] == "https://nmp.test/apis/customization/v2/workspaces/ws-a/automodel/jobs" + assert capture["body"]["spec"]["training"]["training_type"] == "sft" + + +def test_cli_submit_accepts_job_json_file(monkeypatch: pytest.MonkeyPatch) -> None: + """Contributor CLI: ``submit JOB.json -w ws`` forwards workspace to submit_remote.""" + submitted: dict = {} + + def fake_submit_remote( + _scheduler, + job_cls: type, + spec_data: dict, + base_url: str | None, + workspace: str, + profile: str | None = None, + options: dict | None = None, + metadata: dict | None = None, + http_client: httpx.Client | None = None, + headers: dict[str, str] | None = None, + ) -> dict: + submitted["workspace"] = workspace + submitted["spec"] = spec_data + submitted["base_url"] = base_url + return {"id": "job-99"} + + monkeypatch.setattr( + "nemo_platform_plugin.commands.NemoJobScheduler.submit_remote", + fake_submit_remote, + ) + monkeypatch.setattr( + "nemo_platform_plugin.discovery.discover_jobs", + lambda: {"customization.automodel.jobs": AutomodelJob}, + ) + + automodel_cli = AutomodelContributor().get_cli() + runner = CliRunner() + result = runner.invoke( + automodel_cli, + [ + "submit", + str(FIXTURES / "minimal_sft_lora.json"), + "--workspace", + "acme-corp", + "--base-url", + "https://nmp.test", + ], + ) + assert result.exit_code == 0, result.stdout + result.stderr + assert submitted["workspace"] == "acme-corp" + assert submitted["base_url"] == "https://nmp.test" + assert submitted["spec"]["model"] == "default/qwen3-1.7b" + + +def test_cli_run_is_disabled() -> None: + automodel_cli = AutomodelContributor().get_cli() + runner = CliRunner() + result = runner.invoke(automodel_cli, ["run", str(FIXTURES / "minimal_sft_lora.json")]) + assert result.exit_code == 1 + assert "does not support local run" in result.stderr + + +def test_cli_expose_input_and_output_schemas() -> None: + automodel_cli = AutomodelContributor().get_cli() + runner = CliRunner() + result = runner.invoke(automodel_cli, ["explain"]) + assert result.exit_code == 0 + payload = json.loads(result.stdout) + assert "input_spec_schema" in payload + assert "spec_schema" in payload + assert "/automodel/jobs" in payload["endpoint"] diff --git a/plugins/nemo-automodel/tests/test_contributor.py b/plugins/nemo-automodel/tests/test_contributor.py new file mode 100644 index 0000000000..6c1e540cd3 --- /dev/null +++ b/plugins/nemo-automodel/tests/test_contributor.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from fastapi import FastAPI +from nemo_automodel_plugin.contributor import AutomodelContributor + + +def test_contributor_mounts_job_collection() -> None: + contributor = AutomodelContributor() + app = FastAPI() + for spec in contributor.get_routers(): + app.include_router(spec.router, prefix=spec.prefix) + + paths = {route.path for route in app.routes if hasattr(route, "path")} + assert "/v2/workspaces/{workspace}/automodel/healthz" in paths + assert "/v2/workspaces/{workspace}/automodel/jobs" in paths + + +def test_contributor_get_cli_exposes_flat_verbs() -> None: + import typer + + cli = AutomodelContributor().get_cli() + assert isinstance(cli, typer.Typer) + assert cli.info.name == "automodel" + assert not any(g.name == "jobs" for g in cli.registered_groups) + assert {cmd.name for cmd in cli.registered_commands} >= {"run", "submit", "explain"} diff --git a/plugins/nemo-automodel/tests/test_schema.py b/plugins/nemo-automodel/tests/test_schema.py new file mode 100644 index 0000000000..867250e4c7 --- /dev/null +++ b/plugins/nemo-automodel/tests/test_schema.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from nemo_automodel_plugin.schema import AutomodelJobInput + + +def test_reject_output_model() -> None: + with pytest.raises(ValueError, match="output_model"): + AutomodelJobInput.model_validate( + { + "model": "llama", + "dataset": {"training": "default/train"}, + "training": {"training_type": "sft"}, + "output_model": "old-field", + }, + ) + + +def test_distillation_requires_teacher() -> None: + with pytest.raises(ValueError, match="teacher_model"): + AutomodelJobInput.model_validate( + { + "model": "llama", + "dataset": {"training": "default/train"}, + "training": {"training_type": "distillation"}, + }, + ) diff --git a/plugins/nemo-customizer/README.md b/plugins/nemo-customizer/README.md new file mode 100644 index 0000000000..927ceda3ce --- /dev/null +++ b/plugins/nemo-customizer/README.md @@ -0,0 +1,7 @@ +# nemo-customizer + +Router service for `/apis/customization`. Training backends (Automodel, RL, Megatron, …) register as **`nemo.customization.contributors`** entry points (discovered via `nemo_platform_plugin.discovery`). + +Registers **`nemo.sdk`** → `customization` for `client.customization.*` (composes contributor SDK modules such as `client.customization.automodel.jobs`). + +See [docs/CUSTOMIZATION.md](docs/CUSTOMIZATION.md) for contributor authoring. diff --git a/plugins/nemo-customizer/docs/CUSTOMIZATION.md b/plugins/nemo-customizer/docs/CUSTOMIZATION.md new file mode 100644 index 0000000000..dfb63576da --- /dev/null +++ b/plugins/nemo-customizer/docs/CUSTOMIZATION.md @@ -0,0 +1,23 @@ +# Customization contributor guide + +Register a training backend under **`nemo.customization.contributors`** (not `nemo.services`). + +## Contract + +Implement `CustomizationContributor`: + +- `name` — must match the entry-point key (e.g. `automodel`) +- `get_routers()` — `RouterSpec` list with a **unique** prefix under `v2/workspaces/{workspace}//` +- `get_cli()` — optional `typer.Typer` mounted at `nemo customization ` +- SDK: contributors implement HTTP/CLI only; **`nemo-customizer-plugin`** owns `nemo.sdk` → `customization` and composes backends (e.g. `client.customization.automodel.jobs` from `nemo-automodel-plugin`) + +## pyproject.toml + +```toml +[project.entry-points."nemo.customization.contributors"] +automodel = "nemo_automodel_plugin.contributor:AutomodelContributor" +``` + +## Jobs + +Use `add_job_routes(YourJob, service_name="customization", ...)` so Jobs records use `source=customization`. diff --git a/plugins/nemo-customizer/pyproject.toml b/plugins/nemo-customizer/pyproject.toml new file mode 100644 index 0000000000..bb5f14789a --- /dev/null +++ b/plugins/nemo-customizer/pyproject.toml @@ -0,0 +1,53 @@ +[project] +name = "nemo-customizer-plugin" +version = "0.1.0" +description = "Customization router for NeMo Platform training backends." +readme = "README.md" +requires-python = ">=3.11,<3.14" +dependencies = [ + "nemo-platform-plugin", + "nemo-platform", + "datasets>=3.3.1", + "pydantic>=2.10.6", + "transformers>=4.48.0", + "typer>=0.12.5", +] + +[project.entry-points."nemo.services"] +customization = "nemo_customizer.router:CustomizationRouterService" + +[project.entry-points."nemo.cli"] +customization = "nemo_customizer.cli:CustomizationCLI" + +[project.entry-points."nemo.sdk"] +customization = "nemo_customizer.sdk.resources:customization_sdk_resources" + +[project.entry-points."nemo.skills"] +customizer = "nemo_customizer.skills:get_skills_path" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/nemo_customizer"] + +[tool.uv.sources] +nemo-platform-plugin = { workspace = true } +nemo-platform = { workspace = true } + +[dependency-groups] +dev = [ + "pytest>=8.3.4", + "pytest-asyncio>=0.25.3", + "ruff>=0.11.8", + "fastapi>=0.115.0", +] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +pythonpath = ["src"] +testpaths = ["tests"] + +[tool.pyright] +extraPaths = ["src"] diff --git a/plugins/nemo-customizer/src/nemo_customizer/__init__.py b/plugins/nemo-customizer/src/nemo_customizer/__init__.py new file mode 100644 index 0000000000..66f8740d56 --- /dev/null +++ b/plugins/nemo-customizer/src/nemo_customizer/__init__.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Customization router plugin for NeMo Platform.""" + +from nemo_customizer.contributor import CustomizationContributor +from nemo_customizer.discovery import discover_customization_contributors + +__all__ = [ + "CustomizationContributor", + "discover_customization_contributors", +] diff --git a/plugins/nemo-customizer/src/nemo_customizer/cli.py b/plugins/nemo-customizer/src/nemo_customizer/cli.py new file mode 100644 index 0000000000..73948e1dc0 --- /dev/null +++ b/plugins/nemo-customizer/src/nemo_customizer/cli.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""CLI router for customization — mounts contributor subgroups.""" + +from __future__ import annotations + +from typing import ClassVar + +import typer +from nemo_platform_plugin.cli import NemoCLI +from nemo_platform_plugin.discovery import discover_customization_contributors + + +class CustomizationCLI(NemoCLI): + """``nemo customization`` root command.""" + + name: ClassVar[str] = "customization" + description: ClassVar[str] = "Customization training backends (Automodel, …)." + + def get_cli(self) -> typer.Typer: + app = typer.Typer( + name=self.name, + help=self.description, + no_args_is_help=True, + ) + + contributors = discover_customization_contributors() + if not contributors: + typer.echo( + "No customization contributors installed. Add nemo-automodel (or another backend) to enabled-plugins.", + err=True, + ) + return app + + for key in sorted(contributors.keys()): + contributor = contributors[key] + subgroup = contributor.get_cli() + if subgroup is not None: + app.add_typer(subgroup, name=key) + + return app diff --git a/plugins/nemo-customizer/src/nemo_customizer/contributor.py b/plugins/nemo-customizer/src/nemo_customizer/contributor.py new file mode 100644 index 0000000000..67b51114ec --- /dev/null +++ b/plugins/nemo-customizer/src/nemo_customizer/contributor.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Re-export customization contributor protocol from nemo-platform-plugin.""" + +from nemo_platform_plugin.customization_contributor import CustomizationContributor + +__all__ = ["CustomizationContributor"] diff --git a/plugins/nemo-customizer/src/nemo_customizer/discovery.py b/plugins/nemo-customizer/src/nemo_customizer/discovery.py new file mode 100644 index 0000000000..390e768274 --- /dev/null +++ b/plugins/nemo-customizer/src/nemo_customizer/discovery.py @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Re-export customization contributor discovery from nemo-platform-plugin.""" + +from nemo_platform_plugin.discovery import ( + CUSTOMIZATION_CONTRIBUTORS_GROUP, + discover_customization_contributor_classes, + discover_customization_contributors, +) + +__all__ = [ + "CUSTOMIZATION_CONTRIBUTORS_GROUP", + "discover_customization_contributor_classes", + "discover_customization_contributors", +] diff --git a/plugins/nemo-customizer/src/nemo_customizer/router.py b/plugins/nemo-customizer/src/nemo_customizer/router.py new file mode 100644 index 0000000000..addc683c80 --- /dev/null +++ b/plugins/nemo-customizer/src/nemo_customizer/router.py @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Customization router service — merges contributor HTTP routes.""" + +from __future__ import annotations + +from typing import ClassVar + +from fastapi import APIRouter +from nemo_platform_plugin.discovery import ( + CUSTOMIZATION_CONTRIBUTORS_GROUP, + discover_customization_contributors, +) +from nemo_platform_plugin.service import NemoService, RouterSpec + + +class CustomizationRouterError(RuntimeError): + """Raised when the customization router cannot start.""" + + +_ROUTER_BASE_DEPENDENCIES = ("entities", "auth", "jobs", "secrets", "files", "models") + + +def merge_router_dependencies(contributors: dict[str, object]) -> list[str]: + """Union platform router deps with each contributor's ``dependencies``.""" + deps = set(_ROUTER_BASE_DEPENDENCIES) + for contributor in contributors.values(): + contrib_deps = getattr(type(contributor), "dependencies", None) or [] + deps.update(contrib_deps) + return sorted(deps) + + +def _assert_no_prefix_collisions(contributors: dict[str, object]) -> None: + prefixes: dict[str, str] = {} + for key, contributor in contributors.items(): + for spec in contributor.get_routers(): # type: ignore[union-attr] + prefix = spec.prefix.strip("/") + if prefix in prefixes: + raise CustomizationRouterError( + f"Route prefix collision: contributors {prefixes[prefix]!r} and {key!r} " + f"both use prefix {spec.prefix!r}", + ) + prefixes[prefix] = key + + +class CustomizationRouterService(NemoService): + """Sole ``nemo.services`` owner for ``/apis/customization``.""" + + name: ClassVar[str] = "customization" + dependencies: ClassVar[list[str]] = list(_ROUTER_BASE_DEPENDENCIES) + + def __init__(self) -> None: + self._contributors = discover_customization_contributors() + if not self._contributors: + raise CustomizationRouterError( + "Customization router is enabled but no contributors were discovered. " + "Install a backend plugin (e.g. nemo-automodel) and ensure " + f"'{CUSTOMIZATION_CONTRIBUTORS_GROUP}' entry points are registered.", + ) + _assert_no_prefix_collisions(self._contributors) + type(self).dependencies = merge_router_dependencies(self._contributors) + + def get_routers(self) -> list[RouterSpec]: + router = APIRouter() + + @router.get("/healthz") + async def healthz() -> dict[str, object]: + return { + "plugin": self.name, + "status": "ok", + "contributors": sorted(self._contributors.keys()), + } + + specs: list[RouterSpec] = [ + RouterSpec( + router=router, + tag="Customization", + description="Customization router health.", + prefix="", + ), + ] + + for key in sorted(self._contributors.keys()): + contributor = self._contributors[key] + contributor_specs = contributor.get_routers() + for spec in contributor_specs: + specs.append( + RouterSpec( + router=spec.router, + tag=spec.tag or f"Customization {key}", + description=spec.description, + prefix=spec.prefix, + ), + ) + return specs diff --git a/plugins/nemo-customizer/src/nemo_customizer/sdk/__init__.py b/plugins/nemo-customizer/src/nemo_customizer/sdk/__init__.py new file mode 100644 index 0000000000..35c8a2a594 --- /dev/null +++ b/plugins/nemo-customizer/src/nemo_customizer/sdk/__init__.py @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Customization router SDK (``nemo.sdk`` entry point ``customization``).""" + +from nemo_customizer.sdk.resources import ( + AsyncCustomization, + Customization, + customization_sdk_resources, +) + +__all__ = [ + "AsyncCustomization", + "Customization", + "customization_sdk_resources", +] diff --git a/plugins/nemo-customizer/src/nemo_customizer/sdk/resources.py b/plugins/nemo-customizer/src/nemo_customizer/sdk/resources.py new file mode 100644 index 0000000000..b8cc1fb996 --- /dev/null +++ b/plugins/nemo-customizer/src/nemo_customizer/sdk/resources.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Customization SDK hub — composes contributor backends under ``client.customization``.""" + +from __future__ import annotations + +import importlib +import logging +from typing import Any + +from nemo_platform import AsyncNeMoPlatform, NeMoPlatform +from nemo_platform_plugin.discovery import discover_customization_contributors +from nemo_platform_plugin.sdk import NemoPluginSDKResources + +logger = logging.getLogger(__name__) + +# Contributor entry-point key → (module, sync class, async class) +_CONTRIBUTOR_SDK: dict[str, tuple[str, str, str]] = { + "automodel": ( + "nemo_automodel_plugin.sdk.resources", + "AutomodelCustomization", + "AsyncAutomodelCustomization", + ), +} + + +def _load_contributor_sdk_class(module_path: str, class_name: str) -> type[Any]: + module = importlib.import_module(module_path) + return getattr(module, class_name) + + +class Customization: + """Sync SDK namespace mounted as ``client.customization``.""" + + def __init__(self, platform: NeMoPlatform) -> None: + contributors = discover_customization_contributors() + for key, (module_path, sync_cls, _async_cls) in _CONTRIBUTOR_SDK.items(): + if key not in contributors: + continue + try: + cls = _load_contributor_sdk_class(module_path, sync_cls) + setattr(self, key, cls(platform)) + except ImportError: + logger.warning( + "Customization contributor %r is installed but SDK module %s is missing", + key, + module_path, + ) + + +class AsyncCustomization: + """Async SDK namespace mounted as ``client.customization``.""" + + def __init__(self, platform: AsyncNeMoPlatform) -> None: + contributors = discover_customization_contributors() + for key, (module_path, _sync_cls, async_cls) in _CONTRIBUTOR_SDK.items(): + if key not in contributors: + continue + try: + cls = _load_contributor_sdk_class(module_path, async_cls) + setattr(self, key, cls(platform)) + except ImportError: + logger.warning( + "Customization contributor %r is installed but SDK module %s is missing", + key, + module_path, + ) + + +customization_sdk_resources = NemoPluginSDKResources( + sync_resource=Customization, + async_resource=AsyncCustomization, +) diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills.py b/plugins/nemo-customizer/src/nemo_customizer/skills.py new file mode 100644 index 0000000000..982dff6ba1 --- /dev/null +++ b/plugins/nemo-customizer/src/nemo_customizer/skills.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Skills surface for the customization (customizer) plugin.""" + +from __future__ import annotations + +from pathlib import Path + + +def get_skills_path() -> Path: + """Return the directory containing plugin-provided skills.""" + + return Path(__file__).parent / "skills" diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/SKILL.md b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/SKILL.md new file mode 100644 index 0000000000..11e34a06b9 --- /dev/null +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/SKILL.md @@ -0,0 +1,287 @@ +--- +name: nemo-customizer +description: >- + Fine-tune models on NeMo Platform via `nemo customization automodel submit`: + HF dataset conversion, filesets, model entities, SFT/LoRA job JSON (hyperparameters, + batch, schedule, optimizer), and job polling. Use for train, fine-tune, customize, + SFT, LoRA, learning rate, epochs, or nemo customization. +triggers: + - nemo-customizer + - nemo customizer + - fine-tune + - fine tune + - finetune + - train a model + - customize a model + - sft + - lora + - automodel + - unsloth + - nemo customization + - nemo-customization + - customizer + - customization training + - automodel submit +not-for: + - nemo-build-agent (agent scaffold/deploy, not weight training) + - nemo-explore (agent design only) + - safe-synthesizer (tabular synthetic data training) +compatibility: >- + Requires nemo-customizer-plugin and a customization contributor (`nemo.customization.contributors`). + Platform must expose jobs, files, and models APIs. +maturity: active +license: Apache-2.0 +user-invocable: true +allowed-tools: [Bash, Read, Grep] +--- + +# NeMo Customizer + +End-to-end **SFT + LoRA** on NeMo Platform. Default plugin: **automodel**. Batch shell work; reuse resources with `--exist-ok`; skip CLI `--help` unless a command fails. + +## Gotchas + +- Run all `uv run` commands from the **nemo-platform** git root (top-level `pyproject.toml`), not a plugin subfolder. +- Set `NEMO_BASE_URL` (or `NMP_BASE_URL`) only when the user gives a platform URL; default `http://127.0.0.1:8080`. +- **Never set `max_steps` together with `epochs`.** `max_steps` is a global cap and stops mid-epoch. Test fixtures include `max_steps` for smoke tests — do not copy into production jobs. +- **Job done = top-level `status`** in `completed` | `error` | `cancelled`. Steps can all be `completed` while the job is still `active` (upload, entity registration). `status_details.phase` may stay `training` with `progress_pct: 100` for a long time — keep polling. `poll_automodel_job.sh` exits **1** on `error` or `cancelled`. +- Model spec fills async: **submit without polling** `nemo models get` unless submit fails. +- HF dataset id from the user → convert locally; do not ask for local paths first. +- Dataset fileset name = HF dataset **name** only (`tau/commonsense_qa` → `commonsense_qa`), not the model name. +- Prefer **CHAT** JSONL when the model has a chat template; details in `references/dataset-formats.md`. +- User asks to tune **batch or parallelism** → **Batch sizing** / **Multi-GPU** below. Other fields (LR, epochs, LoRA rank, distillation) → `references/hyperparameters.md` (`nemo customization automodel explain` for schema). +- Skill **defaults** (`micro_batch_size` 1, `global_batch_size` 4) are safe on unknown VRAM. When the user has **≥48 GB** on one GPU, use **Batch sizing** instead of defaults. +- **Do not use local `docker info`** to pick automodel vs unsloth. After auth, run `uv run nemo jobs list-execution-profiles -f json` against the user's platform (see `references/troubleshooting.md`). Default output is a table — **`-f json` is required** for scripting; parse **stdout only** (do not pipe `2>&1` into `json.load`). +- For submit/image/plugin errors, read `references/troubleshooting.md`. + +## Workflow + +``` +- [ ] export NEMO_BASE_URL (if user provided endpoint) +- [ ] cd nemo-platform && uv run nemo auth login --unsigned-token --email +- [ ] uv run nemo jobs list-execution-profiles -f json — GPU profile → automodel; else see troubleshooting (no local docker check) +- [ ] Convert HF dataset → /tmp/train-data/*.jsonl (see references/hf-conversion.md) +- [ ] Create dataset fileset (--exist-ok), upload train.jsonl (+ validation.jsonl), nemo files list to verify +- [ ] Create HF weights fileset + model entity if missing (--exist-ok) +- [ ] Write /tmp/job.json (batch sizing for ≥48 GB GPU; else Defaults table) +- [ ] uv run nemo customization automodel submit /tmp/job.json --workspace default +- [ ] Poll until top-level terminal (scripts/poll_automodel_job.sh or 60–120s manual polls) +- [ ] Report using output template below +``` + +## Fast path + +Substitute ``, ``, ``, ``, ``, ``. + +**Setup** + +```bash +export NEMO_BASE_URL=http://127.0.0.1:8080 # user override only +cd /path/to/nemo-platform +uv run nemo auth login --unsigned-token --email admin@example.com +uv run nemo jobs list-execution-profiles -f json # platform GPU profiles → automodel; set training.execution_profile if needed +``` + +**1. Dataset** — convert per `references/hf-conversion.md`, then: + +```bash +DATASET= # e.g. commonsense_qa +uv run nemo files filesets create "$DATASET" --workspace default --purpose dataset --exist-ok +uv run nemo files upload /tmp/train-data/train.jsonl "$DATASET" --workspace default --remote-path train.jsonl +# validation.jsonl if present +uv run nemo files list "$DATASET" --workspace default +``` + +**2. Model** — skip if entity exists (`nemo models list --workspace default`). + +```bash +WEIGHTS= # e.g. qwen3-1.7b +MODEL_ENTITY= # Models API entity (not dataset fileset, not HF id) +HF_REPO= # e.g. Qwen/Qwen3-1.7B + +uv run nemo files filesets create "$WEIGHTS" --workspace default --purpose model --exist-ok \ + --storage '{"type":"huggingface","repo_id":"'"$HF_REPO"'","repo_type":"model","revision":"main"}' + +uv run nemo models create "$MODEL_ENTITY" --workspace default --exist-ok \ + --input-data '{"name":"'"$MODEL_ENTITY"'","fileset":"default/'"$WEIGHTS"'","custom_fields":{"hf_model_id":"'"$HF_REPO"'"}}' +``` + +**3. Job JSON** — write `/tmp/job.json`. `model` is the **registered model entity** (`default/`), not an HF repo id or dataset fileset. Full hyperparameter reference: `references/hyperparameters.md`. + +```json +{ + "model": "default/", + "dataset": { + "training": "default/", + "validation": "default/" + }, + "training": { + "training_type": "sft", + "finetuning_type": "lora", + "lora": { "rank": 16, "alpha": 32 }, + "max_seq_length": 2048 + }, + "schedule": { "epochs": 1 }, + "batch": { "global_batch_size": 4, "micro_batch_size": 1 }, + "optimizer": { "learning_rate": 5e-5, "weight_decay": 0.01, "warmup_steps": 0 }, + "parallelism": { "num_nodes": 1, "num_gpus_per_node": 1, "tensor_parallel_size": 1 }, + "output": { "name": "" } +} +``` + +**4. Submit and poll** + +```bash +uv run nemo customization automodel submit /tmp/job.json --workspace default +bash plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/scripts/poll_automodel_job.sh automodel- 90 +``` + +Or poll manually: `uv run nemo jobs get-status automodel-` every 60–120s. + +## Defaults + +| Field | Value | +|-------|-------| +| Workspace | `default` | +| Plugin | `automodel` | +| Training | SFT + LoRA, `max_seq_length` 2048 | +| Schedule | `epochs` ≥ 1; omit `max_steps` | +| Parallelism | 1 node, 1 GPU, TP=1 | +| Batch | `global_batch_size` 4, `micro_batch_size` 1 (unknown VRAM; see **Batch sizing** for ≥48 GB) | +| Optimizer | `learning_rate` 5e-5 | +| Auth email | `admin@example.com` unless user specifies | + +## Batch sizing (≥48 GB VRAM) + +Assume **one GPU with at least 48 GB** (e.g. RTX 5880 / A6000 / L40), `parallelism` = 1 node × 1 GPU, `tensor_parallel_size` 1, bf16, `training_type` `sft`, LoRA **rank 16** unless the user asks otherwise. + +**How to size** + +1. Read **model size** from the entity (`nemo models get`) or HF card (parameter count). +2. Pick **`finetuning_type`**: `lora` (adapter only, default) vs `all_weights` (full SFT — much heavier). +3. Set **`max_seq_length`** (2048 is the skill default; shorter seq → more batch headroom). +4. Set **`micro_batch_size`** first (drives peak VRAM), then **`global_batch_size`** as a multiple of `micro_batch_size` (gradient accumulation when GBS > micro). + +**Constraint:** `global_batch_size` must be divisible by `micro_batch_size × data_parallel_size`, where `data_parallel_size = (num_nodes × num_gpus_per_node) / (tensor_parallel_size × pipeline_parallel_size × context_parallel_size)` (1 for a single-GPU job). + +### LoRA (`finetuning_type: lora`) — `max_seq_length` 2048 + +**VRAM does not scale linearly with `micro_batch_size`.** LoRA loads the full base weights once; activation memory grows slowly. On 48 GB, **`micro_batch_size` must decrease as model size grows** (smaller models always ≥ larger models in the table). Use **`global_batch_size` ≈ 4 × `micro_batch_size`**. + +**Default batch** — start here for a reliable full epoch. **High utilization** — optional; double from default (or ramp in steps) to reach **~35–40 GiB**. Halve both if OOM (exit **137**) or training crashes (exit **1**). + +| Model params | Default `micro` | Default GBS | `learning_rate` | High-util `micro` | High-util GBS | +|--------------|------------------:|------------:|----------------:|------------------:|--------------:| +| ≤4B | 32 | 128 | `1e-4` | 64 | 256 | +| 4B–8B | 24 | 96 | `8e-5` | 48 | 192 | +| 8B–14B | 16 | 64 | `8e-5` | 24 | 96 | +| >14B | 8 | 32 | `5e-5` | 16 | 64 | + +Validated (`commonsense_qa` @ 2048, 48 GB, one job per GPU): **Qwen3-1.7B** — `micro` 16 / GBS 64 ~8 min; defaults above leave headroom to ramp. **Qwen3-8B** — `micro` 2–4 ≈16–18.5 GiB (under-filled); **`micro` 16 / GBS 64** stable default (~153 steps/epoch); high-util **`micro` 24 / GBS 96** (32 / 128 hit ~40 GiB but failed mid-epoch with exit 1). + +### Multi-GPU (same node) + +Pick the path by whether the **base model fits in ~48 GB on one GPU** (LoRA or full SFT): + +| Situation | `tensor_parallel_size` | Goal | +|-----------|------------------------:|------| +| Model **fits** on one ≥48 GB GPU | **1** | **Data parallel** — more GPUs = faster training; keep `micro` per GPU, scale `global_batch_size` | +| Model **does not fit** on one ≥48 GB GPU | **> 1** (e.g. 2 on a 2-GPU node) | **Tensor parallel** — shard layers across GPUs so the model fits; lower `micro` / GBS vs single-GPU tables | + +**Data parallel (TP = 1)** — default for Qwen3-8B LoRA and similar on 48 GB cards: + +| Rule | Detail | +|------|--------| +| `micro_batch_size` | **Per GPU** — same as a stable single-GPU run | +| `global_batch_size` | ≈ **single-GPU GBS × `num_gpus_per_node`**; step count ≈ `samples / GBS` | +| Divisibility | `global_batch_size` ÷ **`micro_batch_size × num_gpus_per_node`** must be an integer | +| Scheduling | **One job** owns all GPUs; no overlapping 1-GPU and multi-GPU jobs | + +```json +"parallelism": { "num_nodes": 1, "num_gpus_per_node": 2, "tensor_parallel_size": 1 }, +"batch": { "global_batch_size": 128, "micro_batch_size": 16 } +``` + +**Tensor parallel (TP > 1)** — when weights + activations OOM on a single ≥48 GB GPU (large full SFT, very long `max_seq_length`, or models above the LoRA sizing table without fitting): + +- Set **`num_gpus_per_node`** and **`tensor_parallel_size`** so **`num_gpus_per_node` is divisible by `tensor_parallel_size`** (e.g. 2 GPUs → `tensor_parallel_size: 2`, or 4 GPUs → TP 2 or 4). +- **`data_parallel_size`** = `(num_nodes × num_gpus_per_node) / (tensor_parallel_size × pipeline_parallel_size × context_parallel_size)` — use this in the GBS divisibility rule instead of raw GPU count. +- Start with **lower `micro_batch_size`** than the single-GPU table; increase only if VRAM allows. MoE models: if `expert_parallel_size > 1`, **`tensor_parallel_size` must be 1**. + +```json +"parallelism": { "num_nodes": 1, "num_gpus_per_node": 2, "tensor_parallel_size": 2 }, +"batch": { "global_batch_size": 8, "micro_batch_size": 1 } +``` + +`execution_profile` is usually still **`"gpu"`** — confirm with `uv run nemo jobs list-execution-profiles -f json`. + +**Example — Qwen3-8B LoRA, 2× 48 GB (fits one GPU):** single-GPU **micro 16 / GBS 64** → 2-GPU data parallel **micro 16 / GBS 128**, `learning_rate` `8e-5`. + +### Full-weight SFT (`finetuning_type: all_weights`) — `max_seq_length` 2048 + +| Model params | `micro_batch_size` | `global_batch_size` | `learning_rate` | +|--------------|-------------------:|--------------------:|----------------:| +| ≤2B | 2 | 8 | `2e-5` | +| 2B–4B | 1 | 4 | `1e-5` | +| 4B–8B | 1 | 2 | `5e-6` | +| >8B | 1 | 1 | lower LR or use TP / shorter seq | + +Output type is **model** (full checkpoint), not adapter. Expect much longer runs than LoRA at the same batch. + +### `max_seq_length` scaling + +Scale **`micro_batch_size`** from the 2048 tables (round down, minimum 1): + +| `max_seq_length` | Multiply `micro_batch_size` by | +|------------------|-------------------------------:| +| 512 | 4× | +| 1024 | 2× | +| 2048 | 1× (tables above) | +| 4096 | 0.5× | + +Then set `global_batch_size` to a multiple of the new `micro_batch_size` (often keep the same ratio as the table, e.g. GBS = 4 × micro for LoRA). + +### LoRA rank + +Higher rank uses more VRAM. If OOM at rank 16, drop to rank 8 before lowering batch; if headroom remains, rank 32 is fine for training (deploy rank ≤32 on default NIM/vLLM). + +### Tuning loop + +| Symptom | Action | +|---------|--------| +| CUDA OOM | Halve `micro_batch_size`, then `global_batch_size`, then `max_seq_length` | +| Slow / low GPU memory use | Step up toward the **high-util** column (or double default `micro`+GBS); stop at ~35–40 GiB or when training fails, then use **default** for the retry | +| User wants max throughput | Raise `micro_batch_size` first; keep GBS ≈ 4× micro — avoid `micro_batch_size` 1 with huge GBS | + +Field glossary, distillation/KD, and schema pointers: `references/hyperparameters.md` (batch/multi-GPU → **this file**, not hyperparameters). + +## Worked example + +`Qwen/Qwen3-1.7B` + `tau/commonsense_qa` → CHAT JSONL, fileset `commonsense_qa`, entity `qwen3-1.7b`, output `qwen3-1.7b-commonsense-qa-lora`, `epochs: 1` (no `max_steps`). On ≥48 GB GPU use LoRA ≤4B **default**: `micro` 32, GBS 128, `learning_rate` `1e-4` (high-util: 64 / 256). + +## Report to user + +```markdown +## Fine-tune result + +- **Job:** automodel- +- **Model entity:** default/ +- **Output adapter fileset:** +- **Status:** +- **Notes:** +``` + +## Reference files + +| When | Read | +|------|------| +| HF conversion or MCQA shaping | `references/hf-conversion.md` | +| CHAT vs SFT vs CUSTOM | `references/dataset-formats.md` | +| Field glossary, distillation/KD, schema | `references/hyperparameters.md` (not batch sizing) | +| Batch sizing (≥48 GB), OOM / throughput | **Batch sizing** section above | +| Multi-GPU same node | **Multi-GPU (same node)** under batch sizing | +| Backend choice, execution profiles, submit failure, images, CLI | `references/troubleshooting.md` | +| Live JSON schema | `uv run nemo customization automodel explain` | +| Job JSON fixture | `plugins/nemo-automodel/tests/fixtures/qwen3_0.6b_sft_lora.json` (ignore `max_steps` for real runs) | + +Related: `plugins/nemo-automodel/README.md`, `plugins/nemo-customizer/docs/CUSTOMIZATION.md`, skills **`nemo-files`**, **`nemo-status`**. diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/dataset-formats.md b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/dataset-formats.md new file mode 100644 index 0000000000..ad6fd62f83 --- /dev/null +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/dataset-formats.md @@ -0,0 +1,16 @@ +# Dataset formats (automodel) + +Automodel detects schema from the **first JSONL line** (`DatasetSchema` in `services/automodel/.../datasets/preparation.py`). + +Upload `train.jsonl` and optional `validation.jsonl` at the **fileset root**. Use the same fileset for `dataset.training` and `dataset.validation` in job JSON. + +| Schema | JSONL shape | Job JSON | +|--------|-------------|----------| +| **CHAT** (preferred when model has chat template) | `{"messages": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}` | (none) | +| **SFT** | `{"prompt": "...", "completion": "..."}` | (none) | +| **CUSTOM** | Any two columns, e.g. `{"input": "...", "output": "..."}` | `"prompt_template": "{input} {output}"` on `dataset` | +| **EMBEDDING** | `{"query": "...", "pos_doc": "...", "neg_doc": ["...", "..."]}` | embedding training type when applicable | + +**Conversion preference:** CHAT if `AutoTokenizer(...).chat_template` or model `spec.is_chat` / `spec.chat_template` → else SFT. Use CUSTOM or EMBEDDING only when the user asks or the task requires it. + +For **CUSTOM**, placeholders in `prompt_template` must match column names exactly (two placeholders). diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hf-conversion.md b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hf-conversion.md new file mode 100644 index 0000000000..e7521dc06b --- /dev/null +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hf-conversion.md @@ -0,0 +1,54 @@ +# Hugging Face dataset conversion + +Run from **nemo-platform** git root: `uv run python …` (plugin brings `datasets` + `transformers`). + +Do **not** ask the user for local paths when they gave an HF dataset id — convert and upload in the same session. + +## Chat-template check + +```python +from transformers import AutoTokenizer +has_chat = bool(getattr(AutoTokenizer.from_pretrained("", trust_remote_code=True), "chat_template", None)) +``` + +If the model entity already exists: `nemo models get --workspace default` → use `spec.is_chat` or `spec.chat_template` instead of re-downloading tokenizer weights. + +## Conversion script (adapt `to_chat` per dataset) + +```python +from datasets import load_dataset +from transformers import AutoTokenizer +import json +from pathlib import Path + +HF_REPO = "" +HF_DATASET = "" # e.g. tau/commonsense_qa +DATASET_NAME = HF_DATASET.split("/")[-1].lower() # fileset name, e.g. commonsense_qa + +has_chat = bool(getattr(AutoTokenizer.from_pretrained(HF_REPO, trust_remote_code=True), "chat_template", None)) + +def to_chat(ex): + # MCQA example (tau/commonsense_qa): + labels, texts = ex["choices"]["label"], ex["choices"]["text"] + choices = "\n".join(f"{l}. {t}" for l, t in zip(labels, texts)) + user = f"Question: {ex['question']}\nChoices:\n{choices}\nAnswer:" + assistant = texts[labels.index(ex["answerKey"])] + return {"messages": [{"role": "user", "content": user}, {"role": "assistant", "content": assistant}]} + +def to_sft(ex): + row = to_chat(ex) + return {"prompt": row["messages"][0]["content"], "completion": row["messages"][1]["content"]} + +convert = to_chat if has_chat else to_sft + +ds = load_dataset(HF_DATASET) +out = Path("/tmp/train-data") +out.mkdir(exist_ok=True) +for split in ("train", "validation"): + if split in ds: + with (out / f"{split}.jsonl").open("w") as f: + for ex in ds[split]: + f.write(json.dumps(convert(ex)) + "\n") +``` + +Then upload (see main skill). Validate with `nemo files list --workspace default`. diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md new file mode 100644 index 0000000000..3e275ef58d --- /dev/null +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md @@ -0,0 +1,313 @@ +# Hyperparameters (automodel job JSON) + +Job JSON for `nemo customization automodel submit` uses **`AutomodelJobInput`** (`plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py`). Only fields in that schema are accepted (`extra="forbid"`). + +**Schema dump:** from nemo-platform root: + +```bash +uv run nemo customization automodel explain +``` + +**Contract examples:** `tests/customizer-automodel-contract/input_configs/` (legacy shape; map `batch_size` → `global_batch_size` in submit JSON). + +**Batch sizing, 48 GB VRAM tables, multi-GPU (data parallel vs tensor parallel), and throughput tuning** live in **`SKILL.md`** (§ Batch sizing, § Multi-GPU). This file is the **field glossary**, full JSON template, distillation/KD, and schema pointers — not the place to pick `micro_batch_size` / `global_batch_size` for production runs. + +--- + +## Job JSON layout + +| Section | Purpose | +|---------|---------| +| `model` | **Base model entity** ref (`default/`) — weights to fine-tune | +| `dataset` | **Dataset filesets** (`default/`); optional `prompt_template` for CUSTOM schema | +| `training` | Method, LoRA, `max_seq_length`, distillation/KD fields | +| `schedule` | Epochs, optional step cap, validation cadence, seed | +| `batch` | Global/micro batch, sequence packing | +| `optimizer` | LR, weight decay, warmup | +| `parallelism` | Nodes, GPUs, TP/PP/CP/EP | +| `output` | Output adapter/model fileset name | +| `integrations` | Optional W&B / MLflow | + +### `model` field (base model entity) + +`model` must name a **Models API entity** for the checkpoint being trained — not a dataset fileset, not an output adapter from a prior job, and not a raw Hugging Face repo id. + +| Valid | Invalid | +|-------|---------| +| `default/qwen3-1.7b` (entity from `nemo models create`) | `Qwen/Qwen3-1.7B` (HF id) | +| `default/llama-3.2-1b-instruct` | `default/commonsense_qa` (dataset fileset) | +| `other-ws/my-model` (qualified ref) | `qwen3-1.7b-commonsense-qa-lora` (output fileset only, unless registered as entity) | + +Register before submit (same as skill fast path): HF **model** fileset → `nemo models create …` with `"fileset":"default/"`. List: `nemo models list --workspace default`. + +Full template: + +```json +{ + "model": "default/", + "dataset": { + "training": "default/", + "validation": "default/", + "prompt_template": null + }, + "training": { + "training_type": "sft", + "finetuning_type": "lora", + "lora": { + "rank": 16, + "alpha": 32, + "merge": false, + "target_modules": null + }, + "max_seq_length": 2048, + "execution_profile": null + }, + "schedule": { + "epochs": 1, + "max_steps": null, + "val_check_interval": null, + "seed": null + }, + "batch": { + "global_batch_size": 4, + "micro_batch_size": 1, + "sequence_packing": false + }, + "optimizer": { + "learning_rate": 5e-5, + "weight_decay": 0.01, + "warmup_steps": 0 + }, + "parallelism": { + "num_nodes": 1, + "num_gpus_per_node": 1, + "tensor_parallel_size": 1, + "pipeline_parallel_size": 1, + "context_parallel_size": 1, + "expert_parallel_size": null + }, + "output": { "name": "", "description": null }, + "integrations": null +} +``` + +--- + +## Field reference + +### `training` + +| Field | Default | Notes | +|-------|---------|-------| +| `training_type` | `sft` | `distillation` requires `teacher_model` (entity ref) | +| `finetuning_type` | `lora` | `all_weights` (full fine-tune), `lora_merged` (merge adapter into base) | +| `lora.rank` | `16` | Higher → more capacity, more VRAM. Typical training range 8–32; **cap at 32** if the adapter will be served with default NIM / vLLM (rank > 32 may not load) | +| `lora.alpha` | `32` | Scaling; common rule of thumb **alpha ≈ 2× rank** | +| `lora.merge` | `false` | If true with `lora_merged`, output is full weights not adapter | +| `lora.target_modules` | `null` | e.g. `["q_proj","v_proj"]`; null = platform default targets | +| `max_seq_length` | `2048` | Truncate/pack to this length; lower if OOM | +| `teacher_model` | — | **Model entity ref** (not HF id). Required for distillation; see below | +| `distillation_ratio` | `0.5` | KD blend (0–1) | +| `distillation_temperature` | `1.0` | KD temperature | +| `teacher_precision` | `bf16` | `bf16` \| `fp16` \| `fp32` | +| `offload_teacher` | `false` | Offload teacher weights to CPU | + +LoRA block is auto-created when `finetuning_type` is `lora` or `lora_merged`. + +### `schedule` + +| Field | Default | Notes | +|-------|---------|-------| +| `epochs` | `1` | Must be **≥ 1**. Full passes over training set | +| `max_steps` | `null` | **Global step cap.** Omit for epoch-based runs | +| `val_check_interval` | `null` | `≤ 1.0` = fraction of epoch; `> 1` = every N steps | +| `seed` | `null` | Reproducibility | + +**Gotcha:** Do **not** set `max_steps` with `epochs` for normal training. `max_steps` stops early (e.g. `epochs: 1` + `max_steps: 100` ends at step 100). Use `max_steps` **alone** only for smoke tests. + +### `batch` + +| Field | Default | Notes | +|-------|---------|-------| +| `global_batch_size` | `8` (schema) | Effective batch across all GPUs; **≥48 GB LoRA tables → `SKILL.md`** | +| `micro_batch_size` | `1` (schema) | **Per GPU**; same SKILL tables for single- and multi-GPU (TP=1) | +| `sequence_packing` | `false` | Pack short sequences for throughput (needs compatible data) | + +**Validation:** `global_batch_size` must be divisible by `micro_batch_size × data_parallel_size`, where: + +`data_parallel_size = (num_nodes × num_gpus_per_node) / (tensor_parallel_size × pipeline_parallel_size × context_parallel_size)` + +Example: 1 node, 2 GPUs, TP=1 → DP=2 → GBS must be a multiple of `2 × micro_batch_size`. See **`SKILL.md` § Multi-GPU** for data parallel vs tensor parallel. + +### `optimizer` + +| Field | Default | Notes | +|-------|---------|-------| +| `learning_rate` | `5e-6` (schema) | Skill uses **5e-5** for small LoRA SFT; see tuning below | +| `weight_decay` | `0.01` | L2-style regularization | +| `warmup_steps` | `0` | Linear warmup; try ~10% of total steps for long runs | + +`adam_beta1` / `adam_beta2` are **not** in the simplified submit schema (fixed in compiler adapter). Use contract JSONs only if your platform version adds them. + +### `parallelism` + +| Field | Default | Notes | +|-------|---------|-------| +| `num_nodes` | `1` | Multi-node distributed jobs | +| `num_gpus_per_node` | `1` | GPUs per node | +| `tensor_parallel_size` | `1` | **> 1** when the model does not fit on one ≥48 GB GPU — see **`SKILL.md` § Multi-GPU** | +| `pipeline_parallel_size` | `1` | Pipeline stages | +| `context_parallel_size` | `1` | Long-context sharding | +| `expert_parallel_size` | `null` | MoE only; must divide `data_parallel_size × context_parallel_size` | + +**MoE:** If `expert_parallel_size > 1` and multiple GPUs, `tensor_parallel_size` must be **1**. + +### `integrations` (optional) + +```json +"integrations": { + "wandb": { "enabled": true, "project": "my-project", "api_key_secret": "wandb-api-key" }, + "mlflow": null +} +``` + +--- + +## Tuning guide (when the user asks) + +Apply user overrides to `/tmp/job.json` before submit. For **batch / GPU count / parallelism**, follow **`SKILL.md`** (defaults table + § Batch sizing + § Multi-GPU). Below covers **non-batch** fields and defers VRAM/batch symptoms to the skill. + +| Symptom / goal | Try first | +|----------------|-----------| +| CUDA OOM | **`SKILL.md` tuning loop:** halve `micro_batch_size`, then `global_batch_size`, then `max_seq_length`; use TP > 1 only if the model does not fit one ≥48 GB GPU | +| Slow / low GPU use | **`SKILL.md`:** step toward high-util column or double `micro`+GBS until ~35–40 GiB; multi-GPU data parallel if model fits one GPU | +| Underfitting | More `epochs`, slightly higher `learning_rate`, higher LoRA `rank` (≤ 32 for NIM/vLLM deploy) | +| Overfitting | Fewer `epochs`, lower `learning_rate`, higher `weight_decay`, smaller `rank` | +| Quick smoke test | `max_steps` only (e.g. 10–50), **omit or ignore epoch goal**; or `epochs: 1` on tiny slice | +| Reproducibility | Set `schedule.seed` | + +### Learning rate (LoRA SFT, starting points) + +| Model scale | Suggested `learning_rate` | +|-------------|---------------------------| +| ≤ 3B | `5e-5` – `1e-4` | +| 3B – 8B | `2e-5` – `5e-5` | +| > 8B | `1e-5` – `2e-5` | + +Schema default is `5e-6` (conservative). Fixtures: `qwen3_0.6b_sft_lora.json` uses `5e-5`; `minimal_sft_lora.json` uses `5e-6`. + +### LoRA rank / alpha + +**Deployment cap:** Default **NIM** and **vLLM** LoRA serving paths support rank **≤ 32**. Use `rank` 32 (not higher) when the fine-tuned adapter will be deployed for inference on those stacks unless the user confirms a higher rank is supported. + +| Use case | `rank` | `alpha` | +|----------|--------|---------| +| Default / balanced | 16 | 32 | +| Low VRAM / light touch | 8 | 16 | +| More capacity (inference-safe max) | 32 | 64 | + +### Epochs vs dataset size + +One epoch = one full pass over `train.jsonl`. Steps per epoch ≈ `train_samples / global_batch_size` (e.g. ~10k samples, GBS 64 → ~153 steps). Plan poll time from the **GBS you chose in `SKILL.md`**, not the unknown-VRAM default (GBS 4). + +--- + +## Presets (non-batch fields) + +Use **`SKILL.md` § Batch sizing** and **§ Multi-GPU** for `batch` and `parallelism` on ≥48 GB GPUs. Presets below only override schedule / training / optimizer. + +**Smoke test (step-capped)** + +```json +"schedule": { "epochs": 1, "max_steps": 50 } +``` + +**Higher-quality LoRA (more VRAM/time)** + +```json +"training": { "lora": { "rank": 32, "alpha": 64 }, "max_seq_length": 2048 }, +"schedule": { "epochs": 3 }, +"optimizer": { "learning_rate": 2e-5, "warmup_steps": 100 } +``` + +Pair with batch rows from **`SKILL.md`** (e.g. ≤4B default `micro` 32 / GBS 128, not `micro` 1 / GBS 4). + +--- + +## Distillation (`training_type: "distillation"`) + +Use only when the user requests KD/distillation. **`model`** is the **student** entity; **`teacher_model`** is a separate **teacher** entity in the same workspace (unless qualified as `other-ws/name`). + +### Teacher model entity + +`teacher_model` must be a registered **model entity ref**, same shape as `model`: + +| Form | Example | +|------|---------| +| Same workspace | `default/llama-3.2-3b-instruct` | +| Explicit workspace | `default/` | + +It is **not** a Hugging Face repo id. Register the teacher like the student before submit: + +```bash +TEACHER_WEIGHTS=llama-3.2-3b-instruct # fileset name +TEACHER_ENTITY=llama-3.2-3b-instruct # entity name +TEACHER_HF=meta-llama/Llama-3.2-3B-Instruct + +uv run nemo files filesets create "$TEACHER_WEIGHTS" --workspace default --purpose model --exist-ok \ + --storage '{"type":"huggingface","repo_id":"'"$TEACHER_HF"'","repo_type":"model","revision":"main"}' + +uv run nemo models create "$TEACHER_ENTITY" --workspace default --exist-ok \ + --input-data '{"name":"'"$TEACHER_ENTITY"'","fileset":"default/'"$TEACHER_WEIGHTS"'","custom_fields":{"hf_model_id":"'"$TEACHER_HF"'"}}' +``` + +Verify: `nemo models get --workspace default`. Reuse an existing entity with `nemo models list` when present. + +**Compatibility:** Student and teacher must share the **same vocabulary / tokenizer family** (compiler loads both for KD). Mismatched tokenizers fail at runtime. Prefer a larger instruct model as teacher and a smaller base/chat model as student in the same family when possible. + +**VRAM:** Set `offload_teacher: true` if the job OOMs loading student + teacher; `teacher_precision: "bf16"` is the default. + +### Job JSON + +```json +{ + "model": "default/", + "dataset": { "training": "default/" }, + "training": { + "training_type": "distillation", + "finetuning_type": "lora", + "teacher_model": "default/", + "distillation_ratio": 0.5, + "distillation_temperature": 1.0, + "teacher_precision": "bf16", + "offload_teacher": false, + "max_seq_length": 2048 + }, + "schedule": { "epochs": 1 }, + "batch": { "global_batch_size": 64, "micro_batch_size": 16 }, + "optimizer": { "learning_rate": 8e-5 }, + "parallelism": { "num_nodes": 1, "num_gpus_per_node": 1, "tensor_parallel_size": 1 }, + "output": { "name": "" } +} +``` + +(`batch` / `parallelism` example uses an 8B-scale row from **`SKILL.md`**; adjust for student size.) + +| Field | Meaning | +|-------|---------| +| `distillation_ratio` | Blend of KD vs CE loss (`0` = CE only, `1` = KD only) | +| `distillation_temperature` | Softmax temperature for teacher logits | +| `offload_teacher` | CPU-offload frozen teacher weights to save GPU memory | + +--- + +## Source of truth + +| Resource | Path | Use for | +|----------|------|---------| +| **Batch / multi-GPU / 48 GB LoRA** | `SKILL.md` (§ Batch sizing, § Multi-GPU) | Choosing `micro`, GBS, LR, TP vs data parallel | +| Submit schema | `plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py` | Allowed JSON fields | +| Schema → compiler mapping | `services/automodel/src/nmp/automodel/adapter.py` | `dataset.training` → compiler `dataset` string | +| API field descriptions | `services/automodel/src/nmp/automodel/api/v2/jobs/schemas.py` | Compiler-internal shape (not submit JSON) | +| JSON examples | `plugins/nemo-automodel/tests/fixtures/*.json` | Copy-paste templates (ignore fixture `max_steps` in prod) | +| Full spec doc | `plugins/nemo-automodel/SCOPE.md` (simplified JSON section) | Design notes | diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/troubleshooting.md b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/troubleshooting.md new file mode 100644 index 0000000000..15242573ab --- /dev/null +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/troubleshooting.md @@ -0,0 +1,69 @@ +# Troubleshooting + +Read this file when submit fails, jobs fail on images, or the user asks for Unsloth. + +## Backend choice (automodel vs unsloth) + +**Do not** run `docker info` on the agent machine. The platform often runs elsewhere (`NEMO_BASE_URL`). Ask the **connected platform** what executors it exposes. + +After `nemo auth login`, list profiles: + +```bash +uv run nemo jobs list-execution-profiles -f json +``` + +REST equivalent (same payload): `GET /apis/jobs/v2/execution-profiles` on the platform base URL with the saved auth token. + +Each entry has `provider`, `profile` (name), and `backend` (e.g. `docker`, `kubernetes_job`, `volcano_job`, `subprocess`). + +| Condition | Plugin | +|-----------|--------| +| User asks for Unsloth | `unsloth` (if installed) | +| Response includes **`provider`: `gpu` or `gpu_distributed`** | **`automodel`** (default) | +| No GPU profiles (only `subprocess` and/or CPU `provider`) | Platform cannot schedule GPU container training → use **`unsloth`** locally if the user has a GPU, or report that remote automodel is unavailable | + +Automodel training steps need a **GPU execution profile** on the platform. `subprocess` profiles run host commands and are not a substitute for automodel’s GPU container step. + +### Pick `training.execution_profile` + +When using automodel, set `training.execution_profile` in job JSON to the **`profile`** string of a GPU row from the list (e.g. `default`, `docker_gpu`). If omitted, the plugin default is usually `gpu` — submit errors mentioning an unknown profile mean you should re-list and set an exact name from the API. + +Quick filter (stdout only — do not use `2>&1` or `json.load` breaks on stderr warnings): + +```bash +uv run nemo jobs list-execution-profiles -f json 2>/dev/null | python3 -c " +import sys, json +for p in json.load(sys.stdin): + if p.get('provider') in ('gpu', 'gpu_distributed'): + print(p['profile'], p.get('backend'), p.get('provider')) +" +``` + +Do not run `nemo customization --help` unless submit returns unknown plugin. + +Automodel uses **`submit` only** (no `run`). Dataset refs in job JSON: `default/`. + +## Missing training images + +Set **before** starting the platform (not per job): + +```bash +export NMP_IMAGE_REGISTRY= +export NMP_IMAGE_TAG= +export NMP_AUTOMODEL_IMAGE_REGISTRY=$NMP_IMAGE_REGISTRY +``` + +Pull automodel images only when the job error mentions a missing image. + +## CLI quick reference + +| Action | Command | +|--------|---------| +| Execution profiles | `nemo jobs list-execution-profiles -f json` | +| Create dataset fileset | `nemo files filesets create --workspace default --purpose dataset --exist-ok` | +| Create HF weights fileset | `nemo files filesets create --workspace default --purpose model --exist-ok --storage '{"type":"huggingface","repo_id":"","repo_type":"model","revision":"main"}'` | +| Upload | `nemo files upload --workspace default --remote-path train.jsonl` | +| List files | `nemo files list --workspace default` | +| Create model | `nemo models create --workspace default --exist-ok --input-data ''` | +| Submit | `nemo customization automodel submit --workspace default` | +| Status | `nemo jobs get-status automodel-` | diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/scripts/poll_automodel_job.sh b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/scripts/poll_automodel_job.sh new file mode 100755 index 0000000000..7eae73315e --- /dev/null +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/scripts/poll_automodel_job.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Poll automodel job until top-level status is terminal. +# Usage: poll_automodel_job.sh automodel- [interval_seconds] +# Requires: NEMO_BASE_URL or NMP_BASE_URL, run from nemo-platform root with `uv run`. +# Exit 0 on completed; exit 1 on error, cancelled, or get-status failure. + +set -euo pipefail + +JOB="${1:?usage: poll_automodel_job.sh automodel- [interval_seconds]}" +INTERVAL="${2:-90}" + +while true; do + JSON=$(uv run nemo jobs get-status "$JOB" 2>/dev/null) || { + echo "get-status failed for $JOB" >&2 + exit 1 + } + read -r STATUS PHASE <<<"$(printf '%s' "$JSON" | python3 -c " +import sys, json +d = json.load(sys.stdin) +print(d['status'], d.get('status_details', {}).get('phase', '')) +")" + echo "$(date +%H:%M:%S) status=$STATUS phase=$PHASE" + case "$STATUS" in + completed) + printf '%s\n' "$JSON" | python3 -m json.tool + exit 0 + ;; + error|cancelled) + printf '%s\n' "$JSON" | python3 -m json.tool >&2 + exit 1 + ;; + esac + sleep "$INTERVAL" +done diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/tests.json b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/tests.json new file mode 100644 index 0000000000..f564074615 --- /dev/null +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/tests.json @@ -0,0 +1,65 @@ +{ + "skill": "nemo-customizer", + "tests": [ + { + "type": "explicit", + "prompt": "Use the nemo-customizer skill to fine-tune Qwen with automodel.", + "expected_skill": "nemo-customizer" + }, + { + "type": "explicit", + "prompt": "Run nemo-customizer. I need to submit an automodel SFT LoRA job on NeMo Platform.", + "expected_skill": "nemo-customizer" + }, + { + "type": "explicit", + "prompt": "Invoke nemo customization via the customizer skill and walk me through filesets and model entity setup.", + "expected_skill": "nemo-customizer" + }, + { + "type": "implicit", + "prompt": "Fine-tune a model with LoRA SFT via nemo customization and a qwen3 Hugging Face weights fileset.", + "expected_skill": "nemo-customizer" + }, + { + "type": "implicit", + "prompt": "Help me run SFT LoRA training with nemo customization automodel.", + "expected_skill": "nemo-customizer" + }, + { + "type": "implicit", + "prompt": "Train a small chat model on a dataset I have locally and register the output on the platform.", + "expected_skill": "nemo-customizer" + }, + { + "type": "contextual", + "prompt": "NeMo Platform is running. Before any customization training, help me explore what my support agent should do.", + "expected_skill_not": "nemo-customizer" + }, + { + "type": "contextual", + "prompt": "I uploaded train.jsonl for fun. Mostly I want nemo-build-agent to deploy my LangGraph NAT workflow from the spec.", + "expected_skill_not": "nemo-customizer" + }, + { + "type": "contextual", + "prompt": "Jobs controller is up. Next I need nemo-status and evaluator benchmarks, not model weight training.", + "expected_skill_not": "nemo-customizer" + }, + { + "type": "negative-control", + "prompt": "Use nemo-build-agent to scaffold and deploy my agent from agents/calculator.spec.md.", + "expected_skill_not": "nemo-customizer" + }, + { + "type": "negative-control", + "prompt": "Run safe-synthesizer on my CSV for tabular synthetic data generation.", + "expected_skill_not": "nemo-customizer" + }, + { + "type": "negative-control", + "prompt": "Attach guardrails middleware to my virtual model in the inference gateway.", + "expected_skill_not": "nemo-customizer" + } + ] +} diff --git a/plugins/nemo-customizer/tests/test_customization_discovery_reexport.py b/plugins/nemo-customizer/tests/test_customization_discovery_reexport.py new file mode 100644 index 0000000000..120a31fefb --- /dev/null +++ b/plugins/nemo-customizer/tests/test_customization_discovery_reexport.py @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from nemo_customizer.discovery import ( + CUSTOMIZATION_CONTRIBUTORS_GROUP, + discover_customization_contributor_classes, + discover_customization_contributors, +) +from nemo_platform_plugin.discovery import ( + discover_customization_contributors as platform_discover, +) + + +def test_reexport_matches_platform_discovery() -> None: + assert discover_customization_contributors is platform_discover + assert CUSTOMIZATION_CONTRIBUTORS_GROUP == "nemo.customization.contributors" + discover_customization_contributors.cache_clear() + assert isinstance(discover_customization_contributors(), dict) + assert isinstance(discover_customization_contributor_classes(), dict) diff --git a/plugins/nemo-customizer/tests/test_router.py b/plugins/nemo-customizer/tests/test_router.py new file mode 100644 index 0000000000..6d9a954620 --- /dev/null +++ b/plugins/nemo-customizer/tests/test_router.py @@ -0,0 +1,103 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import ClassVar + +import pytest +import typer +from fastapi import APIRouter, FastAPI +from fastapi.testclient import TestClient +from nemo_customizer.router import ( + CustomizationRouterError, + CustomizationRouterService, + merge_router_dependencies, +) +from nemo_platform_plugin.service import RouterSpec + + +class _FakeContributor: + name: ClassVar[str] = "fake" + dependencies: ClassVar[list[str]] = ["studio"] + + def get_routers(self) -> list[RouterSpec]: + router = APIRouter() + + @router.get("/ping") + async def ping() -> dict[str, str]: + return {"backend": "fake"} + + return [ + RouterSpec( + router=router, + prefix="/v2/workspaces/{workspace}/fake", + tag="Fake", + ), + ] + + def get_cli(self) -> typer.Typer: + app = typer.Typer() + + @app.command("info") + def info() -> None: + typer.echo("fake") + + return app + + +def test_merge_router_dependencies_unions_contributor_deps() -> None: + deps = merge_router_dependencies({"fake": _FakeContributor()}) + assert "studio" in deps + assert "jobs" in deps + + +def test_router_sets_merged_dependencies(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + "nemo_customizer.router.discover_customization_contributors", + lambda: {"fake": _FakeContributor()}, + ) + CustomizationRouterService() + assert "studio" in CustomizationRouterService.dependencies + + +def test_router_raises_without_contributors(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + "nemo_customizer.router.discover_customization_contributors", + lambda: {}, + ) + with pytest.raises(CustomizationRouterError, match="no contributors"): + CustomizationRouterService() + + +def test_router_merges_contributor_routes(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + "nemo_customizer.router.discover_customization_contributors", + lambda: {"fake": _FakeContributor()}, + ) + service = CustomizationRouterService() + app = FastAPI() + for spec in service.get_routers(): + if spec.prefix: + app.include_router(spec.router, prefix=spec.prefix) + else: + app.include_router(spec.router) + + client = TestClient(app) + assert client.get("/healthz").json()["contributors"] == ["fake"] + assert client.get("/v2/workspaces/ws-a/fake/ping").json() == {"backend": "fake"} + + +def test_prefix_collision_raises(monkeypatch: pytest.MonkeyPatch) -> None: + class _DupA(_FakeContributor): + name = "a" + + class _DupB(_FakeContributor): + name = "b" + + monkeypatch.setattr( + "nemo_customizer.router.discover_customization_contributors", + lambda: {"a": _DupA(), "b": _DupB()}, + ) + with pytest.raises(CustomizationRouterError, match="collision"): + CustomizationRouterService() diff --git a/plugins/nemo-customizer/tests/test_sdk.py b/plugins/nemo-customizer/tests/test_sdk.py new file mode 100644 index 0000000000..a430935d48 --- /dev/null +++ b/plugins/nemo-customizer/tests/test_sdk.py @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +from nemo_customizer.sdk.resources import ( + AsyncCustomization, + Customization, + customization_sdk_resources, +) +from nemo_platform_plugin.sdk import NemoPluginSDKResources + + +def test_customization_sdk_resources_entry_point_shape() -> None: + assert isinstance(customization_sdk_resources, NemoPluginSDKResources) + assert customization_sdk_resources.sync_resource is Customization + assert customization_sdk_resources.async_resource is AsyncCustomization + + +def test_customization_composes_automodel_when_contributor_present() -> None: + platform = MagicMock() + platform._client = MagicMock() + platform.workspace = "default" + platform.base_url = "http://localhost:8000" + platform.default_headers = {} + + fake_contributor = object() + with patch( + "nemo_customizer.sdk.resources.discover_customization_contributors", + return_value={"automodel": fake_contributor}, + ): + customization = Customization(platform) + + assert hasattr(customization, "automodel") + assert hasattr(customization.automodel, "jobs") diff --git a/plugins/nemo-customizer/tests/test_skills.py b/plugins/nemo-customizer/tests/test_skills.py new file mode 100644 index 0000000000..5f60256d6a --- /dev/null +++ b/plugins/nemo-customizer/tests/test_skills.py @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + + +from nemo_customizer.skills import get_skills_path + + +def test_get_skills_path_exists() -> None: + path = get_skills_path() + assert path.is_dir() + + +def test_nemo_customizer_skill_present() -> None: + skill_dir = get_skills_path() / "nemo-customizer" + skill = skill_dir / "SKILL.md" + tests = skill_dir / "tests.json" + assert skill.is_file() + assert tests.is_file() + text = skill.read_text() + assert "name: nemo-customizer" in text + assert "nemo customization automodel submit" in text diff --git a/pyproject.toml b/pyproject.toml index ac2146f544..fdca6e8f63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -171,6 +171,8 @@ enabled-plugins = [ "nemo-safe-synthesizer-plugin", "nemo-switchyard", "nemo-agents-plugin", + "nemo-customizer-plugin", + "nemo-automodel-plugin", ] # Legacy runtime needed specifically for task images that still invoke @@ -366,6 +368,9 @@ nemo-safe-synthesizer-plugin = { workspace = true } nemo-switchyard = { workspace = true } nemo-agents-plugin = { workspace = true } nemo-agents-example-calculator = { workspace = true } +nemo-customizer-plugin = { workspace = true } +nemo-automodel-plugin = { workspace = true } +nmp-automodel = { workspace = true } [tool.uv.workspace] @@ -414,6 +419,9 @@ members = [ "plugins/nemo-switchyard", "plugins/nemo-agents", "plugins/nemo-agents/examples/calculator-agent", + "plugins/nemo-customizer", + "plugins/nemo-automodel", + "services/automodel", ] diff --git a/sdk/python/nemo-platform/src/nemo_platform/skills/nemo-fine-tune/SKILL.md b/sdk/python/nemo-platform/src/nemo_platform/skills/nemo-fine-tune/SKILL.md deleted file mode 100644 index 690118a7a8..0000000000 --- a/sdk/python/nemo-platform/src/nemo_platform/skills/nemo-fine-tune/SKILL.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -name: nemo-fine-tune -description: Fine-tune a model on NeMo Platform. Not yet available; this skill describes the path for when fine-tuning ships. Use for any "train a model," "fine-tune," "customize a model," or "finetune" intent so the agent tells the user the feature isn't shipped and does not go off and implement training with some other library. -triggers: - - fine-tune - - fine tune - - finetune - - train a model - - train on my data - - customize a model - - sft a model - - model customization - - model fine-tuning -not-for: - - nemo-build-agent (use for agent scaffolding and deployment, not model training) - - nemo-explore (use for agent design conversation) - - nemo-skill-selection (use to disambiguate user intent) -compatibility: NeMo Platform any version. No prerequisites today since fine-tuning is not yet shipped. When fine-tuning lands, this skill will document Customizer plugin requirements (host-gpu mode, training data format, supported base models). -maturity: beta -license: Apache-2.0 -user-invocable: true -allowed-tools: [Read] ---- - -# Fine-tuning on NeMo Platform - -**Fine-tuning is not yet available on NeMo Platform.** Tell the user this directly. Do not run any `nemo customization` CLI commands or scaffold a fine-tuning job; the underlying functionality is not shipped. - -When fine-tuning lands, it will be delivered through a Customizer plugin that wraps NVIDIA's training stack (AutoModel, Megatron-Bridge, and related). This skill will be filled in at that point. - -## What to tell the user today - -- Fine-tuning is on the NeMo Platform roadmap and is not currently functional. Any CLI surface that looks like it should work (`nemo customization jobs ...`) is not connected to a working training backend. -- Other NeMo Platform capabilities they can use today: harden an agent (`nemo-skill-selection` → guardrails / auditor / anonymizer), evaluate an agent (`nemo-skill-selection` → evaluator), tune an agent's prompts and routing (`nemo-skill-selection` → optimization). -- If they need fine-tuning urgently, point them at upstream NVIDIA training tools (NeMo Framework, NeMo-RL, Megatron-LM) and tell them this skill will be wired up once the Customizer plugin lands. - -## Verification - -There is nothing to verify. Do not claim a fine-tuning task succeeded. If the user asks the agent to run fine-tuning anyway, refuse and explain why. - -## When fine-tuning ships - -This skill will gain pre-flight checks, a training-data preparation walkthrough, job submission, progress monitoring, and result download. Track the Customizer plugin in the NeMo Platform roadmap; this skill updates when that ships. diff --git a/sdk/python/nemo-platform/src/nemo_platform/skills/nemo-skill-selection/SKILL.md b/sdk/python/nemo-platform/src/nemo_platform/skills/nemo-skill-selection/SKILL.md index 3148c879ad..b059793280 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/skills/nemo-skill-selection/SKILL.md +++ b/sdk/python/nemo-platform/src/nemo_platform/skills/nemo-skill-selection/SKILL.md @@ -1,6 +1,6 @@ --- name: nemo-skill-selection -description: Top-level skill selector for any task involving NeMo Platform (NVIDIA's agent platform). Picks the right downstream skill (setup, explore, spec, build, try, status, teardown, fine-tune) from natural-language intent. Use over generic brainstorming, planning, or onboarding skills for any NeMo Platform task. +description: Top-level skill selector for any task involving NeMo Platform (NVIDIA's agent platform). Picks the right downstream skill (setup, explore, spec, build, try, status, teardown, customization training) from natural-language intent. Use over generic brainstorming, planning, or onboarding skills for any NeMo Platform task. triggers: - build an agent - create an agent @@ -48,7 +48,7 @@ Match the user's intent to one downstream skill. Pick exactly one. | "ask my agent", "try the agent", "test it" | `nemo-try-agent` | Send a query to a deployed agent or fall back to model chat | | "status", "what is running", "platform health", "is the platform up", "what's deployed", "show me what's running" | `nemo-status` | Read-only dashboard: platform, agents, providers, models | | "shut down", "stop NeMo", "tear down", "clean up" | `nemo-teardown` | Stop the cluster (keep data, delete platform data, or full cleanup) | -| "fine-tune", "customize the model", "train on my data" | `nemo-fine-tune` | Fine-tuning is not yet available on NeMo Platform. Pick this so the agent tells the user it's not shipped instead of going off to implement training with some other library. | +| "fine-tune", "customize the model", "train on my data", "SFT", "LoRA" | `nemo-customizer` | Model customization via installed customization contributor plugins (`nemo-customizer-plugin`). Requires plugin skills to be installed (`nemo skills install` / enabled-plugins). | | "optimize my agent", "make it cheaper", "reduce latency", "smaller model", "switchyard", "routing split", "compare against a newer model" | `agents-optimize` (plugin-owned, in `plugins/nemo-agents`) | Cost / latency / quality optimization for a **deployed** agent. Routing splits, skill tuning, prompt tuning, new-model scans. | | "secure my agent", "harden my agent", "check for PII", "leaked secrets", "guardrail coverage" | `agents-secure` (plugin-owned, in `plugins/nemo-agents`) | Safety and security audit for a **deployed** agent. Guardrails, PII, secrets scan. | | "evaluate my agent", "run a benchmark", "eval suite" | `nemo-evaluator` (plugin-owned, in `plugins/nemo-evaluator`) | Evaluation metrics, LLM-judge, benchmark jobs against a deployed agent or model. | @@ -104,12 +104,12 @@ NeMo Platform skills I can route to: nemo-try-agent query a deployed agent or chat with a model nemo-status read-only platform health dashboard nemo-teardown guided shutdown - nemo-fine-tune fine-tuning (not yet shipped; reports that honestly) Plugin-owned skills: agents-optimize cost / latency / quality optimization for a deployed agent agents-secure safety and security audit for a deployed agent nemo-evaluator evaluation metrics, LLM-judge, benchmark jobs + nemo-customizer fine-tuning of models guardrails content-safety middleware via virtual models auditor red-team vulnerability scanning (garak) data-designer synthetic dataset generation @@ -142,5 +142,5 @@ Do not proactively suggest Studio as the path for anything a skill already cover - **One skill at a time.** Do not load more than one downstream skill in the same turn. Each downstream skill is a full procedure with its own context budget. - **Install must happen before any skill can do useful work.** Build, try, and status all assume the platform is up. If the user has not run the CLI install (`make bootstrap` + `nemo setup`), the skills cannot work around that; hand them to `setup` for instructions. - **NeMo Platform is the product name.** Capital N, e, M, o, P. Not "nemo" or "Nemo." NAT on first mention is "NVIDIA NeMo Agent Toolkit (NAT)." -- **Fine-tuning is not yet available.** When the user asks to fine-tune, train, or customize a model, pick `nemo-fine-tune` so the agent tells the user it's not shipped instead of trying to wire up training with some other library. Do not run `nemo customization` CLI commands; the backend is not connected. +- **Model customization** goes to the `nemo-customizer` plugin skill when `nemo-customizer-plugin` (and a training backend) are installed. If that skill is not available, tell the user to enable customization plugins and install skills — do not improvise training with an external library. - **Framework honesty.** If the user describes an agent in CrewAI, AutoGen, plain LangChain, or Pydantic AI, tell them up front that NeMo Platform's optimization and evaluation surfaces operate on NAT-wrapped LangGraph agents. They will need to wrap their agent before the build path produces value. diff --git a/services/automodel/README.md b/services/automodel/README.md new file mode 100644 index 0000000000..da10845815 --- /dev/null +++ b/services/automodel/README.md @@ -0,0 +1,3 @@ +# nmp-automodel + +Compiler and task entrypoints for NeMo Automodel training jobs on the platform. **No HTTP server** — consumed by `nemo-automodel-plugin` and Jobs task images (`nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-tasks`, `.../nmp-automodel-training`). diff --git a/services/automodel/docker/Dockerfile.mamba-wheel b/services/automodel/docker/Dockerfile.mamba-wheel new file mode 100644 index 0000000000..3574283585 --- /dev/null +++ b/services/automodel/docker/Dockerfile.mamba-wheel @@ -0,0 +1,245 @@ +# syntax=docker/dockerfile:1 +####### +# Mamba Wheel Builder +# +# Builds Python wheels for: +# - causal-conv1d (CUDA extension required by mamba-ssm) +# - mamba-ssm (selective state space model) +# +# Both only ship source distributions on PyPI and require nvcc to compile. +# The two builds are independent stages so BuildKit runs them in parallel. +# Each image stores its wheel at /wheels/*.whl. +# Build via Platform bake group: docker buildx bake -f docker-bake.automodel.hcl nmp-automodel-gpu-wheels +# +# Build args: +# CAUSAL_CONV1D_VERSION - git tag to build (default: v1.5.3) +# MAMBA_22_COMMIT - git SHA or tag to build +# CUDA_VERSION - CUDA devel image version (default: 12.8.1) +# TORCH_CUDA_ARCH_LIST - semicolon-separated SM targets (default: "8.0;8.6;9.0") +####### + +ARG CUDA_VERSION=12.8.1 + +# ============================================================================= +# Shared base: CUDA + Python 3.11 + torch (required by extension builds) +# ============================================================================= +FROM nvidia/cuda:${CUDA_VERSION}-devel-ubuntu22.04 AS mamba-wheel-base + +ARG TORCH_CUDA_ARCH_LIST="7.5;8.0;8.6;9.0" + +ENV DEBIAN_FRONTEND=noninteractive +ENV TORCH_CUDA_ARCH_LIST=${TORCH_CUDA_ARCH_LIST} + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + git \ + python3.11 \ + python3.11-dev \ + python3.11-venv \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=ghcr.io/astral-sh/uv:0.9.14 /uv /usr/local/bin/uv +COPY services/automodel/docker/locks/mamba-wheel-build-py311 /opt/mamba-wheel-build-py311 + +# Create the Python 3.11 build venv from a committed lockfile. +RUN uv venv --python 3.11 /opt/venv && \ + UV_PROJECT_ENVIRONMENT=/opt/venv uv sync \ + --project /opt/mamba-wheel-build-py311 \ + --locked \ + --no-install-project && \ + echo "=== torch version (py311 wheel build) ===" && \ + /opt/venv/bin/python -c "import torch; print(f'torch={torch.__version__}, CUDA={torch.version.cuda}')" + +ENV VIRTUAL_ENV=/opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# ============================================================================= +# Python 3.12 base: extends mamba-wheel-base with Python 3.12 for cp312 wheels +# ============================================================================= +FROM mamba-wheel-base AS mamba-wheel-base-py312 + +# Install Python 3.12 via deadsnakes PPA (Ubuntu 22.04 ships 3.10 by default) +RUN apt-get update && apt-get install -y --no-install-recommends \ + software-properties-common \ + && add-apt-repository ppa:deadsnakes/ppa \ + && apt-get update && apt-get install -y --no-install-recommends \ + python3.12 \ + python3.12-dev \ + python3.12-venv \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +COPY services/automodel/docker/locks/mamba-wheel-build-py312 /opt/mamba-wheel-build-py312 + +# Create the Python 3.12 build venv from a committed lockfile. +RUN uv venv --python 3.12 /opt/venv312 && \ + UV_PROJECT_ENVIRONMENT=/opt/venv312 uv sync \ + --project /opt/mamba-wheel-build-py312 \ + --locked \ + --no-install-project && \ + echo "=== torch version (py312 wheel build) ===" && \ + /opt/venv312/bin/python -c "import torch; print(f'torch={torch.__version__}, CUDA={torch.version.cuda}')" + +ENV VIRTUAL_ENV=/opt/venv312 +ENV PATH="/opt/venv312/bin:$PATH" + +# special builder for 13.1.1 cuda +FROM nvcr.io/nvidia/pytorch:26.02-py3 AS mamba-wheel-base-py312-cu13.1.1 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=ghcr.io/astral-sh/uv:0.9.14 /uv /usr/local/bin/uv + + +# ============================================================================= +# causal-conv1d wheel — Python 3.11 (for nmp-gpu-tasks and nmp-customizer-tasks) +# ============================================================================= +FROM mamba-wheel-base AS causal-conv1d-wheel-builder + +ARG CAUSAL_CONV1D_VERSION=v1.5.3 + +RUN mkdir -p /wheels && \ + git clone --depth 1 --branch ${CAUSAL_CONV1D_VERSION} \ + https://github.com/Dao-AILab/causal-conv1d.git /src/causal-conv1d && \ + cd /src/causal-conv1d && \ + CAUSAL_CONV1D_FORCE_BUILD=TRUE uv build --wheel --no-build-isolation --out-dir=/wheels . && \ + rm -rf /src/causal-conv1d + +# ============================================================================= +# causal-conv1d wheel — Python 3.12 (for Python 3.12 consumers) +# ============================================================================= +FROM mamba-wheel-base-py312 AS causal-conv1d-wheel-builder-py312 + +ARG CAUSAL_CONV1D_VERSION=v1.5.3 + +RUN mkdir -p /wheels && \ + git clone --depth 1 --branch ${CAUSAL_CONV1D_VERSION} \ + https://github.com/Dao-AILab/causal-conv1d.git /src/causal-conv1d && \ + cd /src/causal-conv1d && \ + CAUSAL_CONV1D_FORCE_BUILD=TRUE uv build --wheel --no-build-isolation --out-dir=/wheels . && \ + rm -rf /src/causal-conv1d + +# ============================================================================= +# causal-conv1d wheel — Python 3.12 (for Python 3.12 consumers) - Using CUDA 13.1.1 +# ============================================================================= +FROM mamba-wheel-base-py312-cu13.1.1 AS causal-conv1d-wheel-builder-py312-cu13.1.1 + +ARG CAUSAL_CONV1D_VERSION=v1.5.3 + +RUN mkdir -p /wheels && \ + git clone --depth 1 --branch ${CAUSAL_CONV1D_VERSION} \ + https://github.com/Dao-AILab/causal-conv1d.git /src/causal-conv1d && \ + cd /src/causal-conv1d && \ + CAUSAL_CONV1D_FORCE_BUILD=TRUE uv build --wheel --no-build-isolation --out-dir=/wheels . && \ + rm -rf /src/causal-conv1d + +# The final causal-conv1d-wheel image contains: +# - causal_conv1d-*-cp311-*.whl (for Python 3.11 consumers: nmp-gpu-tasks, nmp-customizer-tasks) +# - causal_conv1d-*-cp312-*.whl (for Python 3.12 consumers) +# Consumers must pin the Python tag glob (e.g. causal_conv1d-*cp311*.whl) to select the right one. +FROM scratch AS causal-conv1d-wheel +COPY --from=causal-conv1d-wheel-builder /wheels /wheels +COPY --from=causal-conv1d-wheel-builder-py312 /wheels /wheels +COPY --from=causal-conv1d-wheel-builder-py312-cu13.1.1 /wheels /wheels/cu13.1.1 + +# ============================================================================= +# mamba-ssm 2.2.5 wheel — Python 3.11 (for nmp-gpu-tasks) +# ============================================================================= +FROM mamba-wheel-base AS mamba-ssm-wheel-builder + +# post commit after 2.2.5 +ARG MAMBA_22_COMMIT=6b32be06d026e170b3fdaf3ae6282c5a6ff57b06 + +RUN mkdir -p /wheels && \ + git clone https://github.com/state-spaces/mamba.git /src/mamba && \ + cd /src/mamba && \ + git checkout ${MAMBA_22_COMMIT} && \ + sed -i "/triton/d" setup.py && \ + sed -i "/triton/d" pyproject.toml && \ + uv build --wheel --no-build-isolation --out-dir=/wheels . && \ + rm -rf /src/mamba + +# ============================================================================= +# mamba-ssm 2.2.5 wheel — Python 3.12 (for Python 3.12 consumers, e.g. automodel) +# ============================================================================= +FROM mamba-wheel-base-py312 AS mamba-ssm-25-wheel-builder-py312 + +# post commit after 2.2.5 +ARG MAMBA_22_COMMIT=6b32be06d026e170b3fdaf3ae6282c5a6ff57b06 + +RUN mkdir -p /wheels && \ + git clone https://github.com/state-spaces/mamba.git /src/mamba && \ + cd /src/mamba && \ + git checkout ${MAMBA_22_COMMIT} && \ + sed -i "/triton/d" setup.py && \ + sed -i "/triton/d" pyproject.toml && \ + uv build --wheel --no-build-isolation --out-dir=/wheels . && \ + rm -rf /src/mamba + +# ============================================================================= +# mamba-ssm 2.3.0 wheel — Python 3.11 (for nmp-customizer-tasks) +# ============================================================================= +FROM mamba-wheel-base AS mamba-ssm-23-wheel-builder + +ARG MAMBA_23_COMMIT=v2.3.0 + +RUN mkdir -p /wheels && \ + git clone https://github.com/state-spaces/mamba.git /src/mamba && \ + cd /src/mamba && \ + git checkout ${MAMBA_23_COMMIT} && \ + sed -i "/triton/d" setup.py && \ + sed -i "/triton/d" pyproject.toml && \ + uv build --wheel --no-build-isolation --out-dir=/wheels . && \ + rm -rf /src/mamba + +# ============================================================================= +# mamba-ssm 2.3.0 wheel — Python 3.12 (for Python 3.12 consumers) +# ============================================================================= +FROM mamba-wheel-base-py312 AS mamba-ssm-23-wheel-builder-py312 + +ARG MAMBA_23_COMMIT=v2.3.0 + +RUN mkdir -p /wheels && \ + git clone https://github.com/state-spaces/mamba.git /src/mamba && \ + cd /src/mamba && \ + git checkout ${MAMBA_23_COMMIT} && \ + sed -i "/triton/d" setup.py && \ + sed -i "/triton/d" pyproject.toml && \ + uv build --wheel --no-build-isolation --out-dir=/wheels . && \ + rm -rf /src/mamba + + +# ============================================================================= +# mamba-ssm 2.3.0 wheel — Python 3.12 (for Python 3.12 consumers) - Using CUDA 13.1.1 +# ============================================================================= +FROM mamba-wheel-base-py312-cu13.1.1 AS mamba-ssm-23-wheel-builder-py312-cu13.1.1 + +ARG MAMBA_23_COMMIT=v2.3.0 + +RUN mkdir -p /wheels && \ + git clone https://github.com/state-spaces/mamba.git /src/mamba && \ + cd /src/mamba && \ + git checkout ${MAMBA_23_COMMIT} && \ + sed -i "/triton/d" setup.py && \ + sed -i "/triton/d" pyproject.toml && \ + uv build --wheel --no-build-isolation --out-dir=/wheels . && \ + rm -rf /src/mamba + + +# The final mamba-ssm-wheel image contains four versions: +# - mamba_ssm-2.2.5-cp311-*.whl (from MAMBA_22_COMMIT=6b32be06, for nmp-gpu-tasks / Python 3.11) +# - mamba_ssm-2.2.5-cp312-*.whl (from MAMBA_22_COMMIT=6b32be06, for Python 3.12 consumers, e.g. automodel) +# - mamba_ssm-2.3.0-cp311-*.whl (from v2.3.0, for nmp-customizer-tasks / Python 3.11) +# - mamba_ssm-2.3.0-cp312-*.whl (from v2.3.0, for Python 3.12 consumers) +# Consumers must pin both version AND Python tag glob to select the correct wheel. +FROM scratch AS mamba-ssm-wheel +COPY --from=mamba-ssm-wheel-builder /wheels /wheels +COPY --from=mamba-ssm-25-wheel-builder-py312 /wheels /wheels +COPY --from=mamba-ssm-23-wheel-builder /wheels /wheels +COPY --from=mamba-ssm-23-wheel-builder-py312 /wheels /wheels +COPY --from=mamba-ssm-23-wheel-builder-py312-cu13.1.1 /wheels /wheels/cu13.1.1 diff --git a/services/automodel/docker/Dockerfile.nmp-automodel-base b/services/automodel/docker/Dockerfile.nmp-automodel-base new file mode 100644 index 0000000000..512b488988 --- /dev/null +++ b/services/automodel/docker/Dockerfile.nmp-automodel-base @@ -0,0 +1,92 @@ +# syntax=docker/dockerfile:1 +# nmp-automodel base - PyTorch NGC image + Automodel + CUDA extension wheels. +# +# Mirrors nmp/docker/Dockerfile.nmp-customizer customizer-automodel-base-builder. +# Publish target: nmp-automodel-base-builder (tags as nmp-automodel-base). + +ARG CAUSAL_CONV1D_WHEEL_IMAGE=local +ARG MAMBA_SSM_WHEEL_IMAGE=local +ARG AUTOMODEL_COMMIT=0e9909f56ba48ef9761fc6f49323ba9d0a0835b2 + +FROM ${CAUSAL_CONV1D_WHEEL_IMAGE} AS causal-conv1d-wheel-src +FROM ${MAMBA_SSM_WHEEL_IMAGE} AS mamba-ssm-wheel-src + +FROM alpine/git AS automodel-clone +ARG AUTOMODEL_COMMIT +RUN git clone --branch main https://github.com/NVIDIA-NeMo/Automodel.git /opt/Automodel && \ + cd /opt/Automodel && \ + git checkout ${AUTOMODEL_COMMIT} && \ + rm -rf /opt/Automodel/.git + +FROM nvcr.io/nvidia/pytorch:26.02-py3 AS nmp-automodel-base-builder + +WORKDIR /opt + +COPY --from=ghcr.io/astral-sh/uv:0.9.14 /uv /bin/uv + +ENV VIRTUAL_ENV=/opt/venv \ + UV_PROJECT_ENVIRONMENT=/opt/venv \ + UV_LINK_MODE=copy \ + UV_COMPILE_BYTECODE=1 +ENV PATH="/opt/venv/bin:/root/.local/bin:$PATH" + +RUN uv venv ${UV_PROJECT_ENVIRONMENT} --system-site-packages + +COPY --from=automodel-clone /opt/Automodel /opt/Automodel +COPY services/customizer/src/cherry-picks /opt/cherry-picks +RUN cd /opt/Automodel && patch -p1 < /opt/cherry-picks/e6d2930a.diff + +RUN cd /opt/Automodel && \ + bash docker/common/update_pyproject_pytorch.sh /opt/Automodel + +RUN --mount=type=cache,target=/root/.cache/uv \ + cd /opt/Automodel && \ + UV_HTTP_TIMEOUT=120 uv sync --locked --extra all --all-groups + +# Install AFTER Automodel sync - uv sync drops packages not in its lockfile. +RUN --mount=from=causal-conv1d-wheel-src,target=/tmp/causal-conv1d-wheel-src,readonly \ + --mount=from=mamba-ssm-wheel-src,target=/tmp/mamba-ssm-wheel-src,readonly \ + uv pip install --no-cache-dir --no-deps \ + /tmp/causal-conv1d-wheel-src/wheels/cu13.1.1/causal_conv1d-*cp312*.whl \ + /tmp/mamba-ssm-wheel-src/wheels/cu13.1.1/mamba_ssm-2.3.0-cp312*.whl + +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install --no-build-isolation --no-deps git+https://github.com/fanshiqing/grouped_gemm@v1.1.4 + +RUN --mount=type=cache,target=/root/.cache/uv \ + git clone https://github.com/bitsandbytes-foundation/bitsandbytes.git && \ + cd bitsandbytes && \ + git checkout 0.49.1 && \ + cmake -DCOMPUTE_CAPABILITY="75;80;86;87;89;90;100;103;110;120;121" -DCOMPUTE_BACKEND=cuda -DCMAKE_CUDA_COMPILER=/usr/local/cuda/bin/nvcc -S . && \ + make -j"$(nproc)" && \ + uv pip install scikit-build-core --no-deps && \ + uv pip install --no-build-isolation --no-deps --force-reinstall . && \ + uv pip uninstall scikit-build-core + +RUN if [ -f /usr/local/bin/torchrun ]; then \ + sed -i '1c\#!/opt/venv/bin/python' /usr/local/bin/torchrun; \ + fi + +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install "hf-transfer>=0.1.8,<0.2" + +RUN --mount=type=cache,target=/root/.cache/uv \ + rm -rf /opt/venv/lib/python*/site-packages/vllm \ + /opt/venv/lib/python*/site-packages/vllm-*.dist-info && \ + uv pip install \ + "black>=26.3.1" \ + "pyasn1>=0.6.3" \ + "onnx>=1.21.0" + +# Published base image (same filesystem as builder). +FROM nvcr.io/nvidia/pytorch:26.02-py3 AS nmp-automodel-base +COPY --from=nmp-automodel-base-builder /opt/venv /opt/venv +COPY --from=nmp-automodel-base-builder /opt/Automodel /opt/Automodel +# Builder pins uv 0.9.14 but does not ship it in the venv layer; PyTorch base may ship 0.10.x. +COPY --from=ghcr.io/astral-sh/uv:0.9.14 /uv /bin/uv + +ENV VIRTUAL_ENV=/opt/venv \ + UV_PROJECT_ENVIRONMENT=/opt/venv \ + HF_HUB_ENABLE_HF_TRANSFER=1 +ENV PATH="/bin:/opt/venv/bin:/root/.local/bin:$PATH" +WORKDIR /opt diff --git a/services/automodel/docker/Dockerfile.nmp-automodel-tasks b/services/automodel/docker/Dockerfile.nmp-automodel-tasks new file mode 100644 index 0000000000..5e42558a5f --- /dev/null +++ b/services/automodel/docker/Dockerfile.nmp-automodel-tasks @@ -0,0 +1,49 @@ +# syntax=docker/dockerfile:1 +# nmp-automodel tasks - file_io, model_entity, and other platform task steps. +# Built on nmp-automodel-base (GPU-capable; runs on CPU or GPU nodes). + +ARG BASE_TAG_AUTOMODEL=local +ARG BASE_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev +ARG SMOKE_MARKER=smoke_nmp_automodel_tasks + +FROM ${BASE_REGISTRY}/nmp-automodel-base:${BASE_TAG_AUTOMODEL} AS nmp-automodel-base + +FROM nmp-automodel-base AS runtime + +# Pin uv for platform workspace installs (base may lack /bin/uv or PATH may prefer 0.10.x). +COPY --from=ghcr.io/astral-sh/uv:0.9.14 /uv /bin/uv +ENV PATH="/bin:${PATH}" + +ARG USERNAME=ubuntu +ARG USER_UID=1000 +ARG USER_GID=1000 + +ENV HF_HUB_ENABLE_HF_TRANSFER=1 \ + OTEL_PYTHON_EXCLUDED_URLS="health" + +COPY --from=platform-workspace / /app +WORKDIR /app + +RUN mkdir -p /home/${USERNAME}/.cache && \ + chown -R ${USER_UID}:${USER_GID} /home/${USERNAME} /app/services/automodel + +# /app/pyproject.toml is pyproject.workspace.toml (see Dockerfile.platform-workspace). +# --inexact: keep PyTorch / Automodel packages already in the base venv; add nmp-automodel + deps. +ENV UV_LINK_MODE=copy \ + UV_PROJECT_ENVIRONMENT=${VIRTUAL_ENV} + +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --package nmp-automodel --package nmp-models --no-dev --inexact + +ENV PATH="${VIRTUAL_ENV}/bin:${PATH}" +ENTRYPOINT ["/opt/venv/bin/python"] +CMD ["-m", "nmp.automodel.tasks", "--help"] + +USER ${USER_UID}:${USER_GID} + +FROM runtime AS smoke-test +ARG SMOKE_MARKER +USER 0 +COPY tests/smoke_gpu/ /smoke_test/ +RUN uv pip install --python ${VIRTUAL_ENV}/bin/python --no-cache --reinstall pytest && \ + ${VIRTUAL_ENV}/bin/pytest /smoke_test/ -m ${SMOKE_MARKER} -v diff --git a/services/automodel/docker/Dockerfile.nmp-automodel-training b/services/automodel/docker/Dockerfile.nmp-automodel-training new file mode 100644 index 0000000000..75a4fcdc44 --- /dev/null +++ b/services/automodel/docker/Dockerfile.nmp-automodel-training @@ -0,0 +1,54 @@ +# syntax=docker/dockerfile:1 +# nmp-automodel training - GPU finetune step (nemo_automodel recipes + nmp-automodel package). +# Same platform glue as tasks; separate image tag for the compiler training step. + +ARG BASE_TAG_AUTOMODEL=local +ARG BASE_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev +ARG SMOKE_MARKER=smoke_nmp_automodel_training + +FROM ${BASE_REGISTRY}/nmp-automodel-base:${BASE_TAG_AUTOMODEL} AS nmp-automodel-base + +FROM nmp-automodel-base AS runtime + +COPY --from=ghcr.io/astral-sh/uv:0.9.14 /uv /bin/uv +ENV PATH="/bin:${PATH}" + +ARG USERNAME=ubuntu +ARG USER_UID=1000 +ARG USER_GID=1000 + +ENV HF_HUB_ENABLE_HF_TRANSFER=1 \ + OTEL_PYTHON_EXCLUDED_URLS="health" + +COPY --from=platform-workspace / /app +WORKDIR /app + +RUN mkdir -p /home/${USERNAME}/.cache && \ + chown -R ${USER_UID}:${USER_GID} /home/${USERNAME} /app/services/automodel + +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install --python ${VIRTUAL_ENV}/bin/python --no-cache \ + --overrides /app/services/automodel/docker/no_override_requirements.txt \ + -e /app/sdk/python/nemo-platform \ + -e /app/packages/nemo_platform_plugin \ + -e /app/packages/nmp_common \ + -e /app/services/automodel + +# Re-pin nemo_automodel from the base clone without re-resolving transformers (already in base venv). +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install --python ${VIRTUAL_ENV}/bin/python --no-cache --no-deps \ + -e /opt/Automodel + + +ENV PATH="${VIRTUAL_ENV}/bin:${PATH}" +ENTRYPOINT ["/opt/venv/bin/python"] +CMD ["-m", "nmp.automodel.tasks.training", "--help"] + +USER ${USER_UID}:${USER_GID} + +FROM runtime AS smoke-test +ARG SMOKE_MARKER +USER 0 +COPY tests/smoke_gpu/ /smoke_test/ +RUN uv pip install --python ${VIRTUAL_ENV}/bin/python --no-cache --reinstall pytest && \ + ${VIRTUAL_ENV}/bin/pytest /smoke_test/ -m ${SMOKE_MARKER} -v diff --git a/services/automodel/docker/Dockerfile.platform-workspace b/services/automodel/docker/Dockerfile.platform-workspace new file mode 100644 index 0000000000..70aeae3f3c --- /dev/null +++ b/services/automodel/docker/Dockerfile.platform-workspace @@ -0,0 +1,20 @@ +# syntax=docker/dockerfile:1 +# Minimal Platform workspace slice for nmp-automodel container installs. +# Used as a named build context (platform-workspace). +# Keep in sync with services/automodel/docker/pyproject.workspace.toml members. + +FROM scratch AS platform-workspace +# Do not copy repo-root pyproject.toml/uv.lock — they reference the full monorepo workspace. +COPY services/automodel/docker/pyproject.workspace.toml pyproject.toml +# nemo-platform-sdk hatch build force-includes docs/ and mkdocs.yml from repo root. +# docs/api/openapi.yaml is a symlink to ../../openapi/openapi.yaml — copy both. +COPY docs docs +COPY openapi openapi +COPY mkdocs.yml mkdocs.yml +COPY packages/nmp_build_tools packages/nmp_build_tools +COPY packages/models packages/models +COPY packages/nmp_common packages/nmp_common +COPY packages/nemo_platform_plugin packages/nemo_platform_plugin +COPY sdk/python/nemo-platform sdk/python/nemo-platform +COPY services/automodel services/automodel +COPY services/core/models services/core/models diff --git a/services/automodel/docker/README.md b/services/automodel/docker/README.md new file mode 100644 index 0000000000..13e0ec2c71 --- /dev/null +++ b/services/automodel/docker/README.md @@ -0,0 +1,98 @@ +# nmp-automodel container images + +Three images derived from the legacy `nmp` **customizer-automodel** base builder (not the full `customizer-automodel` HTTP service image). Published as flat NVCR repo names under **`nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-*`** (no nested `nmp/...` path — NVCR rejects that on push). + +| Image | Dockerfile | Role | +|-------|------------|------| +| `nmp-automodel-base` | `Dockerfile.nmp-automodel-base` | PyTorch 26.02 + Automodel + `mamba-ssm` / `causal-conv1d` wheels | +| `nmp-automodel-tasks` | `Dockerfile.nmp-automodel-tasks` | Platform task glue (`file_io`, `model_entity`, `model_spec`); GPU-capable base | +| `nmp-automodel-training` | `Dockerfile.nmp-automodel-training` | Training step (`nmp.automodel.tasks.training`) | + +Full references (default tag `local`): + +- `nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-base:local` +- `nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-tasks:local` +- `nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-training:local` + +Bake file: **`docker-bake.automodel.hcl`** at the Platform repo root (`context = "."`). Run all commands from the Platform repo root. + +## `docker buildx bake --print` + +`--print` only parses the HCL and prints JSON. A **0.0s FINISHED** result is normal — no image is built. Use it to verify targets, tags, and platforms before a real build. + +## Prerequisites + +1. **CUDA extension wheels** (`causal-conv1d-wheel`, `mamba-ssm-wheel`) - built from this directory or pulled from NGC. The wheel Dockerfile and uv locks live under `docker/locks/` (ported from `nmp`). + +2. **Base image tag** - after building the base, set `BASE_TAG_AUTOMODEL` (or push to `BASE_REGISTRY`) before building tasks/training. + +## Build wheels and push to NGC (from Platform root) + +```bash +cd /path/to/Platform + +docker login nvcr.io + +export WHEELS_TAG="$(git rev-parse --short HEAD)" +# Bake variables (WHEELS_REGISTRY, WHEELS_TAG, IMAGE_REGISTRY) are overridden via env, not --set. +# Example: +# export WHEELS_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev +# export IMAGE_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev + +docker buildx bake --print -f docker-bake.automodel.hcl nmp-automodel-gpu-wheels + +docker buildx bake \ + -f docker-bake.automodel.hcl \ + nmp-automodel-gpu-wheels \ + --push \ + --set "*.platform=linux/amd64" +``` + +Override platform: `export BUILD_PLATFORM=linux/amd64` or `--set "*.platform=linux/amd64"`. + +## Build automodel images (from Platform root) + +```bash +cd /path/to/Platform + +export WHEELS_TAG="${WHEELS_TAG:-3fd6986ff173b598446ffac06d9be3f84b482495}" +export BAKE_TAG="${WHEELS_TAG}" + +docker buildx bake \ + -f docker-bake.automodel.hcl \ + nmp-automodel-base-builder \ + --push \ + --set "*.platform=linux/amd64" + +docker buildx bake \ + -f docker-bake.automodel.hcl \ + nmp-automodel \ + --push \ + --set "*.platform=linux/amd64" +``` + +To use wheels already published without rebuilding, `export WHEELS_TAG=` and matching `BAKE_TAG`. + +Override registry: `export WHEELS_REGISTRY=...` and `export IMAGE_REGISTRY=...` before bake. + +## Tasks / training runtime (platform glue) + +**Base (`nmp-automodel-base`):** Same as `customizer-automodel-base-builder` — NGC PyTorch 26.02, Automodel `uv sync --locked`, pinned `transformers`/`torch`. + +**Tasks image:** `uv sync --package nmp-automodel --no-dev --inexact` from the minimal workspace. CPU steps only need platform SDK glue; upgrading ancillary packages here does not affect training. + +**Training image:** Do **not** use `uv sync` — it upgrades `transformers` and breaks `PreTrainedModel`. Use **`uv pip install -e`** with **`--overrides no_override_requirements.txt`** (customizer pattern), then `uv pip install --no-deps -e /opt/Automodel` to re-pin `nemo_automodel` from the base clone (not PyPI). + +## Runtime + +Entrypoint is `/opt/venv/bin/python`. Job steps pass `-m nmp.automodel.tasks.` (see `nmp.automodel.app.jobs.compiler`). Local smoke: + +```bash +# No extra args → uses image CMD (python -m nmp.automodel.tasks --help). +docker run --rm $NMP_AUTOMODEL_TASKS_IMAGE + +# Extra args replace CMD; include -m nmp.automodel.tasks or you get plain `python --help`. +docker run --rm $NMP_AUTOMODEL_TASKS_IMAGE -m nmp.automodel.tasks --list +``` + +The job compiler resolves `nmp-automodel-tasks` and `nmp-automodel-training` under `NMP_AUTOMODEL_IMAGE_REGISTRY` (default `nvcr.io/0921617854601259/nemo-platform-dev`). See `nmp.automodel.images`. diff --git a/services/automodel/docker/docker-bake.hcl b/services/automodel/docker/docker-bake.hcl new file mode 100644 index 0000000000..47cb2b0c46 --- /dev/null +++ b/services/automodel/docker/docker-bake.hcl @@ -0,0 +1,4 @@ +# Moved to Platform repo root (same pattern as nmp/docker-bake.hcl): +# docker buildx bake -f docker-bake.automodel.hcl +# +# Context is "." (repo root when run from Platform/). Do not use ../../.. here. diff --git a/services/automodel/docker/locks/README.md b/services/automodel/docker/locks/README.md new file mode 100644 index 0000000000..1dbcfdc3c9 --- /dev/null +++ b/services/automodel/docker/locks/README.md @@ -0,0 +1,11 @@ +# Mamba / causal-conv1d wheel build locks + +Copied from `nmp/docker/locks/` for building `causal-conv1d-wheel` and `mamba-ssm-wheel` images from the Platform repo (see `Dockerfile.mamba-wheel` and `docker-bake.automodel.hcl` group `nmp-automodel-gpu-wheels`). + +To refresh locks after dependency changes: + +```bash +cd /path/to/Platform +uv lock --project services/automodel/docker/locks/mamba-wheel-build-py311 --python 3.11 +uv lock --project services/automodel/docker/locks/mamba-wheel-build-py312 --python 3.12 +``` diff --git a/services/automodel/docker/locks/mamba-wheel-build-py311/pyproject.toml b/services/automodel/docker/locks/mamba-wheel-build-py311/pyproject.toml new file mode 100644 index 0000000000..275dc68f35 --- /dev/null +++ b/services/automodel/docker/locks/mamba-wheel-build-py311/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "mamba-wheel-build-py311" +version = "0.0.0" +requires-python = ">=3.11,<3.12" +dependencies = [ + "packaging", + "setuptools", + "wheel", + "torch==2.10.0+cu128; sys_platform == 'linux'", +] + +[tool.uv] +required-version = ">=0.9.14,<0.10.0" +prerelease = "if-necessary-or-explicit" +index-strategy = "unsafe-best-match" +environments = [ + "sys_platform == 'linux' and platform_machine == 'x86_64'", + "sys_platform == 'linux' and platform_machine == 'aarch64'", +] + +[tool.uv.sources] +torch = { index = "pytorch-cu128", marker = "sys_platform == 'linux'" } + +[[tool.uv.index]] +name = "pytorch-cu128" +url = "https://download.pytorch.org/whl/cu128" +explicit = true diff --git a/services/automodel/docker/locks/mamba-wheel-build-py311/uv.lock b/services/automodel/docker/locks/mamba-wheel-build-py311/uv.lock new file mode 100644 index 0000000000..6868467397 --- /dev/null +++ b/services/automodel/docker/locks/mamba-wheel-build-py311/uv.lock @@ -0,0 +1,355 @@ +version = 1 +revision = 3 +requires-python = "==3.11.*" +resolution-markers = [ + "platform_machine == 'x86_64' and sys_platform == 'linux'", + "platform_machine == 'aarch64' and sys_platform == 'linux'", +] +supported-markers = [ + "platform_machine == 'x86_64' and sys_platform == 'linux'", + "platform_machine == 'aarch64' and sys_platform == 'linux'", +] + +[[package]] +name = "cuda-bindings" +version = "12.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/2b/ebcbb60aa6dba830474cd360c42e10282f7a343c0a1f58d24fbd3b7c2d77/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6a429dc6c13148ff1e27c44f40a3dd23203823e637b87fd0854205195988306", size = 11840604, upload-time = "2025-10-21T14:51:34.565Z" }, + { url = "https://files.pythonhosted.org/packages/45/e7/b47792cc2d01c7e1d37c32402182524774dadd2d26339bd224e0e913832e/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c912a3d9e6b6651853eed8eed96d6800d69c08e94052c292fec3f282c5a817c9", size = 12210593, upload-time = "2025-10-21T14:51:36.574Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/f9/1b9b60a30fc463c14cdea7a77228131a0ccc89572e8df9cb86c9648271ab/cuda_pathfinder-1.5.2-py3-none-any.whl", hash = "sha256:0c5f160a7756c5b072723cbbd6d861e38917ef956c68150b02f0b6e9271c71fa", size = 49988, upload-time = "2026-04-06T23:01:05.17Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "mamba-wheel-build-py311" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "packaging", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "setuptools", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "torch", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "wheel", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[package.metadata] +requires-dist = [ + { name = "packaging" }, + { name = "setuptools" }, + { name = "torch", marker = "sys_platform == 'linux'", specifier = "==2.10.0+cu128", index = "https://download.pytorch.org/whl/cu128" }, + { name = "wheel" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/99/db44d685f0e257ff0e213ade1964fc459b4a690a73293220e98feb3307cf/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b86f6dd8935884615a0683b663891d43781b819ac4f2ba2b0c9604676af346d0", size = 590537124, upload-time = "2025-03-07T01:43:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1f/b3bd73445e5cb342727fd24fe1f7b748f690b460acadc27ea22f904502c8/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4412396548808ddfed3f17a467b104ba7751e6b58678a4b840675c56d21cf7ed", size = 9533318, upload-time = "2025-03-07T01:40:10.421Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d1/e50d0acaab360482034b84b6e27ee83c6738f7d32182b987f9c7a4e32962/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc1fec1e1637854b4c0a65fb9a8346b51dd9ee69e61ebaccc82058441f15bce8", size = 43106076, upload-time = "2025-03-07T01:41:59.817Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/75/f865a3b236e4647605ea34cc450900854ba123834a5f1598e160b9530c3a/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d", size = 965265, upload-time = "2025-03-07T01:39:43.533Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/41/e79269ce215c857c935fd86bcfe91a451a584dfc27f1e068f568b9ad1ab7/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c9132cc3f8958447b4910a1720036d9eff5928cc3179b0a51fb6d167c6cc87d8", size = 705026878, upload-time = "2025-06-06T21:52:51.348Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/bc/7771846d3a0272026c416fbb7e5f4c1f146d6d80704534d0b187dd6f4800/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:848ef7224d6305cdb2a4df928759dca7b1201874787083b6e7550dd6765ce69a", size = 193109211, upload-time = "2025-03-07T01:44:56.873Z" }, + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f5/5607710447a6fe9fd9b3283956fceeee8a06cda1d2f56ce31371f595db2a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:4beb6d4cce47c1a0f1013d72e02b0994730359e17801d395bdcbf20cfb3bb00a", size = 1120705, upload-time = "2025-03-07T01:45:41.434Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/5e/92aa15eca622a388b80fbf8375d4760738df6285b1e92c43d37390a33a9a/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:dfab99248034673b779bc6decafdc3404a8a6f502462201f2f31f11354204acd", size = 63625754, upload-time = "2025-03-07T01:46:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/32/f7cd6ce8a7690544d084ea21c26e910a97e077c9b7f07bf5de623ee19981/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:db9ed69dbef9715071232caa9b69c52ac7de3a95773c2db65bdba85916e4e5c0", size = 267229841, upload-time = "2025-03-07T01:46:54.356Z" }, + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/f7/cd777c4109681367721b00a106f491e0d0d15cfa1fd59672ce580ce42a97/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b6c161cb130be1a07a27ea6923df8141f3c295852f4b260c65f18f3e0a091dc", size = 288117129, upload-time = "2025-03-07T01:47:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/b9/598f6ff36faaece4b3c50d26f50e38661499ff34346f00e057760b35cc9d/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8878dce784d0fac90131b6817b607e803c36e629ba34dc5b433471382196b6a5", size = 283835557, upload-time = "2025-02-26T00:16:54.265Z" }, + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/1c/857979db0ef194ca5e21478a0612bcdbbe59458d7694361882279947b349/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:31432ad4d1fb1004eb0c56203dc9bc2178a1ba69d1d9e02d64a6938ab5e40e7a", size = 322400625, upload-time = "2025-06-26T04:11:04.496Z" }, + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a2/8cee5da30d13430e87bf99bb33455d2724d0a4a9cb5d7926d80ccb96d008/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:adccd7161ace7261e01bb91e44e88da350895c270d23f744f0820c818b7229e7", size = 38386204, upload-time = "2025-03-07T01:49:43.612Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/6a/03aa43cc9bd3ad91553a88b5f6fb25ed6a3752ae86ce2180221962bc2aa5/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b48363fc6964dede448029434c6abed6c5e37f823cb43c3bcde7ecfc0457e15", size = 138936938, upload-time = "2025-09-06T00:32:05.589Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/c0/1b303feea90d296f6176f32a2a70b5ef230f9bdeb3a72bddb0dc922dc137/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d7ad891da111ebafbf7e015d34879f7112832fc239ff0d7d776b6cb685274615", size = 91161, upload-time = "2025-03-07T01:42:23.922Z" }, + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "torch" +version = "2.10.0+cu128" +source = { registry = "https://download.pytorch.org/whl/cu128" } +dependencies = [ + { name = "cuda-bindings", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "filelock", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "fsspec", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "jinja2", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "networkx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-cupti-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-runtime-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cudnn-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cufft-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cufile-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-curand-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusolver-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparselt-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nccl-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvshmem-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvtx-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sympy", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "triton", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "typing-extensions", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:85ed7944655ea6fd69377692e9cbfd7bba28d99696ceae79985e7caa99cf0a95" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1d01ffaebf64715c0f507a39463149cb19e596ff702bd4bcf862601f2881dabc" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/2c/96f92f3c60387e14cc45aed49487f3486f89ea27106c1b1376913c62abe4/triton-3.6.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49df5ef37379c0c2b5c0012286f80174fcf0e073e5ade1ca9a86c36814553651", size = 176081190, upload-time = "2026-01-20T16:16:00.523Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "wheel" +version = "0.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/24/a2eb353a6edac9a0303977c4cb048134959dd2a51b48a269dfc9dde00c8a/wheel-0.46.3.tar.gz", hash = "sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803", size = 60605, upload-time = "2026-01-22T12:39:49.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl", hash = "sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d", size = 30557, upload-time = "2026-01-22T12:39:48.099Z" }, +] diff --git a/services/automodel/docker/locks/mamba-wheel-build-py312/pyproject.toml b/services/automodel/docker/locks/mamba-wheel-build-py312/pyproject.toml new file mode 100644 index 0000000000..49aecc29f6 --- /dev/null +++ b/services/automodel/docker/locks/mamba-wheel-build-py312/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "mamba-wheel-build-py312" +version = "0.0.0" +requires-python = ">=3.12,<3.13" +dependencies = [ + "packaging", + "setuptools", + "wheel", + "torch==2.10.0+cu128; sys_platform == 'linux'", +] + +[tool.uv] +required-version = ">=0.9.14,<0.10.0" +prerelease = "if-necessary-or-explicit" +index-strategy = "unsafe-best-match" +environments = [ + "sys_platform == 'linux' and platform_machine == 'x86_64'", + "sys_platform == 'linux' and platform_machine == 'aarch64'", +] + +[tool.uv.sources] +torch = { index = "pytorch-cu128", marker = "sys_platform == 'linux'" } + +[[tool.uv.index]] +name = "pytorch-cu128" +url = "https://download.pytorch.org/whl/cu128" +explicit = true diff --git a/services/automodel/docker/locks/mamba-wheel-build-py312/uv.lock b/services/automodel/docker/locks/mamba-wheel-build-py312/uv.lock new file mode 100644 index 0000000000..6d8bec6ddc --- /dev/null +++ b/services/automodel/docker/locks/mamba-wheel-build-py312/uv.lock @@ -0,0 +1,356 @@ +version = 1 +revision = 3 +requires-python = "==3.12.*" +resolution-markers = [ + "platform_machine == 'x86_64' and sys_platform == 'linux'", + "platform_machine == 'aarch64' and sys_platform == 'linux'", +] +supported-markers = [ + "platform_machine == 'x86_64' and sys_platform == 'linux'", + "platform_machine == 'aarch64' and sys_platform == 'linux'", +] + +[[package]] +name = "cuda-bindings" +version = "12.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c2/65bfd79292b8ff18be4dd7f7442cea37bcbc1a228c1886f1dea515c45b67/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:694ba35023846625ef471257e6b5a4bc8af690f961d197d77d34b1d1db393f56", size = 11760260, upload-time = "2025-10-21T14:51:40.79Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/f9/1b9b60a30fc463c14cdea7a77228131a0ccc89572e8df9cb86c9648271ab/cuda_pathfinder-1.5.2-py3-none-any.whl", hash = "sha256:0c5f160a7756c5b072723cbbd6d861e38917ef956c68150b02f0b6e9271c71fa", size = 49988, upload-time = "2026-04-06T23:01:05.17Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "mamba-wheel-build-py312" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "packaging", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "setuptools", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "torch", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "wheel", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[package.metadata] +requires-dist = [ + { name = "packaging" }, + { name = "setuptools" }, + { name = "torch", marker = "sys_platform == 'linux'", specifier = "==2.10.0+cu128", index = "https://download.pytorch.org/whl/cu128" }, + { name = "wheel" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/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" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/99/db44d685f0e257ff0e213ade1964fc459b4a690a73293220e98feb3307cf/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b86f6dd8935884615a0683b663891d43781b819ac4f2ba2b0c9604676af346d0", size = 590537124, upload-time = "2025-03-07T01:43:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1f/b3bd73445e5cb342727fd24fe1f7b748f690b460acadc27ea22f904502c8/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4412396548808ddfed3f17a467b104ba7751e6b58678a4b840675c56d21cf7ed", size = 9533318, upload-time = "2025-03-07T01:40:10.421Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d1/e50d0acaab360482034b84b6e27ee83c6738f7d32182b987f9c7a4e32962/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc1fec1e1637854b4c0a65fb9a8346b51dd9ee69e61ebaccc82058441f15bce8", size = 43106076, upload-time = "2025-03-07T01:41:59.817Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/75/f865a3b236e4647605ea34cc450900854ba123834a5f1598e160b9530c3a/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d", size = 965265, upload-time = "2025-03-07T01:39:43.533Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/41/e79269ce215c857c935fd86bcfe91a451a584dfc27f1e068f568b9ad1ab7/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c9132cc3f8958447b4910a1720036d9eff5928cc3179b0a51fb6d167c6cc87d8", size = 705026878, upload-time = "2025-06-06T21:52:51.348Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/bc/7771846d3a0272026c416fbb7e5f4c1f146d6d80704534d0b187dd6f4800/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:848ef7224d6305cdb2a4df928759dca7b1201874787083b6e7550dd6765ce69a", size = 193109211, upload-time = "2025-03-07T01:44:56.873Z" }, + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f5/5607710447a6fe9fd9b3283956fceeee8a06cda1d2f56ce31371f595db2a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:4beb6d4cce47c1a0f1013d72e02b0994730359e17801d395bdcbf20cfb3bb00a", size = 1120705, upload-time = "2025-03-07T01:45:41.434Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/5e/92aa15eca622a388b80fbf8375d4760738df6285b1e92c43d37390a33a9a/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:dfab99248034673b779bc6decafdc3404a8a6f502462201f2f31f11354204acd", size = 63625754, upload-time = "2025-03-07T01:46:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/32/f7cd6ce8a7690544d084ea21c26e910a97e077c9b7f07bf5de623ee19981/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:db9ed69dbef9715071232caa9b69c52ac7de3a95773c2db65bdba85916e4e5c0", size = 267229841, upload-time = "2025-03-07T01:46:54.356Z" }, + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/f7/cd777c4109681367721b00a106f491e0d0d15cfa1fd59672ce580ce42a97/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b6c161cb130be1a07a27ea6923df8141f3c295852f4b260c65f18f3e0a091dc", size = 288117129, upload-time = "2025-03-07T01:47:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/b9/598f6ff36faaece4b3c50d26f50e38661499ff34346f00e057760b35cc9d/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8878dce784d0fac90131b6817b607e803c36e629ba34dc5b433471382196b6a5", size = 283835557, upload-time = "2025-02-26T00:16:54.265Z" }, + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/1c/857979db0ef194ca5e21478a0612bcdbbe59458d7694361882279947b349/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:31432ad4d1fb1004eb0c56203dc9bc2178a1ba69d1d9e02d64a6938ab5e40e7a", size = 322400625, upload-time = "2025-06-26T04:11:04.496Z" }, + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a2/8cee5da30d13430e87bf99bb33455d2724d0a4a9cb5d7926d80ccb96d008/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:adccd7161ace7261e01bb91e44e88da350895c270d23f744f0820c818b7229e7", size = 38386204, upload-time = "2025-03-07T01:49:43.612Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/6a/03aa43cc9bd3ad91553a88b5f6fb25ed6a3752ae86ce2180221962bc2aa5/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b48363fc6964dede448029434c6abed6c5e37f823cb43c3bcde7ecfc0457e15", size = 138936938, upload-time = "2025-09-06T00:32:05.589Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/c0/1b303feea90d296f6176f32a2a70b5ef230f9bdeb3a72bddb0dc922dc137/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d7ad891da111ebafbf7e015d34879f7112832fc239ff0d7d776b6cb685274615", size = 91161, upload-time = "2025-03-07T01:42:23.922Z" }, + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "torch" +version = "2.10.0+cu128" +source = { registry = "https://download.pytorch.org/whl/cu128" } +dependencies = [ + { name = "cuda-bindings", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "filelock", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "fsspec", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "jinja2", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "networkx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-cupti-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-runtime-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cudnn-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cufft-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cufile-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-curand-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusolver-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparselt-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nccl-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvshmem-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvtx-cu12", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "setuptools", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sympy", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "triton", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "typing-extensions", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6f09cdf2415516be028ae82e6b985bcfc3eac37bc52ab401142689f6224516ca" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:628e89bd5110ced7debee2a57c69959725b7fbc64eab81a39dd70e46c7e28ba5" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "wheel" +version = "0.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/24/a2eb353a6edac9a0303977c4cb048134959dd2a51b48a269dfc9dde00c8a/wheel-0.46.3.tar.gz", hash = "sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803", size = 60605, upload-time = "2026-01-22T12:39:49.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl", hash = "sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d", size = 30557, upload-time = "2026-01-22T12:39:48.099Z" }, +] diff --git a/services/automodel/docker/no_override_requirements.txt b/services/automodel/docker/no_override_requirements.txt new file mode 100644 index 0000000000..03482e620e --- /dev/null +++ b/services/automodel/docker/no_override_requirements.txt @@ -0,0 +1,10 @@ +# Preserve ML stack from nmp-automodel-base (Automodel uv sync on NGC PyTorch). +# Same pattern as customizer NO_OVERRIDE_REQUIREMENTS_PATH: impossible marker blocks +# install/upgrade so the base venv pins remain intact when adding platform glue. +transformers; sys_platform == 'never' +torch; sys_platform == 'never' +torchvision; sys_platform == 'never' +tokenizers; sys_platform == 'never' +accelerate; sys_platform == 'never' +safetensors; sys_platform == 'never' +numpy; sys_platform == 'never' diff --git a/services/automodel/docker/pyproject.workspace.toml b/services/automodel/docker/pyproject.workspace.toml new file mode 100644 index 0000000000..b63b105f7f --- /dev/null +++ b/services/automodel/docker/pyproject.workspace.toml @@ -0,0 +1,31 @@ +# Minimal uv workspace for nmp-automodel container image builds only. +# Replaces the repo-root pyproject.toml in Dockerfile.platform-workspace so +# partial COPY trees are not validated against the full monorepo workspace. + +[project] +name = "nemo-platform-automodel-image" +version = "0.0.0" +requires-python = ">=3.11,<3.14" + +[tool.uv] +required-version = ">=0.9.14,<0.10.0" + +[tool.uv.workspace] +members = [ + "packages/nmp_build_tools", + "packages/models", + "sdk/python/nemo-platform", + "packages/nemo_platform_plugin", + "packages/nmp_common", + "services/automodel", + "services/core/models", +] + +[tool.uv.sources] +nmp-build-tools = { workspace = true } +models = { workspace = true } +nemo-platform-sdk = { workspace = true } +nemo-platform-plugin = { workspace = true } +nmp-common = { workspace = true } +nmp-automodel = { workspace = true } +nmp-models = { workspace = true } diff --git a/services/automodel/pyproject.toml b/services/automodel/pyproject.toml new file mode 100644 index 0000000000..fbc3eed955 --- /dev/null +++ b/services/automodel/pyproject.toml @@ -0,0 +1,40 @@ +[project] +name = "nmp-automodel" +version = "0.1.0" +description = "NeMo Automodel job compiler and platform tasks (no HTTP server)." +readme = "README.md" +requires-python = ">=3.11,<3.14" +dependencies = [ + "nmp-common", + "nemo-platform-sdk", + "pydantic>=2.10.6", + "pydantic-settings>=2.6.1", + "httpx>=0.27.0", + "aiofiles>=24.1.0", + "tenacity>=8.5.0", + "jsonschema>=4.23.0", +] + +[project.optional-dependencies] +dev = ["pytest>=8.3.4", "pytest-asyncio>=0.25.3", "pytest-mock>=3.14.0"] + +[project.scripts] +nmp-automodel-file-io = "nmp.automodel.tasks.file_io:run" +nmp-automodel-training = "nmp.automodel.tasks.training.__main__:run" +nmp-automodel-model-entity = "nmp.automodel.tasks.model_entity.__main__:run" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/nmp"] + +[tool.uv.sources] +nmp-common = { workspace = true } +nemo-platform-sdk = { workspace = true } + +[tool.pytest.ini_options] +asyncio_mode = "auto" +pythonpath = ["src"] +testpaths = ["tests"] diff --git a/services/automodel/src/nmp/automodel/__init__.py b/services/automodel/src/nmp/automodel/__init__.py new file mode 100644 index 0000000000..2606556210 --- /dev/null +++ b/services/automodel/src/nmp/automodel/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""NeMo Automodel platform compiler and tasks.""" diff --git a/services/automodel/src/nmp/automodel/adapter.py b/services/automodel/src/nmp/automodel/adapter.py new file mode 100644 index 0000000000..ccb3bfa7a8 --- /dev/null +++ b/services/automodel/src/nmp/automodel/adapter.py @@ -0,0 +1,132 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Convert plugin ``AutomodelJobOutput`` shape to legacy ``CustomizationJobOutput`` for the compiler.""" + +from __future__ import annotations + +from typing import Any, Literal + +from nmp.automodel.api.v2.jobs.schemas import ( + CustomizationJobOutput, + DistillationTraining, + IntegrationParams, + LoRAParams, + OutputResponse, + ParallelismParams, + SFTTraining, + WandBParams, +) +from nmp.common.api.common import SecretRef + + +def _map_finetuning_type(value: str) -> str: + if value == "all_weights": + return "all_weights" + if value == "lora_merged": + return "lora_merged" + return "lora" + + +def _build_peft(training: dict[str, Any]) -> LoRAParams | None: + ft = training.get("finetuning_type", "lora") + if ft == "all_weights": + return None + lora = training.get("lora") or {} + return LoRAParams( + rank=lora.get("rank", 16), + alpha=lora.get("alpha", 32), + merge=ft == "lora_merged" or lora.get("merge", False), + target_modules=lora.get("target_modules"), + ) + + +def _build_training_block(spec: dict[str, Any]) -> SFTTraining | DistillationTraining: + training = spec["training"] + schedule = spec.get("schedule") or {} + batch = spec.get("batch") or {} + optimizer = spec.get("optimizer") or {} + parallelism = spec.get("parallelism") or {} + + common: dict[str, Any] = { + "peft": _build_peft(training), + "learning_rate": optimizer.get("learning_rate", 1e-4), + "weight_decay": optimizer.get("weight_decay", 0.01), + "warmup_steps": optimizer.get("warmup_steps", 0), + "epochs": schedule.get("epochs", 1), + "max_steps": schedule.get("max_steps"), + "val_check_interval": schedule.get("val_check_interval"), + "batch_size": batch.get("global_batch_size", 8), + "micro_batch_size": batch.get("micro_batch_size", 1), + "sequence_packing": batch.get("sequence_packing", False), + "max_seq_length": training.get("max_seq_length", 2048), + "seed": schedule.get("seed"), + "parallelism": ParallelismParams( + num_nodes=parallelism.get("num_nodes", 1), + num_gpus_per_node=parallelism.get("num_gpus_per_node", 1), + tensor_parallel_size=parallelism.get("tensor_parallel_size", 1), + pipeline_parallel_size=parallelism.get("pipeline_parallel_size", 1), + context_parallel_size=parallelism.get("context_parallel_size", 1), + expert_parallel_size=parallelism.get("expert_parallel_size"), + ), + "execution_profile": training.get("execution_profile"), + } + + training_type: Literal["sft", "distillation"] = training.get("training_type", "sft") + if training_type == "distillation": + return DistillationTraining( + **common, + teacher_model=training["teacher_model"], + teacher_precision=training.get("teacher_precision", "bf16"), + distillation_ratio=training.get("distillation_ratio", 0.5), + distillation_temperature=training.get("distillation_temperature", 1.0), + ) + return SFTTraining(**common) + + +def _build_integrations(spec: dict[str, Any]) -> IntegrationParams | None: + raw = spec.get("integrations") + if not raw: + return None + wandb = raw.get("wandb") + wandb_params = None + if wandb: + secret = wandb.get("api_key_secret") + wandb_params = WandBParams( + project=wandb.get("project"), + api_key_secret=SecretRef(secret) if isinstance(secret, str) else secret, + ) + return IntegrationParams(wandb=wandb_params, mlflow=raw.get("mlflow")) + + +def automodel_spec_to_compiler_output(spec: dict[str, Any] | Any) -> CustomizationJobOutput: + """Map simplified Automodel job output (plugin schema) to ``CustomizationJobOutput``.""" + if hasattr(spec, "model_dump"): + data = spec.model_dump(mode="python") + else: + data = dict(spec) + + dataset = data["dataset"] + training_uri = dataset["training"] if isinstance(dataset, dict) else dataset + + output = data["output"] + if isinstance(output, dict): + out_type = output.get("type", "model") + output_resp = OutputResponse( + name=output["name"], + type=out_type, + fileset=output["fileset"], + description=output.get("description"), + ) + else: + output_resp = output + + return CustomizationJobOutput( + name=data.get("name"), + model=data["model"], + dataset=training_uri, + training=_build_training_block(data), + integrations=_build_integrations(data), + deployment_config=None, + output=output_resp, + ) diff --git a/services/automodel/src/nmp/automodel/api/__init__.py b/services/automodel/src/nmp/automodel/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/automodel/src/nmp/automodel/api/v2/__init__.py b/services/automodel/src/nmp/automodel/api/v2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/automodel/src/nmp/automodel/api/v2/jobs/__init__.py b/services/automodel/src/nmp/automodel/api/v2/jobs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/automodel/src/nmp/automodel/api/v2/jobs/schemas.py b/services/automodel/src/nmp/automodel/api/v2/jobs/schemas.py new file mode 100644 index 0000000000..31a115816a --- /dev/null +++ b/services/automodel/src/nmp/automodel/api/v2/jobs/schemas.py @@ -0,0 +1,639 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""API schemas for customization job endpoints.""" + +from typing import Annotated, Any, Dict, Literal, Optional, Self, Union + +from nmp.automodel.entities.validators import validate_fileset_uri +from nmp.automodel.entities.values import FinetuningType, OutputNameType, Precision +from nmp.common.api.common import SecretRef +from nmp.common.entities.constants import ( + MAX_LENGTH_255, + REGEX_WORD_CHARACTER_DOT_DASH, +) +from pydantic import AfterValidator, BaseModel, ConfigDict, Discriminator, Field, model_validator + +# Important!!! Do not import Pydantic models from this file into tasks. +# Instead, duplicate models from this file into corresponding task module schemas.py. + + +class ValidationError(ValueError): + """Raised when job input validation fails.""" + + pass + + +# ============================================================ +# Sub-Configurations +# ============================================================ + + +class QuantizationParams(BaseModel): + """Base model quantization for memory-efficient PEFT training. + + Supports two scenarios: + - Full-precision base model: quantized on-the-fly at load time + - Pre-quantized base model: loaded directly at the specified precision + + In both cases, base model weights are frozen and only the PEFT adapter + parameters are trained in full precision. + """ + + precision: Literal["4bit", "8bit"] = Field( + default="4bit", + description="Quantization precision. '4bit' (NF4) for maximum memory savings, " + "'8bit' (LLM.int8) for a balance of quality and memory.", + ) + + +class _PEFTParams(BaseModel): + """Base configuration shared by all PEFT methods.""" + + # Quantization only makes sense with PEFT (quantized base weights are frozen, so you need trainable + # adapter parameters), which is why it lives here rather than on _TrainingBase. + quantization: Optional[QuantizationParams] = Field( + default=None, + description="Enable quantized training to reduce GPU memory. " + "If the base model is full-precision, it will be quantized at load time. " + "If the base model is already pre-quantized, this configures the expected precision. " + "The trained adapter remains full-precision.", + ) + + +class LoRAParams(_PEFTParams): + """LoRA adapter configuration.""" + + type: Literal["lora"] = "lora" + + rank: int = Field( + default=8, + ge=1, + le=256, + description="LoRA rank (low-rank dimension). Higher values increase capacity but use more memory.", + ) + alpha: int = Field( + default=32, + ge=1, + description="LoRA alpha scaling factor. Common practice: alpha = 2-4x rank.", + ) + dropout: float = Field( + default=0.0, + ge=0.0, + le=1.0, + description="LoRA dropout probability for regularization.", + ) + target_modules: Optional[list[str]] = Field( + default=None, + description="Module name patterns to apply LoRA to (e.g., ['*.q_proj', '*.v_proj']). " + "If not set, applies to all '*proj' linear layers.", + ) + merge: bool = Field( + default=False, + description="Merge LoRA weights into base model after training. " + "Produces a full-weight checkpoint instead of an adapter.", + ) + use_dora: bool = Field( + default=False, + description="Enable DoRA (Weight-Decomposed Low-Rank Adaptation). " + "Decomposes weight updates into magnitude and direction components. " + "Can improve quality especially at low ranks, but adds training overhead.", + ) + + @model_validator(mode="after") + def _validate_unsupported_features(self) -> Self: + if self.quantization is not None: + raise ValueError("Quantized LoRA training is not yet supported.") + if self.use_dora: + raise ValueError("DoRA is not yet supported.") + return self + + +# When a second PEFT method is added (e.g., IA3Config), change this to: +# PeftMethod = Annotated[Union[LoRAParams, IA3Config], Discriminator("type")] +PeftMethod = LoRAParams + + +class ParallelismParams(BaseModel): + """Distributed training parallelism configuration. + + Most users only need num_gpus_per_node. Advanced users can configure + tensor/pipeline/context/expert parallelism for large models. + """ + + num_gpus_per_node: int = Field(default=1, gt=0, description="Number of gpus per node.") + num_nodes: int = Field(default=1, gt=0, description="Number of nodes.") + tensor_parallel_size: int = Field(default=1, gt=0, description="Tensor parallel size.") + pipeline_parallel_size: int = Field(default=1, gt=0, description="Pipeline parallel size.") + context_parallel_size: int = Field(default=1, gt=0, description="Context parallel size.") + expert_parallel_size: Optional[int] = Field(default=None, gt=0, description="Expert parallel size (MoE models).") + sequence_parallel: bool = Field(default=False, description="Enable sequence parallelism.") + + +# ============================================================ +# Training Method Discriminated Union +# ============================================================ + + +class _TrainingBase(BaseModel): + """Common training configuration shared by all methods. + + Flat hyperparameters match the ML practitioner mental model + (like HuggingFace TrainingArguments / TRL SFTConfig). + Only parallelism is grouped — it's enterprise infrastructure. + """ + + # --- PEFT (orthogonal to training method) --- + peft: Optional[PeftMethod] = Field( + default=None, + description="PEFT adapter configuration. If set, trains a parameter-efficient adapter. " + "If omitted, performs full-weight fine-tuning.", + ) + + # --- Optimizer --- + learning_rate: float = Field( + default=1e-4, + description="Peak learning rate. Optimal value will depend on training type and PEFT. " + "For SFT without LoRA, start with 5e-5. If using LoRA start with 1e-4. Lowering the value " + "can enable for slower, more precise training; Raising the value speeds up learning.", + ) + min_learning_rate: Optional[float] = Field( + default=None, + description="Minimum learning rate for cosine decay. Optional; used with learning rate schedules.", + ) + weight_decay: float = Field( + default=0.01, + description="Weight decay coefficient. Helps prevent overfitting.", + ) + adam_beta1: float = Field( + default=0.9, + description="Adam beta1 parameter. Adjust for optimizer tuning.", + ) + adam_beta2: float = Field( + default=0.999, + description="Adam beta2 parameter. Adjust for optimizer tuning.", + ) + warmup_steps: int = Field( + default=0, + ge=0, + description="Linear warmup steps. Recommended: 10% of total training steps for stable training.", + ) + optimizer: Optional[str] = Field(default=None, description="Optimizer name (e.g., 'adamw').") + + # --- Schedule --- + epochs: int = Field( + default=1, + gt=0, + description="Number of complete passes through the dataset. The ideal number of epochs depends " + "on the training method, the number of training samples, and size of the model. Start with 3 for " + "a reasonable value. Monitor the validation and training loss curves. If both are still " + "decreasing, you can increase this number.", + ) + max_steps: Optional[int] = Field( + default=None, + gt=0, + description="Max training steps. Overrides epochs if set.", + ) + log_every_n_steps: Optional[int] = Field( + default=None, + description="Logging frequency in steps. Controls how often training metrics are logged.", + ) + val_check_interval: Optional[float] = Field( + default=None, + description="Validation interval. Float <= 1.0 is fraction of epoch; > 1.0 is step count.", + ) + + # --- Batch --- + batch_size: int = Field( + default=32, + gt=0, + description="Global batch size across all GPUs. Higher = faster but more memory. If OOM, reduce this first.", + ) + micro_batch_size: int = Field( + default=1, + gt=0, + description="Per-GPU micro batch size. Keep small (1-2) for large models to avoid OOM.", + ) + sequence_packing: bool = Field( + default=False, + description="Enable sequence packing for efficiency. Can improve training speed.", + ) + + # --- Model --- + max_seq_length: int = Field( + default=2048, + gt=0, + description="Maximum token sequence length for training. Higher = more memory, longer training.", + ) + precision: Optional[Precision] = Field( + default=None, + description="Model precision for training. Auto-detected if unset.", + ) + seed: Optional[int] = Field( + default=None, + description="Random seed for reproducibility. Optional.", + ) + + # --- Enterprise Infrastucture --- + parallelism: ParallelismParams = Field(default_factory=ParallelismParams) + execution_profile: Optional[str] = Field( + default=None, + min_length=1, + description="Execution profile for the GPU training step. Maps to an operator-configured profile " + "(e.g., 'a100', 'high_priority'). If omitted, uses the service-level default.", + ) + + model_config = {"protected_namespaces": ()} + + @property + def finetuning_type(self) -> FinetuningType: + """Derived from peft config: presence → adapter type, absence → full-weight.""" + if self.peft is None: + return FinetuningType.ALL_WEIGHTS + if isinstance(self.peft, LoRAParams): + return FinetuningType.LORA_MERGED if self.peft.merge else FinetuningType.LORA + raise ValueError(f"Unknown PEFT type: {type(self.peft).__name__}") + + +class SFTTraining(_TrainingBase): + """Supervised Fine-Tuning.""" + + type: Literal["sft"] = "sft" + + +class DistillationTraining(_TrainingBase): + """Knowledge Distillation with a teacher model. + + Customizer's differentiator — not available in Unsloth. + Trains the student model to match the teacher's output distribution. + """ + + type: Literal["distillation"] = "distillation" + teacher_model: str = Field( + description="Teacher model URN (e.g., 'workspace/model-name'). " + "Must have the same vocabulary as the student model.", + ) + teacher_precision: Literal["bf16", "fp16", "fp32"] = Field( + default="bf16", + description="Precision for loading the frozen teacher model. " + "Lower precision reduces memory but may affect logit quality.", + ) + distillation_ratio: float = Field( + default=0.5, + ge=0.0, + le=1.0, + description="Balance between CE loss and KD loss. 0.0 = CE only, 1.0 = KD only.", + ) + distillation_temperature: float = Field( + default=1.0, + gt=0.0, + description="Softmax temperature for KD. Higher = softer probability distributions.", + ) + + +class DPOTraining(_TrainingBase): + """Direct Preference Optimization.""" + + type: Literal["dpo"] = "dpo" + ref_policy_kl_penalty: float = Field( + default=0.05, ge=0.0, description="KL penalty coefficient (beta in DPO paper)." + ) + preference_average_log_probs: bool = Field( + default=False, description="Average log probabilities for preference loss calculation." + ) + sft_average_log_probs: bool = Field( + default=False, description="Average log probabilities for SFT regularization loss." + ) + preference_loss_weight: float = Field(default=1.0, ge=0.0, description="Weight for the preference (DPO) loss term.") + sft_loss_weight: float = Field( + default=0.0, ge=0.0, description="Weight for SFT regularization loss (0 = disabled)." + ) + max_grad_norm: float = Field(default=1.0, ge=0.0, description="Maximum gradient norm for clipping.") + + @model_validator(mode="after") + def _peft_not_yet_supported(self) -> Self: + if self.peft is not None: + raise ValueError( + "PEFT is not yet supported with DPO training. Use full-weight training by omitting the 'peft' field." + ) + return self + + +AnyTraining = Union[SFTTraining, DistillationTraining, DPOTraining] +TrainingMethod = Annotated[AnyTraining, Discriminator("type")] + + +# ============================================================ +# Integration Configs (unchanged) +# ============================================================ + + +class WandBParams(BaseModel): + """Weights & Biases integration configuration. + + To use W&B, provide an api_key_secret referencing a secret that contains + the WANDB_API_KEY value. Optionally provide base_url for self-hosted W&B servers. + """ + + project: Optional[str] = Field( + default=None, + description="W&B project name (groups related runs). Defaults to output.name if not set.", + ) + name: Optional[str] = Field( + default=None, + description="W&B run name. Defaults to job_id if not provided.", + ) + entity: Optional[str] = Field( + default=None, + description="W&B entity (team or username).", + ) + tags: Optional[list[str]] = Field( + default=None, + description="W&B tags for filtering runs.", + ) + notes: Optional[str] = Field( + default=None, + description="W&B notes/description for the run.", + ) + base_url: Optional[str] = Field( + default=None, + description="Base URL for self-hosted W&B server (e.g., 'https://wandb.mycompany.com'). " + "If not provided, uses the default W&B cloud service.", + ) + api_key_secret: SecretRef | None = Field( + default=None, + description="Reference to a secret containing the WANDB_API_KEY. " + "Format: 'secret_name' (uses request workspace) or 'workspace/secret_name' (explicit workspace).", + ) + + +class MLflowParams(BaseModel): + """MLflow integration configuration.""" + + experiment_name: Optional[str] = Field( + default=None, + description="MLflow experiment name (groups related runs). Defaults to output.name if not set.", + ) + run_name: Optional[str] = Field( + default=None, + description="MLflow run name. Defaults to job_id if not provided.", + ) + tags: Optional[dict[str, str]] = Field( + default=None, + description="MLflow tags as key-value pairs for filtering runs.", + ) + description: Optional[str] = Field( + default=None, + description="MLflow run description.", + ) + tracking_uri: Optional[str] = Field( + default=None, + description="MLflow tracking server URI (e.g., 'http://mlflow.mycompany.com:5000'). " + "Can also be set via MLFLOW_TRACKING_URI environment variable.", + ) + + +class IntegrationParams(BaseModel): + """Third-party integration configurations. + + Each integration type has its own optional field. To enable an integration, + provide its configuration object. Omit or set to None to disable. + """ + + wandb: Optional[WandBParams] = Field( + default=None, + description="Weights & Biases integration configuration.", + ) + mlflow: Optional[MLflowParams] = Field( + default=None, + description="MLflow integration configuration.", + ) + + +# ============================================================ +# Deployment Config +# ============================================================ + + +class ToolCallParams(BaseModel): + """Tool calling configuration for NIM deployments.""" + + tool_call_parser: Optional[str] = Field( + default=None, + description="Name of the tool call parser to use (e.g., 'openai', 'hermes', 'pythonic', 'llama3_json', 'mistral').", + ) + tool_call_plugin: Optional[str] = Field( + default=None, + pattern=r"^[\w\-.]+/[\w\-.]+$", + description="Reference to a fileset containing the custom tool call plugin Python file. " + "Expected format: '{workspace}/{fileset_name}'.", + ) + auto_tool_choice: Optional[bool] = Field( + default=None, + description="Whether to enable automatic tool choice.", + ) + + +class DeploymentParams(BaseModel): + """Inline deployment parameters for creating a new ModelDeploymentConfig.""" + + gpu: int = Field( + default=1, + description="Number of GPUs required for the deployment", + ) + + additional_envs: Optional[dict[str, str]] = Field( + default=None, + description="Additional environment variables for the deployment", + ) + + disk_size: Optional[str] = Field( + default=None, + description="Disk size for the deployment", + ) + + image_name: Optional[str] = Field( + default=None, + description="Container image name from NGC. If not specified, defaults to multi-llm", + ) + + image_tag: Optional[str] = Field( + default=None, + description="Container image tag from NGC", + ) + + lora_enabled: bool = Field( + default=True, + description="When automatically deploying a full SFT training, this parameter being set to true will allow subsequent LoRA adapters to be trained and deployed against it.", + ) + + tool_call_config: Optional[ToolCallParams] = Field( + default=None, + description="Tool calling configuration override for the NIM deployment.", + ) + + +# ============================================================ +# Output +# ============================================================ + + +class _OutputBase(BaseModel): + """Shared fields for output artifact request and response.""" + + name: str = Field( + pattern=REGEX_WORD_CHARACTER_DOT_DASH, + max_length=MAX_LENGTH_255, + description="Name of the output artifact. Used to identify it during deployment and inference.", + examples=["my-finetuned-llama", "llama-3-8b-lora-v2"], + ) + + +class OutputRequest(_OutputBase): + """Output artifact configuration provided by the user.""" + + +class OutputResponse(_OutputBase): + """Resolved output artifact details returned by the server.""" + + type: OutputNameType = Field( + description="Output artifact type. Either `model` (full fine-tuned weights) or `adapter` (LoRA adapter weights).", + examples=["model", "adapter"], + ) + fileset: str = Field( + pattern=REGEX_WORD_CHARACTER_DOT_DASH, + max_length=MAX_LENGTH_255, + description="FileSet name where output artifacts are stored.", + examples=["my-model-a1b2c3d4e5f6"], + ) + + +# ============================================================ +# Job Schemas +# ============================================================ + + +class _CustomizationJobBase(BaseModel): + """Base schema with common fields for customization jobs.""" + + model: str = Field(description="Model reference (e.g., 'workspace/model-name').") + dataset: Annotated[str, AfterValidator(validate_fileset_uri)] = Field( + description="Training dataset fileset as 'workspace/name' or 'name' (resolved in the job path workspace)." + ) + training: TrainingMethod = Field(description="Training method and hyperparameters.") + integrations: Optional[IntegrationParams] = Field( + default=None, + description="Third-party integrations (e.g., Weights & Biases, MLflow).", + ) + deployment_config: Optional[str | DeploymentParams] = Field( + default=None, + description="Deployment configuration for auto-deploying the model after training. " + "Pass a string to reference an existing ModelDeploymentConfig by name " + "(e.g., 'my-config' or 'workspace/my-config'). " + "An object provides inline NIM deployment parameters. " + "Omit to skip deployment.", + ) + custom_fields: Dict[str, Any] = Field(default_factory=dict, description="Custom user-defined fields.") + + model_config = ConfigDict(protected_namespaces=(), regex_engine="python-re") + + +class CustomizationJobInput(_CustomizationJobBase): + """Input schema for creating customization jobs.""" + + output: Optional[OutputRequest] = Field( + default=None, + description="Output artifact configuration. If omitted, name is auto-generated as " + "`{model}-{dataset}-`. The output type (model vs adapter) is always " + "inferred from the training configuration.", + examples=[{"name": "my-finetuned-llama"}], + ) + + @model_validator(mode="before") + @classmethod + def reject_legacy_fields(cls, data: object) -> object: + if isinstance(data, dict) and "output_model" in data: + raise ValueError("spec.output_model was removed. Use spec.output instead.") + return data + + @model_validator(mode="after") + def _reject_lora_without_lora_enabled(self) -> Self: + peft = self.training.peft + dc = self.deployment_config + if isinstance(peft, LoRAParams) and not peft.merge and isinstance(dc, DeploymentParams) and not dc.lora_enabled: + raise ValueError( + "deployment_config.lora_enabled must be true (or omitted) when training a LoRA adapter. " + "Setting lora_enabled=false would deploy the base model without LoRA support, " + "making the trained adapter unservable." + ) + return self + + +class CustomizationJobOutput(_CustomizationJobBase): + """Customization job details returned by the server.""" + + output: OutputResponse = Field( + description="Output artifact created by this job.", + examples=[ + {"name": "my-finetuned-llama", "type": "model", "fileset": "my-finetuned-llama"}, + {"name": "llama-3-8b-lora-v2", "type": "adapter", "fileset": "llama-3-8b-lora-v2-a1b2c3d4e5f6"}, + ], + ) + + def validate_for_training(self) -> None: + """Validate this job input for training execution. + + Call this after any enrichment has been applied. + + Raises: + ValidationError: If validation fails. + """ + training = self.training + p = training.parallelism + num_nodes = p.num_nodes + num_gpus_per_node = p.num_gpus_per_node + tp = p.tensor_parallel_size + pp = p.pipeline_parallel_size + cp = p.context_parallel_size + ep = p.expert_parallel_size + + total_gpus = num_gpus_per_node * num_nodes + model_parallel_size = tp * pp * cp + if total_gpus % model_parallel_size != 0: + raise ValidationError( + f"Total GPUs ({total_gpus}) must be divisible by " + f"tensor_parallel_size ({tp}) * " + f"pipeline_parallel_size ({pp}) * " + f"context_parallel_size ({cp}) = {model_parallel_size}" + ) + + derived_dp = total_gpus // model_parallel_size + + # Note: Expert model parallelism (EP) is NOT a dimension that divides world_size like TP/PP. + # Instead, EP operates orthogonally, therefore we validate it separately. + # It distributes experts across the dp × cp dimension. + # FSDP2 requires: (dp_size × cp_size) % ep_size == 0 + if ep is not None: + dp_cp = derived_dp * cp + if dp_cp % ep != 0: + raise ValidationError( + f"(data_parallel_size * context_parallel_size) ({derived_dp} * {cp} = {dp_cp}) " + f"must be divisible by expert_parallel_size ({ep})" + ) + # MoE models on multi-GPU don't support tensor parallelism + # in Automodel's MoE parallelizer. See: nemo_automodel/components/moe/parallelizer.py + if ep > 1 and tp > 1 and total_gpus > 1: + raise ValidationError( + f"Tensor parallelism (tensor_parallel_size={tp}) is not supported for MoE models. " + f"When expert_parallel_size > 1 ({ep}), tensor_parallel_size must be 1." + ) + + gb = training.batch_size + mb = training.micro_batch_size + divisor = mb * derived_dp + if gb % divisor != 0: + raise ValidationError( + f"batch_size ({gb}) must be divisible by " + f"micro_batch_size ({mb}) * data_parallel_size ({derived_dp}) = {divisor}. " + f"Consider adjusting batch_size to {divisor * max(1, gb // divisor)} or {divisor * (gb // divisor + 1)}." + ) diff --git a/services/automodel/src/nmp/automodel/app/__init__.py b/services/automodel/src/nmp/automodel/app/__init__.py new file mode 100644 index 0000000000..35a0c9116b --- /dev/null +++ b/services/automodel/src/nmp/automodel/app/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Customizer application module.""" diff --git a/services/automodel/src/nmp/automodel/app/constants.py b/services/automodel/src/nmp/automodel/app/constants.py new file mode 100644 index 0000000000..083498adaf --- /dev/null +++ b/services/automodel/src/nmp/automodel/app/constants.py @@ -0,0 +1,173 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from nmp.common.jobs.constants import DEFAULT_JOB_STORAGE_PATH + +SERVICE_NAME = "customizer" + +# Global default seed for reproducibility +DEFAULT_SEED = 1111 + +# Relative directory names (used as subdirectory names under job storage) +DEFAULT_MODEL_OUTPUT_DIR_NAME = "model" +DEFAULT_DATASET_OUTPUT_DIR_NAME = "dataset" +DEFAULT_TEACHER_MODEL_DIR_NAME = "teacher_model" +DEFAULT_TRAINING_OUTPUT_DIR_NAME = "training" +DEFAULT_OUTPUT_MODEL_DIR_NAME = "output_model" +DEFAULT_TRAINING_RESULT_FILE_NAME = "customizer_training_result.json" + +# Absolute paths (used in PlatformJobSpec for cross-step file sharing via PVC) +DEFAULT_MODEL_PATH = f"{DEFAULT_JOB_STORAGE_PATH}/{DEFAULT_MODEL_OUTPUT_DIR_NAME}" +DEFAULT_DATASET_PATH = f"{DEFAULT_JOB_STORAGE_PATH}/{DEFAULT_DATASET_OUTPUT_DIR_NAME}" +DEFAULT_TEACHER_MODEL_PATH = f"{DEFAULT_JOB_STORAGE_PATH}/{DEFAULT_TEACHER_MODEL_DIR_NAME}" +DEFAULT_TRAINING_OUTPUT_PATH = f"{DEFAULT_JOB_STORAGE_PATH}/{DEFAULT_TRAINING_OUTPUT_DIR_NAME}" +DEFAULT_OUTPUT_MODEL_PATH = f"{DEFAULT_JOB_STORAGE_PATH}/{DEFAULT_OUTPUT_MODEL_DIR_NAME}" + +NMP_JOBS_URL_ENVVAR = "NMP_JOBS_URL" +NMP_FILES_URL_ENVVAR = "NMP_FILES_URL" + +# Models whose checkpoints require transformers-v4-compatible config.json output. +# When v4_compatible is enabled, the original pretrained config.json is preserved +# alongside a config.v5.json so downstream consumers (e.g. vLLM) that expect +# a v4-format config continue to work. +# using frozenset for faster lookup +V4_MODEL_FOR_CAUSAL_LM_MAPPING_NAMES: frozenset[str] = frozenset( + { + "ApertusForCausalLM", + "ArceeForCausalLM", + "AriaTextForCausalLM", + "BambaForCausalLM", + "BartForCausalLM", + "BertLMHeadModel", + "BertGenerationDecoder", + "BigBirdForCausalLM", + "BigBirdPegasusForCausalLM", + "BioGptForCausalLM", + "BitNetForCausalLM", + "BlenderbotForCausalLM", + "BlenderbotSmallForCausalLM", + "BloomForCausalLM", + "BltForCausalLM", + "CamembertForCausalLM", + "LlamaForCausalLM", + "CodeGenForCausalLM", + "CohereForCausalLM", + "Cohere2ForCausalLM", + "CpmAntForCausalLM", + "CTRLLMHeadModel", + "Data2VecTextForCausalLM", + "DbrxForCausalLM", + "DeepseekV2ForCausalLM", + "DeepseekV3ForCausalLM", + "DiffLlamaForCausalLM", + "DogeForCausalLM", + "Dots1ForCausalLM", + "ElectraForCausalLM", + "Emu3ForCausalLM", + "ErnieForCausalLM", + "Ernie4_5ForCausalLM", + "Ernie4_5_MoeForCausalLM", + "Exaone4ForCausalLM", + "FalconForCausalLM", + "FalconH1ForCausalLM", + "FalconMambaForCausalLM", + "FlexOlmoForCausalLM", + "FuyuForCausalLM", + "GemmaForCausalLM", + "Gemma2ForCausalLM", + "Gemma3ForConditionalGeneration", + "Gemma3ForCausalLM", + "Gemma3nForConditionalGeneration", + "Gemma3nForCausalLM", + "GitForCausalLM", + "GlmForCausalLM", + "Glm4ForCausalLM", + "Glm4MoeForCausalLM", + "GotOcr2ForConditionalGeneration", + "GPT2LMHeadModel", + "GPTBigCodeForCausalLM", + "GPTNeoForCausalLM", + "GPTNeoXForCausalLM", + "GPTNeoXJapaneseForCausalLM", + "GptOssForCausalLM", + "GPTJForCausalLM", + "GraniteForCausalLM", + "GraniteMoeForCausalLM", + "GraniteMoeHybridForCausalLM", + "GraniteMoeSharedForCausalLM", + "HeliumForCausalLM", + "HunYuanDenseV1ForCausalLM", + "HunYuanMoEV1ForCausalLM", + "JambaForCausalLM", + "JetMoeForCausalLM", + "Lfm2ForCausalLM", + "Llama4ForCausalLM", + "LongcatFlashForCausalLM", + "MambaForCausalLM", + "Mamba2ForCausalLM", + "MarianForCausalLM", + "MBartForCausalLM", + "MegaForCausalLM", + "MegatronBertForCausalLM", + "MiniMaxForCausalLM", + "MinistralForCausalLM", + "MistralForCausalLM", + "MixtralForCausalLM", + "MllamaForCausalLM", + "ModernBertDecoderForCausalLM", + "MoshiForCausalLM", + "MptForCausalLM", + "MusicgenForCausalLM", + "MusicgenMelodyForCausalLM", + "MvpForCausalLM", + "NemotronForCausalLM", + "OlmoForCausalLM", + "Olmo2ForCausalLM", + "Olmo3ForCausalLM", + "OlmoeForCausalLM", + "OpenLlamaForCausalLM", + "OpenAIGPTLMHeadModel", + "OPTForCausalLM", + "PegasusForCausalLM", + "PersimmonForCausalLM", + "PhiForCausalLM", + "Phi3ForCausalLM", + "Phi4MultimodalForCausalLM", + "PhimoeForCausalLM", + "PLBartForCausalLM", + "ProphetNetForCausalLM", + "QDQBertLMHeadModel", + "Qwen2ForCausalLM", + "Qwen2MoeForCausalLM", + "Qwen3ForCausalLM", + "Qwen3MoeForCausalLM", + "Qwen3NextForCausalLM", + "RecurrentGemmaForCausalLM", + "ReformerModelWithLMHead", + "RemBertForCausalLM", + "RobertaForCausalLM", + "RobertaPreLayerNormForCausalLM", + "RoCBertForCausalLM", + "RoFormerForCausalLM", + "RwkvForCausalLM", + "SeedOssForCausalLM", + "SmolLM3ForCausalLM", + "Speech2Text2ForCausalLM", + "StableLmForCausalLM", + "Starcoder2ForCausalLM", + "TransfoXLLMHeadModel", + "TrOCRForCausalLM", + "VaultGemmaForCausalLM", + "WhisperForCausalLM", + "XGLMForCausalLM", + "XLMWithLMHeadModel", + "XLMProphetNetForCausalLM", + "XLMRobertaForCausalLM", + "XLMRobertaXLForCausalLM", + "XLNetLMHeadModel", + "xLSTMForCausalLM", + "XmodForCausalLM", + "ZambaForCausalLM", + "Zamba2ForCausalLM", + } +) diff --git a/services/automodel/src/nmp/automodel/app/jobs/__init__.py b/services/automodel/src/nmp/automodel/app/jobs/__init__.py new file mode 100644 index 0000000000..e5725ea5a4 --- /dev/null +++ b/services/automodel/src/nmp/automodel/app/jobs/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/services/automodel/src/nmp/automodel/app/jobs/compiler.py b/services/automodel/src/nmp/automodel/app/jobs/compiler.py new file mode 100644 index 0000000000..f43b11906d --- /dev/null +++ b/services/automodel/src/nmp/automodel/app/jobs/compiler.py @@ -0,0 +1,501 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Job compiler - transforms CustomizationJobOutput into PlatformJobSpec.""" + +import logging + +from nemo_platform import AsyncNeMoPlatform, NotFoundError +from nemo_platform.types.models.model_entity import ModelEntity +from nmp.automodel.api.v2.jobs.schemas import ( + CustomizationJobOutput, + DeploymentParams, + DistillationTraining, + LoRAParams, + ValidationError, +) +from nmp.automodel.app.constants import ( + DEFAULT_DATASET_PATH, + DEFAULT_MODEL_PATH, + DEFAULT_OUTPUT_MODEL_PATH, + DEFAULT_TEACHER_MODEL_PATH, +) +from nmp.automodel.app.jobs.file_io.schemas import ( + DownloadItem, + FileIOTaskConfig, + FileSetRef, + UploadItem, +) +from nmp.automodel.app.jobs.model_entity.schemas import ( + DeploymentParameters as ModelEntityDeploymentParameters, +) +from nmp.automodel.app.jobs.model_entity.schemas import ( + ModelEntityTaskConfig, +) +from nmp.automodel.app.jobs.model_entity.schemas import ( + PEFTConfig as ModelEntityPEFTConfig, +) +from nmp.automodel.app.jobs.training.compiler import ( + _extract_model_name, + _resolve_is_embedding_model, + compile_training_step, +) +from nmp.automodel.config import config +from nmp.automodel.entities.values import FinetuningType +from nmp.automodel.images import AUTOMODEL_PYTHON_ENTRYPOINT, get_tasks_image +from nmp.automodel.platform_client import fetch_model_entity +from nmp.common.auth import AuthClient, auth_client_context +from nmp.common.entities.utils import parse_entity_ref +from nmp.common.jobs.api_factory import ( + ContainerSpec, + CPUExecutionProviderSpec, + EnvironmentVariable, + PlatformJobSpec, + PlatformJobStep, + ResourcesLimitsSpec, + ResourcesRequestsSpec, + ResourcesSpec, +) +from nmp.common.jobs.constants import DEFAULT_JOB_STORAGE_PATH, PERSISTENT_JOB_STORAGE_PATH_ENVVAR +from nmp.common.jobs.exceptions import PlatformJobCompilationError + +logger = logging.getLogger(__name__) + + +def _get_cpu_resources() -> ResourcesSpec: + """Get default CPU resources for download/upload tasks.""" + return ResourcesSpec( + limits=ResourcesLimitsSpec( + cpu=config.default_job_resource_cpu_limit, + memory=config.default_job_resource_memory_limit, + ), + requests=ResourcesRequestsSpec( + cpu=config.default_job_resource_cpu_request, + memory=config.default_job_resource_memory_request, + ), + ) + + +def _get_base_environment() -> list[EnvironmentVariable]: + """Get base environment variables for all tasks.""" + return [ + EnvironmentVariable( + name=PERSISTENT_JOB_STORAGE_PATH_ENVVAR, + value=DEFAULT_JOB_STORAGE_PATH, + ), + ] + + +def _extract_model_uri(me: ModelEntity) -> str | None: + """Extract model_uri from the model entity. + + Args: + me: The model entity. + + Returns: + The fileset string if available, None otherwise. + """ + return me.fileset if me.fileset else None + + +def _require_fileset_for_download(fileset_name: str | None, entity_label: str) -> str: + """Require a platform fileset reference for checkpoint download.""" + if not fileset_name or not str(fileset_name).strip(): + raise PlatformJobCompilationError( + f"{entity_label} has no fileset. " + "Attach a platform FileSet (workspace/name) with model weights before running training.", + ) + return str(fileset_name) + + +def _append_download_if_present( + downloads: list[DownloadItem], + fileset_name: str | None, + dest: str, + field_name: str, +) -> None: + """Append a download item if a FileSet ref is present.""" + if not fileset_name: + return + fileset = FileSetRef.model_validate(fileset_name) + downloads.append(DownloadItem(src=fileset, dest=dest)) + logger.info(f"Detected {field_name} FileSet reference: {fileset}") + + +def _build_file_download_config( + job_spec: CustomizationJobOutput, + me: ModelEntity, + teacher_me: ModelEntity | None = None, +) -> FileIOTaskConfig: + """Build the configuration for the file_io task. + + Extracts FileSet references from model_uri and dataset fields. + Fileset refs use workspace/name or name (optional legacy fileset:// prefix is stripped). + + Args: + job_spec: The customization job output specification. + me: The model entity being trained. + teacher_me: Optional teacher model entity for knowledge distillation jobs. + + Returns: + FileIOTaskConfig with download items for any fileset refs found. + + """ + downloads: list[DownloadItem] = [] + + model_fileset = _require_fileset_for_download( + _extract_model_uri(me), + entity_label=f"Model '{me.workspace}/{me.name}'", + ) + _append_download_if_present( + downloads, + fileset_name=model_fileset, + dest=DEFAULT_MODEL_PATH, + field_name="model", + ) + _append_download_if_present( + downloads, + fileset_name=job_spec.dataset, + dest=DEFAULT_DATASET_PATH, + field_name="dataset", + ) + + if teacher_me is not None: + teacher_fileset = _require_fileset_for_download( + _extract_model_uri(teacher_me), + entity_label=f"Teacher model '{teacher_me.workspace}/{teacher_me.name}'", + ) + _append_download_if_present( + downloads, + fileset_name=teacher_fileset, + dest=DEFAULT_TEACHER_MODEL_PATH, + field_name="teacher_model", + ) + + return FileIOTaskConfig(download=downloads) + + +def _build_output_fileset_metadata(me: ModelEntity) -> dict | None: + """Build tool_calling metadata to propagate to the output fileset. + + Extracts chat_template and tool_call_config from the source model entity's spec + so the model-spec-runner will apply them to the output model entity. + + Returns: + A dict like {"tool_calling": {...}} suitable for fileset metadata, or None + if there is nothing to propagate. + """ + if me.spec is None: + return None + + tool_calling: dict = {} + + if me.spec.chat_template: + tool_calling["chat_template"] = me.spec.chat_template + + if me.spec.tool_call_config: + tcc = me.spec.tool_call_config + if tcc.tool_call_parser: + tool_calling["tool_call_parser"] = tcc.tool_call_parser + if tcc.tool_call_plugin: + tool_calling["tool_call_plugin"] = tcc.tool_call_plugin + if tcc.auto_tool_choice is not None: + tool_calling["auto_tool_choice"] = tcc.auto_tool_choice + + return {"tool_calling": tool_calling} if tool_calling else None + + +def _build_file_upload_config( + output_fileset_name: str, + fileset_metadata: dict | None = None, +) -> FileIOTaskConfig: + """Build the configuration for the file_io upload task with a generated fileset name. + + The fileset name is generated at compile time and will be combined with + the job's workspace at runtime to form the full FileSet reference. + + Args: + output_fileset_name: The generated name for the output FileSet. + fileset_metadata: Optional metadata to set on the output fileset (e.g., tool_calling + config propagated from the source model entity). + + Returns: + FileIOTaskConfig with upload items configured to use the generated name. + """ + return FileIOTaskConfig( + upload=[ + UploadItem( + src=DEFAULT_OUTPUT_MODEL_PATH, + # workspace is None because at this layer, we don't know the job's workspace. + dest=FileSetRef(workspace=None, name=output_fileset_name), + metadata=fileset_metadata, + ) + ], + ) + + +def _build_model_entity_config( + workspace: str, job_spec: CustomizationJobOutput, trust_remote_code: bool = False +) -> ModelEntityTaskConfig: + """Build the configuration for the model_entity task. + + Args: + workspace: The workspace for this job. + job_spec: The customization job input specification. + trust_remote_code: Whether to trust remote code for the checkpoint. + + Returns: + ModelEntityTaskConfig with model entity creation settings. + """ + base_model = _extract_model_name(job_spec) + + assert job_spec.output is not None, "output must be set by input-to-output transformer" + training = job_spec.training + + peft_config: ModelEntityPEFTConfig | None = None + if isinstance(training.peft, LoRAParams): + peft_config = ModelEntityPEFTConfig( + type=training.finetuning_type, + alpha=training.peft.alpha, + rank=training.peft.rank, + ) + + # Only forward the user-supplied deployment_config from the job spec. + # tool_call_config from the *source* model entity's spec is propagated + # separately via fileset metadata (see _build_output_fileset_metadata), + # so we intentionally do not merge it here. + deployment_config: str | ModelEntityDeploymentParameters | None = None + if isinstance(job_spec.deployment_config, str): + deployment_config = job_spec.deployment_config + elif job_spec.deployment_config is not None: + deployment_config = ModelEntityDeploymentParameters.model_validate(job_spec.deployment_config.model_dump()) + + return ModelEntityTaskConfig( + name=job_spec.output.name, + workspace=workspace, + description="Customized model from job", + fileset=FileSetRef( + workspace=None, + name=job_spec.output.fileset, + ), + base_model=base_model, + model_entity=job_spec.model, + peft=peft_config, + trust_remote_code=trust_remote_code, + deployment_config=deployment_config, + ) + + +async def _resolve_deployment_config_ref( + config_ref: str, + workspace: str, + sdk: AsyncNeMoPlatform, +): + """Resolve a ``name`` or ``workspace/name`` string to a ModelDeploymentConfig.""" + ref = parse_entity_ref(config_ref, default_workspace=workspace) + try: + return await sdk.inference.deployment_configs.retrieve(name=ref.name, workspace=ref.workspace) + except NotFoundError as e: + raise PlatformJobCompilationError( + f"deployment_config references '{config_ref}' which does not exist in workspace '{ref.workspace}'." + ) from e + except Exception as e: + raise PlatformJobCompilationError(f"Failed to resolve deployment_config '{config_ref}': {e}") from e + + +async def _validate_deployment_config( + workspace: str, + transformed_spec: CustomizationJobOutput, + sdk: AsyncNeMoPlatform, + auth_client: AuthClient, +) -> None: + """Validate deployment_config consistency before training starts. + + Catches contradictory or impossible configurations early so the user + gets a clear error instead of a silent failure after expensive training. + """ + dc = transformed_spec.deployment_config + if dc is None: + return + + # Inline deployment params: check permission-gated fields. + if isinstance(dc, DeploymentParams): + tcc = dc.tool_call_config + if tcc and tcc.tool_call_plugin: + if not await auth_client.has_permissions(workspace, ["models.tool-call-plugin.set"]): + raise PlatformJobCompilationError( + "Insufficient permissions to set tool_call_plugin. " + "Requires the models.tool-call-plugin.set permission." + ) + return + + # String reference to an existing deployment config: validate consistency. + if not isinstance(dc, str): + return + + ft_type = transformed_spec.training.finetuning_type + is_lora = ft_type == FinetuningType.LORA + produces_new_model = ft_type in (FinetuningType.ALL_WEIGHTS, FinetuningType.LORA_MERGED) + resolved_config = await _resolve_deployment_config_ref(dc, workspace, sdk) + + # LoRA job referencing a config that has lora_enabled=False + if is_lora and resolved_config.nim_deployment and resolved_config.nim_deployment.lora_enabled is False: + raise PlatformJobCompilationError( + f"deployment_config references '{dc}' which has lora_enabled=false, " + "but this is a LoRA training job. The deployment would not load LoRA adapters. " + "Use a deployment config with lora_enabled=true, or provide inline deployment parameters." + ) + + # SFT or lora_merged referencing a string config + if produces_new_model: + output_name = transformed_spec.output.name + try: + existing_me = await sdk.models.retrieve(name=output_name, workspace=workspace) + except NotFoundError: + # Output model entity doesn't exist yet, so a string + # ref is inherently invalid -- it was created for a different model. + raise PlatformJobCompilationError( + f"deployment_config cannot be a string reference ('{dc}') for {ft_type.value} training " + "that creates a new model entity. The referenced config was created for a different model. " + "Use inline deployment parameters (e.g., DeploymentParams(gpu=1, lora_enabled=True)) instead." + ) + + # Output model entity already exists (retraining to create a new FileSet). + # Verify the config actually targets this model entity. + nim = resolved_config.nim_deployment + config_targets_model = (resolved_config.model_entity_id == f"{existing_me.workspace}/{existing_me.name}") or ( + nim and nim.model_name == existing_me.name and nim.model_namespace == existing_me.workspace + ) + if not config_targets_model: + raise PlatformJobCompilationError( + f"deployment_config references '{dc}' which targets a different model entity " + f"than the output model '{existing_me.workspace}/{existing_me.name}'. " + "The deployment config must target the same model entity being retrained, " + "or use inline deployment parameters instead." + ) + + +async def platform_job_config_compiler( + workspace: str, + job_spec: CustomizationJobOutput, + sdk: AsyncNeMoPlatform, +) -> PlatformJobSpec: + """Compile canonical job spec into a four-step PlatformJobSpec.""" + transformed_spec = job_spec + logger.info("Compiling Automodel job to PlatformJobSpec: %s", transformed_spec.model_dump_json(indent=2)) + + try: + transformed_spec.validate_for_training() + except ValidationError as e: + raise PlatformJobCompilationError(str(e)) from e + + # output is a required field in CustomizationJobOutput + cpu_resources = _get_cpu_resources() + base_env = _get_base_environment() + + # Fetch the primary model entity + me = await fetch_model_entity(transformed_spec.model, workspace, sdk) + + # For distillation jobs, also fetch the teacher model entity + teacher_me: ModelEntity | None = None + if isinstance(transformed_spec.training, DistillationTraining): + try: + teacher_me = await fetch_model_entity(transformed_spec.training.teacher_model, workspace, sdk) + except ValueError as e: + raise PlatformJobCompilationError( + f"Teacher model '{transformed_spec.training.teacher_model}' not found. " + "Verify the teacher model entity exists." + ) from e + except PermissionError as e: + raise PlatformJobCompilationError( + f"Access denied to teacher model '{transformed_spec.training.teacher_model}'." + ) from e + + if transformed_spec.deployment_config is not None: + auth_client = auth_client_context.get() + if auth_client is None: + raise PlatformJobCompilationError( + "No auth context available; cannot validate deployment config permissions.", + ) + await _validate_deployment_config(workspace, transformed_spec, sdk, auth_client) + + file_io_download_config = _build_file_download_config(transformed_spec, me, teacher_me) + is_embedding_model_flag = _resolve_is_embedding_model(me) + + # The embedding NIM requires ONNX format, which cannot represent standalone LoRA adapters. + # LoRA with merge=True (lora_merged) is allowed because it produces a full-weight model after training. + if is_embedding_model_flag and transformed_spec.training.finetuning_type == FinetuningType.LORA: + raise PlatformJobCompilationError( + "NeMo Platform does not support unmerged LoRA for embedding models because the embedding NIM requires ONNX format, " + "which cannot represent standalone adapters. " + "Use peft with merge=True (lora_merged) or omit peft for all_weights training." + ) + + # Extract chat_template and tool_call_config from the source model entity's spec + # (populated from fileset metadata by the model-spec-runner background task). + # These are propagated to: + # 1. The training step config (chat_template takes highest priority in template resolution) + # 2. The output fileset metadata (so the model-spec-runner sets them on the output model) + fileset_metadata = _build_output_fileset_metadata(me) + file_io_upload_config = _build_file_upload_config(transformed_spec.output.fileset, fileset_metadata) + + # Build model_entity config for creating the model entity + trust_remote_code = me.trust_remote_code or False + model_entity_config = _build_model_entity_config(workspace, transformed_spec, trust_remote_code) + + steps = [ + # Step 1: Download model and dataset files from Files service + PlatformJobStep( + name="model-and-dataset-download", + executor=CPUExecutionProviderSpec( + provider="cpu", + container=ContainerSpec( + image=get_tasks_image(), + entrypoint=AUTOMODEL_PYTHON_ENTRYPOINT, + command=["-m", "nmp.automodel.tasks.file_io"], + ), + resources=cpu_resources, + ), + environment=base_env, + config=file_io_download_config.model_dump(mode="json"), + ), + # Step 2: Training job + compile_training_step( + transformed_spec, + base_env, + me, + teacher_me=teacher_me, + ), + # Step 3: Upload customized model + PlatformJobStep( + name="model-upload", + executor=CPUExecutionProviderSpec( + provider="cpu", + container=ContainerSpec( + image=get_tasks_image(), + entrypoint=AUTOMODEL_PYTHON_ENTRYPOINT, + command=["-m", "nmp.automodel.tasks.file_io"], + ), + resources=cpu_resources, + ), + environment=base_env, + config=file_io_upload_config.model_dump(mode="json"), + ), + # Step 4: Create model entity + PlatformJobStep( + name="model-entity-creation", + executor=CPUExecutionProviderSpec( + provider="cpu", + container=ContainerSpec( + image=get_tasks_image(), + entrypoint=AUTOMODEL_PYTHON_ENTRYPOINT, + command=["-m", "nmp.automodel.tasks.model_entity"], + ), + resources=cpu_resources, + ), + environment=base_env, + config=model_entity_config.model_dump(mode="json"), + ), + ] + + return PlatformJobSpec(steps=steps) diff --git a/services/automodel/src/nmp/automodel/app/jobs/context.py b/services/automodel/src/nmp/automodel/app/jobs/context.py new file mode 100644 index 0000000000..4987dfe6b7 --- /dev/null +++ b/services/automodel/src/nmp/automodel/app/jobs/context.py @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import os +from dataclasses import dataclass +from pathlib import Path +from typing import Self + +from nmp.automodel.app.constants import ( + DEFAULT_JOB_STORAGE_PATH, + NMP_FILES_URL_ENVVAR, + NMP_JOBS_URL_ENVVAR, +) +from nmp.common.entities.constants import DEFAULT_WORKSPACE +from nmp.common.jobs.constants import ( + DEFAULT_NEMO_JOB_STEP_CONFIG_FILE_PATH, + NEMO_JOB_ATTEMPT_ID_ENVVAR, + NEMO_JOB_ID_ENVVAR, + NEMO_JOB_STEP_CONFIG_FILE_PATH_ENVVAR, + NEMO_JOB_STEP_ENVVAR, + NEMO_JOB_TASK_ENVVAR, + NEMO_JOB_WORKSPACE_ENVVAR, + PERSISTENT_JOB_STORAGE_PATH_ENVVAR, +) + +DEFAULT_JOB_ID = "unknown-job-id" +DEFAULT_ATTEMPT_ID = "attempt-0" +DEFAULT_STEP = "unknown-step" +DEFAULT_TASK = "unknown-task" + + +# Jobs task names should comply with NAME_PATTERN of EntityCreateInput.name for the Jobs API. +# Generated tasks in k8s don't start with a lowercase letter per NAME_PATTERN, so we normalize +# by adding the prefix when missing. +# In Docker environment core/jobs/src/nmp/core/jobs/controllers/backends/docker.py, +# tasks are prefixed with `task-` by default: task_id = f"task-{uuid.uuid4().hex}" +def _normalize_task_name(task: str) -> str: + """Ensure task name uses the expected Jobs prefix.""" + if task.startswith("task-"): + return task + return f"task-{task}" + + +@dataclass(frozen=True) +class NMPJobContext: + """NeMo Platform Job context populated from Job Controller environment variables""" + + workspace: str + job_id: str + attempt_id: str + step: str + task: str + + # Service URLs + jobs_url: str | None + files_url: str | None + + # Storage paths + storage_path: Path + config_path: Path + + @property + def normalized_task(self) -> str: + """Task normalized for Jobs API compatibility.""" + return _normalize_task_name(self.task) + + @classmethod + def from_env(cls) -> Self: + """Create a NMPJobContext from environment variables""" + return cls( + workspace=os.environ.get(NEMO_JOB_WORKSPACE_ENVVAR, DEFAULT_WORKSPACE), + job_id=os.environ.get(NEMO_JOB_ID_ENVVAR, DEFAULT_JOB_ID), + attempt_id=os.environ.get(NEMO_JOB_ATTEMPT_ID_ENVVAR, DEFAULT_ATTEMPT_ID), + step=os.environ.get(NEMO_JOB_STEP_ENVVAR, DEFAULT_STEP), + task=os.environ.get(NEMO_JOB_TASK_ENVVAR, DEFAULT_TASK), + jobs_url=os.environ.get(NMP_JOBS_URL_ENVVAR), + files_url=os.environ.get(NMP_FILES_URL_ENVVAR), + storage_path=Path(os.environ.get(PERSISTENT_JOB_STORAGE_PATH_ENVVAR, DEFAULT_JOB_STORAGE_PATH)), + config_path=Path( + os.environ.get(NEMO_JOB_STEP_CONFIG_FILE_PATH_ENVVAR, DEFAULT_NEMO_JOB_STEP_CONFIG_FILE_PATH) + ), + ) diff --git a/services/automodel/src/nmp/automodel/app/jobs/file_io/schemas.py b/services/automodel/src/nmp/automodel/app/jobs/file_io/schemas.py new file mode 100644 index 0000000000..c6a214fcc7 --- /dev/null +++ b/services/automodel/src/nmp/automodel/app/jobs/file_io/schemas.py @@ -0,0 +1,182 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import dataclass +from enum import StrEnum +from typing import Optional + +from pydantic import BaseModel, Field, model_validator + +FILESET_PROTOCOL = "fileset://" + + +class TaskStatus(StrEnum): + """Status of a file I/O task.""" + + RUNNING = "running" + COMPLETED = "completed" + ERROR = "error" + + +class TaskPhase(StrEnum): + """Phase of a file I/O task.""" + + DOWNLOADING = "downloading" + UPLOADING = "uploading" + COMPLETED = "completed" + + +class FileSetRef(BaseModel): + """Reference to a FileSet.""" + + # workspace is optional because at compile time, the workspace is not known. + # None tells the file_io task to use the job's workspace from the NMPJobContext. + workspace: Optional[str] = None + name: str + + def __str__(self) -> str: + if self.workspace is None: + return f"{self.name}" + return f"{self.workspace}/{self.name}" + + def __repr__(self) -> str: + return f"FileSetRef(workspace={self.workspace}, name={self.name})" + + @classmethod + def _parse_string_parts(cls, ref: str) -> tuple[Optional[str], str] | None: + """Parse a FileSet reference string into a tuple of workspace and name.""" + if len(ref) == 0: + return None + if ref.startswith(FILESET_PROTOCOL): + ref = ref[len(FILESET_PROTOCOL) :] + parts = ref.split("/", 1) + if len(parts) == 1: + return None, parts[0] + if len(parts) == 2: + return parts[0], parts[1] + return None + + @classmethod + def extract_name(cls, ref: str) -> str: + """Extract the fileset/entity name from a reference string. + + Supports: + - workspace/name + - name + - fileset://workspace/name (legacy, stripped) + """ + return cls.model_validate(ref).name + + @model_validator(mode="before") + @classmethod + def _convert_string_input(cls, v: str) -> dict: + """Convert a FileSet reference string into a dict of workspace and name. + + This makes it possible to create a FileSetRef from a string directly. + """ + if isinstance(v, str): + result = cls._parse_string_parts(v) + if result is None: + raise ValueError(f"Invalid FileSet reference: {v}. Expected format: workspace/name") + workspace, name = result + return {"workspace": workspace, "name": name} + return v + + +class DownloadItem(BaseModel): + """Configures a single download: fileset -> local path. + + Note: dest is an absolute path where files will be downloaded. + This path should be under the job's shared storage (e.g., /var/run/scratch/job/model). + """ + + src: FileSetRef = Field( + description="FileSet reference for the source files. " + "Accepts 'workspace/name' or 'name' (job workspace used when omitted)." + ) + dest: str = Field( + default=".", description="Absolute destination path for downloaded files (e.g., '/var/run/scratch/job/model')." + ) + + +class UploadItem(BaseModel): + """Configures a single upload: local path -> fileset.""" + + src: str = Field( + description="Absolute source path for files to upload (e.g., '/var/run/scratch/job/output_model')." + ) + dest: FileSetRef = Field( + description="FileSet reference for the destination. " + "Accepts 'workspace/name' or 'name' (job workspace used when omitted)." + ) + metadata: Optional[dict] = Field( + default=None, + description="Optional metadata to set on the created fileset (e.g., tool_calling config " + "propagated from the source model entity).", + ) + + +class FileIOTaskConfig(BaseModel): + """Configuration for the file_io task. + + Used when running: python -m nmp.automodel.tasks.file_io + """ + + download: list[DownloadItem] = Field(default_factory=list, description="List of FileSets to download.") + upload: list[UploadItem] = Field(default_factory=list, description="List of files to upload to FileSets.") + + +class TaskCompilationError(Exception): + """Error compiling a task configuration.""" + + pass + + +class FileDownloadError(Exception): + """Error downloading files from Files service.""" + + pass + + +class FileUploadError(Exception): + """Error uploading files to Files service.""" + + pass + + +class ProgressReportError(Exception): + """Error reporting progress to the Jobs service.""" + + pass + + +class PathTraversalError(ValueError): + """Error when a path attempts to escape the allowed base directory. + + This is a security error raised when user-provided paths like '../..' would + result in file operations outside the designated storage directory. + """ + + pass + + +@dataclass +class FileStats: + """Statistics for a file operation.""" + + total_bytes: int = 0 + failed_files: int = 0 + + +@dataclass +class DownloadStats(FileStats): + """Statistics for a download operation.""" + + files_downloaded: int = 0 + + +@dataclass +class UploadStats(FileStats): + """Statistics for a upload operation.""" + + files_uploaded: int = 0 diff --git a/services/automodel/src/nmp/automodel/app/jobs/model_entity/__init__.py b/services/automodel/src/nmp/automodel/app/jobs/model_entity/__init__.py new file mode 100644 index 0000000000..c5ddfda4d4 --- /dev/null +++ b/services/automodel/src/nmp/automodel/app/jobs/model_entity/__init__.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Model entity job configuration.""" + +from .schemas import ModelEntityCreationError, ModelEntityTaskConfig + +__all__ = [ + "ModelEntityCreationError", + "ModelEntityTaskConfig", +] diff --git a/services/automodel/src/nmp/automodel/app/jobs/model_entity/schemas.py b/services/automodel/src/nmp/automodel/app/jobs/model_entity/schemas.py new file mode 100644 index 0000000000..b2cd122d23 --- /dev/null +++ b/services/automodel/src/nmp/automodel/app/jobs/model_entity/schemas.py @@ -0,0 +1,106 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Schemas for the model_entity task configuration.""" + +from typing import Optional + +from nmp.automodel.app.jobs.file_io.schemas import FileSetRef +from nmp.automodel.entities.values import FinetuningType +from pydantic import BaseModel, Field + + +class ToolCallConfig(BaseModel): + """Tool calling configuration for NIM deployments.""" + + tool_call_parser: Optional[str] = Field(default=None, description="Name of the tool call parser to use.") + tool_call_plugin: Optional[str] = Field( + default=None, + pattern=r"^[\w\-.]+/[\w\-.]+$", + description="Reference to a fileset containing the custom tool call plugin Python file. " + "Expected format: '{workspace}/{fileset_name}'.", + ) + auto_tool_choice: Optional[bool] = Field(default=None, description="Whether to enable automatic tool choice.") + + +class DeploymentParameters(BaseModel): + """Inline deployment parameters for creating a new ModelDeploymentConfig.""" + + gpu: int = Field(default=1, description="Number of GPUs required for deployment") + additional_envs: Optional[dict[str, str]] = Field( + default=None, + description="Additional environment variables for deployment", + ) + disk_size: Optional[str] = Field(default=None, description="Disk size for deployment") + image_name: Optional[str] = Field( + default=None, + description="Container image name from NGC. Defaults to multi-llm when unset", + ) + image_tag: Optional[str] = Field(default=None, description="Container image tag from NGC") + lora_enabled: bool = Field( + default=True, + description=( + "When auto-deploying full SFT training, setting this true allows " + "subsequent LoRA adapters to be deployed against the model." + ), + ) + tool_call_config: Optional[ToolCallConfig] = Field( + default=None, + description="Tool calling configuration override for the NIM deployment.", + ) + + +class PEFTConfig(BaseModel): + """PEFT configuration for LoRA and LoRA-merged fine-tuning.""" + + type: FinetuningType + rank: int + alpha: int + + +class ModelEntityTaskConfig(BaseModel): + """Configuration for the model_entity task. + + Used when running: python -m nmp.automodel.tasks.model_entity + """ + + name: str = Field( + description="Name of the model entity to create", + ) + workspace: str = Field( + description="Workspace of the model entity to create", + ) + description: Optional[str] = Field( + default=None, + description="Optional description of the model", + ) + fileset: FileSetRef = Field( + description="FileSet reference containing the customized model artifacts", + ) + model_entity: str = Field(..., description="The model entity this model was based on.") + base_model: Optional[str] = Field( + default=None, + description="Link to the base model used for customization", + ) + peft: Optional[PEFTConfig] = Field( + default=None, + description="PEFT configuration. Set for LoRA/LoRA-merged, None for full SFT.", + ) + + trust_remote_code: bool = Field( + default=False, + description="Whether to trust remote code for the checkpoint, propagated from the source model entity.", + ) + + deployment_config: Optional[str | DeploymentParameters] = Field( + default=None, + description="Deployment configuration. A string references an existing ModelDeploymentConfig " + "by name. An object provides inline NIM deployment parameters. " + "Omit to skip deployment.", + ) + + +class ModelEntityCreationError(Exception): + """Error creating model entity.""" + + pass diff --git a/services/automodel/src/nmp/automodel/app/jobs/training/compiler.py b/services/automodel/src/nmp/automodel/app/jobs/training/compiler.py new file mode 100644 index 0000000000..1af931f92e --- /dev/null +++ b/services/automodel/src/nmp/automodel/app/jobs/training/compiler.py @@ -0,0 +1,399 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Training step compiler.""" + +import logging + +from nemo_platform.types.models.model_entity import ModelEntity +from nmp.automodel.api.v2.jobs.schemas import ( + AnyTraining, + CustomizationJobOutput, + DistillationTraining, + LoRAParams, + MLflowParams, + WandBParams, +) +from nmp.automodel.app.constants import ( + DEFAULT_DATASET_PATH, + DEFAULT_MODEL_PATH, + DEFAULT_TEACHER_MODEL_PATH, + V4_MODEL_FOR_CAUSAL_LM_MAPPING_NAMES, +) +from nmp.automodel.app.jobs.training.schemas import ( + DistillationConfig, + LoRAConfig, + MLflowConfig, + ModelConfig, + TrainingStepConfig, + WandBConfig, +) +from nmp.automodel.config import config +from nmp.automodel.entities.values import Precision, TrainingType +from nmp.automodel.images import AUTOMODEL_PYTHON_ENTRYPOINT, get_training_image +from nmp.common.jobs.api_factory import ( + ContainerSpec, + DistributedGPUExecutionProviderSpec, + EnvironmentVariable, + EnvironmentVariableFromSecret, + GPUExecutionProviderSpec, + PlatformJobStep, + ResourcesSpec, + StepLifecycle, +) +from nmp.common.model_utils import is_embedding_model + +logger = logging.getLogger(__name__) + + +def _resolve_is_embedding_model(me: ModelEntity) -> bool: + """Resolve embedding flag while preserving compatibility with legacy specs.""" + if me.spec is None: + return is_embedding_model(me.name) + + # Do not rely on `me.spec is not None` alone: + # older persisted ModelSpec payloads may not include `is_embedding_model`. + # Pydantic fills missing fields with the default (False), which would + # incorrectly classify legacy embedding models as LLMs. + model_fields_set = getattr(me.spec, "model_fields_set", getattr(me.spec, "__fields_set__", set())) + if "is_embedding_model" not in model_fields_set: + return is_embedding_model(me.name) + + return me.spec.is_embedding_model or False + + +def _resolve_v4_compatible(me: ModelEntity) -> bool: + """Check if the model requires transformers-v4-compatible checkpoint output.""" + if me.spec is None: + return False + checkpoint_model_name = getattr(me.spec, "checkpoint_model_name", None) + is_v4_compatible = checkpoint_model_name in V4_MODEL_FOR_CAUSAL_LM_MAPPING_NAMES + logger.info(f"Checkpoint model name {checkpoint_model_name} is v4 compatible: {is_v4_compatible}") + return is_v4_compatible + + +def _resolve_custom_implementation_override(me: ModelEntity) -> bool: + if me.spec is None: + return False + + checkpoint_model_name = getattr(me.spec, "checkpoint_model_name", None) + if checkpoint_model_name == "NemotronHForCausalLM" and getattr(me.spec, "moe_config", None) is None: + # V2 Model is being used, v3 uses MoE - However V2 gets recognized as V3 and fails + return True + + if ( + checkpoint_model_name == "MistralForCausalLM" + and getattr(me.spec, "family", None) == "mistral" + and getattr(me.spec, "is_chat", False) + ): + # Mistral 7b v0.3 Instruct has the custom tokenizer implementation fail with: + """2026-03-02 18:35:51 | INFO | root | Using model config to instantiate tokenizer + 2026-03-02 18:35:53 | INFO | nemo_automodel._transformers.auto_tokenizer | Using custom tokenizer MistralCommonBackend for model type 'mistral' + 2026-03-02 18:35:53 | WARNING | nemo_automodel._transformers.tokenization.tokenization_mistral_common | Multiple tokenizer files found in directory: /var/run/scratch/job/model. Using tokenizer.model.v3. + Instantiation failed for `ColumnMappedTextInstructionDataset` + Accepted signature : (path_or_dataset_id: Union[str, List[str]], column_mapping: Dict[str, str], tokenizer, *, split: Optional[str] = 'train', name: Optional[str] = None, answer_only_loss_mask: bool = True, seq_length: Optional[int] = None, padding: Union[str, bool] = 'do_not_pad', truncation: Union[str, bool] = 'do_not_truncate', limit_dataset_samples: Optional[int] = None, use_hf_chat_template: bool = False) -> None + Positional args : () + Keyword args : { 'answer_only_loss_mask': True, + 'column_mapping': {'answer': 'completion', 'question': 'prompt'}, + 'padding': 'do_not_pad', + 'path_or_dataset_id': '/run/scratch/job/training/dataset/train.jsonl', + 'seq_length': 1024, + 'split': 'train', + 'tokenizer': '******', + 'truncation': 'longest_first'} + Exception : piece id is out of range. + """ + return True + + return False + + +def compile_training_step( + job_spec: CustomizationJobOutput, + base_env: list[EnvironmentVariable], + me: ModelEntity, + teacher_me: ModelEntity | None = None, +) -> PlatformJobStep: + """Compile job input to a PlatformJobStep for training. + + Args: + job_spec: The customization job output specification. + base_env: Base environment variables for the job step. + me: The model entity being trained. + teacher_me: Optional teacher model entity for knowledge distillation jobs. + + """ + job_spec.validate_for_training() + if TrainingType(job_spec.training.type) == TrainingType.DPO: + raise ValueError("DPO training is not supported by nmp-automodel") + trust_remote_code = me.trust_remote_code or False + chat_template = me.spec.chat_template if me.spec else None + is_embedding_model = _resolve_is_embedding_model(me) + override_custom_impl = _resolve_custom_implementation_override(me) + v4_compatible = _resolve_v4_compatible(me) + training = job_spec.training + p = training.parallelism + num_gpus_per_node = p.num_gpus_per_node + + training_config = TrainingStepConfig( + model=_translate_model_config( + job_spec, + DEFAULT_MODEL_PATH, + trust_remote_code=trust_remote_code, + is_embedding_model=is_embedding_model, + chat_template=chat_template, + override_custom_impl=override_custom_impl, + v4_compatible=v4_compatible, + ), + dataset=TrainingStepConfig.DatasetConfig( + path=DEFAULT_DATASET_PATH, + ), + training=_translate_training_config(training, me, teacher_me=teacher_me), + schedule=TrainingStepConfig.ScheduleConfig( + epochs=training.epochs, + max_steps=training.max_steps, + val_check_interval=training.val_check_interval, + ), + batch=TrainingStepConfig.BatchConfig( + global_batch_size=training.batch_size, + micro_batch_size=training.micro_batch_size, + sequence_packing=training.sequence_packing, + ), + optimizer=TrainingStepConfig.OptimizerConfig( + learning_rate=training.learning_rate, + min_learning_rate=training.min_learning_rate, + weight_decay=training.weight_decay, + beta1=training.adam_beta1, + beta2=training.adam_beta2, + warmup_steps=training.warmup_steps, + ), + parallelism=TrainingStepConfig.ParallelismConfig( + num_nodes=p.num_nodes, + num_gpus_per_node=num_gpus_per_node, + tensor_parallel_size=p.tensor_parallel_size, + pipeline_parallel_size=p.pipeline_parallel_size, + context_parallel_size=p.context_parallel_size, + expert_parallel_size=p.expert_parallel_size, + sequence_parallel=p.sequence_parallel, + ), + integrations=_translate_integrations(job_spec), + output_model=job_spec.output.name, + ) + + container = ContainerSpec( + image=_get_training_image(), + entrypoint=AUTOMODEL_PYTHON_ENTRYPOINT, + command=["-m", "nmp.automodel.tasks.training"], + ) + + profile = ( + training.execution_profile + if training.execution_profile is not None + else config.default_training_execution_profile + ) + + if p.num_nodes > 1: + logger.debug(f"Using distributed GPU executor: num_nodes={p.num_nodes}, num_gpus_per_node={num_gpus_per_node}") + executor = DistributedGPUExecutionProviderSpec( + provider="gpu_distributed", + profile=profile, + container=container, + resources=ResourcesSpec( + num_gpus=num_gpus_per_node, + num_nodes=p.num_nodes, + ), + ) + else: + logger.debug(f"Using single-node GPU executor: num_gpus={num_gpus_per_node}") + executor = GPUExecutionProviderSpec( + provider="gpu", + profile=profile, + container=container, + resources=ResourcesSpec( + num_gpus=num_gpus_per_node, + ), + ) + + secret_envs = _collect_integration_secret_envs(job_spec) + + return PlatformJobStep( + name="customization-training-job", + executor=executor, + environment=[*base_env, *secret_envs, EnvironmentVariable(name="HF_DATASETS_OFFLINE", value="1")], + config=training_config.model_dump(mode="json"), + lifecycle=StepLifecycle(staleness_timeout_seconds=config.training_staleness_timeout_seconds), + ) + + +def _translate_model_config( + job_spec: CustomizationJobOutput, + path: str, + trust_remote_code: bool = False, + is_embedding_model: bool = False, + chat_template: str | None = None, + override_custom_impl: bool = False, + v4_compatible: bool = False, +) -> ModelConfig: + """Translate job spec to internal ModelConfig.""" + training = job_spec.training + return ModelConfig( + path=path, + name=_extract_model_name(job_spec), + max_seq_length=training.max_seq_length, + precision=training.precision, + trust_remote_code=trust_remote_code, + is_embedding_model=is_embedding_model, + chat_template=chat_template, + override_custom_impl=override_custom_impl, + v4_compatible=v4_compatible, + ) + + +def _translate_training_config( + training: AnyTraining, + me: ModelEntity, + teacher_me: ModelEntity | None = None, +) -> TrainingStepConfig.TrainingConfig: + """Translate API training method to internal TrainingConfig. + + Args: + training: The API training configuration. + me: The primary model entity. + teacher_me: Teacher model entity, populated for distillation jobs. + """ + training_type = TrainingType(training.type) + lora = _translate_lora_config(training.peft, me) if isinstance(training.peft, LoRAParams) else None + + kd = None + if isinstance(training, DistillationTraining): + teacher_trust_remote_code = (teacher_me.trust_remote_code or False) if teacher_me else False + kd = DistillationConfig( + teacher_model=ModelConfig( + path=DEFAULT_TEACHER_MODEL_PATH, + name=training.teacher_model, + precision=Precision(training.teacher_precision), + trust_remote_code=teacher_trust_remote_code, + ), + ratio=training.distillation_ratio, + temperature=training.distillation_temperature, + ) + + return TrainingStepConfig.TrainingConfig( + training_type=training_type, + finetuning_type=training.finetuning_type, + lora=lora, + kd=kd, + ) + + +def _translate_lora_config(api_lora: LoRAParams, me: ModelEntity) -> LoRAConfig: + """Translate API LoRAConfig to internal LoRAConfig.""" + lora = LoRAConfig( + rank=api_lora.rank, + alpha=api_lora.alpha, + dropout=api_lora.dropout, + target_modules=api_lora.target_modules, + use_triton=True, + ) + + if not lora.target_modules: + if me.spec and me.spec.checkpoint_model_name == "NemotronHForCausalLM": + # Need to remove out_proj from the list of target modules + modules = set() + if me.spec.linear_layers: + for ll in me.spec.linear_layers: + m = ll.name.split(".")[-1] + if m.endswith("proj"): + modules.add(f"*.{m}") + modules.discard("*.out_proj") + + # In cases when model_spec has linear_layers as null, we need to set the target_modules to default + # If target_modules is empty we get this error during training: + # Expected match_all_linear to be true or target_modules/exclude_modules to be non-empty + lora.target_modules = list(modules) if modules else ["*proj"] + else: + lora.target_modules = ["*proj"] + return lora + + +def _translate_wandb_config(api_wandb: WandBParams | None) -> WandBConfig | None: + """Translate API WandBParams to internal WandBConfig.""" + if api_wandb is None: + return None + + return WandBConfig( + project=api_wandb.project, + name=api_wandb.name, + entity=api_wandb.entity, + tags=api_wandb.tags, + notes=api_wandb.notes, + base_url=api_wandb.base_url, + ) + + +def _translate_mlflow_config(api_mlflow: MLflowParams | None) -> MLflowConfig | None: + """Translate API MLflowParams to internal MLflowConfig.""" + if api_mlflow is None: + return None + + return MLflowConfig( + experiment_name=api_mlflow.experiment_name, + run_name=api_mlflow.run_name, + tags=api_mlflow.tags, + description=api_mlflow.description, + tracking_uri=api_mlflow.tracking_uri, + ) + + +def _translate_integrations(job_spec: CustomizationJobOutput) -> TrainingStepConfig.IntegrationsConfig: + """Translate API IntegrationsConfig to internal IntegrationsConfig.""" + if not job_spec.integrations: + return TrainingStepConfig.IntegrationsConfig() + + return TrainingStepConfig.IntegrationsConfig( + wandb=_translate_wandb_config(job_spec.integrations.wandb), + mlflow=_translate_mlflow_config(job_spec.integrations.mlflow), + ) + + +def _collect_integration_secret_envs(job_input: CustomizationJobOutput) -> list[EnvironmentVariable]: + """Collect secret environment variables from integration configs. + + Secrets are propagated via PlatformJobStep.environment (not config) so that + the Jobs service can resolve secret references at runtime. + """ + secret_envs: list[EnvironmentVariable] = [] + if not job_input.integrations: + return secret_envs + + if job_input.integrations.wandb and job_input.integrations.wandb.api_key_secret: + secret_envs.append( + EnvironmentVariable( + name="WANDB_API_KEY", + from_secret=EnvironmentVariableFromSecret( + name=job_input.integrations.wandb.api_key_secret.root, + ), + ) + ) + + return secret_envs + + +def _extract_model_name(job_spec: CustomizationJobOutput) -> str | None: + """Extract the canonical model name from the model field for template lookup. + + The model name follows the pattern "workspace/name" (e.g., "meta/llama-3.1-8b-instruct") + which matches the keys in DEFAULT_CHAT_TEMPLATES. + """ + model = job_spec.model + + if "/" in model: + logger.debug(f"Extracted model name from URN: {model}") + return model + + return None + + +def _get_training_image() -> str: + """Training container image for the Automodel task.""" + return config.training_automodel_image or get_training_image() diff --git a/services/automodel/src/nmp/automodel/app/jobs/training/schemas.py b/services/automodel/src/nmp/automodel/app/jobs/training/schemas.py new file mode 100644 index 0000000000..93d8ae7f45 --- /dev/null +++ b/services/automodel/src/nmp/automodel/app/jobs/training/schemas.py @@ -0,0 +1,293 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from enum import Enum +from typing import Optional + +from nmp.automodel.app.constants import ( + DEFAULT_OUTPUT_MODEL_PATH, + DEFAULT_SEED, + DEFAULT_TRAINING_OUTPUT_PATH, +) +from nmp.automodel.entities.values import CheckpointFormat, FinetuningType, Precision, TrainingType +from pydantic import BaseModel, Field + + +class OptimizerType(str, Enum): + """Optimizer and scheduler combination types.""" + + ADAMW_WITH_COSINE_ANNEALING = "adamw_with_cosine_annealing" + ADAM_WITH_COSINE_ANNEALING = "adam_with_cosine_annealing" + ADAMW_WITH_FLAT_LR = "adamw_with_flat_lr" + ADAM_WITH_FLAT_LR = "adam_with_flat_lr" + + +class LoRAConfig(BaseModel): + """Internal LoRA configuration with implementation details. + + This differs from the API LoRAParams: + - Includes use_triton, match_all_linear (implementation details) + - exclude_modules for advanced control + - Can add new fields freely without breaking API + """ + + # Core LoRA parameters (from API) + rank: int = Field(default=8, description="LoRA rank (low-rank dimension)") + alpha: int = Field(default=32, description="LoRA alpha scaling factor") + dropout: float = Field(default=0.0, description="LoRA dropout probability") + + # Module targeting + target_modules: Optional[list[str]] = Field( + default=None, description="Module name patterns to apply LoRA to (e.g., ['*.proj'])" + ) + exclude_modules: Optional[list[str]] = Field(default=None, description="Module name patterns to exclude from LoRA") + + # Implementation details (not in API) + use_triton: bool = Field(default=True, description="Use optimized Triton LoRA kernel") + + +class ModelConfig(BaseModel): + """Internal model configuration.""" + + path: str = Field(description="Path to a model directory (contains config, weights, tokenizer etc.)") + name: Optional[str] = Field( + default=None, + description="Model identifier (e.g., 'meta/llama-3.1-8b-instruct')", + ) + max_seq_length: int = Field( + default=2048, + description="Maximum token sequence length for training; longer sequences are truncated", + ) + + # Model loading options + precision: Optional[Precision] = Field( + default=None, + description="Model weight dtype (e.g., 'bf16', 'fp16'). None implies auto-detects from model config", + ) + attn_implementation: Optional[str] = Field( + default="sdpa", + description="Attention backend: 'sdpa' (PyTorch native), 'flash_attention_2' (requires flash-attn), 'eager' (no optimization)", + ) + trust_remote_code: bool = Field( + default=False, + description="Allow executing custom model code from the checkpoint. Required for some community models", + ) + is_embedding_model: bool = Field( + default=False, + description="Whether the model is an embedding model", + ) + chat_template: Optional[str] = Field( + default=None, + description="Jinja2 chat template from the model entity spec or fileset metadata. " + "Takes highest priority in resolve_chat_template when set.", + ) + + override_custom_impl: bool = Field( + default=False, + description="Some of the custom implementations in nemo automodel cause loading failures when used with other models in the same family, this forces the use_hf=True flag to use non custom implementations.", + ) + + v4_compatible: bool = Field( + default=False, + description="Enable transformers-v4-compatible checkpoint output that preserves the original transformers-v4-style config.json output.", + ) + + +class DistillationConfig(BaseModel): + """Internal Knowledge Distillation configuration. + + teacher is a ModelConfig with resolved path, not a URN. + """ + + # Teacher model (resolved path) + teacher_model: ModelConfig = Field(description="Teacher model configuration with resolved path") + + # KD hyperparameters + ratio: float = Field(default=0.5, description="Balance between CE loss and KD loss") + temperature: float = Field(default=1.0, description="Softmax temperature for KD") + + # Implementation detail (not in API) + offload_teacher: bool = Field(default=False, description="Offload teacher model to CPU for memory efficiency") + + +class EmbeddingConfig(BaseModel): + """Internal Embedding/Biencoder model finetuning configuration. + + This is used internally when a model is detected as an embedding model + by its name. The defaults here match the recommended settings for + NeMo embedding models. + + Note: Embedding models are detected by model name (e.g., contains 'embed'), + not by a separate training type. They use standard SFT training type. + + Model architecture parameters (share_encoder, pooling, l2_normalize, temperature, + add_linear_pooler, out_dimension) use sensible defaults and are not exposed here. + """ + + # Training configuration + train_n_passages: int = Field( + default=5, + description=( + "Total number of passages per query during training: 1 positive + (n-1) negatives. " + "For example, train_n_passages=5 means 1 positive and 4 negative passages per query." + ), + ) + eval_negative_size: Optional[int] = Field( + default=None, + description=( + "Number of negative passages per query during validation. " + "Recommended to keep as train_n_passages - 1 for consistent train/eval behavior. " + "If not set, defaults to train_n_passages - 1." + ), + ) + + # Memory optimization + do_gradient_checkpointing: bool = Field( + default=False, + description=( + "Enable gradient checkpointing to reduce memory usage at the cost of slower training. " + "Useful for larger embedding models or memory-constrained environments." + ), + ) + + # Tokenization configuration + query_max_length: int = Field(default=512, description="Maximum token length for query tokenization") + passage_max_length: int = Field(default=512, description="Maximum token length for passage tokenization") + query_prefix: str = Field(default="query:", description="Prefix to prepend to queries before tokenization") + passage_prefix: str = Field(default="passage:", description="Prefix to prepend to passages before tokenization") + + +class WandBConfig(BaseModel): + """Internal Weights & Biases configuration.""" + + project: Optional[str] = Field(default=None, description="W&B project name") + name: Optional[str] = Field(default=None, description="W&B run name") + entity: Optional[str] = Field(default=None, description="W&B entity") + tags: Optional[list[str]] = Field(default=None, description="W&B tags") + notes: Optional[str] = Field(default=None, description="W&B notes") + base_url: Optional[str] = Field(default=None, description="Self-hosted W&B server URL") + + +class MLflowConfig(BaseModel): + """Internal MLflow configuration.""" + + experiment_name: Optional[str] = Field(default=None, description="MLflow experiment name") + run_name: Optional[str] = Field(default=None, description="MLflow run name") + tags: Optional[dict[str, str]] = Field(default=None, description="MLflow tags") + description: Optional[str] = Field(default=None, description="MLflow description") + tracking_uri: Optional[str] = Field(default=None, description="MLflow tracking URI") + + +class TrainingStepConfig(BaseModel): + """Normalized training configuration compiled into nemo-automodel recipe YAML.""" + + class DatasetConfig(BaseModel): + path: str + prompt_template: Optional[str] = None + add_bos: Optional[bool] = None + add_eos: Optional[bool] = None + + class TrainingConfig(BaseModel): + training_type: TrainingType + finetuning_type: Optional[FinetuningType] = None + lora: Optional[LoRAConfig] = None + kd: Optional[DistillationConfig] = None + + class ScheduleConfig(BaseModel): + epochs: int = 1 + max_steps: Optional[int] = None + val_check_interval: Optional[float] = None + + class BatchConfig(BaseModel): + global_batch_size: int = Field(default=32, gt=0) + micro_batch_size: int = Field(default=1, gt=0) + sequence_packing: bool = False + sequence_packing_max_samples: int = 1000 + + class OptimizerConfig(BaseModel): + optimizer_type: Optional[OptimizerType] = Field(default=None) + learning_rate: float = 1e-4 + min_learning_rate: Optional[float] = None + eps: float = 1e-8 + weight_decay: float = 0.01 + beta1: float = 0.9 + beta2: float = 0.999 + warmup_steps: int = 0 + + class ParallelismConfig(BaseModel): + num_nodes: int = 1 + num_gpus_per_node: int = 1 + tensor_parallel_size: int = 1 + pipeline_parallel_size: int = 1 + context_parallel_size: int = 1 + expert_parallel_size: Optional[int] = None + sequence_parallel: bool = False + + class IntegrationsConfig(BaseModel): + wandb: Optional[WandBConfig] = None + mlflow: Optional[MLflowConfig] = None + + # === Main Config Fields === + model: ModelConfig + dataset: DatasetConfig + training: TrainingConfig + schedule: ScheduleConfig + batch: BatchConfig + optimizer: OptimizerConfig + parallelism: ParallelismConfig + integrations: IntegrationsConfig = Field(default_factory=IntegrationsConfig) + + # === Output Paths === + output_model: str # Set at compile-time from CustomizationJobOutput + workspace_path: str = Field(default=DEFAULT_TRAINING_OUTPUT_PATH) + output_path: str = Field(default=DEFAULT_OUTPUT_MODEL_PATH) + + # === Miscellaneous === + seed: int = Field( + default=DEFAULT_SEED, description="Random seed for ensuring reproducibility in all random processes." + ) + training_timeout: Optional[int] = None + + +class GPUInfo(BaseModel): + """GPU architecture information captured during training.""" + + architecture: str + device_name: str + memory_gb: float + cuda_version: str + + +class CheckpointInfo(BaseModel): + """Output checkpoint information.""" + + path: str + format: CheckpointFormat + precision: Optional[Precision] = Field( + default=None, description="Checkpoint precision. None when auto-detected from model config." + ) + + +class TrainingMetrics(BaseModel): + """Final training metrics.""" + + final_loss: Optional[float] = None + final_val_loss: Optional[float] = None + best_val_loss: Optional[float] = None + total_steps: int = 0 + total_epochs: int = 0 + + +class TrainingResult(BaseModel): + """ + Result written by training task. + + Written to: {workspace_path}/training_result.json + """ + + success: bool + error_message: Optional[str] = None + checkpoint: Optional[CheckpointInfo] = None + gpu_info: Optional[GPUInfo] = None + metrics: TrainingMetrics = Field(default_factory=TrainingMetrics) + training_duration_seconds: Optional[float] = None diff --git a/services/automodel/src/nmp/automodel/compile.py b/services/automodel/src/nmp/automodel/compile.py new file mode 100644 index 0000000000..2781fb9e65 --- /dev/null +++ b/services/automodel/src/nmp/automodel/compile.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Public compile entrypoint for Automodel jobs.""" + +from __future__ import annotations + +from nmp.automodel.adapter import automodel_spec_to_compiler_output +from nmp.automodel.api.v2.jobs.schemas import CustomizationJobOutput +from nmp.automodel.app.jobs.compiler import platform_job_config_compiler as _compile_canonical + + +async def platform_job_config_compiler( + job_spec: CustomizationJobOutput | object, + workspace: str, + sdk: object, + job_name: str | None = None, + profile: str | None = None, +) -> object: + """Compile Automodel job spec (plugin or legacy shape) to PlatformJobSpec.""" + if not isinstance(job_spec, CustomizationJobOutput): + job_spec = automodel_spec_to_compiler_output(job_spec) + if profile and job_spec.training.execution_profile is None: + job_spec = job_spec.model_copy( + update={"training": job_spec.training.model_copy(update={"execution_profile": profile})}, + ) + return await _compile_canonical( + workspace, + job_spec, + sdk, # type: ignore[arg-type] + ) + + +__all__ = ["platform_job_config_compiler", "automodel_spec_to_compiler_output"] diff --git a/services/automodel/src/nmp/automodel/config.py b/services/automodel/src/nmp/automodel/config.py new file mode 100644 index 0000000000..4a698993cf --- /dev/null +++ b/services/automodel/src/nmp/automodel/config.py @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Configuration for the nmp-automodel compiler and tasks.""" + +from nmp.common.config import create_service_config_class, get_platform_config, get_service_config +from pydantic import Field + + +class AutomodelConfig(create_service_config_class("automodel")): # type: ignore + """Environment variables use the NMP_AUTOMODEL_ prefix.""" + + image_registry: str = Field( + default="nvcr.io/0921617854601259/nemo-platform-dev", + description=( + "Registry host/path prefix for nmp-automodel-tasks and nmp-automodel-training. " + "Override via NMP_AUTOMODEL_IMAGE_REGISTRY for other environments." + ), + ) + training_image: str | None = Field( + default=None, + description="Override GPU training image (default: nmp-automodel-training under image_registry).", + ) + tasks_image: str | None = Field( + default=None, + description="Override CPU tasks image (default: nmp-automodel-tasks under image_registry).", + ) + + default_job_resource_cpu_request: str = Field(default="1") + default_job_resource_memory_request: str = Field(default="8Gi") + default_job_resource_cpu_limit: str = Field(default="4") + default_job_resource_memory_limit: str = Field(default="16Gi") + + training_staleness_timeout_seconds: int = Field( + default=3600, + description="Terminate training if no task progress within this many seconds (0 disables).", + ) + + default_training_execution_profile: str = Field( + default="gpu", + description="Default GPU execution profile when the job spec omits training.execution_profile.", + ) + + +config = get_service_config(AutomodelConfig) +platform_config = get_platform_config() + +# Legacy compiler attribute names +config.training_automodel_image = config.training_image diff --git a/services/automodel/src/nmp/automodel/entities/__init__.py b/services/automodel/src/nmp/automodel/entities/__init__.py new file mode 100644 index 0000000000..13e49b9372 --- /dev/null +++ b/services/automodel/src/nmp/automodel/entities/__init__.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Customizer entity definitions. + +This module exports: +- Entity classes (database/persistence models) +- Shared value types (enums and read-only metadata) + +Configuration types (LoRAConfig, ModelConfig, etc.) are NOT exported here. +They belong in their respective layers: +- API types → api/v2/jobs/schemas.py +- Internal types → app/jobs/training/schemas.py +""" + +from .values import ( + CheckpointFormat, + FinetuningType, + Precision, + TrainingType, +) + +__all__ = [ + # Enums + "CheckpointFormat", + "FinetuningType", + "Precision", + "TrainingType", +] diff --git a/services/automodel/src/nmp/automodel/entities/validators.py b/services/automodel/src/nmp/automodel/entities/validators.py new file mode 100644 index 0000000000..b4c9705450 --- /dev/null +++ b/services/automodel/src/nmp/automodel/entities/validators.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Shared validation logic for entity fields.""" + +import re +from typing import Optional + +from nmp.automodel.app.jobs.file_io.schemas import FILESET_PROTOCOL, FileSetRef +from nmp.common.entities.constants import REGEX_WORD_CHARACTER_DOT_DASH + +_NAME_REGEX = re.compile(REGEX_WORD_CHARACTER_DOT_DASH) +_UNSUPPORTED_PROTOCOLS = ("hf://", "ngc://", "s3://", "gs://") + + +def _normalize_fileset_ref(uri: str) -> str: + """Parse and return canonical fileset reference (no ``fileset://`` prefix).""" + normalized = uri.strip() + for prefix in _UNSUPPORTED_PROTOCOLS: + if normalized.startswith(prefix): + raise ValueError( + f"Unsupported dataset URI protocol. Use 'workspace/name' or 'name' (resolved in the job workspace). Got: {uri}", + ) + if normalized.startswith(FILESET_PROTOCOL): + normalized = normalized[len(FILESET_PROTOCOL) :] + ref = FileSetRef.model_validate(normalized) + if not _NAME_REGEX.match(ref.name): + raise ValueError( + f"Invalid dataset name: '{ref.name}'. Entity names must contain only word characters, dots, and hyphens.", + ) + return str(ref) + + +def validate_fileset_uri(uri: str) -> str: + """Validate a fileset reference as ``workspace/name`` or ``name``. + + The job path ``workspace`` is used when the reference is a bare name. + A legacy ``fileset://`` prefix is accepted and stripped. + """ + return _normalize_fileset_ref(uri) + + +def validate_optional_fileset_uri(uri: Optional[str]) -> Optional[str]: + """Validate fileset reference, allowing None.""" + if uri is None: + return None + return validate_fileset_uri(uri) diff --git a/services/automodel/src/nmp/automodel/entities/values.py b/services/automodel/src/nmp/automodel/entities/values.py new file mode 100644 index 0000000000..b236ac9b4b --- /dev/null +++ b/services/automodel/src/nmp/automodel/entities/values.py @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Value types for the Customizer service.""" + +from enum import Enum, StrEnum + + +class CheckpointFormat(str, Enum): + """Model checkpoint format (input or output).""" + + HF = "hf" # Standard HuggingFace format + HF_PEFT = "hf-peft" # HuggingFace PEFT adapter (LoRA, etc.) + NEMO = "nemo" # NeMo checkpoint format + + +class Precision(str, Enum): + """Model precision for training.""" + + FP8 = "fp8" + BF16 = "bf16" + FP16 = "fp16" + FP32 = "fp32" + + def to_torch_dtype(self) -> str: + """ + Convert to a torch dtype string compatible with HuggingFace/Automodel. + + Returns: + String like "bfloat16", "float16", "float32" that can be passed to + from_pretrained(torch_dtype=...) or Automodel's dtype_from_str(). + + Raises: + ValueError: If this precision cannot be represented as a torch dtype. + FP8 requires separate quantization config, BF16_MIXED is a training mode. + """ + mapping = { + Precision.BF16: "bfloat16", + Precision.FP16: "float16", + Precision.FP32: "float32", + } + if self not in mapping: + raise ValueError( + f"Precision '{self.value}' cannot be converted to a torch dtype. " + f"Supported: {[p.value for p in mapping.keys()]}. " + f"Note: FP8 requires separate quantization config, BF16_MIXED is a training mode." + ) + return mapping[self] + + @classmethod + def from_hf_dtype(cls, hf_dtype: str) -> "Precision": + """ + Create Precision from a HuggingFace torch_dtype string. + + Args: + hf_dtype: String like "bfloat16", "float16", "float32", "float". + + Returns: + Corresponding Precision enum value. + + Raises: + ValueError: If the dtype string is not recognized. + """ + mapping = { + "bfloat16": cls.BF16, + "float16": cls.FP16, + "float32": cls.FP32, + "float": cls.FP32, + } + if hf_dtype not in mapping: + raise ValueError(f"Unknown HuggingFace dtype '{hf_dtype}'. Supported: {list(mapping.keys())}") + return mapping[hf_dtype] + + +class TrainingType(str, Enum): + """Training algorithm type.""" + + SFT = "sft" + DISTILLATION = "distillation" + DPO = "dpo" + GRPO = "grpo" + + +class FinetuningType(str, Enum): + """Finetuning strategy (full weights vs PEFT).""" + + ALL_WEIGHTS = "all_weights" + LORA = "lora" + LORA_MERGED = "lora_merged" + + +class OutputNameType(StrEnum): + """Output artifact type.""" + + ADAPTER = "adapter" + MODEL = "model" diff --git a/services/automodel/src/nmp/automodel/images.py b/services/automodel/src/nmp/automodel/images.py new file mode 100644 index 0000000000..9f782b283c --- /dev/null +++ b/services/automodel/src/nmp/automodel/images.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Docker image resolution for nmp-automodel job steps.""" + +from __future__ import annotations + +from nmp.automodel.config import config +from nmp.common.jobs.image import get_qualified_image + +# Default NGC dev registry for platform-built automodel images (flat repo names for NVCR). +DEFAULT_AUTOMODEL_IMAGE_REGISTRY = "nvcr.io/0921617854601259/nemo-platform-dev" + +BASE_IMAGE_NAME = "nmp-automodel-base" +TASKS_IMAGE_NAME = "nmp-automodel-tasks" +TRAINING_IMAGE_NAME = "nmp-automodel-training" + +# Must match ENTRYPOINT in Dockerfile.nmp-automodel-{tasks,training}. +# Job specs must set this explicitly: Docker API create() replaces the image +# entrypoint when the platform passes entrypoint=[]. +AUTOMODEL_PYTHON_ENTRYPOINT = ["/opt/venv/bin/python"] + + +def get_automodel_qualified_image(name: str, override: str | None = None) -> str: + """Resolve a job step image reference. + + Args: + name: Image repository name under the registry (e.g. ``nmp-automodel-tasks``). + override: Full image ref from ``NMP_AUTOMODEL_TASKS_IMAGE`` / ``NMP_AUTOMODEL_TRAINING_IMAGE``. + + Returns: + Fully qualified image (``{registry}/{name}:{tag}``) unless ``override`` is set. + """ + if override: + return override + registry = config.image_registry or DEFAULT_AUTOMODEL_IMAGE_REGISTRY + return get_qualified_image(name, registry=registry) + + +def get_tasks_image() -> str: + """CPU task steps (file_io, model_entity).""" + return get_automodel_qualified_image(TASKS_IMAGE_NAME, config.tasks_image) + + +def get_training_image() -> str: + """GPU training step.""" + return get_automodel_qualified_image(TRAINING_IMAGE_NAME, config.training_image) diff --git a/services/automodel/src/nmp/automodel/platform_client.py b/services/automodel/src/nmp/automodel/platform_client.py new file mode 100644 index 0000000000..d55672d1ab --- /dev/null +++ b/services/automodel/src/nmp/automodel/platform_client.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from nemo_platform import AsyncNeMoPlatform +from nemo_platform._exceptions import NotFoundError, PermissionDeniedError +from nemo_platform.types.models import ModelEntity +from nmp.automodel.app.jobs.file_io.schemas import FileSetRef +from nmp.common.entities.utils import parse_entity_ref + + +async def check_dataset_access(sdk: AsyncNeMoPlatform, dataset_uri: str, default_workspace: str) -> None: + """Verify the caller can access the dataset fileset.""" + ref = FileSetRef.model_validate(dataset_uri) + workspace = ref.workspace or default_workspace + try: + await sdk.files.filesets.retrieve(workspace=workspace, name=ref.name) + except PermissionDeniedError: + raise PermissionError(f"Access denied to dataset fileset '{workspace}/{ref.name}'") from None + except NotFoundError: + raise ValueError( + f"Dataset fileset '{ref.name}' not found in workspace '{workspace}'. Verify the dataset exists." + ) from None + + +async def fetch_model_entity( + model_ref: str, + default_workspace: str, + sdk: AsyncNeMoPlatform, +) -> ModelEntity: + """Retrieve a model entity by reference string.""" + resolved_ref = parse_entity_ref(model_ref, default_workspace) + try: + return await sdk.models.retrieve(name=resolved_ref.name, workspace=resolved_ref.workspace, verbose=True) + except PermissionDeniedError: + raise PermissionError(f"Access denied to model '{resolved_ref.workspace}/{resolved_ref.name}'") from None + except NotFoundError: + raise ValueError( + f"Model entity not found: '{resolved_ref.workspace}/{resolved_ref.name}'. Verify the model entity exists." + ) from None diff --git a/services/automodel/src/nmp/automodel/tasks/__init__.py b/services/automodel/src/nmp/automodel/tasks/__init__.py new file mode 100644 index 0000000000..63c713713d --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Automodel task entrypoints (see ``nmp.automodel.tasks.`` subpackages).""" diff --git a/services/automodel/src/nmp/automodel/tasks/__main__.py b/services/automodel/src/nmp/automodel/tasks/__main__.py new file mode 100644 index 0000000000..6e6e865482 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/__main__.py @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Default entrypoint for the nmp-automodel-tasks image (help / task listing). + +Production job steps invoke a specific module directly, e.g. +``python -m nmp.automodel.tasks.file_io``. +""" + +from __future__ import annotations + +import argparse +import sys + +_TASK_MODULES = ( + ("file_io", "nmp.automodel.tasks.file_io", "Download/upload model and dataset files"), + ("model_entity", "nmp.automodel.tasks.model_entity", "Create output model entity"), +) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + prog="python -m nmp.automodel.tasks", + description="NeMo Automodel CPU task image. The jobs compiler runs one module per step.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="Examples:\n" + " python -m nmp.automodel.tasks --help\n" + " python -m nmp.automodel.tasks.file_io\n" + " python -m nmp.automodel.tasks.model_entity\n\n" + "GPU training uses the nmp-automodel-training image:\n" + " python -m nmp.automodel.tasks.training\n", + ) + parser.add_argument( + "--list", + action="store_true", + help="List task modules and exit (default when no job config is provided).", + ) + args = parser.parse_args(argv) + if args.list or len(argv or sys.argv[1:]) == 0: + print("Task modules:\n") + for name, module, summary in _TASK_MODULES: + print(f" {name:14} {module}") + print(f" {summary}\n") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/services/automodel/src/nmp/automodel/tasks/docker/README.md b/services/automodel/src/nmp/automodel/tasks/docker/README.md new file mode 100644 index 0000000000..b57974656d --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/docker/README.md @@ -0,0 +1,74 @@ +# File I/O Task Docker Testing + +Scripts for running the file_io task container locally. + +## Prerequisites + +1. **Build the Docker image** from the repository root: +This will build `my-registry/nmp-cpu-tasks:local` image that will be used for this task. + + ```bash + cd /path/to/nmp + make docker/nmp-cpu-tasks + ``` + +2. **Have NeMo Platform running** (files service) at `http://localhost:8080` + +## Quick Start + +### Run with Docker Compose + +```bash +cd services/customizer/src/nmp/customizer/tasks/file_io/docker + +# Run the task +docker compose up + +# Run with custom image +FILE_IO_IMAGE=my-registry/nmp-cpu-tasks:dev docker compose up + +# Run interactively +docker compose run --rm file-io run task --task nmp.customizer.tasks.file_io +``` + +## Configuration + +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `NMP_BASE_URL` | Base URL for NeMo Platform | `http://host.docker.internal:8000` | +| `NMP_FILES_URL` | Files service URL | `http://host.docker.internal:8000` | +| `NMP_JOBS_URL` | Jobs service URL (for progress) | `http://host.docker.internal:8000` | +| `NEMO_JOB_ID` | Job identifier | `test-file-io-job` | +| `NEMO_JOB_STEP` | Step name | `FileIO` | +| `NEMO_JOB_TASK` | Task identifier | `file-io-task` | +| `NEMO_JOB_WORKSPACE` | Workspace name | `default` | +| `LOG_LEVEL` | Logging level | `INFO` | +| `FILE_IO_IMAGE` | Docker image to use | `my-registry/nmp-cpu-tasks:local` | + +### Config File Format + +The `sample_config.json` defines what files to upload/download: + +```json +{ + "upload": [ + { + "src": "local_folder", + "dest": "workspace/fileset-name" + } + ], + "download": [ + { + "src": "workspace/fileset-name", + "dest": "local_folder" + } + ] +} +``` + +- `upload[].src`: Path relative to job storage defined by NEMO_JOB_PERSISTENT_JOB_STORAGE_PATH (mounted at `/var/run/scratch`) +- `upload[].dest`: Target FileSet in format `workspace/fileset-name` +- `download[].src`: Source FileSet in format `workspace/fileset-name` +- `download[].dest`: Path relative to job storage defined by NEMO_JOB_PERSISTENT_JOB_STORAGE_PATH diff --git a/services/automodel/src/nmp/automodel/tasks/docker/docker-compose.yaml b/services/automodel/src/nmp/automodel/tasks/docker/docker-compose.yaml new file mode 100644 index 0000000000..eae2b4a163 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/docker/docker-compose.yaml @@ -0,0 +1,52 @@ +# Docker Compose for file_io task local testing +# +# Usage: +# # Start the task (runs once and exits) +# docker compose up +# +# # Run with custom command +# docker compose run --rm file-io run task --task nmp.customizer.tasks.file_io +# +# Prerequisites: +# - Build the image first (from Platform repo root): +# docker buildx bake -f docker-bake.automodel.hcl nmp-automodel-tasks-docker +# - Have NeMo Platform running at http://localhost:8080 +# - Create sample_config.json (or use the one provided) + +services: + file-io: + image: ${FILE_IO_IMAGE:-nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-tasks:local} + container_name: file-io-task + + # Mount config file and storage directory + # Using test data from services/customizer/tests/tasks/file_io/data/ + # files will be downloaded under services/customizer/tests/tasks/file_io/data/temp which is in .gitignore + volumes: + - ../../../../../../tests/tasks/file_io/data:/var/run/scratch + + environment: + # NeMo Platform URLs - use host.docker.internal to reach host services + NMP_BASE_URL: ${NMP_BASE_URL:-http://host.docker.internal:8000} + NMP_FILES_URL: ${NMP_FILES_URL:-http://host.docker.internal:8000} + NMP_JOBS_URL: ${NMP_JOBS_URL:-http://host.docker.internal:8000} + + # Job configuration paths (container paths) + NEMO_JOB_STEP_CONFIG_FILE_PATH: /var/run/scratch/sample_config.json + NEMO_JOB_PERSISTENT_JOB_STORAGE_PATH: /var/run/scratch + + # Job metadata + NEMO_JOB_ID: ${NEMO_JOB_ID:-multi-file-job} + NEMO_JOB_STEP: ${NEMO_JOB_STEP:-FileIO} + NEMO_JOB_TASK: ${NEMO_JOB_TASK:-file-io-task} + NEMO_JOB_WORKSPACE: ${NEMO_JOB_WORKSPACE:-default} + + # Logging + LOG_LEVEL: ${LOG_LEVEL:-INFO} + + # Default command - run the file_io task + # Note: ENTRYPOINT is already "nemo-platform", so command should NOT include it + command: ["run", "task", "--task", "nmp.automodel.tasks.file_io"] + + # For macOS/Windows Docker Desktop - allows reaching host services + extra_hosts: + - "host.docker.internal:host-gateway" diff --git a/services/automodel/src/nmp/automodel/tasks/file_io/__init__.py b/services/automodel/src/nmp/automodel/tasks/file_io/__init__.py new file mode 100644 index 0000000000..8df0acb9ed --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/file_io/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""File I/O task for Automodel customization jobs.""" + +from nmp.automodel.tasks.file_io.run import run + +__all__ = ["run"] diff --git a/services/automodel/src/nmp/automodel/tasks/file_io/__main__.py b/services/automodel/src/nmp/automodel/tasks/file_io/__main__.py new file mode 100644 index 0000000000..68981865e6 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/file_io/__main__.py @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import sys + +from nmp.automodel.tasks.file_io.run import run + +if __name__ == "__main__": + sys.exit(run()) diff --git a/services/automodel/src/nmp/automodel/tasks/file_io/callbacks.py b/services/automodel/src/nmp/automodel/tasks/file_io/callbacks.py new file mode 100644 index 0000000000..51a2a18046 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/file_io/callbacks.py @@ -0,0 +1,783 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Custom fsspec callbacks for progress reporting during file I/O operations.""" + +import logging +import os +import threading +from abc import abstractmethod +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from fsspec.callbacks import Callback, TqdmCallback +from nmp.automodel.app.jobs.file_io.schemas import DownloadStats, TaskPhase, UploadStats +from nmp.automodel.tasks.file_io.progress_reporter import ProgressReporter +from nmp.common.jobs.schemas import PlatformJobStatus + +logger = logging.getLogger(__name__) + + +def get_percentage(current: int, total: int) -> int: + """Get percentage of current / total. + + Args: + current: The current value (numerator). + total: The total value (denominator). + + Returns: + Integer percentage from 0-100. + + Raises: + ValueError: If current > total, or if either value is negative. + + """ + if current > total: + raise ValueError( + f"Unexpected value of the current and total values: current={current} cannot be greater than total={total}", + ) + if total < 0: + raise ValueError(f"Unexpected negative value of the total value: total={total}, current={current}") + if current < 0: + raise ValueError(f"Unexpected negative value of the current value: current={current}, total={total}") + + if total == 0: + return 0 + return int((current / total) * 100) + + +@dataclass +class FileInfo: + """A dataclass for file information.""" + + path: str + size: int + + +class TqdmPerFileUploadCallback(Callback): + """A callback that creates a separate tqdm progress bar for each file. + + Unlike TqdmCallback which shows overall progress, this callback creates a new + tqdm progress bar when branched() is called for each file. Each file's progress + bar shows byte-level progress for that individual file. + + Usage: + callback = TqdmPerFileUploadCallback() + filesystem_sdk.put(src, dest, recursive=True, callback=callback) + # Creates a separate progress bar for each file being uploaded + """ + + def __init__(self, src_path: Path, **kwargs: Any): + """Initialize the per-file tqdm callback. + + Args: + **kwargs: Additional arguments passed to the base Callback. + + """ + self.src_path = src_path + super().__init__(**kwargs) + + def branched(self, full_src_path: str, full_dest_path: str, **kwargs: Any) -> TqdmCallback: + """Create a TqdmCallback for this specific file transfer. + + Args: + full_src_path: Source file path. + full_dest_path: Destination file path. + **kwargs: Additional keyword arguments. + + Returns: + A TqdmCallback configured for byte-level progress of this file. + + """ + # Extract just the filename for the progress bar description + if self.src_path.is_file(): + relative_path_upload_dir = self.src_path.name + else: + relative_path_upload_dir = Path(full_src_path).relative_to(self.src_path) + return TqdmCallback( + # https://tqdm.github.io/docs/tqdm + tqdm_kwargs={ + "desc": f"Uploading {relative_path_upload_dir!s}", + # use bytes as the unit + "unit": "B", + # scale the unit to be more readable (e.g. 1024 bytes = 1 KB) + "unit_scale": True, + # divide the unit by 1024 to get the next unit + "unit_divisor": 1024, + # The minimum number of iterations (bytes processed) that must occur before the progress bar refreshes + "miniters": 1, + }, + ) + + +class TqdmPerFileDownloadCallback(Callback): + """A callback that creates a separate tqdm progress bar for each file download. + + Similar to TqdmPerFileUploadCallback but for download operations. Creates a new + tqdm progress bar when branched() is called for each file being downloaded. + + The callback accepts a file_sizes dict to set the total size for each file's + progress bar. This is necessary because the SDK may not receive Content-Length + headers for streaming downloads (e.g., when chunked transfer encoding is used). + + Usage: + # Build file_sizes from listing + files = list_fileset_files(fileset) + file_sizes = {f.path.lstrip("/"): f.size for f in files} + + callback = TqdmPerFileDownloadCallback( + dest_path=dest_dir, + fileset_path="workspace/fileset", + file_sizes=file_sizes, + ) + filesystem_sdk.get(src, dest, recursive=True, callback=callback) + # Creates a separate progress bar for each file being downloaded + """ + + def __init__(self, dest_path: Path, fileset_path: str, file_sizes: dict[str, int] | None = None, **kwargs: Any): + """Initialize the per-file tqdm download callback. + + Args: + dest_path: The local destination directory path. + fileset_path: The fileset path (e.g., "workspace/fileset") used to extract + relative file paths from full source paths. + file_sizes: Optional dict mapping relative file paths to their sizes in bytes. + Used to set the progress bar's total for percentage display. + **kwargs: Additional arguments passed to the base Callback. + + """ + self.dest_path = dest_path + self.fileset_path = fileset_path.rstrip("/") + self.file_sizes = file_sizes or {} + super().__init__(**kwargs) + + def branched(self, full_src_path: str, full_dest_path: str, **kwargs: Any) -> TqdmCallback: + """Create a TqdmCallback for this specific file download. + + Args: + full_src_path: Source file path in the fileset (e.g., "workspace/fileset/dir/file.txt"). + full_dest_path: Destination local file path. + **kwargs: Additional keyword arguments. + + Returns: + A TqdmCallback configured for byte-level progress of this file. + + """ + # Extract relative path for the progress bar description + # full_dest_path is the full local path, we want to show just the filename or relative path + dest_full_path = Path(full_dest_path) + if self.dest_path.is_file(): + relative_path = dest_full_path.name + else: + try: + relative_path = dest_full_path.relative_to(self.dest_path) + except ValueError: + # If can't compute relative path, use filename + relative_path = dest_full_path.name + + # Extract relative file path from full source path to look up size + # full_src_path format: "workspace/fileset/relative/path/to/file.txt" + # We need to extract "relative/path/to/file.txt" + relative_file_path = full_src_path + if full_src_path.startswith(self.fileset_path): + relative_file_path = full_src_path[len(self.fileset_path) :].lstrip("/") + + # Look up file size from pre-computed mapping + file_size = self.file_sizes.get(relative_file_path) + + callback = TqdmCallback( + tqdm_kwargs={ + "desc": f"Downloading {relative_path!s}", + "unit": "B", + "unit_scale": True, + "unit_divisor": 1024, + "miniters": 1, + }, + ) + + # Set size if we know it - this enables percentage display in tqdm + # Must be called via set_size() rather than tqdm_kwargs["total"] because + # the SDK may also call set_size() from Content-Length header + if file_size is not None: + callback.set_size(file_size) + + return callback + + +class BaseProgressCallback(Callback): + """Base class for file upload/download progress callbacks. + + This abstract base class provides common functionality for tracking file transfer + progress and reporting to the Jobs service. Subclasses implement operation-specific + behavior (upload vs download). + + Thread Safety: + This callback uses threading.Lock for synchronization. FilesetFileSystem is + async-first and transfers files concurrently. The lock protects against + concurrent access when multiple files complete simultaneously. + + Attributes: + progress_reporter: The progress reporter for sending updates to Jobs service. + fileset_name: The name of the fileset (workspace/name format). + total_files: Total number of files to transfer. + total_size: Total size of all files in bytes. + stats: Mutable stats object to track progress (UploadStats or DownloadStats). + _lock: Threading lock for thread-safe stats updates. + + """ + + progress_reporter: ProgressReporter + fileset_name: str + total_files: int + total_size: int + stats: UploadStats | DownloadStats + _lock: threading.Lock + + def __init__( + self, + progress_reporter: ProgressReporter, + fileset_name: str, + total_files: int, + total_size: int, + stats: UploadStats | DownloadStats, + **kwargs: Any, + ): + """Initialize the progress callback. + + Args: + progress_reporter: The progress reporter for sending updates to Jobs service. + fileset_name: The name of the fileset (workspace/name format). + total_files: Total number of files to transfer. + total_size: Total size of all files in bytes. + stats: Mutable stats object to track progress. + **kwargs: Additional arguments passed to the base Callback. + + """ + super().__init__(**kwargs) + self.progress_reporter = progress_reporter + self.fileset_name = str(fileset_name) + self.total_files = total_files + self.total_size = total_size + self.stats = stats + self._lock = threading.Lock() + + @staticmethod + def list_local_files(src_path: Path) -> list[FileInfo]: + """List all files from a local path (file or directory). + + If src_path is a file, returns a single FileInfo with the filename. + If src_path is a directory, recursively lists all files. + + Returns list of FileInfo objects with 'path' (relative path) and 'size' keys. + This mirrors the format returned by list_fileset_files. + """ + if not src_path.exists(): + logger.warning(f"Failed to list local files. Source path does not exist: {src_path}") + return [] + + try: + # Handle single file + if src_path.is_file(): + logger.info(f"Found 1 file: {src_path.name}") + return [ + FileInfo( + path=src_path.name, + size=src_path.stat().st_size, + ), + ] + + # Handle directory + files = [] + for root, _, filenames in os.walk(src_path): + for filename in filenames: + full_path = Path(root) / filename + relative_path = full_path.relative_to(src_path) + files.append( + FileInfo( + path=str(relative_path), + size=full_path.stat().st_size, + ), + ) + logger.info(f"Found {len(files)} files in {src_path}") + return files + except Exception as e: + logger.warning(f"Failed to list local files. Source path: {src_path}. Error: {e}") + return [] + + @abstractmethod + def branched(self, source_path: str, dest_path: str, **kwargs: Any) -> "BaseSingleFileCallback": + """Create a child callback for a single file transfer. + + Args: + source_path: Source file path. + dest_path: Destination file path. + **kwargs: Additional keyword arguments. + + Returns: + A BaseSingleFileCallback subclass for tracking this file's transfer. + + """ + ... + + +class BaseSingleFileCallback(Callback): + """Base class for single file upload/download callbacks. + + This abstract base class provides common functionality for tracking individual + file transfers within a batch operation. Subclasses implement operation-specific + behavior via the template method pattern. + + The close() method uses the template method pattern, calling abstract methods + that subclasses override to provide operation-specific behavior: + - _get_phase(): Returns the TaskPhase for this operation + - _get_file_display_path(): Returns the path to display for logging + - _update_stats(): Updates the parent's stats for this operation + - _build_status_details(): Builds the status_details dict for progress reporting + """ + + parent: BaseProgressCallback + source_path: str + dest_path: str + _completed: bool + + def __init__( + self, + parent: BaseProgressCallback, + source_path: str, + dest_path: str, + **kwargs: Any, + ): + """Initialize the single file callback. + + Args: + parent: The parent progress callback. + source_path: Path to the source file. + dest_path: Destination path for the file. + **kwargs: Additional arguments passed to the base Callback. + + """ + super().__init__(**kwargs) + self.parent = parent + self.source_path = source_path + self.dest_path = dest_path + self._completed = False + + @abstractmethod + def _get_phase(self) -> str: + """Return the TaskPhase for this operation.""" + ... + + @abstractmethod + def _get_file_display_path(self) -> str: + """Return the path to use for display/logging.""" + ... + + @abstractmethod + def _update_stats(self) -> None: + """Update the parent's stats for this operation (called within lock).""" + ... + + @abstractmethod + def _get_files_count(self) -> int: + """Return the current files count from stats (called within lock).""" + ... + + @abstractmethod + def _build_status_details(self, files_count: int, total_bytes: int, current_file: str) -> dict[str, Any]: + """Build the status_details dict for progress reporting. + + Args: + files_count: Number of files transferred so far. + total_bytes: Total bytes transferred so far. + current_file: Name of the current file for display. + + Returns: + Dictionary with status details for the progress report. + + """ + ... + + def close(self) -> None: + """Called when the file transfer completes. + + Updates the parent's statistics and reports progress to the Jobs service. + Thread-safe: uses parent's lock to protect stats updates. + """ + if self._completed: + return + + self._completed = True + parent = self.parent + + # Extract the filename for logging/display + current_file = self._get_file_display_path() + + # Thread-safe stats update + with parent._lock: + # Update stats (operation-specific) + self._update_stats() + + # Capture current values while holding the lock + files_count = self._get_files_count() + total_bytes = parent.stats.total_bytes + + logger.debug(f"File transferred: {current_file} ({files_count}/{parent.total_files})") + + # Report progress to Jobs service (outside lock to avoid holding it during I/O) + parent.progress_reporter.update_progress( + status=PlatformJobStatus.ACTIVE, + status_details=self._build_status_details(files_count, total_bytes, current_file), + ) + + def __enter__(self) -> "BaseSingleFileCallback": + return self + + def __exit__(self, *exc_args: object) -> None: + self.close() + + +class FileUploadProgressCallback(BaseProgressCallback): + """Callback for tracking file upload progress and reporting to the Jobs service. + + This callback integrates with fsspec's callback mechanism to report progress + after each file is uploaded. It uses the branched callback pattern where: + - The parent callback tracks overall upload statistics + - Child callbacks are created for each file via `branched()` + - When a child callback closes, it signals file completion to the parent + + Usage: + callback = FileUploadProgressCallback( + progress_reporter=reporter, + src_path=src_path, + fileset_name="workspace/fileset", + stats=upload_stats, + ) + filesystem_sdk.put(src, dest, recursive=True, callback=callback) + """ + + stats: UploadStats + + def __init__( + self, + progress_reporter: ProgressReporter, + src_path: Path, + fileset_name: str, + stats: UploadStats, + **kwargs: Any, + ): + """Initialize the upload progress callback. + + Args: + progress_reporter: The progress reporter for sending updates to Jobs service. + src_path: The source path (file or directory) to upload. + fileset_name: The name of the target fileset (workspace/name format). + stats: Mutable UploadStats object to track progress. + **kwargs: Additional arguments passed to the base Callback. + + """ + # List files to get stats before upload + files = self.list_local_files(src_path) + + if not files: + logger.warning(f"Source path {src_path} contains no files") + + total_files = len(files) + total_size = sum(f.size for f in files) + + # Initialize base class with computed values + super().__init__( + progress_reporter=progress_reporter, + fileset_name=fileset_name, + total_files=total_files, + total_size=total_size, + stats=stats, + **kwargs, + ) + + logger.info(f"Uploading {total_files} files ({total_size} bytes) to {self.fileset_name}") + + # Report initial progress + progress_reporter.update_progress( + status=PlatformJobStatus.ACTIVE, + status_details={ + "phase": TaskPhase.UPLOADING, + "fileset": self.fileset_name, + "total_files": total_files, + "total_size": total_size, + "uploaded_files": 0, + "uploaded_bytes": 0, + }, + ) + + def branched(self, source_path: str, dest_path: str, **kwargs: Any) -> "SingleFileUploadCallback": + """Create a child callback for a single file upload. + + This method is called by fsspec when starting a file transfer within + a recursive put operation. It returns a child callback that tracks + the individual file's progress and reports completion to the parent. + + Args: + source_path: Source file path. + path_2: Destination file path. + **kwargs: Additional keyword arguments. + + Returns: + A SingleFileUploadCallback for tracking this file's upload. + + """ + return SingleFileUploadCallback( + parent=self, + source_path=source_path, + dest_path=dest_path, + **kwargs, + ) + + +class SingleFileUploadCallback(BaseSingleFileCallback): + """Callback for tracking a single file upload within a batch operation. + + This child callback is created by FileUploadProgressCallback.branched() + for each file being uploaded. When the upload completes and this callback + is closed, it notifies the parent to update overall progress. + """ + + parent: FileUploadProgressCallback + + def _get_phase(self) -> str: + """Return the TaskPhase for upload operations.""" + return TaskPhase.UPLOADING + + def _get_file_display_path(self) -> str: + """Return the destination filename for display.""" + return self.dest_path.split("/")[-1] if "/" in self.dest_path else self.dest_path + + def _update_stats(self) -> None: + """Update the parent's upload stats.""" + self.parent.stats.files_uploaded += 1 + if self.size is not None: + self.parent.stats.total_bytes += self.size + + def _get_files_count(self) -> int: + """Return the current uploaded files count.""" + return self.parent.stats.files_uploaded + + def _build_status_details(self, files_count: int, total_bytes: int, current_file: str) -> dict[str, Any]: + """Build the status_details dict for upload progress reporting.""" + return { + "phase": TaskPhase.UPLOADING, + "fileset": self.parent.fileset_name, + "total_files": self.parent.total_files, + "total_size": self.parent.total_size, + "uploaded_files": files_count, + "uploaded_bytes": total_bytes, + "current_file": current_file, + "progress_pct": get_percentage(files_count, self.parent.total_files), + } + + +class FileDownloadProgressCallback(BaseProgressCallback): + """Callback for tracking file download progress and reporting to the Jobs service. + + Similar to FileUploadProgressCallback but for download operations. + + Usage: + callback = FileDownloadProgressCallback( + progress_reporter=reporter, + fileset_name="workspace/fileset", + total_files=10, + total_size=1024000, + stats=download_stats, + ) + filesystem_sdk.get(src, dest, recursive=True, callback=callback) + """ + + stats: DownloadStats + + def __init__( + self, + progress_reporter: ProgressReporter, + fileset_name: str, + total_files: int, + total_size: int, + stats: DownloadStats, + **kwargs: Any, + ): + """Initialize the download progress callback. + + Args: + progress_reporter: The progress reporter for sending updates to Jobs service. + fileset_name: The name of the source fileset (workspace/name format). + total_files: Total number of files to download. + total_size: Total size of all files in bytes. + stats: Mutable DownloadStats object to track progress. + **kwargs: Additional arguments passed to the base Callback. + + """ + super().__init__( + progress_reporter=progress_reporter, + fileset_name=fileset_name, + total_files=total_files, + total_size=total_size, + stats=stats, + **kwargs, + ) + + logger.info(f"Downloading {total_files} files ({total_size} bytes) from {self.fileset_name}") + + # Report initial progress + progress_reporter.update_progress( + status=PlatformJobStatus.ACTIVE, + status_details={ + "phase": TaskPhase.DOWNLOADING, + "fileset": self.fileset_name, + "total_files": total_files, + "total_size": total_size, + "downloaded_files": 0, + "downloaded_bytes": 0, + }, + ) + + def branched(self, source_path: str, dest_path: str, **kwargs: Any) -> "SingleFileDownloadCallback": + """Create a child callback for a single file download. + + Args: + source_path: Source file path in the fileset. + dest_path: Destination local file path. + **kwargs: Additional keyword arguments. + + Returns: + A SingleFileDownloadCallback for tracking this file's download. + + """ + return SingleFileDownloadCallback( + parent=self, + source_path=source_path, + dest_path=dest_path, + **kwargs, + ) + + +class SingleFileDownloadCallback(BaseSingleFileCallback): + """Callback for tracking a single file download within a batch operation. + + This child callback is created by FileDownloadProgressCallback.branched() + for each file being downloaded. When the download completes and this callback + is closed, it notifies the parent to update overall progress. + """ + + parent: FileDownloadProgressCallback + + def _get_phase(self) -> str: + """Return the TaskPhase for download operations.""" + return TaskPhase.DOWNLOADING + + def _get_file_display_path(self) -> str: + """Return the source filename for display.""" + return self.source_path.split("/")[-1] if "/" in self.source_path else self.source_path + + def _update_stats(self) -> None: + """Update the parent's download stats.""" + self.parent.stats.files_downloaded += 1 + if self.size is not None: + self.parent.stats.total_bytes += self.size + + def _get_files_count(self) -> int: + """Return the current downloaded files count.""" + return self.parent.stats.files_downloaded + + def _build_status_details(self, files_count: int, total_bytes: int, current_file: str) -> dict[str, Any]: + """Build the status_details dict for download progress reporting.""" + return { + "phase": TaskPhase.DOWNLOADING, + "fileset": self.parent.fileset_name, + "total_files": self.parent.total_files, + "total_size": self.parent.total_size, + "downloaded_files": files_count, + "downloaded_bytes": total_bytes, + "current_file": current_file, + "progress_pct": get_percentage(files_count, self.parent.total_files), + } + + +class CompositeCallback(Callback): + """A callback that delegates to multiple child callbacks. + + This allows combining multiple callbacks (e.g., TqdmCallback for console progress + and FileUploadProgressCallback for Jobs service reporting) into a single callback + that can be passed to fsspec operations. + + All callback methods are forwarded to each child callback in order. + + Usage: + tqdm_cb = TqdmCallback(tqdm_kwargs={"desc": "Uploading"}) + progress_cb = FileUploadProgressCallback(...) + composite = CompositeCallback(tqdm_cb, progress_cb) + filesystem_sdk.put(src, dest, recursive=True, callback=composite) + """ + + def __init__(self, *callbacks: Callback, **kwargs: Any): + """Initialize with multiple callbacks. + + Args: + *callbacks: Variable number of Callback instances to delegate to. + **kwargs: Additional arguments passed to the base Callback. + + """ + super().__init__(**kwargs) + self.callbacks = list(callbacks) + + def set_size(self, size: int) -> None: + """Set size on all child callbacks.""" + self.size = size + for cb in self.callbacks: + cb.set_size(size) + + def absolute_update(self, value: int) -> None: + """Update absolute value on all child callbacks.""" + self.value = value + for cb in self.callbacks: + cb.absolute_update(value) + + def relative_update(self, inc: int = 1) -> None: + """Update relative value on all child callbacks.""" + self.value += inc + for cb in self.callbacks: + cb.relative_update(inc) + + def branched(self, source_path: str, dest_path: str, **kwargs: Any) -> "CompositeCallback": + """Create a composite child callback from all child callbacks' branched results. + + Each child callback's branched() method is called, and the results are + wrapped in a new CompositeCallback. + + Args: + source_path: Source path. + dest_path: Destination path. + **kwargs: Additional keyword arguments. + + Returns: + A new CompositeCallback wrapping all child callbacks' branched results. + + """ + child_callbacks = [cb.branched(source_path, dest_path, **kwargs) for cb in self.callbacks] + return CompositeCallback(*child_callbacks) + + def call(self, hook_name: str | None = None, **kwargs: Any) -> None: + """Call hooks on all child callbacks.""" + for cb in self.callbacks: + cb.call(hook_name, **kwargs) + + def close(self) -> None: + """Close all child callbacks.""" + for cb in self.callbacks: + cb.close() + + def __enter__(self) -> "CompositeCallback": + for cb in self.callbacks: + cb.__enter__() + return self + + def __exit__(self, *exc_args: object) -> None: + for cb in self.callbacks: + cb.__exit__(*exc_args) diff --git a/services/automodel/src/nmp/automodel/tasks/file_io/progress_reporter.py b/services/automodel/src/nmp/automodel/tasks/file_io/progress_reporter.py new file mode 100644 index 0000000000..00fa660118 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/file_io/progress_reporter.py @@ -0,0 +1,93 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import logging +from typing import Any, Protocol + +from nemo_platform import NeMoPlatform, omit +from nemo_platform._exceptions import APIError +from nmp.automodel.app.jobs.context import NMPJobContext +from nmp.automodel.app.jobs.file_io.schemas import ProgressReportError +from nmp.automodel.tasks.file_io.utils import sdk_error_handler +from nmp.common.jobs.schemas import PlatformJobStatus + +logger = logging.getLogger(__name__) + + +class ProgressReporter(Protocol): + """Interface for reporting task progress.""" + + def update_progress( + self, + status: PlatformJobStatus, + status_details: dict[str, Any] | None = None, + error_details: dict[str, Any] | None = None, + error_stack: str | None = None, + ) -> None: + """Update task progress.""" + ... + + +class NoOpProgressReporter: + """Progress reporter that does nothing. Used when Jobs service is not configured.""" + + def update_progress( + self, + status: PlatformJobStatus, + status_details: dict[str, Any] | None = None, + error_details: dict[str, Any] | None = None, + error_stack: str | None = None, + ) -> None: + """No-op: silently ignore progress updates.""" + + +class JobsServiceProgressReporter: + """Reports progress to the Jobs service via SDK.""" + + def __init__(self, sdk: NeMoPlatform, workspace: str, job_id: str, step_name: str, task_id: str): + self.sdk = sdk + self.workspace = workspace + self.job_id = job_id + self.step_name = step_name + self.task_id = task_id + + def update_progress( + self, + status: PlatformJobStatus, + status_details: dict[str, object] | None = None, + error_details: dict[str, object] | None = None, + error_stack: str | None = None, + ) -> None: + """Update task progress via SDK.""" + try: + with sdk_error_handler( + ProgressReportError, + f"update progress for task: {self.task_id}, job: {self.job_id}, step: {self.step_name}", + passthrough=(APIError,), + ): + self.sdk.jobs.tasks.create_or_update( + self.task_id, + workspace=self.workspace, + job=self.job_id, + step=self.step_name, + status=status.value, + status_details=status_details if status_details else omit, + error_details=error_details if error_details else omit, + error_stack=error_stack if error_stack else omit, + ) + logger.debug(f"Progress updated: {status} - {status_details}") + except Exception as e: + logger.warning( + f"Failed to report progress for task {self.task_id}, job {self.job_id}, step {self.step_name}: {e}", + ) + + @staticmethod + def create_progress_reporter(sdk: NeMoPlatform, job_ctx: NMPJobContext) -> ProgressReporter: + """Create JobsServiceProgressReporter when jobs_url is set, else NoOpProgressReporter.""" + if job_ctx.jobs_url: + logger.info(f"Progress reporting enabled: {job_ctx.jobs_url}") + return JobsServiceProgressReporter( + sdk, job_ctx.workspace, job_ctx.job_id, job_ctx.step, job_ctx.normalized_task + ) + logger.info("Progress reporting disabled: jobs_url not configured") + return NoOpProgressReporter() diff --git a/services/automodel/src/nmp/automodel/tasks/file_io/run.py b/services/automodel/src/nmp/automodel/tasks/file_io/run.py new file mode 100644 index 0000000000..f14acd7b25 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/file_io/run.py @@ -0,0 +1,560 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""File I/O task entry point. + +Handles file operations between NeMo Platform Files Service and the job's shared PVC. + +The task reads configuration and performs: +- Downloads: If config.download is non-empty, download files from FileSets to local paths +- Uploads: If config.upload is non-empty, upload files from local paths to FileSets + +Usage: + export NEMO_JOB_STEP_CONFIG_FILE_PATH= + python -m nmp.automodel.tasks.file_io +""" + +import logging +from pathlib import Path + +import httpx + +# https://docs.nvidia.com/nemo/microservices/latest/pysdk/index.html#handling-errors +from nemo_platform import ( + APIConnectionError, + APITimeoutError, + ConflictError, + InternalServerError, + NeMoPlatform, + NotFoundError, +) +from nemo_platform.types.files.fileset_file import FilesetFile +from nmp.automodel.app.constants import SERVICE_NAME +from nmp.automodel.app.jobs.context import NMPJobContext +from nmp.automodel.app.jobs.file_io.schemas import ( + DownloadItem, + DownloadStats, + FileDownloadError, + FileSetRef, + FileUploadError, + PathTraversalError, + TaskPhase, + UploadItem, + UploadStats, +) +from nmp.automodel.tasks.file_io.callbacks import ( + CompositeCallback, + FileDownloadProgressCallback, + FileUploadProgressCallback, + TqdmPerFileDownloadCallback, + TqdmPerFileUploadCallback, +) +from nmp.automodel.tasks.file_io.progress_reporter import JobsServiceProgressReporter, ProgressReporter +from nmp.automodel.tasks.file_io.utils import ( + filesystem_sdk_error_handler, + get_config, + sdk_error_handler, + validate_safe_path, + validate_storage_path, +) +from nmp.common.jobs.schemas import PlatformJobStatus +from nmp.common.sdk_factory import get_task_sdk +from tenacity import before_sleep_log, retry, retry_if_exception_type, stop_after_attempt, wait_exponential + +logger = logging.getLogger(__name__) + +# Timeout configurations for SDK operations (httpx.Timeout for API calls) +CREATE_FILESET_TIMEOUT = httpx.Timeout(10.0, connect=10.0) +LIST_FILES_TIMEOUT = httpx.Timeout(10.0, connect=10.0) + +# Timeout configurations for FilesetFileSystem operations. +# These are passed via sdk.with_options(timeout=...) and control the httpx client. +# httpx.Timeout(read=...) is the max wait for a single chunk (16MB by default), NOT total transfer time. +# nemo-platform/src/nemo_platform/filesets/filesystem/filesystem.py > blocksize = 16 * 1024 * 1024 # 16MB +# It's a socket-level timeout. Each individual socket read has its own timeout window. +# SDK defaults httpx.Timeout(timeout=60, connect=5.0) nemo-platform/src/nemo_platform/_constants.py +DOWNLOAD_TIMEOUT = httpx.Timeout(30.0, read=5 * 60) # 30s connect/pool, 5min per-chunk read +UPLOAD_TIMEOUT = httpx.Timeout(30.0, write=10 * 60, read=5 * 60) # 30s connect/pool, 10min write, 5min read + +# Retry configuration +MAX_RETRIES = 3 +INITIAL_BACKOFF_SECONDS = 1.0 +MAX_BACKOFF_SECONDS = 30.0 + +# Transient exceptions that should trigger retries for filesystem operations. +# FilesetFileSystem uses httpx under the hood, so we retry on httpx transient errors +# in addition to SDK-level transient errors. +TRANSIENT_FILESYSTEM_EXCEPTIONS = ( + httpx.TimeoutException, + httpx.ConnectError, + httpx.ReadTimeout, +) + + +class FileIORunner: + def __init__( + self, + sdk: NeMoPlatform, + progress_reporter: ProgressReporter, + job_ctx: NMPJobContext, + ): + self.sdk = sdk + self.progress_reporter = progress_reporter + self.job_ctx = job_ctx + + def list_fileset_files( + self, + fileset: FileSetRef, + ) -> list[FilesetFile]: + """List files in a FileSet. + + Returns list of file info dicts with 'path' and 'size' keys. + """ + try: + with sdk_error_handler(FileDownloadError, f"list files in fileset {fileset}", passthrough=(NotFoundError,)): + response = self.sdk.with_options(timeout=LIST_FILES_TIMEOUT).files.list( + fileset=fileset.name, + workspace=fileset.workspace, + ) + logger.info(f"Found {len(response.data)} files in FileSet {fileset!s}") + return response.data + except NotFoundError as e: + raise FileDownloadError( + f"FileSet {fileset!s} not found. Please ensure the FileSet exists and contains the expected files.", + ) from e + + def download_fileset( + self, + fileset: FileSetRef, + dest_dir: Path, + ) -> DownloadStats: + """Download all files from a FileSet to a destination directory. + + Uses FilesetFileSystem.get() with recursive=True for efficient batch downloads. + Progress is tracked via two callbacks combined in a CompositeCallback: + - TqdmPerFileDownloadCallback: Creates a separate console progress bar per file (shows bytes) + - FileDownloadProgressCallback: Reports progress to Jobs service after each file + + Args: + fileset: The source FileSet reference. + dest_dir: The destination directory path. + + Returns: + DownloadStats with files_downloaded, total_bytes, and failed_files counts. + + Raises: + FileDownloadError: If the download fails. + + """ + stats = DownloadStats() + fileset_name = str(fileset) + + # List files in the fileset to get total count and size + files = self.list_fileset_files(fileset) + + if not files: + logger.warning(f"FileSet {fileset_name} contains no files") + return stats + + total_files = len(files) + total_size = sum(f.size for f in files) + + # Ensure destination directory exists + dest_dir.mkdir(parents=True, exist_ok=True) + + # Build file sizes mapping for progress bar display + # Maps relative file paths to their sizes in bytes + file_sizes = {f.path.lstrip("/"): f.size for f in files} + + # Create callbacks: + # 1. TqdmPerFileDownloadCallback for console progress - creates a separate progress bar per file + tqdm_callback = TqdmPerFileDownloadCallback( + dest_path=dest_dir, + fileset_path=fileset_name, + file_sizes=file_sizes, + ) + + # 2. FileDownloadProgressCallback for Jobs service reporting + jobs_callback = FileDownloadProgressCallback( + progress_reporter=self.progress_reporter, + fileset_name=fileset_name, + total_files=total_files, + total_size=total_size, + stats=stats, + ) + + # Combine both callbacks into a composite that delegates to both + composite_callback = CompositeCallback(tqdm_callback, jobs_callback) + + with filesystem_sdk_error_handler( + FileDownloadError, + f"download from '{fileset_name}' to '{dest_dir}'", + ): + self._download_with_retry( + fileset_name=fileset.name, + fileset_workspace=fileset.workspace, + dest_dir=str(dest_dir), + callback=composite_callback, + ) + + logger.info(f"Download complete: {stats.files_downloaded} files, {stats.total_bytes} bytes") + return stats + + @retry( + stop=stop_after_attempt(MAX_RETRIES), + wait=wait_exponential(multiplier=2, min=INITIAL_BACKOFF_SECONDS, max=MAX_BACKOFF_SECONDS), + retry=retry_if_exception_type(TRANSIENT_FILESYSTEM_EXCEPTIONS), + reraise=True, + before_sleep=before_sleep_log(logger, logging.WARNING), + ) + def _download_with_retry( + self, + fileset_name: str, + fileset_workspace: str | None, + dest_dir: str, + callback: CompositeCallback, + ) -> None: + """Internal method with retry logic for downloading from FilesetFileSystem.""" + self.sdk.with_options(timeout=DOWNLOAD_TIMEOUT).files.download( + fileset=fileset_name, + workspace=fileset_workspace, + local_path=dest_dir, + callback=callback, # type: ignore[arg-type] + ) + + def upload_fileset( + self, + fileset: FileSetRef, + src_path: Path, + ) -> UploadStats: + """Upload all files from a source path (file or directory) to a FileSet. + + Uses FilesetFileSystem.put() with recursive=True for efficient batch uploads. + Progress is tracked via two callbacks combined in a CompositeCallback: + - TqdmPerFileCallback: Creates a separate console progress bar per file (shows bytes) + - FileUploadProgressCallback: Reports progress to Jobs service after each file + + Args: + fileset: The target FileSet reference. + src_path: The source path, can be a single file or a directory. + progress_reporter: Progress reporter for status updates. + + Returns: + UploadStats with files_uploaded, total_bytes, and failed_files counts. + + Raises: + FileUploadError: If the upload fails. + + """ + stats = UploadStats() + fileset_name = str(fileset) + + # Create callbacks: + # 1. TqdmPerFileCallback for console progress - creates a separate progress bar per file + tqdm_callback = TqdmPerFileUploadCallback(src_path=src_path) + + # 2. FileUploadProgressCallback for Jobs service reporting + jobs_callback = FileUploadProgressCallback( + progress_reporter=self.progress_reporter, + src_path=src_path, + fileset_name=fileset_name, + stats=stats, + ) + + # Combine both callbacks into a composite that delegates to both + composite_callback = CompositeCallback(tqdm_callback, jobs_callback) + + # Build local and remote paths for upload + # remote_path is relative within the fileset (e.g., "" for root, "filename" for single file) + if src_path.is_dir(): + # Add trailing slash to source to copy directory CONTENTS (not the directory itself) + # This follows rsync/scp convention: "dir/" copies contents, "dir" copies the directory + local_path = f"{src_path}/" + remote_path = "" # Upload to fileset root + else: + # Single file: upload to fileset root with same filename + local_path = str(src_path) + remote_path = src_path.name + + with filesystem_sdk_error_handler( + FileUploadError, + f"upload from '{src_path}' to '{fileset_name}'", + ): + self._upload_with_retry( + local_path=local_path, + remote_path=remote_path, + fileset_name=fileset.name, + fileset_workspace=fileset.workspace, + callback=composite_callback, + ) + + logger.info(f"Upload complete: {stats.files_uploaded} files, {stats.total_bytes} bytes") + return stats + + @retry( + stop=stop_after_attempt(MAX_RETRIES), + wait=wait_exponential(multiplier=2, min=INITIAL_BACKOFF_SECONDS, max=MAX_BACKOFF_SECONDS), + retry=retry_if_exception_type(TRANSIENT_FILESYSTEM_EXCEPTIONS), + reraise=True, + before_sleep=before_sleep_log(logger, logging.WARNING, exc_info=True), + ) + def _upload_with_retry( + self, + local_path: str, + remote_path: str, + fileset_name: str, + fileset_workspace: str | None, + callback: CompositeCallback, + ) -> None: + """Internal method with retry logic for uploading to FilesetFileSystem.""" + self.sdk.with_options(timeout=UPLOAD_TIMEOUT).files.upload( + local_path=local_path, + remote_path=remote_path, + fileset=fileset_name, + workspace=fileset_workspace, + callback=callback, # type: ignore[arg-type] + ) + + def run_download(self, downloads: list[DownloadItem]) -> None: + """Execute download operations. + + Downloads files from FileSets to job storage based on downloads list. + """ + if not downloads: + logger.info("No downloads configured, skipping download operation") + return + + storage_path = validate_storage_path(self.job_ctx.storage_path) + + logger.info(f"Starting download operation: {len(downloads)} fileset(s) to download") + + # Report task started + self.progress_reporter.update_progress( + status=PlatformJobStatus.ACTIVE, + status_details={ + "phase": TaskPhase.DOWNLOADING, + "total_filesets": len(downloads), + "completed_filesets": 0, + }, + ) + + total_stats = DownloadStats() + + for idx, item in enumerate(downloads): + fileset = item.src + # Validate destination path to prevent path traversal attacks + dest_dir = validate_safe_path(storage_path, item.dest) + + logger.info(f"[{idx + 1}/{len(downloads)}] Downloading from {fileset!s} to {dest_dir}") + + self.progress_reporter.update_progress( + status=PlatformJobStatus.ACTIVE, + status_details={ + "phase": TaskPhase.DOWNLOADING, + "total_filesets": len(downloads), + "completed_filesets": idx, + "current_fileset": f"{fileset!s}", + }, + ) + + stats = self.download_fileset( + fileset, + dest_dir, + ) + total_stats.files_downloaded += stats.files_downloaded + total_stats.total_bytes += stats.total_bytes + + logger.info(f"FileSet download complete: {stats.files_downloaded} files, {stats.total_bytes} bytes") + + logger.info( + f"All downloads complete: {total_stats.files_downloaded} files, {total_stats.total_bytes} bytes total", + ) + + def create_fileset(self, fileset: FileSetRef, metadata: dict | None = None) -> None: + """Create a FileSet. Skip if it already exists. + + Uses retry logic for transient errors and converts SDK exceptions to FileUploadError. + """ + # sdk_error_handler wraps the retry to convert exceptions after all retries exhaust + with sdk_error_handler(FileUploadError, f"create fileset {fileset}", passthrough=(ConflictError,)): + self._create_fileset_with_retry(fileset, metadata) + + # we don't use sdk retry because it would retry on ConflictError which is expected and would be wasteful + @retry( + stop=stop_after_attempt(MAX_RETRIES), + wait=wait_exponential(multiplier=2, min=INITIAL_BACKOFF_SECONDS, max=MAX_BACKOFF_SECONDS), + retry=retry_if_exception_type((InternalServerError, APITimeoutError, APIConnectionError)), + reraise=True, # means that the last exception will be re-raised after the last retry attempt + ) + def _create_fileset_with_retry(self, fileset: FileSetRef, metadata: dict | None = None) -> None: + """Internal method with retry logic for creating a FileSet.""" + try: + create_kwargs: dict = { + "workspace": fileset.workspace, + "name": fileset.name, + "timeout": CREATE_FILESET_TIMEOUT, + "custom_fields": {"service_source": "automodel"}, + } + if metadata is not None: + create_kwargs["metadata"] = metadata + result = self.sdk.with_options(max_retries=0).files.filesets.create(**create_kwargs) + logger.info(f"Created FileSet: {result.workspace}/{result.name}") + except ConflictError: + # Fileset already exists - patch metadata so tool_calling etc. are not lost + workspace = fileset.workspace or self.job_ctx.workspace + if metadata is not None: + try: + self.sdk.with_options(max_retries=0).files.filesets.update( + name=fileset.name, + workspace=workspace, + metadata=metadata, + timeout=CREATE_FILESET_TIMEOUT, + ) + logger.info(f"Patched existing FileSet metadata: {workspace}/{fileset.name}") + except Exception as e: + logger.warning( + f"Could not patch metadata on existing fileset {workspace}/{fileset.name}: {e}. " + "Upload will continue; model-spec may lack tool_calling/chat_template from source." + ) + + def run_upload(self, uploads: list[UploadItem]) -> None: + """Execute upload operations. + + Uploads files from job storage to FileSets based on uploads list. + + Args: + uploads: List of upload items to process. + """ + if not uploads: + logger.info("No uploads configured, skipping upload operation") + return + + storage_path = validate_storage_path(self.job_ctx.storage_path) + + logger.info(f"Starting upload operation: {len(uploads)} fileset(s) to upload") + + # Report task started + self.progress_reporter.update_progress( + status=PlatformJobStatus.ACTIVE, + status_details={ + "phase": TaskPhase.UPLOADING, + "total_filesets": len(uploads), + "completed_filesets": 0, + }, + ) + + total_stats = UploadStats() + + for idx, item in enumerate(uploads): + if item.dest.workspace is None: + item.dest.workspace = self.job_ctx.workspace + fileset = item.dest + # Validate source path to prevent path traversal attacks + src_path = validate_safe_path(storage_path, item.src) + if not src_path.exists(): + raise FileUploadError(f"Source path does not exist: {src_path}. Ensure the source path exists.") + if not src_path.is_dir() and not src_path.is_file(): + raise FileUploadError( + f"Source path is not a file or directory: {src_path}. Ensure the source path is a file or directory.", + ) + + logger.info(f"[{idx + 1}/{len(uploads)}] Uploading from {src_path} to {fileset!s}") + + self.progress_reporter.update_progress( + status=PlatformJobStatus.ACTIVE, + status_details={ + "phase": TaskPhase.UPLOADING, + "total_filesets": len(uploads), + "completed_filesets": idx, + "current_fileset": str(fileset), + }, + ) + + self.create_fileset(fileset, metadata=item.metadata) + + stats = self.upload_fileset( + fileset, + src_path, + ) + total_stats.files_uploaded += stats.files_uploaded + total_stats.total_bytes += stats.total_bytes + + logger.info(f"FileSet upload complete: {stats.files_uploaded} files, {stats.total_bytes} bytes") + + logger.info(f"All uploads complete: {total_stats.files_uploaded} files, {total_stats.total_bytes} bytes total") + + +def run(sdk: NeMoPlatform | None = None, job_ctx: NMPJobContext | None = None) -> int: + """Execute the file I/O task. + + Processes downloads and uploads based on the configuration. + + Args: + sdk: Optional SDK instance for dependency injection (for testing). + If None, creates one via get_task_sdk(). + job_ctx: Optional job context for dependency injection (for testing). + If None, creates one via NMPJobContext.from_env(). + + Returns: + Exit code (0 for success, non-zero for failure). + + """ + job_ctx = job_ctx or NMPJobContext.from_env() + validate_storage_path(job_ctx.storage_path) + + sdk_owned = sdk is None + progress_reporter: ProgressReporter | None = None + try: + sdk = sdk or get_task_sdk(SERVICE_NAME) + # Initialize progress reporter (no-op if Jobs URL not configured) + progress_reporter = JobsServiceProgressReporter.create_progress_reporter(sdk, job_ctx) + runner = FileIORunner(sdk=sdk, progress_reporter=progress_reporter, job_ctx=job_ctx) + + config = get_config(job_ctx.config_path) + + logger.info(f"Starting file I/O task with job context: {job_ctx}") + logger.info(f"Config: {config.model_dump_json(indent=2)}") + logger.info(f"NeMo Platform service URL: {sdk.base_url}") + + # Execute uploads if configured + runner.run_upload(config.upload) + + # Execute downloads if configured + runner.run_download(config.download) + + # Report overall completion + progress_reporter.update_progress( + status=PlatformJobStatus.COMPLETED, + status_details={"phase": TaskPhase.COMPLETED, "message": "File I/O task completed successfully"}, + ) + + return 0 + except PathTraversalError as e: + logger.error(f"Security error - path traversal detected: {e}") + if progress_reporter: + progress_reporter.update_progress( + status=PlatformJobStatus.ERROR, + error_details={"message": str(e), "type": type(e).__name__}, + ) + return 1 + except (FileDownloadError, FileUploadError) as e: + logger.exception(f"File operation failed: {e}") + if progress_reporter: + progress_reporter.update_progress( + status=PlatformJobStatus.ERROR, + error_details={"message": str(e), "type": type(e).__name__}, + ) + return 1 + + except Exception as e: + logger.exception(f"File I/O task failed: {e}") + if progress_reporter: + progress_reporter.update_progress( + status=PlatformJobStatus.ERROR, + error_details={"message": str(e), "type": type(e).__name__}, + ) + return 1 + + finally: + if sdk_owned and sdk is not None: + sdk.close() diff --git a/services/automodel/src/nmp/automodel/tasks/file_io/utils.py b/services/automodel/src/nmp/automodel/tasks/file_io/utils.py new file mode 100644 index 0000000000..e809105248 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/file_io/utils.py @@ -0,0 +1,184 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import json +import logging +from collections.abc import Iterator +from contextlib import contextmanager +from pathlib import Path + +import httpx + +# https://docs.nvidia.com/nemo/microservices/latest/pysdk/index.html#handling-errors +from nemo_platform import ( + APIConnectionError, + APIStatusError, + APITimeoutError, + AuthenticationError, + PermissionDeniedError, +) +from nmp.automodel.app.jobs.file_io.schemas import ( + FileDownloadError, + FileIOTaskConfig, + FileUploadError, + PathTraversalError, + ProgressReportError, +) + +logger = logging.getLogger(__name__) + + +@contextmanager +def filesystem_sdk_error_handler( + error_class: type[FileDownloadError | FileUploadError | ProgressReportError], + operation: str, + passthrough: tuple[type[BaseException], ...] = (), +) -> Iterator[None]: + """Context manager for consistent FilesetFileSystem error handling. + + Catches FilesetFileSystem-specific exceptions and re-raises them as the specified error class + with a consistent message format. + + Args: + error_class: The exception class to raise (FileDownloadError or FileUploadError). + operation: Description of the operation for error messages (e.g., "download file.txt from fileset x/y"). + passthrough: Tuple of exception types to pass through without handling. Allows handling of exceptions outside of the context manager. + + Raises: + error_class: With a descriptive message including the error details. + + """ + try: + yield + except passthrough: + raise + except FileNotFoundError as e: + raise error_class(f"Failed to {operation} due to file not found error. Error: {e}") from e + except PermissionError as e: + raise error_class(f"Failed to {operation} due to permission denied error. Error: {e}") from e + except httpx.TimeoutException as e: + raise error_class(f"Failed to {operation} due to request timeout. Error: {e}") from e + except httpx.ConnectError as e: + raise error_class(f"Failed to {operation} due to connection error. Error: {e}") from e + except Exception as e: + raise error_class(f"Failed to {operation} due to unexpected error {type(e).__name__}: {e}") from e + + +@contextmanager +def sdk_error_handler( + error_class: type[FileDownloadError | FileUploadError | ProgressReportError], + operation: str, + passthrough: tuple[type[BaseException], ...] = (), +) -> Iterator[None]: + """Context manager for consistent SDK error handling. + + Catches SDK-specific exceptions and re-raises them as the specified error class + with a consistent message format. + + Args: + error_class: The exception class to raise (FileDownloadError or FileUploadError). + operation: Description of the operation for error messages (e.g., "download file.txt from fileset x/y"). + passthrough: Tuple of exception types to pass through without handling. Allows handling of exceptions outside of the context manager. + + Raises: + error_class: With a descriptive message including the error details. + + """ + try: + yield + except passthrough: + raise + except APITimeoutError as e: + raise error_class( + f"Failed to {operation} due to request timeout error. Cause: {e.__cause__}. Error: {e}", + ) from e + except APIConnectionError as e: + raise error_class(f"Failed to {operation} due to connection error. Cause: {e.__cause__}. Error: {e}") from e + # Note: AuthenticationError and PermissionDeniedError are subclasses of APIStatusError, + # so they must be caught before APIStatusError + except AuthenticationError as e: + raise error_class(f"Failed to {operation} due to authentication error. Error: {e}") from e + except PermissionDeniedError as e: + raise error_class(f"Failed to {operation} due to permission denied error. Error: {e}") from e + except APIStatusError as e: + raise error_class(f"Failed to {operation} due to API error. Status code: {e.status_code}. Error: {e}") from e + except Exception as e: + raise error_class(f"Failed to {operation} due to unexpected error {type(e).__name__}: {e}") from e + + +def get_config(config_path: Path) -> FileIOTaskConfig: + """Get typed task configuration from a config file. + + Loads the JSON config file and validates it against the FileIOTaskConfig schema. + + Args: + config_path: Path to the JSON configuration file. + + Returns: + Validated FileIOTaskConfig. + """ + with open(config_path) as f: + data = json.load(f) + return FileIOTaskConfig.model_validate(data) + + +def validate_storage_path(storage_path: Path) -> Path: + """Validate that a storage path exists and is a directory. + + Args: + storage_path: The storage path to validate. + + Returns: + The validated storage path. + + Raises: + FileUploadError: If the storage path does not exist or is not a directory. + """ + if not storage_path.exists() or not storage_path.is_dir(): + raise FileUploadError( + f"Storage path does not exist: {storage_path}. Ensure the storage path exists and is a directory.", + ) + return storage_path + + +def validate_safe_path(base_path: Path, user_path: str) -> Path: + """Validate that a user-provided path stays within the base directory. + + Prevents path traversal attacks where user input like "../../etc/passwd" could + escape the intended directory. The function resolves both paths to their + canonical absolute forms and verifies the result is under the base path. + + Args: + base_path: The base directory that the resolved path must stay within. + user_path: The user-provided relative path (e.g., from config). + + Returns: + The resolved absolute path that is guaranteed to be within base_path. + + Raises: + PathTraversalError: If the resolved path would escape base_path. + + Examples: + >>> base = Path("/var/storage") + >>> validate_safe_path(base, "subdir/file.txt") + PosixPath('/var/storage/subdir/file.txt') + + >>> validate_safe_path(base, "../../etc/passwd") + Raises PathTraversalError + + """ + # Resolve base_path to absolute canonical form + resolved_base = base_path.resolve() + + # Join and resolve the user path + # Using resolve() handles .., ., symlinks, etc. + resolved_path = (base_path / user_path).resolve() + + if not resolved_path.is_relative_to(resolved_base): + raise PathTraversalError( + f"Path '{user_path}' resolves outside of the base directory. " + "This may indicate a path traversal attack. " + "Ensure that paths such as ../.. are not used in the download destination path.", + ) + + return resolved_path diff --git a/services/automodel/src/nmp/automodel/tasks/model_entity/__init__.py b/services/automodel/src/nmp/automodel/tasks/model_entity/__init__.py new file mode 100644 index 0000000000..49784f0f55 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/model_entity/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Model entity task for creating model entities after customization.""" + +from nmp.automodel.tasks.model_entity.run import run + +__all__ = ["run"] diff --git a/services/automodel/src/nmp/automodel/tasks/model_entity/__main__.py b/services/automodel/src/nmp/automodel/tasks/model_entity/__main__.py new file mode 100644 index 0000000000..90a4ffe62f --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/model_entity/__main__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Entry point for model_entity task. + +Usage: + python -m nmp.automodel.tasks.model_entity +""" + +import sys + +from .run import run + +if __name__ == "__main__": + sys.exit(run()) diff --git a/services/automodel/src/nmp/automodel/tasks/model_entity/run.py b/services/automodel/src/nmp/automodel/tasks/model_entity/run.py new file mode 100644 index 0000000000..d1a6263d83 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/model_entity/run.py @@ -0,0 +1,436 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Model entity task entry point. + +Handles creating model entities in the Models service after customization completes. + +The task reads configuration and creates a Model Entity that references the +uploaded model artifacts in the Files service. + +Usage: + export NEMO_JOB_STEP_CONFIG_FILE_PATH= + python -m nmp.automodel.tasks.model_entity +""" + +import json +import logging +import re +import time +from pathlib import Path + +from nemo_platform import ( + APIConnectionError, + APITimeoutError, + ConflictError, + InternalServerError, + NeMoPlatform, + NotFoundError, +) +from nemo_platform.types.inference import ( + ModelDeploymentConfig, + ModelDeploymentConfigFilterParam, + ModelDeploymentFilterParam, + NIMDeploymentParam, +) +from nemo_platform.types.models import LoraParam, ModelEntity +from nemo_platform.types.shared_params.tool_call_config import ToolCallConfig as ToolCallConfigParam +from nmp.automodel.app.constants import SERVICE_NAME +from nmp.automodel.app.jobs.context import NMPJobContext +from nmp.automodel.app.jobs.model_entity.schemas import ( + DeploymentParameters, + ModelEntityCreationError, + ModelEntityTaskConfig, +) +from nmp.automodel.entities.values import FinetuningType +from nmp.common.sdk_factory import get_task_sdk +from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential + +logger = logging.getLogger(__name__) + +# Retry configuration +MAX_RETRIES = 3 +INITIAL_BACKOFF_SECONDS = 1.0 +MAX_BACKOFF_SECONDS = 30.0 + +ACTIVE_DEPLOYMENT_STATUSES = frozenset({"CREATED", "PENDING", "READY"}) + +SPEC_POLL_INTERVAL_SECONDS = 10 +SPEC_POLL_TIMEOUT_SECONDS = 600 + + +def get_config(config_path: Path) -> ModelEntityTaskConfig: + """Get typed task configuration from a config file. + + Loads the JSON config file and validates it against the ModelEntityTaskConfig schema. + + Args: + config_path: Path to the JSON configuration file. + + Returns: + Validated ModelEntityTaskConfig. + """ + with open(config_path) as f: + data = json.load(f) + return ModelEntityTaskConfig.model_validate(data) + + +def sanitize_name(prefix: str, name: str) -> str: + """Sanitize model_name: keep only allowed chars, replace invalid with hyphen, avoid consecutive/trailing hyphens + + Must be compatible with - {'pattern': '^[a-z](?!.*--)[a-z0-9\\-@.+_]{1,62}(? ModelEntity: + """Poll until the model_spec task has populated the model's spec. + + The spec must be populated before creating a deployment because the + inference service relies on ``spec.family`` and ``spec.base_num_parameters`` + to select the correct NIM profile. + + Raises: + ModelEntityCreationError: If the spec is not populated within the timeout. + """ + logger.info(f"Waiting for model_spec to populate spec on {workspace}/{name}") + start = time.monotonic() + + while time.monotonic() - start < SPEC_POLL_TIMEOUT_SECONDS: + try: + target = self.sdk.models.retrieve(name=name, workspace=workspace) + if target.spec: + logger.info(f"Spec populated on {workspace}/{name}") + return target + except (APIConnectionError, APITimeoutError, InternalServerError) as e: + logger.warning(f"Transient error polling spec for {workspace}/{name}: {e}") + time.sleep(SPEC_POLL_INTERVAL_SECONDS) + + raise ModelEntityCreationError( + f"Timed out waiting for model spec on {workspace}/{name} " + f"after {SPEC_POLL_TIMEOUT_SECONDS}s. The platform could not auto-detect the " + f"model's specifications. Verify the model checkpoint is valid and in a supported format." + ) + + def get_model_entity(self, model_entity: str, fileset_workspace: str) -> ModelEntity: + parts = model_entity.split("/") + if len(parts) == 1: + me_workspace = fileset_workspace + me_name = parts[0] + else: + me_workspace = parts[0] + me_name = parts[1] + + try: + me: ModelEntity = self.sdk.models.retrieve(name=me_name, workspace=me_workspace) + except NotFoundError as e: + raise ModelEntityCreationError(f"Model entity {me_workspace}/{me_name} not found") from e + + return me + + @retry( + stop=stop_after_attempt(MAX_RETRIES), + wait=wait_exponential(multiplier=2, min=INITIAL_BACKOFF_SECONDS, max=MAX_BACKOFF_SECONDS), + retry=retry_if_exception_type((InternalServerError, APITimeoutError, APIConnectionError)), + reraise=True, + ) + def create_model_entity(self, config: ModelEntityTaskConfig) -> tuple[dict, ModelEntity]: + """Create a model entity in the Models service. + + Args: + config: Configuration for the model entity to create. + + Returns: + Tuple of (result dict, deploy target). For LoRA the deploy target is the + base model entity; for SFT it is the newly created output model entity. + + Raises: + ModelEntityCreationError: If creation fails. + """ + workspace = self.job_ctx.workspace + logger.info(f"Creating model entity: {workspace}/{config.name}") + + fileset_workspace = config.fileset.workspace or workspace + fileset_ref = f"{fileset_workspace}/{config.fileset.name}" + + logger.info(f"Validating fileset exists: {fileset_workspace}/{config.fileset.name}") + try: + self.sdk.files.filesets.retrieve(workspace=fileset_workspace, name=config.fileset.name) + logger.info(f"Fileset validation successful: {fileset_workspace}/{config.fileset.name}") + except Exception as e: + logger.error(f"Fileset validation failed: {fileset_workspace}/{config.fileset.name}") + raise ModelEntityCreationError( + f"Cannot create model entity: fileset '{fileset_workspace}/{config.fileset.name}' does not exist or is not accessible" + ) from e + + base_me: ModelEntity = self.get_model_entity(config.model_entity, fileset_workspace) + + if config.peft is not None and config.peft.type == FinetuningType.LORA: + try: + output_me = self.sdk.models.adapters.create( + model_name=base_me.name, + workspace=base_me.workspace, + name=config.name, + description=config.description, + fileset=fileset_ref, + finetuning_type=config.peft.type.value, + lora_config=LoraParam( + alpha=config.peft.alpha, + rank=config.peft.rank, + ), + enabled=True, + ) + return output_me.model_dump(), base_me + except ConflictError: + logger.warning( + f"adapter {base_me.workspace}/{config.name} already exists for model {base_me.workspace}/{base_me.name}, updating with new fileset" + ) + try: + output_me = self.sdk.models.adapters.update( + adapter=config.name, + model_name=base_me.name, + workspace=base_me.workspace, + fileset=fileset_ref, + description=config.description, + enabled=True, + ) + logger.info( + f"Successfully updated adapter: {base_me.workspace}/{config.name} for base model {base_me.workspace}/{base_me.name}" + ) + return output_me.model_dump(), base_me + except (InternalServerError, APITimeoutError, APIConnectionError): + raise + except Exception as update_error: + logger.exception( + f"Failed to update existing adapter, {base_me.workspace}/{config.name}: {update_error}" + ) + raise ModelEntityCreationError( + f"Adapter '{config.name}' already exists but update failed: {update_error}" + ) from update_error + except Exception as e: + logger.exception(f"Failed to create model adapter: {e}") + raise ModelEntityCreationError(f"Failed to create model adapter: {e}") from e + else: + ft_type = config.peft.type.value if config.peft else FinetuningType.ALL_WEIGHTS.value + + request_body = { + "name": config.name, + "description": config.description, + "fileset": fileset_ref, + "finetuning_type": ft_type, + "trust_remote_code": base_me.trust_remote_code, + } + + if config.base_model: + request_body["base_model"] = config.base_model + + try: + output_me = self.sdk.models.create( + workspace=workspace, + **request_body, + ) + logger.info(f"Successfully created model entity: {output_me.workspace}/{output_me.name}") + return output_me.model_dump(), output_me + + except ConflictError: + logger.warning(f"Model entity already exists: {workspace}/{config.name}, updating existing model") + try: + update_body = {k: v for k, v in request_body.items() if k != "name"} + output_me = self.sdk.models.update( + name=config.name, + workspace=workspace, + **update_body, + ) + logger.info(f"Successfully updated model entity: {output_me.workspace}/{output_me.name}") + return output_me.model_dump(), output_me + except (InternalServerError, APITimeoutError, APIConnectionError): + raise + except Exception as update_error: + logger.exception(f"Failed to update existing model entity: {update_error}") + raise ModelEntityCreationError( + f"Model entity '{config.name}' already exists and update failed: {update_error}" + ) from update_error + + except Exception as e: + logger.exception(f"Failed to create model entity: {e}") + raise ModelEntityCreationError(f"Failed to create model entity: {e}") from e + + def launch_model(self, config: ModelEntityTaskConfig, me: ModelEntity): + """Deploy a model entity after creation. + + For LoRA jobs, ``me`` should be the base model entity. + For SFT jobs, ``me`` should be the output model entity. + """ + dc = config.deployment_config + if dc is None: + return + + # LORA_MERGED produces a full-weight model, so it is deployed like SFT + # and intentionally excluded from LoRA-specific checks below. + is_lora = config.peft is not None and config.peft.type == FinetuningType.LORA + if is_lora and self._has_active_deployment(me): + return + + if is_lora and isinstance(dc, DeploymentParameters) and not dc.lora_enabled: + logger.warning(f"Deployment requested but lora_enabled is false for a LoRA job: {dc}") + return + + # Resolve an existing config or create a new one from inline params. + if isinstance(dc, str): + logger.info(f"Resolving deployment config reference: {dc}") + deployment_config = self._resolve_config_ref(dc, me.workspace) + logger.info(f"Using deployment config: {deployment_config.workspace}/{deployment_config.name}") + else: + deployment_config = self._create_deployment_config(dc, me) + + self._create_deployment(deployment_config, me) + + def _has_active_deployment(self, me: ModelEntity) -> bool: + """Check if the model entity already has an active deployment.""" + deployment_configs = self.sdk.inference.deployment_configs.list( + workspace=me.workspace, + filter=ModelDeploymentConfigFilterParam(model_entity_id=f"{me.workspace}/{me.name}"), + ).data + + for c in deployment_configs: + deployments = self.sdk.inference.deployments.list( + filter=ModelDeploymentFilterParam(config=c.name, workspace=me.workspace) + ).data + for d in deployments: + if d.status in ACTIVE_DEPLOYMENT_STATUSES: + logger.info(f"Active deployment (status={d.status}) exists for config {c.name}, skipping") + return True + + return False + + def _resolve_config_ref(self, config_ref: str, me_workspace: str) -> ModelDeploymentConfig: + """Resolve a ``name`` or ``workspace/name`` reference to a ModelDeploymentConfig.""" + parts = config_ref.split("/") + if len(parts) == 2: + workspace = parts[0] + name = parts[1] + elif len(parts) == 1: + workspace = me_workspace + name = parts[0] + else: + raise ModelEntityCreationError( + f"Invalid deployment config reference '{config_ref}': expected 'name' or 'workspace/name'" + ) + + try: + return self.sdk.inference.deployment_configs.retrieve(workspace=workspace, name=name) + except Exception as e: + raise ModelEntityCreationError( + f"Failed to resolve deployment config '{config_ref}' in workspace '{workspace}': {e}" + ) from e + + def _create_deployment_config(self, deploy_params: DeploymentParameters, me: ModelEntity) -> ModelDeploymentConfig: + """Create (or update) a ModelDeploymentConfig from inline parameters.""" + nim_deployment = NIMDeploymentParam( + image_name=deploy_params.image_name, + image_tag=deploy_params.image_tag, + gpu=deploy_params.gpu, + model_name=me.name, + model_namespace=me.workspace, + additional_envs=deploy_params.additional_envs, + lora_enabled=deploy_params.lora_enabled, + ) + + if deploy_params.tool_call_config: + nim_deployment["tool_call_config"] = ToolCallConfigParam( + **deploy_params.tool_call_config.model_dump(exclude_none=True) + ) + + deployment_cfg_name = sanitize_name("sft-cfg", me.name) + try: + return self.sdk.inference.deployment_configs.create( + workspace=me.workspace, + name=deployment_cfg_name, + nim_deployment=nim_deployment, + ) + except ConflictError: + logger.info(f"Deployment config {me.workspace}/{deployment_cfg_name} already exists, updating") + return self.sdk.inference.deployment_configs.update( + workspace=me.workspace, + name=deployment_cfg_name, + nim_deployment=nim_deployment, + ) + + def _create_deployment(self, deployment_config: ModelDeploymentConfig, me: ModelEntity) -> None: + """Create a deployment from the given ModelDeploymentConfig.""" + logger.info(f"Deployment config: {deployment_config}") + + if not me.spec: + _ = self._wait_for_spec(me.workspace, me.name) + + deployment_name = sanitize_name("sft-deploy", me.name) + try: + deployment = self.sdk.inference.deployments.create( + workspace=deployment_config.workspace, + name=deployment_name, + config=deployment_config.name, + ) + logger.info(f"Deployment created: {deployment}") + except ConflictError: + logger.info(f"Deployment {deployment_config.workspace}/{deployment_name} already exists") + deployment = self.sdk.inference.deployments.retrieve( + workspace=deployment_config.workspace, + name=deployment_name, + ) + + deployment_status = self.sdk.inference.deployments.retrieve( + workspace=deployment.workspace, + name=deployment.name, + ) + logger.info(f"Deployment status: {deployment_status}") + + +def run(sdk: NeMoPlatform | None = None, job_ctx: NMPJobContext | None = None) -> int: + """Execute the model entity creation task. + + Args: + sdk: Optional SDK instance for dependency injection (for testing). + If None, creates one via get_task_sdk(). + job_ctx: Optional job context for dependency injection (for testing). + If None, creates one via NMPJobContext.from_env(). + + Returns: + Exit code (0 for success, non-zero for failure). + """ + job_ctx = job_ctx or NMPJobContext.from_env() + + sdk_owned = sdk is None + try: + sdk = sdk or get_task_sdk(SERVICE_NAME).with_options(workspace=job_ctx.workspace) + runner = ModelEntityRunner(sdk=sdk, job_ctx=job_ctx) + + config = get_config(job_ctx.config_path) + + logger.info(f"Starting model entity task with job context: {job_ctx}") + logger.info(f"Config: {config.model_dump_json(indent=2)}") + logger.info(f"NeMo Platform service URL: {sdk.base_url}") + + result, deploy_target = runner.create_model_entity(config) + logger.info(f"Model entity creation complete: {result}") + + runner.launch_model(config, deploy_target) + return 0 + + except ModelEntityCreationError as e: + logger.exception(f"Model entity creation failed: {e}") + return 1 + except Exception as e: + logger.exception(f"Model entity task failed: {e}") + return 1 + finally: + if sdk_owned and sdk is not None: + sdk.close() diff --git a/services/automodel/src/nmp/automodel/tasks/progress_reporter.py b/services/automodel/src/nmp/automodel/tasks/progress_reporter.py new file mode 100644 index 0000000000..82bb236165 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/progress_reporter.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Re-export file_io progress types for backward-compatible imports.""" + +from nmp.automodel.tasks.file_io.progress_reporter import ( + JobsServiceProgressReporter, + NoOpProgressReporter, + ProgressReporter, +) + +__all__ = ["JobsServiceProgressReporter", "NoOpProgressReporter", "ProgressReporter"] diff --git a/services/automodel/src/nmp/automodel/tasks/training/__init__.py b/services/automodel/src/nmp/automodel/tasks/training/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/automodel/src/nmp/automodel/tasks/training/__main__.py b/services/automodel/src/nmp/automodel/tasks/training/__main__.py new file mode 100644 index 0000000000..f4397f999c --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/__main__.py @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Training task entry point. + +Usage: + python -m nmp.automodel.tasks.training + +In distributed (multi-node) training, all pods run this entry point. +The DistributedContext handles role detection and coordination: +- Rank 0 (coordinator): Runs all phases, reports progress +- Rank > 0 (workers): Participate in training, wait at barriers +""" + +import logging +import sys + +from .runner import TrainingRunner + +logger = logging.getLogger(__name__) + + +def run() -> int: + """Execute training task.""" + try: + with TrainingRunner() as runner: + result = runner.run() + return 0 if result.success else 1 + except Exception as e: + logger.exception(f"Training task failed: {e}") + return 1 + + +if __name__ == "__main__": + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) + sys.exit(run()) diff --git a/services/automodel/src/nmp/automodel/tasks/training/backends/__init__.py b/services/automodel/src/nmp/automodel/tasks/training/backends/__init__.py new file mode 100644 index 0000000000..13ea2859f5 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/backends/__init__.py @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import warnings + +from pydantic.warnings import UnsupportedFieldAttributeWarning + +warnings.filterwarnings("ignore", category=UnsupportedFieldAttributeWarning) + +warnings.filterwarnings( + "ignore", + category=UserWarning, + module="torch.distributed.device_mesh", +) + +warnings.filterwarnings( + "ignore", + category=UserWarning, + module="nemo_automodel.components.moe.state_dict_utils", +) diff --git a/services/automodel/src/nmp/automodel/tasks/training/backends/backend.py b/services/automodel/src/nmp/automodel/tasks/training/backends/backend.py new file mode 100644 index 0000000000..d5fba7bb1a --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/backends/backend.py @@ -0,0 +1,187 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import logging +import signal +import subprocess +import threading +import time +from collections import deque +from pathlib import Path +from typing import Any, Optional + +from nmp.automodel.app.jobs.context import NMPJobContext +from nmp.automodel.tasks.training.errors.parser import ( + MAX_OUTPUT_LINES, + parse_error_from_output, + read_subprocess_output, +) +from nmp.automodel.tasks.training.progress import JobsServiceProgressReporter +from nmp.automodel.tasks.training.protocol import LibraryConfig +from nmp.automodel.tasks.training.schemas import ( + CheckpointInfo, + TrainingMetrics, + TrainingStepConfig, +) +from nmp.automodel.tasks.training.utils import generate_torchrun_flags_from_env + +from .checkpoints import ModelType, find_best_checkpoint, process_checkpoint +from .config import compile_automodel_config + +logger = logging.getLogger(__name__) + +AUTOMODEL_CONFIG_FILENAME = "automodel_config.yaml" + + +class AutomodelBackend: + """Compiles and runs nemo-automodel training for customization jobs.""" + + def __init__(self, job_ctx: NMPJobContext): + self.job_ctx = job_ctx + + def compile_config( + self, + config: TrainingStepConfig, + workspace_dir: Path, + ) -> dict[str, Any]: + """ + Compile Automodel-specific configuration. + + Pure transformation - no file I/O. The runner handles writing to disk. + """ + return compile_automodel_config(config, workspace_dir, self.job_ctx) + + def execute_training( + self, + customizer_config: TrainingStepConfig, + library_config: LibraryConfig, + progress: JobsServiceProgressReporter, + ) -> TrainingMetrics: + """Execute training using CustomizerTrainFinetuneRecipe or CustomizerBiencoderRecipe. + + The config file has already been written to disk by the runner. + Progress reporting happens within the training subprocess via + TrainingProgressCallback, which reads job context from environment + variables. + """ + progress.report_running("training", backend="automodel") + + # Run training with our custom recipe + # Note: The progress parameter is not passed to run_training_with_customizer_recipe + # because progress reporting now happens inside the subprocess via + # TrainingProgressCallback using environment variables. + command = ["torchrun"] + command.extend(generate_torchrun_flags_from_env()) + command.extend( + [ + "-m", + "nmp.automodel.tasks.training.backends.finetune", + "--config", + str(library_config.config_path), + ] + ) + + logger.info(f"Executing: {' '.join(command)}") + + training_process: subprocess.Popen | None = None + + # Rolling buffer to keep recent output lines for error extraction + output_lines: deque[str] = deque(maxlen=MAX_OUTPUT_LINES) + reader_thread: threading.Thread | None = None + + def cleanup(signum, frame): + logger.warning(f"Signal {signum} received, terminating...") + if training_process: + training_process.send_signal(signum) + try: + training_process.wait(timeout=30) + except subprocess.TimeoutExpired: + training_process.kill() + raise SystemExit(signum) + + signal.signal(signal.SIGINT, cleanup) + signal.signal(signal.SIGTERM, cleanup) + + start_time = time.time() + + training_process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, # Line buffered + ) + + # Start reader thread to capture output without blocking + reader_thread = threading.Thread( + target=read_subprocess_output, + args=(training_process, output_lines), + daemon=True, + ) + reader_thread.start() + + try: + training_process.wait(timeout=customizer_config.training_timeout) + except subprocess.TimeoutExpired: + logger.exception("Training timed out") + training_process.kill() + # Reap the killed process to avoid zombies + try: + training_process.wait(timeout=30) + except subprocess.TimeoutExpired: + logger.warning( + "Killed training process did not terminate within 30s - " + "process may be stuck in uninterruptible state" + ) + # Wait for reader thread to capture any remaining output before re-raising + if reader_thread and reader_thread.is_alive(): + reader_thread.join(timeout=5) + raise # Let runner.py convert via create_error_details() + + # Wait for reader thread to finish capturing output + if reader_thread and reader_thread.is_alive(): + reader_thread.join(timeout=5) + + duration = time.time() - start_time + logger.info(f"Training finished in {duration:.1f} seconds") + + if training_process.returncode != 0: + parsed = parse_error_from_output(output_lines, training_process.returncode) + raise parsed.to_exception() + + # Return empty metrics (actual metrics are reported via callbacks during training) + # TODO: Consider parsing training logs or checkpoints to extract final metrics. + return TrainingMetrics(total_steps=0, total_epochs=0) + + def find_best_checkpoint( + self, + workspace_dir: Path, + customizer_config: TrainingStepConfig, + library_config: Optional[LibraryConfig] = None, + ) -> Path: + """Find best Automodel checkpoint.""" + model_type = ModelType.EMBEDDING if customizer_config.model.is_embedding_model else ModelType.LLM + return find_best_checkpoint(workspace_dir, customizer_config, model_type=model_type) + + def process_checkpoint( + self, + checkpoint_path: Path, + output_path: Path, + customizer_config: TrainingStepConfig, + library_config: LibraryConfig | None = None, + ) -> CheckpointInfo: + """Process Automodel checkpoint.""" + model_type = ModelType.EMBEDDING if customizer_config.model.is_embedding_model else ModelType.LLM + + # Extract resolved chat template from library config if available (LLM only) + resolved_template = None + if model_type == ModelType.LLM and library_config and library_config.config_dict: + resolved_template = library_config.config_dict.get("_resolved_chat_template") + + return process_checkpoint( + checkpoint_path, + output_path, + customizer_config, + model_type=model_type, + resolved_chat_template=resolved_template, + ) diff --git a/services/automodel/src/nmp/automodel/tasks/training/backends/callbacks.py b/services/automodel/src/nmp/automodel/tasks/training/backends/callbacks.py new file mode 100644 index 0000000000..04c7b40c2f --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/backends/callbacks.py @@ -0,0 +1,94 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import logging + +from nmp.automodel.tasks.training.progress import JobsServiceProgressReporter + +logger = logging.getLogger(__name__) + + +class TrainingProgressCallback: + """ + Callback for reporting Automodel training progress to the Jobs service. + + This class composes JobsServiceProgressReporter and provides training-specific + methods for reporting detailed metrics during training. + + Metric accumulation: train_loss and val_loss are accumulated as time-series + lists and included in every status_details update under a ``metrics`` key, + enabling loss-curve reconstruction from job status. + """ + + def __init__(self, reporter: JobsServiceProgressReporter): + self._reporter = reporter + + prior = reporter.fetch_current_metrics() + self._train_metrics: list[dict[str, float | int]] = prior.get("train_loss", []) + self._val_metrics: list[dict[str, float | int]] = prior.get("val_loss", []) + if self._train_metrics or self._val_metrics: + logger.info( + "Seeded metrics from server: %d train_loss, %d val_loss entries", + len(self._train_metrics), + len(self._val_metrics), + ) + + def _build_metrics_summary(self) -> dict[str, list[dict[str, float | int]]]: + """Build the accumulated metrics payload for inclusion in status_details.""" + return { + "train_loss": list(self._train_metrics), + "val_loss": list(self._val_metrics), + } + + def report_training_start(self, max_steps: int, num_epochs: int) -> None: + """Report that training has started with schedule information.""" + self._reporter.configure_progress_tracking(max_steps, num_epochs) + self._reporter.report_running(phase="training", step=0, max_steps=max_steps, num_epochs=num_epochs) + + def report_train_step( + self, + step: int, + epoch: int, + loss: float, + lr: float | None = None, + grad_norm: float | None = None, + ) -> None: + """Report training step with metrics.""" + self._train_metrics.append({"step": step, "epoch": epoch, "value": loss}) + self._reporter.report_running( + phase="training", + step=step, + epoch=epoch, + train_loss=loss, + lr=lr, + grad_norm=grad_norm, + metrics=self._build_metrics_summary(), + ) + + def report_validation(self, step: int, epoch: int, val_loss: float) -> None: + """Report validation results.""" + self._val_metrics.append({"step": step, "epoch": epoch, "value": val_loss}) + self._reporter.report_running( + phase="validation", + step=step, + epoch=epoch, + val_loss=val_loss, + metrics=self._build_metrics_summary(), + ) + + def report_checkpoint_saved(self, step: int, epoch: int, checkpoint_path: str | None = None) -> None: + """Report that a checkpoint was saved.""" + self._reporter.report_running( + phase="checkpoint_saved", + step=step, + epoch=epoch, + checkpoint_path=checkpoint_path, + ) + + def report_epoch_end(self, step: int, epoch: int) -> None: + """Report that an epoch has completed.""" + self._reporter.report_running(phase="epoch_end", step=step, epoch=epoch) + + def close(self) -> None: + """Clean up resources.""" + self._reporter.close() diff --git a/services/automodel/src/nmp/automodel/tasks/training/backends/checkpoints.py b/services/automodel/src/nmp/automodel/tasks/training/backends/checkpoints.py new file mode 100644 index 0000000000..f43220fe2f --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/backends/checkpoints.py @@ -0,0 +1,518 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Checkpoint processing for Automodel backend. + +This module handles: +- Finding the best checkpoint after training +- LoRA adapter merging +- Chat template preservation +- FSDP2 architecture fix +- HF export and format conversion +- ONNX export for embedding models + +Supports both LLM and embedding (biencoder) models through unified functions. +""" + +import json +import logging +import re +import shutil +from enum import StrEnum +from pathlib import Path + +from nmp.automodel.tasks.training.chat_templates import ( + apply_chat_template_to_checkpoint, + resolve_chat_template, +) +from nmp.automodel.tasks.training.schemas import ( + CheckpointFormat, + CheckpointInfo, + FinetuningType, + Precision, + TrainingStepConfig, +) + +logger = logging.getLogger(__name__) + + +class ModelType(StrEnum): + """Type of model for checkpoint processing.""" + + LLM = "llm" + EMBEDDING = "embedding" + + +def extract_precision_from_model_config(model_path: str | Path) -> Precision | None: + """ + Extract precision from a HuggingFace model's config.json. + + HuggingFace models store their torch_dtype in config.json (e.g., "bfloat16"). + This function reads that value and maps it to our Precision enum. + + This is used to determine the actual training precision when "auto" was used + for torch_dtype. The precision comes from the base model's config, not from + the output checkpoint (which may only contain adapter weights for LoRA). + + Args: + model_path: Path to the model directory containing config.json + + Returns: + Precision enum value if found, None otherwise + """ + config_path = Path(model_path) / "config.json" + if not config_path.exists(): + logger.warning(f"config.json not found at {config_path}, cannot extract precision") + return None + + try: + with open(config_path, "r") as f: + config = json.load(f) + + torch_dtype = config.get("torch_dtype") + if torch_dtype is None: + logger.warning("torch_dtype not found in config.json") + return None + + try: + precision = Precision.from_hf_dtype(torch_dtype) + logger.info(f"Extracted precision from model config: {torch_dtype} -> {precision.value}") + return precision + except ValueError: + logger.warning(f"Unknown torch_dtype '{torch_dtype}' in config.json, cannot map to Precision") + return None + + except (json.JSONDecodeError, IOError) as e: + logger.warning(f"Failed to read config.json: {e}") + return None + + +def extract_step_number(path: Path) -> int: + """Extract step number from directory name like 'epoch_0_step_99'""" + match = re.search(r"step_(\d+)", path.name) + return int(match.group(1)) if match else -1 + + +def get_model_dir_from_checkpoint(checkpoint_dir: Path, is_peft: bool) -> Path: + """ + Extract model directory from checkpoint directory. + """ + if is_peft: + # For LoRA, checkpoint is saved directly under model/ directory + model_dir = checkpoint_dir / "model" + if model_dir.exists() and model_dir.is_dir(): + logger.info(f"Found LoRA checkpoint at: {model_dir}") + return model_dir.resolve() + else: + # For full-sft, check for consolidated directory first + consolidated_dir = checkpoint_dir / "model" / "consolidated" + if consolidated_dir.exists() and consolidated_dir.is_dir(): + logger.info(f"Found consolidated checkpoint at: {consolidated_dir}") + return consolidated_dir.resolve() + + # Fallback to model/ directory if consolidated doesn't exist + model_dir = checkpoint_dir / "model" + if model_dir.exists() and model_dir.is_dir(): + logger.info(f"Found sharded checkpoint at: {model_dir}") + return model_dir.resolve() + + raise FileNotFoundError(f"Model directory not found in checkpoint {checkpoint_dir}") + + +def find_best_checkpoint( + workspace_dir: Path, + config: TrainingStepConfig, + model_type: ModelType = ModelType.LLM, +) -> Path: + """ + Find the best checkpoint directory. + """ + base_dir = workspace_dir / "checkpoints" + is_peft = config.training.finetuning_type in (FinetuningType.LORA, FinetuningType.LORA_MERGED) + type_label = "embedding" if model_type == ModelType.EMBEDDING else "" + + # Order of preference: + # 1. LOWEST_VAL symlink + # 2. LATEST symlink + # 3. Highest step number + + for link_name in ["LOWEST_VAL", "LATEST"]: + link = base_dir / link_name + if link.exists() and link.is_symlink(): + try: + target = link.resolve() + if target.exists(): + logger.info(f"Using {link_name} {type_label} checkpoint: {target.name}".replace(" ", " ")) + return get_model_dir_from_checkpoint(target, is_peft) + except Exception as e: + logger.warning(f"Failed to resolve {link_name} symlink: {e}") + + # Fallback: scan directories + epoch_step_dirs = list(base_dir.glob("epoch_*_step_*")) + if not epoch_step_dirs: + raise FileNotFoundError(f"No {type_label} checkpoint directories found in {base_dir}".replace(" ", " ")) + + best_checkpoint = max(epoch_step_dirs, key=extract_step_number) + logger.info(f"Using latest {type_label} checkpoint by step number: {best_checkpoint.name}".replace(" ", " ")) + return get_model_dir_from_checkpoint(best_checkpoint, is_peft) + + +def fix_fsdp2_architecture(model_path: Path) -> None: + """ + Fix FSDP2 architecture naming issue in HuggingFace config. + + FSDP2 adds "FSDP" prefix to architecture names (e.g., "FSDPLlamaForCausalLM" + instead of "LlamaForCausalLM"). This function removes that prefix to ensure + the checkpoint is compatible with standard HuggingFace/vLLM loading. + + Reference: https://github.com/huggingface/transformers/commit/dc262ee6f57f2154f5233e53482da14dbe3be834 + """ + config_path = model_path / "config.json" + if not config_path.exists(): + logger.warning(f"config.json not found at {config_path}, skipping FSDP2 fix") + return + + with open(config_path, "r") as f: + config = json.load(f) + + if "architectures" not in config: + return + + original_archs = config["architectures"] + fixed_archs = [arch.removeprefix("FSDP") for arch in original_archs] + + if original_archs != fixed_archs: + config["architectures"] = fixed_archs + with open(config_path, "w") as f: + json.dump(config, f, indent=2) + logger.info(f"Fixed FSDP2 architecture names: {original_archs} -> {fixed_archs}") + + +def merge_lora_adapter( + adapter_path: Path, + base_model_path: str, + output_path: Path, +) -> None: + """ + Merge LoRA adapter weights into the base model. + + Uses HuggingFace's PEFT library to: + 1. Load the base model + 2. Attach the LoRA adapter + 3. Merge weights using merge_and_unload() + 4. Save as a standard HuggingFace checkpoint + + Note: This function only supports LLM models. For embedding models, + use merge_lora_embedding_adapter() instead. + + Args: + adapter_path: Path to the LoRA adapter checkpoint + base_model_path: Path to the base model (for loading weights) + output_path: Where to save the merged model + """ + try: + import torch + from peft import PeftModel + from transformers import AutoModelForCausalLM, AutoTokenizer + except ImportError as e: + raise ImportError( + "LoRA merge requires 'peft' and 'transformers' packages. Ensure they are installed in the container." + ) from e + + logger.info(f"Merging LoRA adapter from {adapter_path} with base model {base_model_path}") + + # Use scratch directory if available for better I/O performance + tmp_path = Path("/scratch/merged_lora") if Path("/scratch").is_dir() else Path("/tmp/merged_lora") + shutil.rmtree(tmp_path, ignore_errors=True) + tmp_path.mkdir(parents=True, exist_ok=True) + + try: + # 1. Load base model in mergeable dtype (not quantized) + logger.info("Loading base model...") + model = AutoModelForCausalLM.from_pretrained( + base_model_path, + torch_dtype=torch.bfloat16, + device_map="auto", + trust_remote_code=True, + ) + + # 2. Attach the LoRA adapter + logger.info("Loading LoRA adapter...") + model = PeftModel.from_pretrained(model, str(adapter_path)) + + # 3. Merge LoRA weights into base model + logger.info("Merging LoRA weights...") + model = model.merge_and_unload() + + # 4. Save merged model + logger.info(f"Saving merged model to {tmp_path}...") + model.save_pretrained(tmp_path, safe_serialization=True) + + # 5. Save tokenizer from base model + tokenizer = AutoTokenizer.from_pretrained(base_model_path, trust_remote_code=True) + tokenizer.save_pretrained(tmp_path) + + # 6. Copy to output path + output_path.mkdir(parents=True, exist_ok=True) + shutil.copytree(tmp_path, output_path, dirs_exist_ok=True) + + logger.info(f"Successfully merged LoRA adapter to {output_path}") + + finally: + # Cleanup temp directory + shutil.rmtree(tmp_path, ignore_errors=True) + + +def merge_lora_embedding_adapter( + adapter_path: Path, + base_model_path: str, + output_path: Path, +) -> None: + """Merge a LoRA adapter into a base embedding model. + + This intentionally mirrors the logic in Automodel's `tools/merge_lora.py`, + but is implemented locally because the customizer container may not have + that module on `PYTHONPATH`. + + Args: + adapter_path: Path to the PEFT adapter directory. + base_model_path: HuggingFace model name or path for the base encoder. + output_path: Where to write the merged model. + """ + try: + import gc + + import torch + from peft import PeftModel + from transformers import AutoModel, AutoTokenizer + except ImportError as e: + raise ImportError( + "LoRA merge requires 'peft' and 'transformers' packages. Ensure they are installed in the container." + ) from e + + logger.info("Merging embedding LoRA adapter from %s with base model %s", adapter_path, base_model_path) + + # Use scratch directory if available for better I/O performance + tmp_path = Path("/scratch/merged_lora") if Path("/scratch").is_dir() else Path("/tmp/merged_lora") + shutil.rmtree(tmp_path, ignore_errors=True) + tmp_path.mkdir(parents=True, exist_ok=True) + model = None + try: + logger.info("Loading base model (AutoModel): %s", base_model_path) + model = AutoModel.from_pretrained( + base_model_path, + torch_dtype=torch.float16, + device_map="auto", + trust_remote_code=True, + ) + + logger.info("Loading adapter from %s", adapter_path) + model = PeftModel.from_pretrained(model, str(adapter_path)) + + logger.info("Merging adapter into base model") + model = model.merge_and_unload() + + logger.info("Saving merged model to %s", tmp_path) + model.save_pretrained(tmp_path, safe_serialization=True) + + try: + tokenizer = AutoTokenizer.from_pretrained(base_model_path, trust_remote_code=True) + tokenizer.save_pretrained(tmp_path) + logger.info("Tokenizer saved to %s", tmp_path) + except Exception as e: + logger.warning("Could not save tokenizer: %s", e) + + output_path.mkdir(parents=True, exist_ok=True) + shutil.copytree(tmp_path, output_path, dirs_exist_ok=True) + logger.info("Successfully merged embedding LoRA adapter to %s", output_path) + + finally: + shutil.rmtree(tmp_path, ignore_errors=True) + torch.cuda.empty_cache() + gc.collect() + + +def export_onnx( + model_path: Path, + output_path: Path, + tokenizer_path: str, +) -> Path: + """Export an embedding model to ONNX format. + + Uses Automodel's export_to_onnx to export to ONNX format. + The resulting `model.onnx` is written into *output_path* alongside + the existing HuggingFace checkpoint files. + + Args: + model_path: Path to the HuggingFace model directory (config.json + weights). + output_path: Directory where ``model.onnx`` will be written. + tokenizer_path: Fallback tokenizer location (base model path). Used when + the checkpoint directory does not contain tokenizer files. + + Returns: + Path to the exported ``model.onnx`` file. + """ + # need to import here for the tests + from nemo_automodel.components.models.biencoder.export_onnx import export_to_onnx + + logger.info(f"Exporting embedding model at path {model_path} to ONNX format at path {output_path}") + + try: + onnx_path = export_to_onnx( + model_path=str(model_path), + output_dir=str(output_path), + tokenizer_path=tokenizer_path, + pooling="avg", + normalize=True, + opset=17, + export_dtype="fp16", + verify=True, + ) + except Exception: + logger.exception(f"ONNX export failed for model at {model_path}") + raise + + logger.info(f"ONNX model exported to {onnx_path}") + return Path(onnx_path) + + +_ONNX_TOP_LEVEL_PATTERNS = {"model.onnx", "model.onnx.data", "tokenizer"} + + +def _restructure_embedding_output(output_path: Path) -> None: + """Move HF artifacts into ``alternates/hf/`` so the NIM selects the ONNX profile. + + NIM scans the top-level directory to choose the model backend. If it sees + ``.safetensors`` files it creates a PyTorch profile, which is unsupported + for custom models in many NIM versions. The legacy customizer kept only + ``model.onnx`` (+ tokenizer/) at the root and placed HF weights under + ``alternates/hf/``. This function reproduces that layout. + """ + alternates_hf = output_path / "alternates" / "hf" + alternates_hf.mkdir(parents=True, exist_ok=True) + + for entry in list(output_path.iterdir()): + if entry.name in _ONNX_TOP_LEVEL_PATTERNS or entry.name == "alternates": + continue + dest = alternates_hf / entry.name + logger.info("Moving %s -> %s", entry, dest) + shutil.move(str(entry), str(dest)) + + logger.info("Restructured embedding output: ONNX at top level, HF in alternates/hf/") + + +def process_checkpoint( + checkpoint_path: Path, + output_path: Path, + customizer_config: TrainingStepConfig, + model_type: ModelType = ModelType.LLM, + resolved_chat_template: str | None = None, +) -> CheckpointInfo: + """ + Process checkpoint to standard output format. + + Works for both LLM and embedding (biencoder) models. + + Handles three scenarios: + 1. Full weights training: Copy checkpoint, fix FSDP2 arch, preserve chat template (LLM only) + 2. LoRA (unmerged): Copy adapter, preserve format as hf-peft + 3. LoRA merged: Merge adapter with base model, output as standard HF + + Args: + checkpoint_path: Path to the checkpoint directory (model files) + output_path: Where to write the processed checkpoint + customizer_config: Training configuration with model paths and settings + model_type: Type of model ("llm" or "embedding") + resolved_chat_template: Pre-resolved chat template from training config (LLM only). + If provided, this template is used. Otherwise, falls back to + priority-based resolution using model.name and model.path. + + Returns: + CheckpointInfo with output path, format, and precision + """ + output_path.mkdir(parents=True, exist_ok=True) + + finetuning_type = customizer_config.training.finetuning_type + base_model_path = customizer_config.model.path + is_embedding = model_type == ModelType.EMBEDDING + type_label = "embedding" if is_embedding else "" + + # Resolve chat template using the same priority logic as training: + # 1. Use pre-resolved template if provided (ensures consistency with training) + # 2. Otherwise, resolve using priority-based selection + chat_template: str | None = None + if not is_embedding: + if resolved_chat_template is not None: + chat_template = resolved_chat_template + logger.info("Using pre-resolved chat template from training config") + else: + # Fall back to priority-based resolution (user_template from fileset metadata takes priority) + chat_template = resolve_chat_template( + model_path=base_model_path, + model_name=customizer_config.model.name, + user_template=customizer_config.model.chat_template, + ) + + if finetuning_type == FinetuningType.LORA_MERGED: + # LoRA merged: merge adapter weights into base model + # For embedding models, this produces a full-weight model compatible with ONNX export and NIM serving. + if is_embedding: + merge_lora_embedding_adapter( + adapter_path=checkpoint_path, + base_model_path=base_model_path, + output_path=output_path, + ) + else: + merge_lora_adapter( + adapter_path=checkpoint_path, + base_model_path=base_model_path, + output_path=output_path, + ) + checkpoint_format = CheckpointFormat.HF + + # Fix FSDP2 architecture naming + fix_fsdp2_architecture(output_path) + # Apply chat template for LLM models only + if chat_template: + apply_chat_template_to_checkpoint(output_path, chat_template) + + elif finetuning_type == FinetuningType.LORA: + # LoRA unmerged: just copy the adapter files + logger.info(f"Copying {type_label} LoRA adapter from {checkpoint_path} to {output_path}".replace(" ", " ")) + shutil.copytree(checkpoint_path, output_path, dirs_exist_ok=True) + checkpoint_format = CheckpointFormat.HF_PEFT + # Note: For hf-peft, chat template is inherited from base model at inference time + + else: + # Full weights training: copy and process + logger.info( + f"Copying {type_label} full weights checkpoint from {checkpoint_path} to {output_path}".replace(" ", " ") + ) + shutil.copytree(checkpoint_path, output_path, dirs_exist_ok=True) + checkpoint_format = CheckpointFormat.HF + + # Fix FSDP2 architecture naming + fix_fsdp2_architecture(output_path) + # Apply chat template for LLM models only + if chat_template: + apply_chat_template_to_checkpoint(output_path, chat_template) + + if is_embedding: + export_onnx( + model_path=output_path, + output_path=output_path, + tokenizer_path=base_model_path, + ) + _restructure_embedding_output(output_path) + + # Determine precision: use explicit config value, or extract from base model + precision = customizer_config.model.precision + if precision is None: + precision = extract_precision_from_model_config(customizer_config.model.path) + + return CheckpointInfo( + path=str(output_path), + format=checkpoint_format, + precision=precision, + ) diff --git a/services/automodel/src/nmp/automodel/tasks/training/backends/config.py b/services/automodel/src/nmp/automodel/tasks/training/backends/config.py new file mode 100644 index 0000000000..b51fba84e3 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/backends/config.py @@ -0,0 +1,848 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Automodel configuration compiler. + +This module transforms the standardized TrainingStepConfig into the format +expected by nemo_automodel's TrainFinetuneRecipeForNextTokenPrediction +or KnowledgeDistillationRecipeForNextTokenPrediction. +""" + +import logging +import os +from pathlib import Path +from typing import Any + +from nemo_automodel._transformers.registry import ModelRegistry +from nmp.automodel.app.jobs.context import NMPJobContext +from nmp.automodel.tasks.training.chat_templates import resolve_chat_template +from nmp.automodel.tasks.training.datasets.preparation import ( + DatasetSchema, + PreparedDataset, + compute_val_check_interval, + detect_dataset_schema, + prepare_dataset, +) +from nmp.automodel.tasks.training.datasets.validation import DatasetValidator +from nmp.automodel.tasks.training.integrations import ( + build_mlflow_config, + build_wandb_config, +) +from nmp.automodel.tasks.training.schemas import ( + EmbeddingConfig, + FinetuningType, + LoRAConfig, + TrainingStepConfig, + TrainingType, +) +from nmp.automodel.tasks.training.sequence_packing import ( + calculate_optimal_pack_size, + estimate_dataset_sequence_lengths, +) + +logger = logging.getLogger(__name__) + + +def compile_automodel_config( + customizer_config: TrainingStepConfig, + workspace_dir: Path, + job_ctx: NMPJobContext, +) -> dict[str, Any]: + """ + Compile Automodel-specific configuration. + + This transforms the standardized TrainingStepConfig into the format + expected by nemo_automodel's TrainFinetuneRecipeForNextTokenPrediction. + """ + cfg: dict[str, Any] = {} + _is_embedding_model = customizer_config.model.is_embedding_model + trust_remote_code = customizer_config.model.trust_remote_code + embedding_config = EmbeddingConfig() + + # === Distributed Environment === + # Required for torch.distributed initialization + cfg["dist_env"] = { + "backend": "nccl", + "timeout_minutes": 30, # Higher timeout for large model loading + } + + # === Random Number Generator === + # Both recipes use StatefulRNG for reproducibility across restarts and multi-node training, + # but they expect the config in different formats: + # - Biencoder recipe: expects cfg["seed"] and creates StatefulRNG internally + # - LLM recipe: expects cfg["rng"] with full StatefulRNG config + seed = int(os.environ.get("PL_GLOBAL_SEED", customizer_config.seed)) + + if _is_embedding_model: + # Biencoder recipe creates StatefulRNG from seed value internally + # See: nemo_automodel/recipes/biencoder/train_biencoder.py + cfg["seed"] = seed + else: + # LLM recipe expects the full rng config object + cfg["rng"] = { + "_target_": "nemo_automodel.components.training.rng.StatefulRNG", + "seed": seed, + "ranked": True, # Different seed per rank for data augmentation + } + + # === Model Configuration === + # Common fields shared by both embedding and causal LM models + cfg["model"] = { + "pretrained_model_name_or_path": customizer_config.model.path, + "torch_dtype": customizer_config.model.precision.to_torch_dtype() + if customizer_config.model.precision + else "auto", + # trust_remote_code is required for models like nvidia/llama-nemotron-embed-1b-v2 + # which use custom model_type "llama_bidirec" with custom modeling code. + "trust_remote_code": trust_remote_code, + } + if customizer_config.model.override_custom_impl: + cfg["model"]["force_hf"] = True + + if _is_embedding_model: + cfg["model"].update( + { + "_target_": "nemo_automodel.components.models.biencoder.NeMoAutoModelBiencoder.from_pretrained", + # Use the same encoder for both queries and passages. default value taken from Automodel example + "share_encoder": True, + # Add a trainable linear layer after pooling to reduce embedding dimension. default value taken from Automodel example + "add_linear_pooler": False, + # How to combine token embeddings into a single document/query embedding. default value taken from Automodel example + "pooling": "avg", + # Normalize embeddings to unit length (length = 1). default value taken from Automodel example + "l2_normalize": True, + # When training an embedding model, we want it to learn that similar things should have similar embeddings + # and different things should have different embeddings. + # Temperature controls how "strict" the model is when learning these relationships. + # Low value (0.02), tells the model to pick the correct doc and penalizes near-misses. + # High value (like 1.0 that's Automodel default) tells the model to be more lenient and allows for near-misses. + # 0.02 is taken from the Automodel example for biencoder training. + "t": 0.02, + # Total number of passages per query during training: 1 positive + (n-1) negatives. + # For example, train_n_passages=5 means 1 positive and 4 negative passages per query. + # This differs from legacy Customizer's 'num_hard_negatives' which only counted negatives + # (num_hard_negatives=4 is equivalent to train_n_passages=5). + "train_n_passages": embedding_config.train_n_passages, + # Number of negative passages per query during validation. + "eval_negative_size": get_eval_negative_size(embedding_config), + # Gradient checkpointing saves memory by not storing all activations during forward pass. + # Instead, it recomputes them during backward pass with a memory trade-off - less memory, slower training. + # Useful for large models or limited GPU memory. + # TODO: consider exposing this in CustomizationJobInput + "do_gradient_checkpointing": embedding_config.do_gradient_checkpointing, + "use_liger_kernel": True, + "use_sdpa_patching": True, + } + ) + + # === Tokenizer === + cfg["tokenizer"] = { + "_target_": "nemo_automodel._transformers.auto_tokenizer.NeMoAutoTokenizer.from_pretrained", + "pretrained_model_name_or_path": customizer_config.model.path, + } + else: + cfg["model"].update( + { + "_target_": "nemo_automodel.NeMoAutoModelForCausalLM.from_pretrained", + "attn_implementation": customizer_config.model.attn_implementation, + } + ) + + # === Distributed Configuration === + p = customizer_config.parallelism + total_gpus = p.num_nodes * p.num_gpus_per_node + # Note dp_size is typically auto-derived by Automodel (world_size / (tp * pp * cp)), + # but we calculate it explicitly here because: + # 1. It's validated upstream in validators.py + # 2. We need it for warmup_steps validation below + # 3. Passing an explicit value ensures consistency rather than relying on Automodel's derivation + dp = total_gpus // (p.tensor_parallel_size * p.pipeline_parallel_size * p.context_parallel_size) + + cfg["distributed"] = { + "_target_": "nemo_automodel.components.distributed.fsdp2.FSDP2Manager", + "dp_size": dp, + "tp_size": p.tensor_parallel_size, + "pp_size": p.pipeline_parallel_size, + "cp_size": p.context_parallel_size, + "ep_size": p.expert_parallel_size, + "sequence_parallel": p.sequence_parallel, + } + if p.pipeline_parallel_size > 1: + cfg["distributed"]["pipeline"] = { + "pp_schedule": "interleaved1f1b", + "pp_microbatch_size": 1, + "scale_grads_in_schedule": False, + } + + # === Dataset Preparation === + # Discover, merge, and optionally split dataset files + prepared = prepare_dataset( + dataset_path=Path(customizer_config.dataset.path), + output_dir=workspace_dir / "dataset", + seed=customizer_config.seed, + ) + logger.info( + f"Prepared dataset: train={prepared.train_samples} samples, validation={prepared.validation_samples} samples, files: " + f"train={prepared.train_file.absolute()}, validation={prepared.validation_file.absolute()}" + ) + validator = DatasetValidator(training_type=customizer_config.training.training_type) + validator.validate_dataset(str(prepared.train_file)) + validator.validate_dataset(str(prepared.validation_file)) + logger.info("Validated datasets successfully") + + # === Step Scheduler (with val_check_interval conversion) === + batch_size = customizer_config.batch.global_batch_size + epochs = customizer_config.schedule.epochs + + # Compute steps per epoch (round up to ensure all samples are used) + steps_per_epoch = (prepared.train_samples + batch_size - 1) // batch_size + total_steps = steps_per_epoch * epochs + + # Determine effective max_steps + user_max_steps = customizer_config.schedule.max_steps + if user_max_steps and user_max_steps > 0: + max_steps = min(user_max_steps, total_steps) + else: + max_steps = total_steps + + logger.info( + f"Training schedule: {prepared.train_samples} samples, batch_size={batch_size}, " + f"steps_per_epoch={steps_per_epoch}, epochs={epochs}, max_steps={max_steps}" + ) + + cfg["step_scheduler"] = { + "global_batch_size": batch_size, + "local_batch_size": customizer_config.batch.micro_batch_size, + "max_steps": max_steps, + "num_epochs": epochs, + } + + val_every_steps = compute_val_check_interval( + steps_per_epoch=steps_per_epoch, + max_steps=max_steps, + val_check_interval=customizer_config.schedule.val_check_interval, + ) + cfg["step_scheduler"]["val_every_steps"] = val_every_steps + cfg["step_scheduler"]["ckpt_every_steps"] = val_every_steps + logger.info(f"Validation interval: {customizer_config.schedule.val_check_interval} -> {val_every_steps} steps") + + # === Validate warmup_steps === + # Automodel requires: lr_warmup_steps < lr_decay_steps (scheduler.py line 96) + # lr_decay_steps = total_optimizer_steps (accounting for gradient accumulation) + warmup_steps = customizer_config.optimizer.warmup_steps + if warmup_steps > 0: + micro_batch_size = customizer_config.batch.micro_batch_size + + # Calculate gradient accumulation steps (how StepScheduler computes it) + grad_acc_steps = batch_size // (micro_batch_size * dp) + + # Calculate total optimizer steps (accounting for gradient accumulation) + total_optimizer_steps = (epochs * prepared.train_samples) // grad_acc_steps + + # lr_decay_steps will be min(max_steps, total_optimizer_steps) + lr_decay_steps = min(total_optimizer_steps, max_steps) + + if warmup_steps >= lr_decay_steps: + raise ValueError( + f"warmup_steps ({warmup_steps}) must be less than lr_decay_steps ({lr_decay_steps}). " + f"Calculation: grad_acc_steps={grad_acc_steps} (batch_size={batch_size} / " + f"(micro_batch_size={micro_batch_size} * dp_size={dp})), " + f"total_optimizer_steps={total_optimizer_steps} (epochs={epochs} * " + f"steps_per_epoch={prepared.train_samples} / grad_acc_steps={grad_acc_steps}), " + f"lr_decay_steps=min({total_optimizer_steps}, {max_steps})={lr_decay_steps}" + ) + + # === Optimizer === + cfg["optimizer"] = { + "_target_": "torch.optim.Adam", + "lr": customizer_config.optimizer.learning_rate, + "weight_decay": customizer_config.optimizer.weight_decay, + "betas": [customizer_config.optimizer.beta1, customizer_config.optimizer.beta2], + "eps": customizer_config.optimizer.eps, # Adam epsilon for numerical stability + } + + cfg["lr_scheduler"] = { + "lr_decay_style": "cosine", + "lr_warmup_steps": customizer_config.optimizer.warmup_steps, + } + if customizer_config.optimizer.min_learning_rate: + cfg["lr_scheduler"]["min_lr"] = customizer_config.optimizer.min_learning_rate + + # === Checkpoint === + cfg["checkpoint"] = { + "enabled": True, + "model_save_format": "safetensors", + "checkpoint_dir": str(workspace_dir / "checkpoints"), + "save_consolidated": True, + # Required for models with quantized base weights (e.g., GPT-OSS) + # Safe to enable even for non-quantized models + "dequantize_base_checkpoint": True, + "v4_compatible": customizer_config.model.v4_compatible, + } + + # === Sequence Packing (must be computed before dataset config) === + # When packing is enabled, we use the pack size as the effective sequence length + # for dataset configuration. This ensures samples are truncated appropriately. + effective_seq_length = customizer_config.model.max_seq_length + if not _is_embedding_model: + if customizer_config.batch.sequence_packing: + # Calculate optimal pack size based on dataset statistics + packing_estimate = estimate_dataset_sequence_lengths( + customizer_config, + train_file=prepared.train_file, + max_samples=customizer_config.batch.sequence_packing_max_samples, + seed=customizer_config.seed, + trust_remote_code=trust_remote_code, + ) + + if packing_estimate is not None: + optimal_pack_size = packing_estimate.pack_size + logger.info( + f"Sequence packing enabled: pack_size={optimal_pack_size}, " + f"avg_seq={packing_estimate.avg_seq_length}, max_seq={packing_estimate.max_seq_length}, " + f"packing_factor={packing_estimate.packing_factor}, samples={packing_estimate.samples_analyzed}" + ) + else: + # Fallback to conservative default (model max_seq_length) + optimal_pack_size = calculate_optimal_pack_size(customizer_config) + logger.info(f"Sequence packing enabled with conservative pack_size={optimal_pack_size}") + + cfg["packed_sequence"] = { + "packed_sequence_size": optimal_pack_size, + "split_across_pack": False, + } + + # Use pack size as the effective sequence length for datasets + effective_seq_length = optimal_pack_size + + # === Dataset Configuration (with schema detection) === + _configure_datasets( + cfg, + customizer_config, + prepared, + effective_seq_length, + seed, + _is_embedding_model, + embedding_config, + ) + + # === Dataloader === + # Embedding datasets configure their own specialized dataloaders in _configure_embedding_dataset + if not _is_embedding_model: + cfg["dataloader"] = { + "_target_": "torchdata.stateful_dataloader.StatefulDataLoader", + "collate_fn": "nemo_automodel.components.datasets.utils.default_collater", + "shuffle": True, + } + cfg["validation_dataloader"] = { + "_target_": "torchdata.stateful_dataloader.StatefulDataLoader", + "collate_fn": "nemo_automodel.components.datasets.utils.default_collater", + } + + # === PEFT (LoRA) === + if customizer_config.training.training_type in ( + TrainingType.SFT, + TrainingType.DISTILLATION, + ) and customizer_config.training.finetuning_type in (FinetuningType.LORA, FinetuningType.LORA_MERGED): + lora = customizer_config.training.lora + if lora is None: + lora = LoRAConfig() + peft_cfg: dict[str, Any] = { + "_target_": "nemo_automodel.components._peft.lora.PeftConfig", + "dim": lora.rank, + "alpha": lora.alpha, + "dropout": lora.dropout, + "use_triton": lora.use_triton, + "target_modules": lora.target_modules, + } + # TODO: Support exclude_modules via the API + # if lora.exclude_modules: + # peft_cfg["exclude_modules"] = lora.exclude_modules + cfg["peft"] = peft_cfg + + # === Loss === + if not _is_embedding_model: + cfg["loss_fn"] = { + "_target_": "nemo_automodel.components.loss.masked_ce.MaskedCrossEntropy", + } + + # === Custom Model Configuration === + # Check for custom Automodel implementations (e.g., MoE models) + # and configure backend/parallelizer settings + if not _is_embedding_model: + _configure_moe_backend(cfg, customizer_config, trust_remote_code=trust_remote_code) + + # === Knowledge Distillation === + if customizer_config.training.training_type == TrainingType.DISTILLATION: + _configure_kd(cfg, customizer_config, trust_remote_code=trust_remote_code) + + # === Integrations (Runtime Environment) === + + # WandB - check for API key in environment + wandb_config = build_wandb_config( + customizer_config=customizer_config, + job_ctx=job_ctx, + framework="automodel", + ) + if wandb_config: + cfg["wandb"] = wandb_config + logger.info(f"WandB enabled: project={wandb_config.get('project')}") + + # MLflow + mlflow_config = build_mlflow_config( + customizer_config=customizer_config, + job_ctx=job_ctx, + framework="automodel", + ) + if mlflow_config: + cfg["mlflow"] = mlflow_config + logger.info(f"MLflow enabled: {mlflow_config.get('tracking_uri')}") + + return cfg + + +def _configure_moe_backend( + cfg: dict[str, Any], customizer_config: TrainingStepConfig, trust_remote_code: bool = False +) -> None: + """ + Configure custom Automodel model implementations for MoE models. + + Automodel has optimized implementations for certain model architectures. + Only MoE models (those with num_local_experts, num_experts, or n_routed_experts in config) + require additional backend and parallelizer configuration. + + Dense models like LlamaForCausalLM may have custom Automodel implementations + (for combined QKV projections, etc.) but don't need MoE-specific config. + + This function: + 1. Detects if the model is an MoE model via config attributes + 2. Only for MoE: Configures the backend (with deepep disabled for stability) + 3. Only for MoE: Configures the parallelizer for expert distribution + """ + # Import here to avoid ModuleNotFoundError in environments where + # transformers is not installed (e.g., during test collection) + from transformers import AutoConfig + + model_path = customizer_config.model.path + + try: + hf_config = AutoConfig.from_pretrained(model_path, trust_remote_code=trust_remote_code) + architectures = getattr(hf_config, "architectures", None) + + # Check if model has a custom Automodel implementation + has_custom_impl = ( + architectures and len(architectures) > 0 and architectures[0] in ModelRegistry.model_arch_name_to_cls + ) + + if has_custom_impl: + # Check if model is MoE by looking for expert-related config attributes + # MoE models use num_local_experts (Mixtral-style), num_experts (older), or n_routed_experts (NemotronH) + num_experts = ( + getattr(hf_config, "num_local_experts", None) + or getattr(hf_config, "num_experts", None) + or getattr(hf_config, "n_routed_experts", None) + ) + is_moe_model = num_experts is not None and num_experts > 1 + if is_moe_model: + logger.info( + f"Detected MoE model with custom Automodel implementation for architecture: {architectures[0]}. " + f"Adding MoE-specific configurations (num_experts={num_experts})." + ) + + # Validate MoE parallelism constraints. + # Automodel's MoE parallelizer does not support tensor parallelism: + # assert tp_axis_name is None or world_mesh[tp_axis_name].size() == 1 + # See: nemo_automodel/components/moe/parallelizer.py + p = customizer_config.parallelism + total_gpus = p.num_nodes * p.num_gpus_per_node + if total_gpus > 1: + if p.tensor_parallel_size > 1: + raise ValueError( + f"Tensor parallelism (tensor_parallel_size={p.tensor_parallel_size}) is not supported for MoE models." + ) + ep = p.expert_parallel_size + if ep is None or ep <= 1: + raise ValueError( + f"MoE model detected (num_experts={num_experts}) but expert_parallel_size " + f"is {ep or 'not set'}. Multi-GPU MoE training requires expert_parallel_size > 1." + ) + + # Backend configuration for MoE models + # DeepEP is disabled for stability - it's a newer feature that can cause issues + cfg.setdefault("model", {})["backend"] = { + "_target_": "nemo_automodel.components.models.common.utils.BackendConfig", + "enable_deepep": False, + } + + else: + logger.info( + f"Detected custom Automodel implementation for architecture: {architectures[0]}. " + "Not an MoE model, skipping MoE-specific configurations." + ) + else: + logger.debug( + f"No custom Automodel implementation found for {model_path}. " + "Using standard HuggingFace model implementation." + ) + except ValueError: + raise # Re-raise validation errors + except Exception as e: + # Don't fail training if we can't check for custom implementations + logger.warning( + f"Failed to check for custom model implementation: {e}. Using standard HuggingFace model implementation." + ) + + +def _configure_datasets( + cfg: dict[str, Any], + customizer_config: TrainingStepConfig, + prepared: PreparedDataset, + seq_length: int, + seed: int, + is_embedding_model: bool = False, + embedding_config: EmbeddingConfig | None = None, +) -> None: + """ + Configure dataset sections based on detected schema. + + Supports: + - Chat format (OpenAI messages): Uses ChatDataset + - SFT format (prompt/completion): Uses ColumnMappedTextInstructionDataset + - Custom format (via prompt_template): Uses ColumnMappedTextInstructionDataset with custom columns + - Embedding format (query/pos_doc/neg_doc): Uses inline retrieval dataset + + Args: + cfg: Configuration dictionary to populate. + customizer_config: Training step configuration. + prepared: Prepared dataset with merged train/val files. + seq_length: Effective sequence length for dataset configuration. + When sequence packing is enabled, this is the pack size. + Otherwise, this is the model's max_seq_length. + seed: Random seed for reproducibility. + is_embedding_model: Whether this is an embedding model (for dataset format hints). + embedding_config: Embedding model configuration (required for embedding datasets). + """ + train_file = prepared.train_file + validation_file = prepared.validation_file + + # Detect schema from training data + schema, column_keys = detect_dataset_schema( + train_file, + prompt_template=customizer_config.dataset.prompt_template, + ) + + # Validate that embedding models use embedding datasets and vice versa + if is_embedding_model and schema != DatasetSchema.EMBEDDING: + raise ValueError( + f"Model '{customizer_config.model.name}' is detected as an embedding model but the dataset " + f"is in '{schema.value}' format. Embedding models require datasets with 'query', 'pos_doc', " + "and 'neg_doc' fields. Please provide a dataset in embedding format." + ) + if schema == DatasetSchema.EMBEDDING and not is_embedding_model: + raise ValueError( + f"Dataset is in embedding format (query/pos_doc/neg_doc) but model " + f"'{customizer_config.model.name}' is not detected as an embedding model. " + "Embedding datasets can only be used with embedding models." + ) + + if schema == DatasetSchema.EMBEDDING: + # Embedding/retrieval dataset - uses inline format directly + if embedding_config is None: + raise ValueError("embedding_config is required for embedding dataset configuration") + _configure_embedding_dataset(cfg, customizer_config, train_file, validation_file, seed, embedding_config) + elif schema == DatasetSchema.CHAT: + # Chat dataset (OpenAI messages format) + _configure_chat_dataset(cfg, customizer_config, train_file, validation_file, seq_length) + else: + # SFT/Custom dataset (prompt/completion or custom columns) + assert column_keys is not None, "column_keys must be set for SFT/CUSTOM schema" + question_col, answer_col = column_keys + _configure_sft_dataset( + cfg, + customizer_config, + train_file, + validation_file, + question_col, + answer_col, + seq_length, + ) + + +def _configure_chat_dataset( + cfg: dict[str, Any], + customizer_config: TrainingStepConfig, + train_file: Path, + val_file: Path, + seq_length: int, +) -> None: + """Configure ChatDataset for OpenAI messages format.""" + logger.info(f"Configuring ChatDataset for chat format data with seq_length={seq_length}") + + # Resolve chat template using priority-based selection: + # 1. Fileset metadata chat_template (from model entity spec, highest priority) + # 2. Custom template from DEFAULT_CHAT_TEMPLATES (if model.name matches) + # 3. Model's built-in tokenizer template (fallback) + chat_template = resolve_chat_template( + model_path=customizer_config.model.path, + model_name=customizer_config.model.name, + user_template=customizer_config.model.chat_template, + ) + pp_enabled = customizer_config.parallelism.pipeline_parallel_size > 1 + # Note: "split" is required by Automodel's pack_dataset() when sequence packing is enabled. + # Without it, build_dataloader() raises AttributeError accessing cfg_ds.split. + cfg["dataset"] = { + "_target_": "nemo_automodel.components.datasets.llm.chat_dataset.ChatDataset", + "path_or_dataset_id": str(train_file), + "split": "train", + "seq_length": seq_length, + "padding": "do_not_pad" if not pp_enabled else "max_length", + } + cfg["validation_dataset"] = { + "_target_": "nemo_automodel.components.datasets.llm.chat_dataset.ChatDataset", + "path_or_dataset_id": str(val_file), + "split": "validation", + "seq_length": seq_length, + "padding": "do_not_pad" if not pp_enabled else "max_length", + } + + # Add chat template if available + if chat_template: + cfg["dataset"]["chat_template"] = chat_template + cfg["validation_dataset"]["chat_template"] = chat_template + logger.info("Added chat template to dataset config") + else: + logger.warning("No chat template found - ChatDataset may fail") + + # Store resolved template in config for checkpoint processing + # This ensures the same template is used during training and applied to output + cfg["_resolved_chat_template"] = chat_template + + +def _configure_sft_dataset( + cfg: dict[str, Any], + customizer_config: TrainingStepConfig, + train_file: Path, + val_file: Path, + question_col: str, + answer_col: str, + seq_length: int, +) -> None: + """Configure ColumnMappedTextInstructionDataset for SFT/custom format.""" + logger.info( + f"Configuring SFT dataset with columns: question={question_col}, answer={answer_col}, seq_length={seq_length}" + ) + pp_enabled = customizer_config.parallelism.pipeline_parallel_size > 1 + # Note: "split" is required by Automodel's pack_dataset() when sequence packing is enabled. + # Without it, build_dataloader() raises AttributeError accessing cfg_ds.split. + cfg["dataset"] = { + "_target_": "nemo_automodel.components.datasets.llm.column_mapped_text_instruction_dataset.ColumnMappedTextInstructionDataset", + "path_or_dataset_id": str(train_file), + "split": "train", + "column_mapping": { + "question": question_col, + "answer": answer_col, + }, + "seq_length": seq_length, + "answer_only_loss_mask": True, + "padding": "do_not_pad" if not pp_enabled else "max_length", + "truncation": "longest_first", + } + cfg["validation_dataset"] = { + "_target_": "nemo_automodel.components.datasets.llm.column_mapped_text_instruction_dataset.ColumnMappedTextInstructionDataset", + "path_or_dataset_id": str(val_file), + "split": "validation", + "column_mapping": { + "question": question_col, + "answer": answer_col, + }, + "seq_length": seq_length, + "answer_only_loss_mask": True, + "padding": "do_not_pad" if not pp_enabled else "max_length", + "truncation": "longest_first", + } + + +def _configure_embedding_dataset( + cfg: dict[str, Any], + customizer_config: TrainingStepConfig, + train_file: Path, + val_file: Path, + seed: int, + embedding_config: EmbeddingConfig, +) -> None: + """Configure embedding/retrieval dataset for biencoder training. + + Uses Automodel's inline retrieval dataset format which directly accepts + Customizer's embedding format without conversion: + {"query": "...", "pos_doc": "...", "neg_doc": ["...", "..."]} + + This uses retrieval_dataset_inline.make_retrieval_dataset which handles: + - Loading inline text directly from JSONL + - RetrievalBiencoderCollator for tokenization and batching + + Args: + cfg: Configuration dictionary to populate. + customizer_config: Training step configuration. + train_file: Path to training JSONL file. + val_file: Path to validation JSONL file. + seed: Random seed for reproducibility. + embedding_config: Embedding model configuration. + """ + + logger.info(f"Configuring embedding dataset with train_n_passages={embedding_config.train_n_passages}") + + cfg["dataloader"] = { + "_target_": "torchdata.stateful_dataloader.StatefulDataLoader", + "dataset": { + "_target_": "nemo_automodel.components.datasets.llm.retrieval_dataset_inline.make_retrieval_dataset", + "data_dir_list": [str(train_file)], + "data_type": "train", + "train_n_passages": embedding_config.train_n_passages, + "seed": seed, + "do_shuffle": True, + }, + "collate_fn": { + "_target_": "nemo_automodel.components.datasets.llm.RetrievalBiencoderCollator", + "q_max_len": embedding_config.query_max_length, + "p_max_len": embedding_config.passage_max_length, + "query_prefix": embedding_config.query_prefix, + "passage_prefix": embedding_config.passage_prefix, + "pad_to_multiple_of": 8, + }, + "shuffle": True, + "num_workers": 0, + } + + if val_file and val_file.exists(): + cfg["validation_dataloader"] = { + "_target_": "torchdata.stateful_dataloader.StatefulDataLoader", + "dataset": { + "_target_": "nemo_automodel.components.datasets.llm.retrieval_dataset_inline.make_retrieval_dataset", + "data_dir_list": [str(val_file)], + "data_type": "eval", + "train_n_passages": embedding_config.train_n_passages, + "eval_negative_size": get_eval_negative_size(embedding_config), + "seed": seed, + "do_shuffle": False, + }, + "collate_fn": { + "_target_": "nemo_automodel.components.datasets.llm.RetrievalBiencoderCollator", + "q_max_len": embedding_config.query_max_length, + "p_max_len": embedding_config.passage_max_length, + "query_prefix": embedding_config.query_prefix, + "passage_prefix": embedding_config.passage_prefix, + "padding": "longest", + "pad_to_multiple_of": 8, + }, + "batch_size": customizer_config.batch.micro_batch_size, + "shuffle": False, + "num_workers": 0, + } + + +def _verify_tokenizer_compatibility(student_path: str, teacher_path: str, trust_remote_code: bool = False) -> None: + """ + Verify that student and teacher models have compatible tokenizers. + + Knowledge distillation requires the student and teacher to have the same + vocabulary so their logit spaces are aligned. This check prevents subtle + bugs where training appears to work but produces garbage outputs. + + Raises: + ValueError: If tokenizers are incompatible + """ + # Import here to avoid ModuleNotFoundError in environments where + # transformers is not installed (e.g., during test collection) + from transformers import AutoTokenizer + + try: + student_tokenizer = AutoTokenizer.from_pretrained(student_path, trust_remote_code=trust_remote_code) + teacher_tokenizer = AutoTokenizer.from_pretrained(teacher_path, trust_remote_code=trust_remote_code) + + if student_tokenizer.vocab_size != teacher_tokenizer.vocab_size: + raise ValueError( + f"Tokenizer vocabulary size mismatch: student has {student_tokenizer.vocab_size} tokens, " + f"teacher has {teacher_tokenizer.vocab_size} tokens. " + "Knowledge distillation requires matching vocabularies." + ) + + # Optional: Could also check for specific token mismatches + logger.info(f"Tokenizer compatibility verified: both models have vocab_size={student_tokenizer.vocab_size}") + + except Exception as e: + if "vocabulary size mismatch" in str(e): + raise + # Log but don't fail for other tokenizer loading issues + # (e.g., network issues, missing files) - the training will fail later with a clearer error + logger.warning(f"Could not verify tokenizer compatibility: {e}") + + +def _configure_kd(cfg: dict[str, Any], customizer_config: TrainingStepConfig, trust_remote_code: bool = False) -> None: + """ + Configure Knowledge Distillation for Automodel's KD recipe. + + Automodel's KnowledgeDistillationRecipeForNextTokenPrediction requires: + - teacher_model: Frozen teacher model for soft targets + - kd_ratio: Balance between CE and KD loss (0=CE only, 1=KD only) + - kd_loss_fn: KL-divergence loss with temperature scaling + - offload_teacher_model: Optional CPU offloading for memory efficiency + """ + kd_config = customizer_config.training.kd + if not kd_config or not kd_config.teacher_model: + raise ValueError( + "Knowledge distillation requires training.kd.teacher to be set. " + "Ensure the job input includes a teacher model." + ) + + # Verify tokenizer compatibility before proceeding + _verify_tokenizer_compatibility( + customizer_config.model.path, + kd_config.teacher_model.path, + trust_remote_code=trust_remote_code, + ) + + # Teacher model (frozen, same architecture loading as student) + # Use teacher's precision if specified, otherwise fall back to student's precision + teacher_precision = kd_config.teacher_model.precision or customizer_config.model.precision + cfg["teacher_model"] = { + "_target_": "nemo_automodel.NeMoAutoModelForCausalLM.from_pretrained", + "pretrained_model_name_or_path": kd_config.teacher_model.path, + "torch_dtype": teacher_precision.to_torch_dtype() if teacher_precision else "auto", + "attn_implementation": kd_config.teacher_model.attn_implementation, + "trust_remote_code": kd_config.teacher_model.trust_remote_code, + } + + # KD loss function with temperature + cfg["kd_loss_fn"] = { + "_target_": "nemo_automodel.components.loss.kd_loss.KDLoss", + "ignore_index": -100, + "temperature": kd_config.temperature, + "fp32_upcast": True, # Recommended for numerical stability + } + + # KD ratio (blend between CE and KD loss) + cfg["kd_ratio"] = kd_config.ratio + + # Optional: Offload teacher to CPU for memory efficiency + if kd_config.offload_teacher: + cfg["offload_teacher_model"] = True + logger.info("Teacher model will be offloaded to CPU between forward passes") + + +def get_eval_negative_size(embedding_config: EmbeddingConfig) -> int: + """Get the effective eval_negative_size value from embedding config. + + Returns the user-specified eval_negative_size if set, otherwise defaults + to train_n_passages - 1 for consistent train/eval behavior. + + The -1 relationship exists because: + - train_n_passages = total passages = 1 positive + N negatives + - eval_negative_size = just the negative count = N + - So: eval_negative_size = train_n_passages - 1 (subtracting the positive) + + Example: train_n_passages=5 (1 pos + 4 neg) -> eval_negative_size=4 + """ + if embedding_config.eval_negative_size is not None: + return embedding_config.eval_negative_size + return embedding_config.train_n_passages - 1 diff --git a/services/automodel/src/nmp/automodel/tasks/training/backends/finetune.py b/services/automodel/src/nmp/automodel/tasks/training/backends/finetune.py new file mode 100644 index 0000000000..abaf469ba1 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/backends/finetune.py @@ -0,0 +1,260 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Automodel training subprocess entry point. + +Wraps nemo_automodel recipes with Jobs-service progress reporting (SFT, KD, embedding). +""" + +from __future__ import annotations + +import logging +from typing import Any, Protocol, runtime_checkable + +from nemo_automodel.components.checkpoint.checkpointing import Checkpointer +from nemo_automodel.components.config._arg_parser import parse_args_and_load_config +from nemo_automodel.components.training.step_scheduler import StepScheduler +from nemo_automodel.recipes.biencoder.train_biencoder import TrainBiencoderRecipe +from nemo_automodel.recipes.llm.kd import KnowledgeDistillationRecipeForNextTokenPrediction +from nemo_automodel.recipes.llm.train_ft import TrainFinetuneRecipeForNextTokenPrediction +from nmp.automodel.app.jobs.context import NMPJobContext +from nmp.automodel.tasks.training.backends.callbacks import TrainingProgressCallback +from nmp.automodel.tasks.training.progress import JobsServiceProgressReporter + +logger = logging.getLogger(__name__) + + +@runtime_checkable +class AutomodelRecipe(Protocol): + """Protocol defining the interface we need from Automodel recipes. + + This makes the dependencies explicit and enables type checking, unlike + the previous mixin approach that relied on implicit attributes. + """ + + cfg: Any + step_scheduler: StepScheduler + checkpointer: Checkpointer + dist_env: Any + + def setup(self) -> None: + """Build all components needed for training.""" + ... + + def run_train_validation_loop(self) -> None: + """Run the main training/validation loop.""" + ... + + def log_train_metrics(self, log_data: Any) -> None: + """Log training metrics.""" + ... + + def log_val_metrics(self, *args: Any, **kwargs: Any) -> None: + """Log validation metrics. + + Note: Signature varies across Automodel recipes: + - LLM/KD: (val_name, log_data, metric_logger=None) + - VLM/biencoder/seq_cls: (log_data) + """ + ... + + def save_checkpoint( + self, + epoch: int, + step: int, + train_loss: float, + val_loss: dict[str, float] | None = None, + best_metric_key: str = "default", + ) -> None: + """Save a checkpoint.""" + ... + + +class AutomodelRecipeWrapper: + """Wraps an Automodel recipe with Jobs-service progress reporting.""" + + def __init__(self, recipe: AutomodelRecipe, job_ctx: NMPJobContext | None = None): + """Initialize the wrapper with an Automodel recipe. + + Args: + recipe: Any recipe implementing the AutomodelRecipe protocol + (SFT, KD, biencoder, etc.). + job_ctx: NeMo Platform job context for progress reporting (optional, + defaults to environment variables). + """ + self._job_ctx = job_ctx or NMPJobContext.from_env() + self._reporter = JobsServiceProgressReporter(self._job_ctx) + self._reporter.report_running("automodel_recipe_setup") + + self._recipe = recipe + self._recipe.setup() + + self.max_steps = getattr(self._recipe.step_scheduler, "max_steps", None) or 100 + self.num_epochs = getattr(self._recipe.step_scheduler, "num_epochs", None) or 1 + + self.callback = TrainingProgressCallback(self._reporter) + logger.info(f"Automodel recipe wrapper initialized: max_steps={self.max_steps}, num_epochs={self.num_epochs}") + + # Store original methods before patching + self._original_log_train_metrics = recipe.log_train_metrics + self._original_log_val_metrics = recipe.log_val_metrics + self._original_save_checkpoint = recipe.save_checkpoint + + # Monkey-patch the recipe's methods to add our callbacks + recipe.log_train_metrics = self._log_train_metrics # type: ignore[method-assign] + recipe.log_val_metrics = self._log_val_metrics # type: ignore[method-assign] + recipe.save_checkpoint = self._save_checkpoint # type: ignore[method-assign] + + @property + def recipe(self) -> AutomodelRecipe: + """Access the underlying recipe.""" + return self._recipe + + def run_train_validation_loop(self) -> None: + """Run training and close the progress callback.""" + try: + self.callback.report_training_start(self.max_steps, self.num_epochs) + self._recipe.run_train_validation_loop() + finally: + if self.callback: + self.callback.close() + logger.info("Training progress callback closed") + + def _log_train_metrics(self, log_data: Any) -> None: + """Wrapped log_train_metrics with Jobs-service reporting.""" + self._original_log_train_metrics(log_data) + if self.callback and log_data: + try: + metrics = getattr(log_data, "metrics", {}) + self.callback.report_train_step( + step=getattr(log_data, "step", 0) + 1, # Convert to 1-based + epoch=getattr(log_data, "epoch", 0) + 1, # Convert to 1-based + loss=metrics.get("loss", 0.0), + lr=metrics.get("lr"), + grad_norm=metrics.get("grad_norm"), + ) + except Exception as e: + logger.warning(f"Failed to report training progress: {e}") + + try: + if self._recipe.step_scheduler.is_last_batch: + self.callback.report_epoch_end( + step=self._recipe.step_scheduler.step + 1, + epoch=self._recipe.step_scheduler.epoch + 1, + ) + except Exception as e: + logger.warning(f"Failed to report epoch end: {e}") + + def _log_val_metrics(self, *args: Any, **kwargs: Any) -> None: + """Wrapped log_val_metrics with Jobs-service reporting. + + Handles different Automodel recipe signatures: + - LLM/KD: (val_name, log_data, metric_logger=None) + - VLM/biencoder/seq_cls: (log_data) + """ + # Call original method first with whatever args were passed + self._original_log_val_metrics(*args, **kwargs) + + # Extract log_data from args (it's always the last positional arg before kwargs) + # LLM signature: (val_name, log_data, metric_logger=None) -> log_data is args[1] + # VLM/biencoder signature: (log_data) -> log_data is args[0] + log_data = None + if len(args) >= 2: + # LLM/KD style: (val_name, log_data, ...) + log_data = args[1] + elif len(args) == 1: + # VLM/biencoder style: (log_data) + log_data = args[0] + + if self.callback and log_data: + try: + metrics = getattr(log_data, "metrics", {}) + self.callback.report_validation( + step=getattr(log_data, "step", 0) + 1, # Convert to 1-based + epoch=getattr(log_data, "epoch", 0) + 1, # Convert to 1-based + val_loss=metrics.get("val_loss", 0.0), + ) + except Exception as e: + logger.warning(f"Failed to report validation progress: {e}") + + def _save_checkpoint( + self, + epoch: int, + step: int, + train_loss: float, + val_loss: dict[str, float] | None = None, + best_metric_key: str = "default", + ) -> None: + """Wrapped save_checkpoint with Jobs-service reporting.""" + self._original_save_checkpoint(epoch, step, train_loss, val_loss, best_metric_key) + if self.callback: + try: + checkpoint_dir = getattr( + getattr(self._recipe.checkpointer, "config", None), + "checkpoint_dir", + None, + ) + self.callback.report_checkpoint_saved( + step=step + 1, # Convert to 1-based + epoch=epoch + 1, # Convert to 1-based + checkpoint_path=str(checkpoint_dir) if checkpoint_dir else None, + ) + except Exception as e: + logger.warning(f"Failed to report checkpoint save: {e}") + + +def _is_kd_config(cfg: Any) -> bool: + """Check if config is for knowledge distillation.""" + return cfg.get("teacher_model") is not None or cfg.get("kd_ratio") is not None + + +def _is_biencoder_config(cfg: Any) -> bool: + """Check if config is for biencoder/embedding model training. + + Detects biencoder configs by checking if model._target_ contains 'biencoder'. + + Note: ConfigNode automatically resolves _target_ to the actual function/class, + so we check the function's __module__ or __qualname__ for 'biencoder'. + """ + try: + model_cfg = cfg.get("model", {}) + if model_cfg is None: + return False + + target = model_cfg.get("_target_") + if target is None: + return False + + # target is resolved to the actual function/class by ConfigNode + # Check its module path or qualified name + module = getattr(target, "__module__", "") or "" + qualname = getattr(target, "__qualname__", "") or "" + return "biencoder" in module.lower() or "biencoder" in qualname.lower() + except (AttributeError, TypeError): + return False + + +def create_automodel_recipe(cfg: Any) -> AutomodelRecipeWrapper: + """Create a progress-reporting wrapper for the recipe implied by *cfg*.""" + if _is_biencoder_config(cfg): + logger.info("Detected biencoder config, using embedding model recipe") + base_recipe = TrainBiencoderRecipe(cfg) + elif _is_kd_config(cfg): + logger.info("Detected Knowledge Distillation config, using KD recipe") + base_recipe = KnowledgeDistillationRecipeForNextTokenPrediction(cfg) + else: + logger.info("Using SFT fine-tuning recipe") + base_recipe = TrainFinetuneRecipeForNextTokenPrediction(cfg) + + return AutomodelRecipeWrapper(base_recipe) + + +def main() -> None: + cfg = parse_args_and_load_config() + recipe = create_automodel_recipe(cfg) + recipe.run_train_validation_loop() + + +if __name__ == "__main__": + main() diff --git a/services/automodel/src/nmp/automodel/tasks/training/backends/requirements.txt b/services/automodel/src/nmp/automodel/tasks/training/backends/requirements.txt new file mode 100644 index 0000000000..1ac43ef3a7 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/backends/requirements.txt @@ -0,0 +1 @@ +nemo_automodel==0.2.0 diff --git a/services/automodel/src/nmp/automodel/tasks/training/chat_templates.py b/services/automodel/src/nmp/automodel/tasks/training/chat_templates.py new file mode 100644 index 0000000000..854823e153 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/chat_templates.py @@ -0,0 +1,196 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Chat template resolution and application for training backends. + +This module provides: +1. Priority-based chat template selection (resolve_chat_template) +2. Applying chat templates to output checkpoints (apply_chat_template_to_checkpoint) + +Chat template priority order: +1. User-provided template (via API) +2. Custom template from DEFAULT_CHAT_TEMPLATES map (enhanced for tool calling) +3. Model's built-in tokenizer template (fallback) + +The custom templates in the templates/ directory extend base model templates with: +- Tool calling support: , , formatting +- Generation markers: {% generation %}...{% endgeneration %} blocks for loss masking +- Enhanced compatibility across models +""" + +import json +import logging +from pathlib import Path + +logger = logging.getLogger(__name__) + +# Directory containing custom chat template jinja files +TEMPLATES_DIR = Path(__file__).parent / "templates" + +# ============================================================================ +# Model Name Constants +# ============================================================================ + +# Meta Llama models +META_LLAMA_31_8B_INSTRUCT = "meta/llama-3.1-8b-instruct" +META_LLAMA_31_70B_INSTRUCT = "meta/llama-3.1-70b-instruct" +META_LLAMA_31_405B_INSTRUCT = "meta/llama-3.1-405b-instruct" +META_LLAMA_32_1B = "meta/llama-3.2-1b" +META_LLAMA_32_1B_INSTRUCT = "meta/llama-3.2-1b-instruct" +META_LLAMA_32_3B_INSTRUCT = "meta/llama-3.2-3b-instruct" +META_LLAMA_33_70B_INSTRUCT = "meta/llama-3.3-70b-instruct" +# NVIDIA Nemotron models +NVIDIA_NEMOTRON_31_8B = "nvidia/nemotron-nano-llama-3.1-8b" +NVIDIA_NEMOTRON_31_70B = "nvidia/nemotron-llama-3.1-70b" +NVIDIA_NEMOTRON_33_49B = "nvidia/nemotron-super-llama-3.3-49b" +NVIDIA_NEMOTRON_33_49B_V1_5 = "nvidia/nemotron-super-llama-3.3-49b-v1.5" +# NIM model names (alternative naming) +NIM_NVIDIA_NEMOTRON_31_8B = "nvidia/llama-3.1-nemotron-nano-8b-v1" +NIM_NVIDIA_NEMOTRON_31_70B = "nvidia/llama-3.1-nemotron-70b-instruct" +NIM_NVIDIA_NEMOTRON_33_49B = "nvidia/llama-3.3-nemotron-super-49b-v1" +NIM_NVIDIA_NEMOTRON_33_49B_V1_5 = "nvidia/llama-3.3-nemotron-super-49b-v1.5" +# Microsoft models +PHI_4 = "microsoft/phi-4" + +# ============================================================================ +# Default Chat Templates Map +# ============================================================================ + +# Maps model names to custom jinja template filenames. +# These templates extend the base model templates with: +# - Tool calling support +# - Generation markers for loss masking +# - Enhanced compatibility +DEFAULT_CHAT_TEMPLATES: dict[str, str] = { + # Llama 3.1 family + META_LLAMA_31_8B_INSTRUCT: "llama-3.1-instruct.jinja", + META_LLAMA_31_70B_INSTRUCT: "llama-3.1-instruct.jinja", + META_LLAMA_31_405B_INSTRUCT: "llama-3.1-instruct.jinja", + # Llama 3.2 family + META_LLAMA_32_1B: "llama-3.2-instruct.jinja", + META_LLAMA_32_1B_INSTRUCT: "llama-3.2-instruct.jinja", + META_LLAMA_32_3B_INSTRUCT: "llama-3.2-instruct.jinja", + # Llama 3.3 family + META_LLAMA_33_70B_INSTRUCT: "llama-3.3-instruct.jinja", + # Nemotron family + NVIDIA_NEMOTRON_31_8B: "nemotron-3.1.jinja", + NVIDIA_NEMOTRON_31_70B: "nemotron-3.1.jinja", + NVIDIA_NEMOTRON_33_49B: "nemotron-super-3.3.jinja", + NVIDIA_NEMOTRON_33_49B_V1_5: "nemotron-super-3.3.jinja", + # NIM Nemotron (alternative naming) + NIM_NVIDIA_NEMOTRON_31_8B: "nemotron-3.1.jinja", + NIM_NVIDIA_NEMOTRON_31_70B: "nemotron-3.1.jinja", + NIM_NVIDIA_NEMOTRON_33_49B: "nemotron-super-3.3.jinja", + NIM_NVIDIA_NEMOTRON_33_49B_V1_5: "nemotron-super-3.3.jinja", + # Microsoft + PHI_4: "phi-4.jinja", +} + + +def _load_template_file(template_filename: str) -> str | None: + """Load a custom template from the templates directory.""" + template_path = TEMPLATES_DIR / template_filename + if template_path.exists(): + with open(template_path, "r", encoding="utf-8") as f: + return f.read() + logger.warning(f"Template file not found: {template_path}") + return None + + +def _get_tokenizer_chat_template(model_path: str) -> str | None: + """ + Get chat template from model's tokenizer. + + Uses AutoTokenizer which handles all model formats (HF, NeMo, custom). + """ + try: + from transformers import AutoTokenizer + + tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) + template = getattr(tokenizer, "chat_template", None) + if template: + logger.debug(f"Found chat template in tokenizer for {model_path}") + return template + except Exception as e: + logger.warning(f"Could not load tokenizer to get chat template: {e}") + return None + + +def resolve_chat_template( + model_path: str, + model_name: str | None = None, + user_template: str | None = None, +) -> str | None: + """ + Resolve chat template using priority-based selection. + + Priority order: + 1. User-provided template (highest priority) + 2. Custom template from DEFAULT_CHAT_TEMPLATES (if model_name matches) + 3. Model's built-in tokenizer template (fallback) + + Args: + model_path: Path to the model directory (for tokenizer fallback). + model_name: Canonical model name (e.g., "meta/llama-3.1-8b-instruct"). + Used to look up custom templates. + user_template: User-provided template string (takes highest priority). + + Returns: + The resolved chat template string, or None if no template found. + """ + # Priority 1: User-provided template + if user_template: + logger.info("Using user-provided chat template") + return user_template + + # Priority 2: Custom template from DEFAULT_CHAT_TEMPLATES + if model_name and model_name in DEFAULT_CHAT_TEMPLATES: + template_filename = DEFAULT_CHAT_TEMPLATES[model_name] + template = _load_template_file(template_filename) + if template: + logger.info(f"Using custom chat template for {model_name}: {template_filename}") + return template + + # Priority 3: Model's built-in tokenizer template + template = _get_tokenizer_chat_template(model_path) + if template: + logger.info(f"Using model's built-in chat template from {model_path}") + return template + + logger.warning(f"No chat template found for model_name={model_name}, model_path={model_path}") + return None + + +def apply_chat_template_to_checkpoint( + output_path: Path, + chat_template: str | None, +) -> None: + """ + Apply chat template to the output checkpoint's tokenizer_config.json. + + Also ensures pad_token is set if missing (uses eos_token as fallback), + which is required by many inference frameworks. + + Args: + output_path: Path to the checkpoint directory containing tokenizer_config.json. + chat_template: The chat template string to apply. If None, skips application. + """ + if not chat_template: + logger.warning("No chat template provided, skipping") + return + + tokenizer_config = output_path / "tokenizer_config.json" + if not tokenizer_config.exists(): + logger.warning(f"tokenizer_config.json not found at {output_path}") + return + + with open(tokenizer_config, "r") as f: + config = json.load(f) + + config["chat_template"] = chat_template + + with open(tokenizer_config, "w") as f: + json.dump(config, f, indent=2) + + logger.info("Applied chat template to output checkpoint") diff --git a/services/automodel/src/nmp/automodel/tasks/training/datasets/preparation.py b/services/automodel/src/nmp/automodel/tasks/training/datasets/preparation.py new file mode 100644 index 0000000000..63473c5d94 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/datasets/preparation.py @@ -0,0 +1,509 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual +# property and proprietary rights in and to this material, related +# documentation and any modifications thereto. Any use, reproduction, +# disclosure or distribution of this material and related documentation +# without an express license agreement from NVIDIA CORPORATION or +# its affiliates is strictly prohibited. + +"""Dataset discovery, merge/split, and schedule helpers for Automodel training.""" + +import json +import logging +import random +import re +import shutil +import subprocess +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from typing import Any, Optional, Union + +from nmp.automodel.app.constants import DEFAULT_SEED + +logger = logging.getLogger(__name__) + +# Dataset directory constants for merged files (we control this structure) +MERGED_DIR = "merged" +TRAIN_FILE = "train.jsonl" +VAL_FILE = "validation.jsonl" + +# Heuristic patterns for discovering training files +TRAIN_PATTERNS = [ + "train*.jsonl", + "training*.jsonl", + "train*.json", + "training*.json", +] +TRAIN_DIRS = ["train", "training"] + +# Heuristic patterns for discovering validation files +VAL_PATTERNS = [ + "val*.jsonl", + "validation*.jsonl", + "val*.json", + "validation*.json", + "dev*.jsonl", + "dev*.json", +] +VAL_DIRS = ["val", "validation", "dev"] + + +class DatasetSchema(str, Enum): + """Detected dataset schema type.""" + + CHAT = "chat" # OpenAI messages format: {"messages": [...]} + SFT = "sft" # Prompt/completion: {"prompt": ..., "completion": ...} + CUSTOM = "custom" # Custom columns via prompt_template + EMBEDDING = "embedding" # Retrieval format: {"query": ..., "pos_doc": ..., "neg_doc": [...]} + + +class DatasetFormatError(Exception): + """Raised when dataset format is invalid or unsupported.""" + + pass + + +def detect_dataset_schema( + file_path: Path, + prompt_template: str | None = None, +) -> tuple[DatasetSchema, tuple[str, ...] | None]: + """ + Detect dataset schema by sampling the first line. + + Supports four formats: + 1. Chat format: {"messages": [{"role": "user", ...}, {"role": "assistant", ...}]} + 2. Embedding format: {"query": "...", "pos_doc": "...", "neg_doc": ["...", "..."]} + 3. SFT format: {"prompt": "...", "completion": "..."} + 4. Custom format: Any two-column format specified via prompt_template like "{input} {output}" + + Args: + file_path: Path to the JSONL dataset file. + prompt_template: Optional template string with two placeholders like "{input} {output}". + + Returns: + Tuple of (schema_type, column_keys) where: + - CHAT: column_keys is None + - EMBEDDING: column_keys is ("query", "pos_doc", "neg_doc") + - SFT/CUSTOM: column_keys is (question_col, answer_col) + + Raises: + DatasetFormatError: If the dataset format cannot be detected or is invalid. + """ + with open(file_path, "r", encoding="utf-8") as f: + line = f.readline() + + try: + obj: dict[str, Any] = json.loads(line) + except json.JSONDecodeError as e: + raise DatasetFormatError(f"Invalid JSON in {file_path}: {e}") + + # Check for chat format (OpenAI messages) + if "messages" in obj and isinstance(obj["messages"], list): + if len(obj["messages"]) > 0 and isinstance(obj["messages"][0], dict): + if "role" in obj["messages"][0]: + logger.info(f"Detected chat dataset format in {file_path}") + return DatasetSchema.CHAT, None + + # Check for embedding/retrieval format + # Format: {"query": "...", "pos_doc": "...", "neg_doc": ["...", "..."]} + if "query" in obj and "pos_doc" in obj and "neg_doc" in obj: + if isinstance(obj["query"], str) and isinstance(obj["pos_doc"], str) and isinstance(obj["neg_doc"], list): + logger.info(f"Detected embedding/retrieval dataset format in {file_path}") + return DatasetSchema.EMBEDDING, ("query", "pos_doc", "neg_doc") + + # Check for custom prompt_template format + if prompt_template: + keys = re.findall(r"\{(.*?)\}", prompt_template) + if len(keys) == 2: + # Validate keys exist in data + if all(k in obj for k in keys): + logger.info(f"Detected custom template format with keys {keys}") + return DatasetSchema.CUSTOM, (keys[0], keys[1]) + else: + raise DatasetFormatError( + f"prompt_template keys {keys} not found in dataset. Available keys: {list(obj.keys())}" + ) + else: + raise DatasetFormatError(f"prompt_template must have exactly 2 placeholders, got: {prompt_template}") + + # Check for standard SFT format (prompt/completion) + if "prompt" in obj and "completion" in obj: + logger.info(f"Detected SFT (prompt/completion) format in {file_path}") + return DatasetSchema.SFT, ("prompt", "completion") + + # Fallback - try to find any two string columns + string_cols = [k for k, v in obj.items() if isinstance(v, str)] + if len(string_cols) >= 2: + logger.warning(f"Could not detect standard format, using first two string columns: {string_cols[:2]}") + return DatasetSchema.SFT, (string_cols[0], string_cols[1]) + + raise DatasetFormatError( + f"Could not detect dataset format. Expected 'messages' (chat) or " + f"'prompt'/'completion' (SFT) columns. Found: {list(obj.keys())}" + ) + + +def _count_jsonl_samples_python(file_path: Path) -> int: + """Pure Python implementation of line counting (fallback).""" + count = 0 + with open(file_path, "r", encoding="utf-8") as f: + for line in f: + if line.strip(): # Non-empty line + count += 1 + return count + + +def count_jsonl_samples(file_path: Path) -> int: + """ + Count the number of non-empty lines in a JSONL file. + + Uses grep for efficiency with large files when available, + falls back to pure Python implementation otherwise. + + Args: + file_path: Path to the JSONL file. + + Returns: + Number of non-empty lines (samples) in the file. + """ + # Check if grep is available + if shutil.which("grep") is None: + return _count_jsonl_samples_python(file_path) + + try: + # Use `grep -c "\S"` to count non-empty lines (excludes trailing empty lines) + result = subprocess.check_output(["grep", "-c", r"\S", str(file_path)], text=True) + return int(result.strip()) + except subprocess.CalledProcessError: + # grep returns exit code 1 if no matches (empty file) + return 0 + except OSError: + # Fallback if subprocess fails for any reason + return _count_jsonl_samples_python(file_path) + + +def compute_val_check_interval( + steps_per_epoch: int, + max_steps: int, + val_check_interval: Optional[Union[int, float]] = None, +) -> int: + """ + Compute how often to run validation (in steps). + + This handles the semantic difference between: + - float <= 1.0: Fraction of epoch (e.g., 0.5 = validate at 50% of each epoch) + - int or float > 1.0: Absolute step count + + Args: + steps_per_epoch: Number of gradient steps per epoch. + max_steps: Maximum training steps. + val_check_interval: User-provided interval (float for fraction, int for steps). + + Returns: + Integer step count for validation interval. + + Raises: + ValueError: If val_check_interval is negative. + """ + effective_steps = min(steps_per_epoch, max_steps) + + if val_check_interval is None or val_check_interval == 0: + # Default: validate once per epoch (or at end if max_steps < steps_per_epoch) + return effective_steps + + if val_check_interval < 0: + raise ValueError("val_check_interval cannot be negative") + + # Float <= 1.0: interpret as fraction of epoch + if isinstance(val_check_interval, float) and val_check_interval <= 1.0: + interval = max(1, int(val_check_interval * steps_per_epoch)) + else: + # Integer or float > 1.0: treat as absolute step count + interval = int(val_check_interval) + + # Cap at effective_steps + interval = min(interval, effective_steps) + + # Ensure validation happens at least once before training ends + if interval >= max_steps: + interval = max(1, max_steps - 1) + + return interval + + +@dataclass +class PreparedDataset: + """Result of dataset preparation.""" + + merged_dir: Path + train_file: Path + validation_file: Path + train_samples: int + validation_samples: int + + +def _discover_files_by_patterns(base_path: Path, patterns: list[str], dirs: list[str]) -> list[Path]: + """ + Discover files matching patterns or in specific directories. + + Searches for: + 1. Files matching glob patterns in base_path + 2. All .jsonl/.json files in specified subdirectories + + Args: + base_path: Root directory to search. + patterns: Glob patterns to match (e.g., ["train*.jsonl"]). + dirs: Subdirectory names to search (e.g., ["train", "training"]). + + Returns: + Sorted list of discovered file paths. + """ + files: set[Path] = set() + + # Pattern matching in base directory + for pattern in patterns: + for match in base_path.glob(pattern): + if match.is_file(): + files.add(match.resolve()) + + # Files in subdirectories + for dir_name in dirs: + subdir = base_path / dir_name + if subdir.is_dir(): + for f in subdir.iterdir(): + if f.is_file() and f.suffix.lower() in (".jsonl", ".json"): + files.add(f.resolve()) + + return sorted(files) # Sorted for deterministic ordering + + +def discover_dataset_files(dataset_path: Path) -> tuple[list[Path], list[Path]]: + """ + Discover training and validation files using heuristics. + + Heuristics applied (in order): + 1. Files matching train*/training* patterns → training + 2. Files in train/ or training/ directories → training + 3. Files matching val*/validation*/dev* patterns → validation + 4. Files in val/, validation/, or dev/ directories → validation + 5. If only one .jsonl file found → treat as training (will auto-split) + + Args: + dataset_path: Path to the dataset directory. + + Returns: + Tuple of (training_files, validation_files). + + Raises: + DatasetFormatError: If no training files can be found. + """ + dataset_path = Path(dataset_path).resolve() + + if not dataset_path.exists(): + raise DatasetFormatError(f"Dataset path does not exist: {dataset_path}") + + # If path is a file, treat it as the training file + if dataset_path.is_file(): + logger.info(f"Dataset path is a file, treating as training data: {dataset_path}") + return [dataset_path], [] + + # Discover training files + train_files = _discover_files_by_patterns(dataset_path, TRAIN_PATTERNS, TRAIN_DIRS) + + # Discover validation files + val_files = _discover_files_by_patterns(dataset_path, VAL_PATTERNS, VAL_DIRS) + + # Fallback: if no files found with patterns, check for any .jsonl files + if not train_files and not val_files: + all_jsonl = sorted(f for f in dataset_path.glob("*.jsonl") if f.is_file()) + if len(all_jsonl) == 1: + logger.info(f"Found single JSONL file, treating as training data: {all_jsonl[0]}") + train_files = all_jsonl + elif len(all_jsonl) > 1: + # Ambiguous - could be train/val or multiple training files + logger.warning( + f"Found {len(all_jsonl)} JSONL files without clear train/val naming. " + f"Treating all as training data: {[f.name for f in all_jsonl]}" + ) + train_files = all_jsonl + + if not train_files: + raise DatasetFormatError( + f"No training files found in {dataset_path}. " + f"Expected files matching patterns like train*.jsonl or a train/ directory." + ) + + logger.info(f"Discovered {len(train_files)} training file(s): {[f.name for f in train_files]}") + if val_files: + logger.info(f"Discovered {len(val_files)} validation file(s): {[f.name for f in val_files]}") + else: + logger.info("No validation files found - will auto-split from training data") + + return train_files, val_files + + +def _merge_files(files: list[Path], output_file: Path) -> int: + """ + Merge multiple JSONL files into a single file. + + Args: + files: List of files to merge. + output_file: Output file path. + + Returns: + Total number of samples (non-empty lines) in merged file. + """ + output_file.parent.mkdir(parents=True, exist_ok=True) + + with open(output_file, "w", encoding="utf-8") as out: + for f in files: + with open(f, "r", encoding="utf-8") as inp: + content = inp.read() + out.write(content) + # Ensure newline between files + if content and not content.endswith("\n"): + out.write("\n") + + return count_jsonl_samples(output_file) + + +def _create_val_split( + train_file: Path, + output_train: Path, + output_val: Path, + val_ratio: float = 0.1, + seed: int = DEFAULT_SEED, +) -> tuple[int, int]: + """ + Split a training file into train and validation sets. + + Args: + train_file: Source training file. + output_train: Output path for training split. + output_val: Output path for validation split. + val_ratio: Fraction of data to use for validation (default: 10%). + seed: Random seed for reproducible splits (default: 1111). + + Returns: + Tuple of (train_samples, validation_samples). + """ + with open(train_file, "r", encoding="utf-8") as f: + lines = [line for line in f if line.strip()] + + # Shuffle for reproducibility (important for multi-node!) + # Uses global seed if not explicitly provided + random.seed(seed) + random.shuffle(lines) + + val_size = max(1, int(len(lines) * val_ratio)) + val_lines = lines[:val_size] + train_lines = lines[val_size:] + + output_train.parent.mkdir(parents=True, exist_ok=True) + output_val.parent.mkdir(parents=True, exist_ok=True) + + with open(output_train, "w", encoding="utf-8") as f: + for line in train_lines: + # Re-serialize to ensure valid JSON and consistent formatting + f.write(json.dumps(json.loads(line)) + "\n") + + with open(output_val, "w", encoding="utf-8") as f: + for line in val_lines: + f.write(json.dumps(json.loads(line)) + "\n") + + logger.info( + f"Created validation split: {len(train_lines)} train samples, {len(val_lines)} val samples " + f"({val_ratio:.0%} split)" + ) + + return len(train_lines), len(val_lines) + + +def prepare_dataset( + dataset_path: Path, + output_dir: Optional[Path] = None, + val_split_ratio: float = 0.1, + seed: int = DEFAULT_SEED, +) -> PreparedDataset: + """ + Prepare dataset for training by discovering, merging, and optionally splitting files. + + This function: + 1. Discovers training and validation files using heuristics + 2. Merges multiple files into single train.jsonl and val.jsonl + 3. Auto-creates validation split if no validation files found + 4. Returns paths to the prepared files + + Args: + dataset_path: Path to the dataset directory or file. + output_dir: Directory for merged output (default: dataset_path/merged). + val_split_ratio: Fraction for auto-split if no validation data (default: 0.1). + seed: Random seed for reproducible validation splits (default: 1111). + + Returns: + PreparedDataset with paths to merged files and sample counts. + + Raises: + DatasetFormatError: If dataset cannot be prepared. + """ + dataset_path = Path(dataset_path).resolve() + + # Determine output directory + if output_dir is None: + if dataset_path.is_file(): + merged_dir = dataset_path.parent / MERGED_DIR + else: + merged_dir = dataset_path / MERGED_DIR + else: + merged_dir = Path(output_dir).resolve() + + train_output = merged_dir / TRAIN_FILE + validation_output = merged_dir / VAL_FILE + + # Discover files + train_files, val_files = discover_dataset_files(dataset_path) + + # Merge training files + if len(train_files) == 1 and not val_files: + # Single file, no validation - need to split + logger.info("Single training file with no validation data - creating split") + train_samples, validation_samples = _create_val_split( + train_files[0], + train_output, + validation_output, + val_ratio=val_split_ratio, + seed=seed, + ) + else: + # Merge training files + train_samples = _merge_files(train_files, train_output) + logger.info(f"Merged {len(train_files)} training file(s) → {train_output} ({train_samples} samples)") + + if val_files: + # Merge validation files + validation_samples = _merge_files(val_files, validation_output) + logger.info( + f"Merged {len(val_files)} validation file(s) → {validation_output} ({validation_samples} samples)" + ) + else: + # Auto-split from merged training file + logger.info("No validation files - creating split from merged training data") + # Read merged, split, re-write + train_samples, validation_samples = _create_val_split( + train_output, + train_output, + validation_output, + val_ratio=val_split_ratio, + seed=seed, + ) + + return PreparedDataset( + merged_dir=merged_dir, + train_file=train_output, + validation_file=validation_output, + train_samples=train_samples, + validation_samples=validation_samples, + ) diff --git a/services/automodel/src/nmp/automodel/tasks/training/datasets/schemas.py b/services/automodel/src/nmp/automodel/tasks/training/datasets/schemas.py new file mode 100644 index 0000000000..487c0151e0 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/datasets/schemas.py @@ -0,0 +1,430 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual +# property and proprietary rights in and to this material, related +# documentation and any modifications thereto. Any use, reproduction, +# disclosure or distribution of this material and related documentation +# without an express license agreement from NVIDIA CORPORATION or +# its affiliates is strictly prohibited. + +# ============================================================================= +# Dataset Schemas for DPO Training +# ============================================================================= +# Preference Dataset Schemas for DPO Training: +# - PreferenceDataset: Native format with context + ranked completions +# - BinaryPreferenceDataset: Simple prompt/chosen/rejected strings +# - HelpSteer3Dataset: NVIDIA HelpSteer3 format with preference scores +# - Tulu3PreferenceDataset: AllenAI Tulu3 format with message lists +# +# SFT Dataset Schemas: +# - SFTDatasetItemSchema: Standard prompt/completion format +from typing import Annotated, Any, List, Literal, Optional, Union + +from pydantic import BaseModel, ConfigDict, Discriminator, Field, Tag, model_validator + +# Dataset class names from nmp.automodel.tasks.training.backends.nemo_rl.preference_datasets +# These constants ensure consistency between the discriminator and Tag values +PREFERENCE_DATASET = "PreferenceDataset" +BINARY_PREFERENCE_DATASET = "BinaryPreferenceDataset" +HELPSTEER3_DATASET = "HelpSteer3" +TULU3_PREFERENCE_DATASET = "Tulu3Preference" + + +class ChatMessage(BaseModel): + """A single message in a conversation.""" + + role: str = Field(..., description="The role of the message sender (e.g., 'user', 'assistant', 'system')") + content: str = Field(..., description="The content of the message") + + +class CompletionItem(BaseModel): + """A ranked completion in a preference dataset.""" + + rank: int = Field(..., description="Rank of this completion (0 = best/chosen, higher = worse)") + completion: List[ChatMessage] = Field(..., description="The completion as a list of messages") + + +class PreferenceDatasetItemSchema(BaseModel): + """Schema for native PreferenceDataset format. + + This is the canonical format used by nemo-rl's PreferenceDataset class. + It supports multi-turn context and multiple ranked completions. + + Example: + { + "context": [{"role": "user", "content": "What is 2+2?"}], + "completions": [ + {"rank": 0, "completion": [{"role": "assistant", "content": "4"}]}, + {"rank": 1, "completion": [{"role": "assistant", "content": "5"}]} + ] + } + """ + + context: List[ChatMessage] = Field( + ..., description="The conversation context (prompt messages including previous turns)" + ) + completions: List[CompletionItem] = Field( + ..., description="List of ranked completions (rank 0 = preferred, rank 1 = rejected, etc.)" + ) + + model_config = ConfigDict(extra="allow") + + +class BinaryPreferenceDatasetItemSchema(BaseModel): + """Schema for BinaryPreferenceDataset format. + + Simple format with prompt, chosen response, and rejected response as strings. + The prompt can be either a string or a list of messages. + + Example: + { + "prompt": "What is the capital of France?", + "chosen": "The capital of France is Paris.", + "rejected": "The capital of France is London." + } + """ + + prompt: Union[str, List[ChatMessage]] = Field(..., description="The input prompt (string or list of messages)") + chosen: str = Field(..., description="The preferred/chosen response") + rejected: str = Field(..., description="The rejected/non-preferred response") + + model_config = ConfigDict(extra="allow") + + +class HelpSteer3DatasetItemSchema(BaseModel): + """Schema for NVIDIA HelpSteer3 preference dataset format. + + Uses numeric preference scores to indicate which response is preferred. + - Negative overall_preference: response1 is preferred + - Positive overall_preference: response2 is preferred + - Zero overall_preference: tie (no preference) + + Example: + { + "context": "Explain quantum computing", + "response1": "Quantum computing uses qubits...", + "response2": "Quantum computing is magic...", + "overall_preference": -2 + } + """ + + context: Union[str, List[ChatMessage]] = Field(..., description="The input context (string or list of messages)") + response1: str = Field(..., description="First response option") + response2: str = Field(..., description="Second response option") + overall_preference: int = Field( + ..., + description="Preference score: negative=response1 preferred, positive=response2 preferred, 0=tie", + ) + + model_config = ConfigDict(extra="allow") + + +class Tulu3PreferenceDatasetItemSchema(BaseModel): + """Schema for AllenAI Tulu3 preference dataset format. + + Contains full conversation histories for both chosen and rejected responses. + The last message in each list must be from the assistant role. + + Example: + { + "chosen": [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi! How can I help?"} + ], + "rejected": [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Go away."} + ] + } + """ + + chosen: List[ChatMessage] = Field( + ..., description="Full conversation with preferred response (last message must be assistant)" + ) + rejected: List[ChatMessage] = Field( + ..., description="Full conversation with rejected response (last message must be assistant)" + ) + + model_config = ConfigDict(extra="allow") + + +def get_preference_dataset_discriminator(v: Any) -> str: + """Determine the preference dataset schema type based on field presence. + + This discriminator function examines the fields present in the data + to determine which schema type it matches. Returns the NeMo RL dataset + class name that corresponds to the detected format: + - PreferenceDataset: Has 'context' and 'completions' fields (native format) + - HelpSteer3: Has 'overall_preference' field (HelpSteer3 format) + - Tulu3PreferenceDataset: Has 'chosen' and 'rejected' as lists of messages + - BinaryPreferenceDataset: Has 'prompt', 'chosen', 'rejected' + + Args: + v: The data to discriminate (dict or model instance) + + Returns: + NeMo RL dataset class name identifying the schema type + """ + if isinstance(v, dict): + # Native PreferenceDataset format: context + completions + if "completions" in v and "context" in v: + return PREFERENCE_DATASET + + # HelpSteer3 format: has overall_preference score + if "overall_preference" in v: + return HELPSTEER3_DATASET + + # Tulu3 format: chosen/rejected are lists of messages (must check BEFORE BinaryPreferenceDataset) + # Tulu3 data may also have 'prompt' field, so we differentiate by checking if chosen/rejected are lists + if "chosen" in v and "rejected" in v: + chosen = v.get("chosen") + if isinstance(chosen, list) and len(chosen) > 0: + # Check if it looks like a message list + if isinstance(chosen[0], dict) and "role" in chosen[0]: + return TULU3_PREFERENCE_DATASET + + # BinaryPreferenceDataset format: prompt + chosen + rejected (as strings) + if "prompt" in v and "chosen" in v and "rejected" in v: + return BINARY_PREFERENCE_DATASET + + return PREFERENCE_DATASET # Default fallback + + +# Union type for all preference dataset formats +DPOPreferenceDatasetSchemaType = Annotated[ + Union[ + Annotated[PreferenceDatasetItemSchema, Tag(PREFERENCE_DATASET)], + Annotated[BinaryPreferenceDatasetItemSchema, Tag(BINARY_PREFERENCE_DATASET)], + Annotated[HelpSteer3DatasetItemSchema, Tag(HELPSTEER3_DATASET)], + Annotated[Tulu3PreferenceDatasetItemSchema, Tag(TULU3_PREFERENCE_DATASET)], + ], + Discriminator(get_preference_dataset_discriminator), +] + + +# ============================================================================= +# SFT Dataset Schemas +# ============================================================================= +class SFTPromptTemplateDatasetItemSchema(BaseModel): + """Schema for standard SFT (Supervised Fine-Tuning) dataset format. + + The standard format has prompt and completion fields, but allows additional + fields for custom templates (e.g., {input}, {output}, {instruction}, etc.). + + Example (standard format): + { + "prompt": "What is the capital of France?", + "completion": "The capital of France is Paris." + } + + Example (custom template format): + { + "instruction": "Answer the question", + "input": "What is the capital of France?", + "output": "The capital of France is Paris." + } + """ + + model_config = ConfigDict(extra="allow") + + # Make all fields optional so custom templates can use any field names + prompt: Optional[str] = Field(None, description="The input prompt (standard format)") + completion: Optional[str] = Field(None, description="The expected completion/output (standard format)") + + +class FunctionCallDetails(BaseModel): + """Details of a function call made by a tool call. + + Example: + { + "name": "get_weather", + "arguments": {"location": "San Francisco"} + } + """ + + name: str = Field(..., description="The name of the function to call") + arguments: dict[str, Any] = Field(..., description="The arguments to pass to the function") + content_type: Optional[str] = Field(None, description="Optional content type of the function response") + + +class ToolCall(BaseModel): + """A tool call in a message.""" + + type: Literal["function"] = Field(..., description="The type of tool call (must be 'function')") + function: FunctionCallDetails = Field(..., description="Function call details including name and arguments") + + +class SFTChatMessage(BaseModel): + """A single message in an SFT chat conversation. + + Each message must have a role and at least one of: content, thinking, or tool_calls. + + Important: content and thinking are mutually exclusive within a single message. + If both are needed, they should be in separate messages (e.g., one message with + thinking followed by another message with content). + """ + + role: str = Field(..., description="The role of the message sender (e.g., 'user', 'assistant', 'system')") + content: str | None = Field(None, description="The content of the message") + thinking: str | None = Field(None, description="Thinking/reasoning content") + tool_calls: list[ToolCall] | None = Field(None, description="Tool calls made in this message") + + @staticmethod + def _schema_extra(schema: dict[str, Any]) -> None: + """Add anyOf constraint requiring at least one of content, thinking, or tool_calls.""" + schema["anyOf"] = [ + { + "required": ["content"], + "properties": {"content": {"type": "string"}}, + "not": {"required": ["thinking"]}, + }, + { + "required": ["thinking"], + "properties": {"thinking": {"type": "string"}}, + "not": {"required": ["content"]}, + }, + {"required": ["tool_calls"], "properties": {"tool_calls": {"minItems": 1}}}, + ] + + model_config = ConfigDict(extra="forbid", json_schema_extra=_schema_extra) + + @model_validator(mode="after") + def check_has_content_or_thinking_or_tool_calls(self) -> "SFTChatMessage": + """Validate that message has at least one of content, thinking, or tool_calls. + + Also enforces that content and thinking are mutually exclusive - they cannot + both be present in the same message. + """ + if self.content is None and self.thinking is None and self.tool_calls is None: + raise ValueError("Message must have at least one of: content, thinking, or tool_calls") + + if self.content is not None and self.thinking is not None: + raise ValueError("Message cannot have both content and thinking - they are mutually exclusive") + + return self + + +class FunctionParameters(BaseModel): + """Parameters schema for a function definition. + + Example: + { + "type": "object", + "properties": { + "location": {"type": "string", "description": "The city name"} + } + } + """ + + type: Literal["object"] = Field(..., description="The type of parameters (must be 'object')") + properties: dict[str, Any] = Field(..., description="The properties/arguments the function accepts") + + +class FunctionDefinitionDetails(BaseModel): + """Details of a function definition for tool calling. + + Example: + { + "name": "get_weather", + "description": "Get the current weather for a location", + "parameters": {"type": "object", "properties": {...}}, + "required": ["location"] + } + """ + + name: str = Field(..., description="The name of the function") + description: str = Field(..., description="A description of what the function does") + parameters: FunctionParameters = Field(..., description="The parameters schema for the function") + required: list[str] | None = Field(None, description="List of required parameter names") + + +class ToolDefinition(BaseModel): + """A tool definition for function calling.""" + + type: Literal["function"] = Field(..., description="The type of tool (must be 'function')") + function: FunctionDefinitionDetails = Field( + ..., description="Function definition with name, description, and parameters" + ) + + +class SFTPChatDatasetItemSchema(BaseModel): + """Schema for SFT chat format based on MESSAGES_SCHEMA. + + This format represents conversations with message lists and optional tool definitions. + + Example: + { + "messages": [ + {"role": "user", "content": "What is 2+2?"}, + {"role": "assistant", "content": "4"} + ], + "tools": [...] # optional + } + """ + + messages: list[SFTChatMessage] = Field(..., description="List of messages in the conversation") + tools: list[ToolDefinition] | None = Field( + None, description="Optional tool definitions available in the conversation" + ) + + model_config = ConfigDict(extra="allow") + + +# Embedding Dataset Schemas +class EmbeddingDatasetItemSchema(BaseModel): + """Schema for embedding dataset format. + + Example: + { + "query": "What is machine learning?", + "pos_doc": "Machine learning is a branch of AI...", + "neg_doc": ["Deep learning is...", "Neural networks are..."] + } + """ + + query: str = Field(..., description="The query text") + pos_doc: str = Field(..., description="The positive document") + neg_doc: list[str] = Field(..., description="List of negative documents") + + model_config = ConfigDict(extra="allow") + + +def get_sft_dataset_discriminator(v: Any) -> str: + """Determine the SFT dataset schema type based on field presence. + + This discriminator examines the fields to determine format: + - "EmbeddingDatasetItemSchema": Has 'query', 'pos_doc', 'neg_doc' fields (embedding format) + - "SFTChatDatasetItemSchema": Has 'messages' field (chat format) + - "SFTPromptTemplateDatasetItemSchema": Has other fields (prompt template format) + + Args: + v: The data to discriminate (dict or model instance) + + Returns: + Schema type name identifying the format + """ + if isinstance(v, dict): + # Embedding format: has query, pos_doc, neg_doc fields + if "query" in v and "pos_doc" in v and "neg_doc" in v: + return "EmbeddingDatasetItemSchema" + + # Chat format: has messages array + if "messages" in v: + return "SFTChatDatasetItemSchema" + + # Prompt template format: has prompt/completion or custom fields + return "SFTPromptTemplateDatasetItemSchema" + + return "SFTPromptTemplateDatasetItemSchema" # Default fallback + + +# Union type for all SFT dataset formats +SFTDatasetSchemaType = Annotated[ + Union[ + Annotated[SFTPromptTemplateDatasetItemSchema, Tag(str(SFTPromptTemplateDatasetItemSchema.__name__))], + Annotated[SFTPChatDatasetItemSchema, Tag(str(SFTPChatDatasetItemSchema.__name__))], + Annotated[EmbeddingDatasetItemSchema, Tag(str(EmbeddingDatasetItemSchema.__name__))], + ], + Discriminator(get_sft_dataset_discriminator), +] diff --git a/services/automodel/src/nmp/automodel/tasks/training/datasets/validation.py b/services/automodel/src/nmp/automodel/tasks/training/datasets/validation.py new file mode 100644 index 0000000000..3e4e7f2000 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/datasets/validation.py @@ -0,0 +1,297 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual +# property and proprietary rights in and to this material, related +# documentation and any modifications thereto. Any use, reproduction, +# disclosure or distribution of this material and related documentation +# without an express license agreement from NVIDIA CORPORATION or +# its affiliates is strictly prohibited. + +import json +import logging +import os +import re +from typing import Any, Callable, Optional + +import jsonschema +from jsonschema import exceptions +from nmp.automodel.entities.values import FinetuningType, TrainingType +from nmp.automodel.tasks.training.datasets.preparation import DatasetFormatError +from nmp.automodel.tasks.training.datasets.schemas import SFTDatasetSchemaType + +logger = logging.getLogger(__name__) + + +def SFT_SCHEMA(prompt_template: str | None = None): + """Generate JSON schema for SFT datasets. + + Uses the SFTDatasetSchemaType union which supports: + - SFTPromptTemplateDatasetItemSchema: Flexible prompt template format + - SFTPChatDatasetItemSchema: Chat format with messages and tools + + Args: + prompt_template: Optional template string with placeholders like "{input} {output}". + If None or empty string, defaults to standard prompt/completion format. + Ignored for chat format detection. + + Returns: + JSON schema dict with required fields based on the format. + """ + from pydantic import TypeAdapter + + # Determine required fields for prompt template format + if prompt_template is not None and prompt_template != "": + # Extract placeholders from template + found_keys = re.findall(r"{(.*?)}", prompt_template) + + # TODO: Are we constrained by len == 2? + # Check for duplicates + if len(found_keys) != len(set(found_keys)): + duplicates = [key for key in found_keys if found_keys.count(key) > 1] + unique_duplicates = list(dict.fromkeys(duplicates)) + raise ValueError( + f"Prompt template contains duplicate placeholders: {unique_duplicates}. " + f"Each placeholder should appear only once." + ) + + prompt_template_keys = found_keys + else: + prompt_template_keys = ["prompt", "completion"] + + # Create TypeAdapter for the SFT union type to generate base JSON schema + adapter = TypeAdapter(SFTDatasetSchemaType) + schema = adapter.json_schema() + + # Add JSON schema metadata + schema["$schema"] = "https://json-schema.org/draft/2020-12/schema" + schema["title"] = "SFT Schema" + + # Update the prompt template sub-schema with required fields from prompt_template_keys + # The schema structure has $defs with the actual schemas, and oneOf/anyOf with $ref pointers + if "$defs" in schema: + # Update the SFTPromptTemplateDatasetItemSchema in $defs + if "SFTPromptTemplateDatasetItemSchema" in schema["$defs"]: + template_schema = schema["$defs"]["SFTPromptTemplateDatasetItemSchema"] + # Add template fields as required properties + if "properties" not in template_schema: + template_schema["properties"] = {} + for key in prompt_template_keys: + template_schema["properties"][key] = {"type": "string"} + template_schema["required"] = prompt_template_keys + template_schema["additionalProperties"] = True + return schema + + +SCHEMAS: dict[str, Callable[[str | None], dict]] = { + TrainingType.SFT.value: SFT_SCHEMA, + TrainingType.DISTILLATION.value: SFT_SCHEMA, +} + + +class DatasetValidator: + """Validator for training datasets. + + This class encapsulates dataset validation logic and avoids parameter drilling + by storing configuration as instance attributes. + + Example usage after prepare_dataset(): + ```python + from nmp.automodel.tasks.training.datasets.preparation import prepare_dataset + from nmp.automodel.tasks.training.datasets.validation import DatasetValidator + + # Prepare datasets + prepared = prepare_dataset( + dataset_path=Path(customizer_config.dataset.path), + output_dir=workspace_dir / "dataset", + ) + + # Validate the prepared datasets + validator = DatasetValidator( + training_type=customizer_config.training.training_type, + finetuning_type=customizer_config.training.finetuning_type, + prompt_template=customizer_config.dataset.prompt_template, + ) + validator.validate_dataset(str(prepared.train_file)) + validator.validate_dataset(str(prepared.validation_file)) + ``` + """ + + def __init__( + self, + training_type: TrainingType, + finetuning_type: Optional[FinetuningType] = None, + prompt_template: str | None = None, + ): + """Initialize validator with training configuration. + + Args: + training_type: The type of training (SFT, distillation, etc.) + finetuning_type: Optional finetuning type (LoRA, all_weights, etc.) + prompt_template: Optional prompt template for SFT datasets + """ + self.training_type = training_type + self.finetuning_type = finetuning_type + self.prompt_template = prompt_template + + def _validate_json_object(self, obj: dict, schema: dict[str, Any]) -> None: + """Validate a JSON object against a schema. + + Args: + obj: The JSON object to validate + schema: The JSON schema to validate against + + Raises: + TypeError: If validation fails + """ + try: + jsonschema.validate(instance=obj, schema=schema) + except exceptions.ValidationError as e: + logger.debug(f"Dataset Schema Validation failed: {str(e)}") + raise TypeError(f"Dataset Schema Validation failed: {e.message}") + except Exception as e: + logger.debug(f"Dataset Schema Validation failed: {str(e)}") + raise TypeError(f"Dataset Schema Validation failed: {e}") + + def detect_dataset_schema(self, file_path: str) -> str: + """Detect the dataset schema from the first line of the file. + + Args: + file_path: Path to the dataset file + + Returns: + Schema name (e.g., 'sft', 'dpo', 'chat') + + Raises: + DatasetFormatError: If file format is invalid or doesn't match any schema + """ + with open(file_path, "r", encoding="utf-8") as f: + line = f.readline() + + try: + obj: dict[str, Any] = json.loads(line) + except Exception as e: + logger.debug(f"{file_path} has entry which is not valid json. Error: {e}\n{line}") + raise DatasetFormatError(f"{file_path} has entry which is not a valid json: {e}") + + for schema_name, schema_factory in SCHEMAS.items(): + try: + validation_schema = schema_factory(self.prompt_template) + self._validate_json_object(obj, validation_schema) + except Exception as e: + logger.debug(f"Parsed jsonl line does not conform to schema {schema_name}. Error: {e}") + else: + logger.debug(f"Parsed jsonl line conforms to schema {schema_name}.") + return schema_name + + raise DatasetFormatError("Dataset does not match any supported format") + + def validate_dataset(self, file_path: str, dataset_type: Optional[str] = None) -> None: + """Validate a single dataset file. + + Args: + file_path: Path to the dataset file + dataset_type: Optional dataset type to validate against. If None, uses training type from config + + Raises: + DatasetFormatError: If dataset is empty or validation fails + """ + # Use provided dataset_type or fall back to training type from config + if dataset_type is None: + dataset_type = self.training_type.value + + schema_factory = SCHEMAS.get(dataset_type) + if not schema_factory: + # Skip validation for unsupported types + return + + if os.path.getsize(file_path) == 0: + raise DatasetFormatError(f"{file_path} is empty") + + validation_schema = schema_factory(self.prompt_template) + + # Validate each line in the JSONL file + with open(file_path, "r", encoding="utf-8") as jsonl_file: + for line in jsonl_file: + line = line.strip() + if not line: + continue + + try: + obj: dict[str, Any] = json.loads(line) + except Exception as e: + logger.debug(f"{file_path} has entry which is not valid json. Error: {e}\n{line}") + raise DatasetFormatError(f"{file_path} has entry which is not valid json: {e}") + + try: + self._validate_json_object(obj, validation_schema) + except Exception as e: + logger.debug( + f"Parsed jsonl line does not conform to schema {validation_schema}. Error: {e}. Object: {obj}" + ) + raise DatasetFormatError( + f"Parsed jsonl line does not conform to schema {validation_schema}. Error: {e}" + ) + + +# Backward compatibility: provide standalone functions that create a validator instance +def detect_dataset_schema( + file_path: str, + training_type: TrainingType, + finetuning_type: Optional[FinetuningType] = None, + prompt_template: str | None = None, +) -> str: + """Detect the dataset schema from the first line of the file. + + Args: + file_path: Path to the dataset file + training_type: The type of training (SFT, DPO, etc.) + finetuning_type: Optional finetuning type (LoRA, all_weights, etc.) + prompt_template: Optional prompt template for SFT datasets + + Returns: + Schema name (e.g., 'sft', 'dpo', 'chat') + """ + validator = DatasetValidator(training_type, finetuning_type, prompt_template=prompt_template) + return validator.detect_dataset_schema(file_path) + + +def validate_dataset( + file_path: str, + training_type: TrainingType, + dataset_type: Optional[str] = None, + finetuning_type: Optional[FinetuningType] = None, + prompt_template: str | None = None, +) -> None: + """Validate a single dataset file. + + Args: + file_path: Path to the dataset file + dataset_type: Dataset type to validate against + training_type: The type of training (SFT, DPO, etc.) + finetuning_type: Optional finetuning type (LoRA, all_weights, etc.) + prompt_template: Optional prompt template for SFT datasets + """ + validator = DatasetValidator(training_type, finetuning_type, prompt_template=prompt_template) + validator.validate_dataset(file_path, dataset_type) + + +def validate_datasets( + file_names: list[str], + training_type: TrainingType, + dataset_type: Optional[str] = None, + finetuning_type: Optional[FinetuningType] = None, + prompt_template: str | None = None, +) -> None: + """Validate a list of dataset files. + + Args: + file_names: List of dataset file paths to validate + dataset_type: Dataset type to validate against (sft, dpo, embedding) + training_type: The type of training (SFT, DPO, etc.) + finetuning_type: Optional finetuning type (LoRA, all_weights, etc.) + prompt_template: Optional prompt template for SFT datasets (ignored for other dataset types) + """ + validator = DatasetValidator(training_type, finetuning_type, prompt_template=prompt_template) + for file_name in file_names: + validator.validate_dataset(file_name, dataset_type) diff --git a/services/automodel/src/nmp/automodel/tasks/training/distributed.py b/services/automodel/src/nmp/automodel/tasks/training/distributed.py new file mode 100644 index 0000000000..ebf0e06df2 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/distributed.py @@ -0,0 +1,245 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Distributed training coordination utilities. + +Provides role detection and file-based barrier synchronization for multi-node +training where multiple pods/containers run the same entry point. +""" + +import logging +import os +import shutil +import time +from dataclasses import dataclass, field +from enum import Enum +from pathlib import Path + +logger = logging.getLogger(__name__) + +# Environment variables for distributed training injected by Volcano's pytorch plugin. +# Do not confuse these with the same env vars injected by torchrun. +# Here, WORLD_SIZE refers to number of nodes, while torchrun's WORLD_SIZE is the number of GPUs. +# RANK refers to the rank of the node, while torchrun's RANK is the global rank of the GPU. +RANK_ENVVAR = "RANK" +WORLD_SIZE_ENVVAR = "WORLD_SIZE" + + +class DistributedRole(Enum): + """Role of this node in distributed training.""" + + COORDINATOR = "coordinator" # Rank 0 - runs all phases + WORKER = "worker" # Rank > 0 - only participates in training + + +@dataclass +class DistributedContext: + """ + Distributed training context with file-based barrier synchronization. + + In multi-node training, all pods run the same entry point. This context + provides: + - Role detection (coordinator vs worker) based on RANK + - File-based barriers for cross-pod synchronization + + File barriers work by: + - Coordinator creates marker files to signal phase completion + - Workers poll for marker files before proceeding + - All ranks can sync via mutual signal-and-wait + + Attributes: + role: Whether this node is coordinator (rank 0) or worker + rank: This node's rank in the distributed job + world_size: Total number of nodes participating + barrier_dir: Directory for barrier marker files (on shared storage). + Must be provided by caller for multi-node; None for single-node. + """ + + role: DistributedRole + rank: int + world_size: int + barrier_dir: Path + _barrier_timeout: float = field(default=600.0, repr=False) + _poll_interval: float = field(default=0.5, repr=False) + + @classmethod + def from_env(cls, barrier_dir: Path) -> "DistributedContext": + """ + Create distributed context from environment variables. + + The caller is responsible for constructing the barrier_dir path, + including any task-specific namespacing for pause/resume support. + + Args: + barrier_dir: Directory for barrier files (on shared storage). + Caller should namespace this by task ID for pause/resume support. + + Environment Variables: + RANK: This node's rank (default: 0) + WORLD_SIZE: Total number of nodes (default: 1) + + Returns: + Configured DistributedContext + """ + rank = int(os.environ.get(RANK_ENVVAR, "0")) + world_size = int(os.environ.get(WORLD_SIZE_ENVVAR, "1")) + + role = DistributedRole.COORDINATOR if rank == 0 else DistributedRole.WORKER + + # Setup barrier directory if distributed + if world_size > 1: + # Coordinator cleans up stale barriers from previous task runs + # (e.g., after pause/resume or retry). This must happen before + # workers start waiting, so we do it here at initialization. + if role == DistributedRole.COORDINATOR and barrier_dir.exists(): + logger.info(f"Cleaning up stale barriers from previous run: {barrier_dir}") + shutil.rmtree(barrier_dir, ignore_errors=True) + + barrier_dir.mkdir(parents=True, exist_ok=True) + + ctx = cls( + role=role, + rank=rank, + world_size=world_size, + barrier_dir=barrier_dir, + ) + + logger.info( + f"Distributed context: rank={rank}, world_size={world_size}, " + f"role={role.value}, barriers={'enabled' if ctx.is_distributed else 'disabled'}" + ) + + return ctx + + @property + def is_coordinator(self) -> bool: + """True if this is the coordinator node (rank 0).""" + return self.role == DistributedRole.COORDINATOR + + @property + def is_distributed(self) -> bool: + """True if running in multi-node mode.""" + return self.world_size > 1 + + # --- Barrier Implementation --- + + def _marker_path(self, barrier_name: str, rank: int) -> Path: + """Get path to barrier marker file for a specific rank.""" + return self.barrier_dir / f"{barrier_name}.rank{rank}.ready" + + def signal(self, barrier_name: str) -> None: + """ + Signal that this rank has reached a synchronization point. + + Creates a marker file indicating this rank is ready. + + Args: + barrier_name: Name of the barrier (should be unique per sync point) + """ + if not self.is_distributed: + return + + marker = self._marker_path(barrier_name, self.rank) + marker.touch() + logger.debug(f"Barrier signal: {barrier_name} (rank {self.rank})") + + def wait_for_coordinator(self, barrier_name: str, timeout: float | None = None) -> None: + """ + Wait for the coordinator (rank 0) to signal. + + Used by workers to wait for coordinator to complete a phase. + + Args: + barrier_name: Name of the barrier to wait for + timeout: Override default timeout (seconds) + + Raises: + TimeoutError: If coordinator doesn't signal within timeout + """ + if not self.is_distributed: + return + + if self.is_coordinator: + # Coordinator doesn't wait for itself + return + + timeout = timeout or self._barrier_timeout + marker = self._marker_path(barrier_name, rank=0) + start = time.time() + + logger.debug(f"Waiting for coordinator at barrier: {barrier_name}") + + while time.time() - start < timeout: + if marker.exists(): + logger.debug(f"Coordinator signaled barrier: {barrier_name}") + return + time.sleep(self._poll_interval) + + raise TimeoutError(f"Timeout waiting for coordinator at barrier '{barrier_name}' after {timeout}s") + + def wait_all(self, barrier_name: str, timeout: float | None = None) -> None: + """ + Wait for all ranks to reach this barrier. + + All ranks must call signal() before any rank proceeds. + + Args: + barrier_name: Name of the barrier + timeout: Override default timeout (seconds) + + Raises: + TimeoutError: If not all ranks signal within timeout + """ + if not self.is_distributed: + return + + timeout = timeout or self._barrier_timeout + start = time.time() + + logger.debug(f"Waiting for all ranks at barrier: {barrier_name}") + + while time.time() - start < timeout: + ready_count = sum(1 for r in range(self.world_size) if self._marker_path(barrier_name, r).exists()) + if ready_count >= self.world_size: + logger.debug(f"All ranks reached barrier: {barrier_name}") + return + time.sleep(self._poll_interval) + + # Report which ranks are missing for debugging + missing = [r for r in range(self.world_size) if not self._marker_path(barrier_name, r).exists()] + raise TimeoutError(f"Timeout at barrier '{barrier_name}' after {timeout}s. Missing ranks: {missing}") + + def sync_point(self, barrier_name: str, timeout: float | None = None) -> None: + """ + Synchronization point where all ranks must arrive before any proceed. + + Combines signal() and wait_all() - this rank signals and then waits + for all other ranks. + + Args: + barrier_name: Name of the sync point + timeout: Override default timeout (seconds) + """ + self.signal(barrier_name) + self.wait_all(barrier_name, timeout) + + def cleanup_barrier(self, barrier_name: str) -> None: + """ + Clean up barrier marker files (coordinator only). + + Call after all ranks have passed the barrier. + + Args: + barrier_name: Name of the barrier to clean up + """ + if not self.is_distributed or not self.is_coordinator: + return + + for r in range(self.world_size): + marker = self._marker_path(barrier_name, r) + try: + if marker.exists(): + marker.unlink() + except OSError as e: + logger.warning(f"Failed to clean up barrier marker {marker}: {e}") diff --git a/services/automodel/src/nmp/automodel/tasks/training/errors/converter.py b/services/automodel/src/nmp/automodel/tasks/training/errors/converter.py new file mode 100644 index 0000000000..d06e632e4f --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/errors/converter.py @@ -0,0 +1,119 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import logging +import subprocess +from pathlib import Path + +from nmp.common.errors import ExceptionConverter, RulesLoader + +from .exceptions import ( + EXCEPTION_REGISTRY, + CustomizerTrainingError, + ErrorDetails, + InternalError, + default_exception_handler, +) + +logger = logging.getLogger(__name__) + +# Path to the error rules YAML file (relative to this module) +_ERROR_RULES_PATH = Path(__file__).parent / "error_rules.yaml" + +# Additional modules to search for exception types not in the registry +# subprocess.TimeoutExpired is used for training timeout detection +_FALLBACK_MODULES = [subprocess] + +# Module-level singleton converter +_converter: ExceptionConverter | None = None + + +def _load_converter() -> ExceptionConverter: + """Load the converter from YAML rules.""" + logger.debug(f"Loading Customizer error rules from: {_ERROR_RULES_PATH}") + + converter = RulesLoader.from_yaml( + _ERROR_RULES_PATH, + exception_registry=EXCEPTION_REGISTRY, + default_handler=default_exception_handler, + fallback_exception=InternalError, + fallback_modules=_FALLBACK_MODULES, + ) + + logger.info(f"Loaded {converter.rule_count} Customizer error mapping rules") + return converter + + +def get_error_converter() -> ExceptionConverter: + """ + Get the singleton ExceptionConverter for Customizer training errors. + + The converter is created once on first access and reused for the module's lifetime. + It loads rules from error_rules.yaml and uses InternalError as fallback. + + Returns: + Configured ExceptionConverter ready to convert exceptions. + + Raises: + FileNotFoundError: If error_rules.yaml is not found. + ValueError: If rules file has invalid syntax. + """ + global _converter + if _converter is None: + _converter = _load_converter() + return _converter + + +def create_error_details(exception: Exception) -> ErrorDetails: + """ + Create error_details dict for Jobs service reporting. + + Converts the exception to a CustomizerTrainingError and returns + a dict suitable for passing to progress_reporter.report_error(). + + If the exception is already a CustomizerTrainingError, returns its + details directly without re-conversion. + + Uses the library's fallback mechanism (InternalError) for unmatched exceptions. + + Args: + exception: The exception to convert. + + Returns: + ErrorDetails with 'message', 'type', and 'detail' keys. + """ + # If already a CustomizerTrainingError, return its details directly + if isinstance(exception, CustomizerTrainingError): + return exception.to_error_details() + + # Convert using the library - fallback_exception=InternalError handles unmatched + converter = get_error_converter() + try: + converter.raise_converted_or_default(exception) + except CustomizerTrainingError as converted: + return converted.to_error_details() + except Exception as e: # noqa: BLE001 - intentional last-resort guard to guarantee dict return + # Unexpected exception type - wrap in InternalError to ensure we always return a dict + logger.warning(f"Unexpected exception type from converter: {type(e).__name__}: {e}") + exc = InternalError( + message=f"An internal error occurred. ({type(exception).__name__}: {exception})", + detail=str(exception), + ) + return exc.to_error_details() + + # Defensive fallback: if converter unexpectedly does not raise, still return valid details + logger.warning( + "Converter returned without raising for exception type %s; using InternalError fallback.", + type(exception).__name__, + ) + exc = InternalError( + message=f"An internal error occurred. ({type(exception).__name__}: {exception})", + detail=str(exception), + ) + return exc.to_error_details() + + +__all__ = [ + "get_error_converter", + "create_error_details", +] diff --git a/services/automodel/src/nmp/automodel/tasks/training/errors/error_rules.yaml b/services/automodel/src/nmp/automodel/tasks/training/errors/error_rules.yaml new file mode 100644 index 0000000000..1a16801b7b --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/errors/error_rules.yaml @@ -0,0 +1,643 @@ +# This file defines rules for converting low-level training exceptions +# into user-friendly CustomizerTrainingError subclasses. +# Rules for all backends are present in the same yaml file. +# +# Rules are evaluated in order; first match wins. +# +# Rule structure: +# - : # When to match (pick ONE) +# exception: # Exception class from EXCEPTION_REGISTRY +# error_details: # Optional user-friendly message + + +rules: + # =========================================================================== + # 1. TRAINING TIMEOUT (subprocess.TimeoutExpired) + # All backends + # =========================================================================== + + - type: TimeoutExpired # subprocess.TimeoutExpired from fallback_modules + exception: TrainingTimeoutError + error_details: "Training exceeded the maximum allowed time limit. To reduce training time: 1) Reduce max_steps or epochs, 2) Use a smaller dataset, 3) Use a smaller model, 4) Use LoRA/PEFT instead of all_weights fine-tuning (LoRA trains faster), or 5) Increase batch_size to process more samples per step (if GPU memory allows). If you need longer training times, contact your administrator to adjust the job timeout limits." + + # =========================================================================== + # 2. DATASET FORMAT ERRORS (400) + # =========================================================================== + + # --- Automodel --- + # Unsupported role in chat messages + - regex: "Unsupported role in messages: \\w+" + exception: DatasetFormatError + error_details: "Your dataset contains chat messages with an invalid role. Each message in a conversation must have a 'role' field with one of the following values: 'system' (for system prompts), 'user' (for user inputs), 'assistant' (for model responses), or 'tool' (for tool/function outputs). Please check your dataset and ensure all messages use valid roles." + + # --- NeMo-RL --- + # Text type error + - regex: "^text must be a string or a list of strings, got .+$" + exception: DatasetFormatError + error_details: "The 'text' field in your dataset has an invalid type. For NeMo-RL training (DPO/GRPO), the text field must be either a single string or a list of strings. Please check your dataset format and ensure the text field contains the correct data type." + + # Prompt file not found + - regex: "^Prompt file .+ not found$" + exception: DatasetFormatError + error_details: "The prompt template file specified in your training dataset configuration does not exist. Prompt templates define how your dataset samples are formatted for training. Please verify the prompt file path is correct and the file is accessible at the specified location." + + # --- Automodel --- + # Empty dataset + - regex: "^no sample to consume: \\d+$" + exception: DatasetFormatError + error_details: "Your dataset is empty or contains zero valid samples after filtering. This can happen if: 1) The dataset file is empty, 2) All samples were filtered out due to format issues, or 3) The dataset path is incorrect. Please verify your dataset contains valid training samples." + + # All samples consumed + - regex: "^no samples left to consume: \\d+, \\d+$" + exception: DatasetFormatError + error_details: "All samples in your dataset have been consumed before completing the requested number of training steps. This happens when your dataset is too small for the configured epochs or max_steps. Please either: 1) Add more samples to your dataset, 2) Reduce the number of epochs, or 3) Reduce max_steps." + + # Error loading example + - regex: "Error while loading example \\d+ from dataset .+" + exception: DatasetFormatError + error_details: "Failed to load a specific sample from your dataset. This typically indicates a malformed sample that doesn't match the expected format. Please check your dataset for: 1) Missing required fields, 2) Invalid JSON formatting, 3) Incorrect data types for fields. The error message includes the sample index to help you locate the problematic entry." + + # =========================================================================== + # 3. MODEL NOT FOUND ERRORS (404) + # Megatron Bridge + # =========================================================================== + + # Checkpoint file not found (input model checkpoint for training) + - regex: "^Checkpoint file not found: .+$" + exception: ModelNotFoundError + error_details: "The input model checkpoint file could not be found. Please verify the base model path is correct and accessible. This checkpoint is used as the starting point for training." + + # No checkpoints found for resume (output checkpoint directory empty) + - regex: "There were no checkpoints found in checkpoint_dir.*Cannot resume" + exception: ModelNotFoundError + error_details: "The output checkpoint directory is empty. Cannot resume training because no previous training checkpoints were found. Ensure a prior training run completed successfully and saved checkpoints." + + # Nemotron model missing HF source + - regex: "Nemotron Super models expect HF source code to exist at .+" + exception: ModelNotFoundError + error_details: "The Nemotron Super model checkpoint is missing the required HuggingFace source code directory (nemotron_src/). This directory must be present inside the model checkpoint. Please ensure you are using a complete Nemotron Super model checkpoint that includes the HuggingFace source files." + + # =========================================================================== + # 4. MODEL LOAD ERRORS (500) + # =========================================================================== + + # --- Automodel --- + # Model weights swap failure + - contains: "_apply(): Couldn't swap" + exception: ModelLoadError + error_details: "Failed to load the base model: weights could not be applied to a model layer. The base model checkpoint may be corrupted, incomplete, or incompatible with the selected training configuration." + + # Model patching failure + - exact: "Failed to patch model" + exception: ModelLoadError + error_details: "Failed to apply optimizations to the base model. The base model architecture may not be supported for the selected training configuration. Try using a different model or training method." + + # Method signature mismatch + - starts_with: "Signature mismatch:" + exception: ModelLoadError + error_details: "The base model has an incompatible method signature. This typically indicates a version mismatch between the base model and the training framework. Please verify you are using a supported model version." + + # Missing lm_head.weight + - exact: "lm_head.weight not found in model" + exception: ModelLoadError + error_details: "The base model is missing the language model head (lm_head.weight). The base model checkpoint may be corrupted, incomplete, or not a valid language model. Please verify the base model is a complete, valid language model checkpoint." + + # --- NeMo-RL --- + # vLLM not installed + - contains: "vLLM is not installed" + exception: ModelLoadError + error_details: "vLLM is not installed in the training environment. This is an issue with the training environment setup, please contact the administrator to raise an issue with the NeMo Platform team." + + # Missing generation output keys + - regex: "^Missing required keys for GenerationOutputSpec: .+$" + exception: ModelLoadError + error_details: "The base model's generation output is missing required fields. The base model may not be compatible with the selected training method (e.g., GRPO). Please verify you are using a supported model for this training type." + + # Missing score output keys + - regex: "^Missing required keys for ScoreOutputSpec: .+$" + exception: ModelLoadError + error_details: "The base model's score output is missing required fields. The base model may not be compatible with the selected training method. Please verify you are using a supported model for this training type." + + # Pretrained run config not found (Megatron HF-to-mcore conversion) + - contains: "Pretrained run config not found at" + exception: ModelLoadError + error_details: "The pretrained model configuration file was not found after Megatron checkpoint conversion. This usually means the HuggingFace-to-Megatron conversion on the head node saved to a directory not accessible by this worker node. This is an infrastructure issue - please ensure shared storage is properly mounted across all nodes, or contact your administrator." + + # --- Megatron Bridge --- + # Shape mismatch for parameter + - regex: "^Shape mismatch for parameter .+: target shape .+ vs source shape .+$" + exception: ModelLoadError + error_details: "The base model parameter shape does not match the checkpoint. The base model checkpoint may be from a different model architecture or an incompatible version. Please ensure the base model matches the expected architecture for this training configuration." + + # Shape mismatch for buffer + - regex: "^Shape mismatch for buffer .+: .+ vs .+$" + exception: ModelLoadError + error_details: "The base model buffer shape does not match the checkpoint. The base model checkpoint may be corrupted, incomplete, or from an incompatible model version. Please verify the base model checkpoint is valid and complete." + + # =========================================================================== + # 5. TRAINING CONFIG ERRORS - PARALLELISM (400) + # =========================================================================== + + # --- Automodel --- + # Pipeline parallelism: tied embeddings not supported + - all_keywords: ["not compatible with pipeline parallelism", "tie_word_embeddings"] + exception: TrainingConfigError + error_details: "The base model has tied embeddings (tie_word_embeddings=True) which is not compatible with pipeline parallelism. Try using a different parallelism configuration or a model without tied embeddings." + + # Pipeline parallelism: encoder-decoder models not supported + - all_keywords: ["not compatible with pipeline parallelism", "Encoder-Decoder"] + exception: TrainingConfigError + error_details: "The base model is an encoder-decoder architecture (like T5 or BART) which is not supported with pipeline parallelism. Please use a decoder-only base model, or disable pipeline parallelism in your training configuration." + + # PP batch size / microbatch validation + - contains: "pp_batch_size // pp_microbatch_size must be >= pp_size" + exception: TrainingConfigError + error_details: "Pipeline parallelism requires: batch_size >= pipeline_parallel_size. The current batch_size is too small to fill all pipeline stages. Either increase batch_size or reduce pipeline_parallel_size." + + # Context parallelism: SDPA not supported + - contains: "Model does not support SDPA required for context parallelism" + exception: TrainingConfigError + error_details: "The base model does not support scaled dot-product attention (SDPA) which is required for context parallelism. Please set context_parallel_size=1 to disable context parallelism." + + # --- NeMo-RL --- + # Megatron and DTensor both enabled + - exact: "Configure either Megatron (policy.megatron_cfg.enabled=true) or DTensor (policy.dtensor_cfg.enabled=true), not both." + exception: TrainingConfigError + error_details: "Internal configuration error: both Megatron and DTensor training backends are enabled, but only one can be active at a time. This is an issue with the training environment setup, please contact the administrator." + + # Neither Megatron nor DTensor enabled + - contains: "Please either set policy.megatron_cfg.enabled=true" + exception: TrainingConfigError + error_details: "Internal configuration error: no training backend is enabled. The training environment requires either Megatron or DTensor backend to be active. This is an issue with the training environment setup, please contact the administrator." + + # World size insufficient for parallelism + - regex: "^World size \\(\\d+\\) is insufficient for the parallelism configuration" + exception: TrainingConfigError + error_details: "Not enough GPUs available for the requested parallelism settings. The total number of GPUs must be at least pipeline_parallel_size * context_parallel_size * tensor_parallel_size. Either reduce parallelism settings or request more GPUs." + + # World size not divisible by parallelism + - regex: "^World size \\(\\d+\\) must be divisible by PP \\* CP \\* TP" + exception: TrainingConfigError + error_details: "The total number of GPUs must be evenly divisible by (pipeline_parallel_size * context_parallel_size * tensor_parallel_size). For example, with PP=2, CP=1, TP=2, you need 4, 8, 12, etc. GPUs. Please adjust your parallelism settings or cluster size." + + # DTensor world size mismatch + - regex: "^World size\\(\\d+\\) must equal to dp_size\\(\\d+\\) \\* tp_size\\(\\d+\\) \\* cp_size\\(\\d+\\) to use DTensor$" + exception: TrainingConfigError + error_details: "The total number of GPUs (world_size) does not match the product of data_parallel_size * tensor_parallel_size * context_parallel_size for the DTensor backend. Please adjust your parallelism settings so they are consistent with the available GPU count." + + # Dynamic batching with PP > 1 + - contains: "Dynamic batching is only supported for single pipeline parallel stage" + exception: TrainingConfigError + error_details: "Dynamic batching is only supported when pipeline_parallel_size=1. With pipeline parallelism (PP > 1), the model is split across GPU stages which requires fixed batch sizes. Please either set pipeline_parallel_size=1 or disable dynamic batching." + + # Dynamic batching exclusive of sequence packing + - contains: "Dynamic Batching is exclusive of Sequence Packing" + exception: TrainingConfigError + error_details: "Dynamic batching and sequence packing cannot be used together. Please disable one of them: either set dynamic_batching=false or set sequence_packing_enabled=false." + + # Sequence packing not supported for VLM models + - contains: "Sequence packing is not supported for VLM models" + exception: TrainingConfigError + error_details: "Sequence packing is not supported for Vision-Language Models (VLMs). Please set sequence_packing_enabled=false when training VLM models." + + # Context parallel not supported for sequence packing (DTensor) + - exact: "Context parallel is not supported for sequence packing. Refer to https://github.com/NVIDIA/NeMo-RL/blob/main/docs/model-quirks.md#context-parallel-with-fsdp2 for more details." + exception: TrainingConfigError + error_details: "Context parallelism cannot be used with sequence packing in the DTensor backend. Please either set context_parallel_size=1 to disable context parallelism, or set sequence_packing_enabled=false to disable sequence packing." + + # Context parallel not supported for Gemma3 + - contains: "Context parallel is not supported for Gemma3ForCausalLM" + exception: TrainingConfigError + error_details: "Context parallelism is not supported for Gemma3 models due to limitations in the PyTorch context parallel implementation. Please set context_parallel_size=1 when training Gemma3 models." + + # Context parallel not supported for VLM models + - contains: "Context parallel is yet not supported for VLM models" + exception: TrainingConfigError + error_details: "Context parallelism is not yet supported for Vision-Language Models (VLMs). Please set context_parallel_size=1 when training VLM models." + + # Context parallelism requires sequence packing (Megatron) + - contains: "Context Parallelism (CP>1) requires sequence packing to be enabled" + exception: TrainingConfigError + error_details: "When using the Megatron backend with context_parallel_size > 1, sequence packing must be enabled. Please either enable sequence packing (sequence_packing_enabled=true) or reduce context_parallel_size to 1." + + # Reward models not supported with Megatron backend + - contains: "Reward models are not yet supported with the Megatron backend" + exception: TrainingConfigError + error_details: "Reward models are not yet supported with the Megatron training backend. This is a current limitation of the framework. Please use the DTensor backend for reward model training, or contact your administrator for alternative configurations." + + # Dynamic sampling max batches reached + - contains: "Dynamic sampling has reached the maximum allowed number of batches" + exception: TrainingConfigError + error_details: "Dynamic sampling exceeded the maximum number of generation batches allowed per training step. This means the training data or reward signal is too challenging for the model to produce enough valid samples. Consider: 1) Simplifying your dataset, 2) Adjusting num_prompts_per_step or num_generations_per_prompt, 3) Checking that your reward function is not too strict." + + # Batch size not divisible by DP + - regex: "Configuration error: \\(num_prompts_per_step \\* num_generations_per_prompt\\) = \\d+ must be divisible by data_parallel size \\d+" + exception: TrainingConfigError + error_details: "The effective batch size (num_prompts_per_step * num_generations_per_prompt) must be evenly divisible by the number of data parallel workers. Please adjust num_prompts_per_step or num_generations_per_prompt so their product divides evenly." + + # =========================================================================== + # 6. TRAINING CONFIG ERRORS - DPO/GRPO (400) + # NeMo-RL + # =========================================================================== + + # Dynamic batching with DPO + - contains: "Dynamic batching is currently not supported with DPO" + exception: TrainingConfigError + error_details: "DPO (Direct Preference Optimization) training does not support dynamic batching. This is an internal configuration issue with the training environment, please contact the administrator." + + # Sequence packing with DPO + - contains: "Sequence packing is currently not supported with DPO" + exception: TrainingConfigError + error_details: "DPO (Direct Preference Optimization) training does not support sequence packing. Please set sequence_packing_enabled=false in your training request." + + # GRPO requires generation config + - contains: "A generation config in the PolicyConfig is required for GRPO" + exception: TrainingConfigError + error_details: "GRPO (Group Relative Policy Optimization) requires a generation configuration to produce responses during training. This is an internal configuration issue with the training environment, please contact the administrator." + + # Validation dataset required + - exact: "Validation dataset is required if validation is enabled" + exception: TrainingConfigError + error_details: "Validation is enabled for this training job, but no validation dataset was provided. Please provide a validation dataset in your training request, or disable validation." + + # Non-colocated inference with Megatron + - contains: "Non-colocated inference is not supported for Megatron generation backends" + exception: TrainingConfigError + error_details: "The current training configuration uses Megatron for generation, which does not support the required inference mode. This is an internal configuration issue with the training environment, please contact the administrator." + + # Async GRPO requires vLLM async + - contains: "Async GRPO requires vLLM backend with vllm_cfg.async_engine=True" + exception: TrainingConfigError + error_details: "Async GRPO training requires the vLLM backend with async engine enabled, but the current configuration does not have this set. This is an internal configuration issue with the training environment, please contact the administrator." + + # Async GRPO requires importance sampling + - contains: "Importance sampling correction must be enabled for async GRPO" + exception: TrainingConfigError + error_details: "Async GRPO training requires importance sampling correction to handle off-policy samples and ensure stable training. This is an internal configuration issue with the training environment, please contact the administrator." + + # Async GRPO doesn't support colocated inference + - contains: "Colocated inference is not supported for async GRPO" + exception: TrainingConfigError + error_details: "Async GRPO training does not support colocated inference (running training and generation on the same GPUs). This is an internal configuration issue with the training environment, please contact the administrator." + + # top_k sampling threshold (vLLM V1 engine limitation) + - contains: "top_k sampling with values <" + exception: TrainingConfigError + error_details: "The top_k value is too low for the vLLM V1 engine. The vLLM V1 engine does not return logprobs after top_k filtering, so very low top_k values produce inaccurate logprob computations. Please increase top_k or remove the top_k constraint." + + # top_p sampling threshold (vLLM V1 engine limitation) + - contains: "top_p sampling with values <" + exception: TrainingConfigError + error_details: "The top_p value is too low for the vLLM V1 engine. The vLLM V1 engine does not return logprobs after top_p filtering, so very low top_p values produce inaccurate logprob computations. Please increase top_p or remove the top_p constraint." + + # MoE aux loss not supported + - contains: "MoE aux loss is currently not supported" + exception: TrainingConfigError + error_details: "Mixture-of-Experts (MoE) auxiliary loss is not currently supported due to a known bug in Megatron-LM. Please disable the MoE auxiliary loss in your training configuration." + + # =========================================================================== + # 7. TRAINING CONFIG ERRORS - PEFT/LORA (400) + # Automodel + # =========================================================================== + + # Triton not installed + - contains: "triton is not installed" + exception: TrainingConfigError + error_details: "The Triton library, which is required for optimized LoRA kernel operations, is not installed in the training environment. This is an issue with the training environment setup, please contact the administrator to ensure Triton is properly installed." + + # LoRA dimensions mismatch + - contains: "Incompatible X and LoRA A dimensions" + exception: TrainingConfigError + error_details: "The LoRA adapter dimensions are incompatible with the base model's layer dimensions. This can happen if you are trying to apply a pre-trained LoRA adapter that was created for a different model architecture. Please ensure the LoRA configuration (lora_dim/rank) is compatible with the base model you are fine-tuning." + + # =========================================================================== + # 8. TRAINING CONFIG ERRORS - PACKING (400) + # NeMo-RL + # =========================================================================== + + # Sequence too long for packing + - regex: "^Sequence length \\d+ exceeds bin capacity \\d+$" + exception: TrainingConfigError + error_details: "When sequence packing is enabled, one or more sequences in your dataset exceed the maximum sequence length (max_seq_length). Sequence packing combines multiple shorter sequences into a single training sample, but each individual sequence must fit within max_seq_length. Please either increase max_seq_length to accommodate longer sequences, or preprocess your dataset to truncate or remove sequences that are too long." + + # Not enough sequences for packing + - regex: "^Cannot create \\d+ bins with only \\d+ sequences" + exception: TrainingConfigError + error_details: "When sequence packing is enabled, the packing algorithm needs enough sequences to efficiently fill the training batches. Your dataset does not have enough sequences for the current batch configuration. Please either add more samples to your dataset, reduce the batch_size, or disable sequence packing by setting sequence_packing_enabled=false." + + # =========================================================================== + # 9. ENVIRONMENT ERRORS (400) + # NeMo-RL + # =========================================================================== + + # Unable to find compatible environment + - regex: "^Unable to find compatible environment - .+$" + exception: TrainingEnvironmentError + error_details: "The specified GRPO environment name is not recognized. GRPO (Group Relative Policy Optimization) requires a valid environment that defines how to evaluate model responses. Please check the environment name in your training request and ensure it matches one of the supported environments for your use case." + + # GRPO environment required + - exact: "hyperparameters.environment is required for GRPO, but it is not set" + exception: TrainingEnvironmentError + error_details: "GRPO (Group Relative Policy Optimization) training requires an environment configuration to evaluate model responses and compute rewards. Please specify the environment in your training request's hyperparameters. The environment determines how the model's generated responses will be scored during reinforcement learning." + + # No environment for task type + - regex: "^No environment found for task type: .+$" + exception: TrainingEnvironmentError + error_details: "No GRPO environment is registered for the specified task type. The environment defines how model responses are evaluated during reinforcement learning. This may indicate an unsupported task type or a misconfiguration. Please verify your task type is supported for GRPO training." + + # =========================================================================== + # 10. CHECKPOINT ERRORS (500) + # =========================================================================== + + # --- Automodel --- + # Checkpoint directory already exists + - regex: "Checkpoint directory .* already exists" + exception: CheckpointError + error_details: "The output checkpoint directory already exists from a previous training run. This typically happens when a previous training job failed or was cancelled but left partial checkpoint files behind. Please use a clean output directory, if you do not have access to remove the existing checkpoint directory, contact your administrator." + + # Global plan validation failure + - exact: "Failed to validate global plan" + exception: CheckpointError + error_details: "Checkpoint validation failed during distributed checkpoint loading. This occurs when the 'global plan' (which coordinates how model weights are distributed across GPUs) cannot be validated. Common causes include: 1) Corrupted checkpoint metadata files, 2) Mismatch between the number of GPUs used when saving vs loading the checkpoint, or 3) Interrupted checkpoint save operation. Please ensure the checkpoint is complete and you are using the same GPU topology as when the checkpoint was saved." + + # Missing key in checkpoint + - starts_with: "Missing key in checkpoint state_dict:" + exception: CheckpointError + error_details: "The checkpoint is missing one or more required model weights. This typically indicates that the checkpoint file is corrupted, incomplete (possibly from an interrupted save), or was created from a different model architecture than the one being loaded. Please verify the checkpoint is complete and matches the expected model architecture." + + # MoE expert weights missing + - contains: "Expert weights missing from checkpoint" + exception: CheckpointError + error_details: "The checkpoint for this Mixture-of-Experts (MoE) model is missing one or more expert weights. MoE models have multiple 'expert' sub-networks, and all expert weights must be present in the checkpoint. This typically indicates the checkpoint is corrupted or was saved incorrectly. Please use a complete, valid MoE checkpoint." + + # --- NeMo-RL --- + # Checkpoint file corrupted (JSONDecodeError) + - type_name: JSONDecodeError + exception: CheckpointError + error_details: "The checkpoint metadata file (training_info.json) is corrupted and cannot be parsed. This file stores training progress information like the current step and loss values. The checkpoint may have been saved incompletely or the file was corrupted during storage." + + # Distributed process group not initialized for checkpoint save + - exact: "Distributed process group is not initialized. Cannot save checkpoint." + exception: CheckpointError + error_details: "Cannot save checkpoint because the distributed process group is not initialized. This typically occurs when the training cluster encountered communication issues before checkpoint saving could complete. This is a transient infrastructure issue - please try running your training job again." + + # Megatron core state not initialized for checkpoint save + - exact: "Megatron core state or model is not initialized. Cannot save checkpoint." + exception: CheckpointError + error_details: "Cannot save checkpoint because the Megatron model state is not initialized. This typically occurs when the model failed to load or initialize correctly before training could produce a checkpoint. Please verify the base model is valid and try again." + + # HF checkpoint already exists + - regex: "^HF checkpoint already exists at .+\\. Delete it to run or set overwrite=True\\.$" + exception: CheckpointError + error_details: "The HuggingFace checkpoint output directory already exists from a previous training run or conversion. This typically happens when a previous training job left partial output behind. Please use a clean output directory, or contact your administrator to remove the existing checkpoint." + + # =========================================================================== + # 11. CUDA/GPU ERRORS (500) + # =========================================================================== + + # --- NeMo-RL --- + # Disk space exhausted - occurs in Ray cluster workers during RL training + # Ray stores session logs in /tmp/ray/session_*/logs/ which can fill up ephemeral node storage + - contains: "No space left on device" + exception: DistributedError + error_details: "Disk space exhausted on the node's ephemeral storage (/tmp). During reinforcement learning training (DPO/GRPO), Ray stores session logs and temporary files in /tmp/ray/ which can fill up the node's local disk. This is separate from the PVC used for checkpoints and datasets. This is typically a transient infrastructure issue - please try running your training job again, or contact your administrator to ensure adequate ephemeral storage is configured for the cluster nodes." + + # CUDA out of memory - catch by type name + - type_name: OutOfMemoryError + exception: CudaError + error_details: "GPU out of memory. To reduce memory usage: 1) Lower batch_size, 2) Reduce max_seq_length, 3) Use LoRA/PEFT instead of all_weights fine-tuning, or 4) Use a model with fewer parameters." + + # CUDA OOM - catch by message pattern + - contains: "CUDA out of memory" + exception: CudaError + error_details: "GPU out of memory. To reduce memory usage: 1) Lower batch_size, 2) Reduce max_seq_length, 3) Use LoRA/PEFT instead of all_weights fine-tuning, or 4) Use a model with fewer parameters." + + # OOM keyword + - contains: "out of memory" + exception: CudaError + error_details: "GPU out of memory. To reduce memory usage: 1) Lower batch_size, 2) Reduce max_seq_length, 3) Use LoRA/PEFT instead of all_weights fine-tuning, or 4) Use a model with fewer parameters." + + # General CUDA errors + - and: + - any_keywords: ["CUDA", "cuda"] + - any_keywords: ["error", "Error", "failed", "Failed"] + exception: CudaError + error_details: "A GPU/CUDA error occurred. Please check GPU availability, ensure the GPU is not being used by another process, and try again." + + # =========================================================================== + # 12. DISTRIBUTED ERRORS (500) + # =========================================================================== + + # --- Automodel --- + # torch.distributed not available + - exact: "torch.distributed not available" + exception: DistributedError + error_details: "The PyTorch distributed package is not available in the training environment. Distributed training requires PyTorch to be built with distributed support enabled. This is an issue with the training environment setup, please contact the administrator to ensure the correct PyTorch version is installed." + + # torch.distributed not initialized + - exact: "expected torch.distributed to be initialized" + exception: DistributedError + error_details: "PyTorch distributed training was not properly initialized before the training process started. This typically happens when the training script is not launched correctly with the distributed launcher (torchrun). This is an issue with the training environment setup, please contact the administrator." + + # Distributed timeout - check for TimeoutError in cause chain + - cause: + type_name: TimeoutError + recursive: true + exception: DistributedError + error_details: "A distributed training operation timed out while waiting for communication between GPUs or nodes. This can happen when: 1) One or more GPU workers crashed or became unresponsive, 2) Network connectivity issues between nodes, 3) Uneven workload causing some GPUs to wait too long for others. This may be a transient issue - please try running your training job again. If the problem persists, contact your administrator." + + # NCCL errors + - any_keywords: ["NCCL", "nccl"] + exception: DistributedError + error_details: "An NCCL (NVIDIA Collective Communications Library) error occurred during GPU-to-GPU communication. NCCL is used to synchronize data between GPUs during distributed training. Common causes include: 1) Network connectivity issues between GPU nodes, 2) GPU hardware problems, 3) Incompatible NCCL versions, or 4) Memory pressure on GPUs. This may be a transient issue - please try running your training job again. If the problem persists, contact your administrator." + + # c10d errors + - contains: "c10d" + exception: DistributedError + error_details: "A PyTorch distributed communication error occurred (c10d is PyTorch's distributed communication backend). This indicates a failure in the inter-process or inter-node communication during distributed training. This may be caused by network issues, process crashes, or resource exhaustion. Please try running your training job again. If the problem persists, contact your administrator." + + # --- NeMo-RL --- + # Not enough GPUs + - and: + - type_name: ResourceInsufficientError + - contains: "Not enough GPUs available" + exception: DistributedError + error_details: "The training cluster does not have enough GPUs available for your requested configuration. Your training job requires more GPUs than are currently available in the cluster. Try reducing the parallelism settings (tensor_parallel_size, pipeline_parallel_size) to require fewer GPUs." + + # Not enough CPUs + - and: + - type_name: ResourceInsufficientError + - contains: "Not enough CPUs available" + exception: DistributedError + error_details: "The training cluster does not have enough CPUs available for your requested configuration. CPUs are needed for data loading and preprocessing alongside GPU training." + + # Maximum retries reached + - and: + - type_name: ResourceInsufficientError + - contains: "Maximum number of retries reached" + exception: DistributedError + error_details: "Failed to allocate cluster resources after multiple retry attempts. This is typically a transient issue - please wait a few minutes and try submitting your training job again. If the problem persists, contact your administrator to check cluster health." + + # Placement group timeout + - contains: "Timed out waiting for placement groups to be ready" + exception: DistributedError + error_details: "Timed out while waiting for Ray placement groups to be allocated. Placement groups are used to co-locate GPU workers on the same nodes for efficient communication. This typically happens when the cluster is under heavy load and cannot allocate the required resources in time. Please try submitting your training job again. If the problem persists, contact your administrator." + + # No valid placement groups + - contains: "No valid placement groups found" + exception: DistributedError + error_details: "No valid Ray placement groups could be found for the training job. This indicates a problem with the distributed training cluster configuration or resource availability. This is an infrastructure issue - please contact your administrator to investigate the cluster setup." + + # Workers per node mismatch + - regex: "^workers_per_node list length \\(\\d+\\) must match" + exception: DistributedError + error_details: "The workers-per-node configuration does not match the number of placement groups allocated. This indicates an internal mismatch in the distributed training setup. This is an infrastructure issue - please contact your administrator." + + # Missing sharding annotations + - exact: "Sharding annotations must be provided to use sharded data distribution" + exception: DistributedError + error_details: "The training configuration requires sharded data distribution but sharding annotations are not provided. Sharding annotations specify how data should be distributed across workers for efficient parallel processing. This is an internal configuration issue - please contact your administrator." + + # =========================================================================== + # 13. GENERATION ERRORS (500) + # NeMo-RL + # =========================================================================== + + # Weight update failed during refit + - regex: "^Updating weights for the generation policy failed during refit" + exception: GenerationError + error_details: "Failed to update the vLLM generation model weights from the training policy during the 'refit' step. In GRPO training, the generation model periodically syncs weights from the training model. This failure may be caused by: 1) CUDA IPC (Inter-Process Communication) issues between training and generation workers, 2) NCCL communication errors, or 3) Memory pressure on GPUs. This is typically a transient issue - please try running your training job again." + + # generate_text with async_engine + - contains: "generate_text cannot be used with async_engine=True" + exception: GenerationError + error_details: "A synchronous generation method was called on an async vLLM engine. When async_engine is enabled, you must use async methods (e.g., generate_text_async). This is an internal configuration issue with the training environment, please contact the administrator." + + # update_weights_via_ipc with async_engine + - contains: "cannot be used with async_engine=True" + exception: GenerationError + error_details: "A synchronous method was called on an async vLLM engine. When async_engine is enabled, all vLLM operations must use their async variants. This is an internal configuration issue with the training environment, please contact the administrator." + + # Error in sample rollout + - regex: "^Error in sample \\d+ rollout: .+$" + exception: GenerationError + error_details: "An error occurred while generating a response (rollout) for one of the training samples during GRPO training. Rollouts are the model-generated responses used to compute rewards and policy gradients. This may be caused by: 1) Invalid input data in the sample, 2) Generation parameters causing issues (e.g., max_tokens too low), or 3) vLLM backend errors. Check your dataset for problematic samples." + + # Async generation not enabled + - contains: "Async generation is not enabled" + exception: GenerationError + error_details: "Async generation was requested but the vLLM engine is not configured with async_engine=True. Async generation allows overlapping training and generation for better throughput. This is an internal configuration issue with the training environment, please contact the administrator." + + # NeMo-Gym/Penguin requires async vLLM + - contains: "you must use vllm generation backend with" + exception: GenerationError + error_details: "The NeMo-Gym (Penguin) environment requires the vLLM generation backend with async_engine enabled. NeMo-Gym provides advanced RL training features that depend on async generation. This is an internal configuration issue with the training environment, please contact the administrator." + + # NeMo-Gym/Penguin requires HTTP server + - contains: "expose the vllm server via" + exception: GenerationError + error_details: "The NeMo-Gym (Penguin) environment requires the vLLM server to be exposed via HTTP (expose_http_server: true). This allows the environment to communicate with the generation model through an HTTP API. This is an internal configuration issue with the training environment, please contact the administrator." + + # NeMo-Gym/Penguin incompatible with reasoning parser + - contains: "Please do not use a reasoning parser in vLLM" + exception: GenerationError + error_details: "The NeMo-Gym (Penguin) environment is incompatible with vLLM's reasoning parser. NeMo-Gym handles all data processing including reasoning traces itself, so having a reasoning parser in vLLM would cause conflicts. This is an internal configuration issue with the training environment, please contact the administrator." + + # No placement groups available for vLLM + - exact: "No placement groups available in the cluster" + exception: GenerationError + error_details: "No Ray placement groups are available for vLLM generation workers. This means the cluster could not allocate the required GPU resources for the generation component of training. This is typically a resource availability issue - please try again or contact your administrator to check cluster capacity." + + # Unable to allocate vLLM worker groups + - contains: "Unable to allocate any worker groups with the available resources" + exception: GenerationError + error_details: "Could not allocate any vLLM worker groups with the available cluster resources. The generation component of DPO/GRPO training requires dedicated GPU resources for vLLM inference workers. Please ensure the cluster has enough GPUs, or reduce the generation parallelism settings." + + # Placement group contains no bundles + - exact: "Placement group contains no bundles" + exception: GenerationError + error_details: "A Ray placement group allocated for vLLM generation workers contains no resource bundles. This indicates an issue with cluster resource allocation. This is an infrastructure issue - please contact your administrator." + + # Failed to retrieve bundle/node mapping from placement group + - contains: "Failed to retrieve bundle/node mapping from placement group" + exception: GenerationError + error_details: "Could not retrieve the bundle-to-node mapping from the Ray placement group for vLLM workers. This indicates an issue with the distributed training cluster setup. This is an infrastructure issue - please contact your administrator." + + # No output received for generation request + - regex: "^No output received for request .+$" + exception: GenerationError + error_details: "The vLLM async generation engine did not produce any output for a generation request. This can happen when: 1) The generation request timed out, 2) The vLLM worker encountered an internal error, or 3) GPU memory was exhausted during generation. This is typically a transient issue - please try running your training job again." + + # =========================================================================== + # 14. INTERNAL ERRORS (500) + # =========================================================================== + + # --- Automodel Pipeline Parallelism Errors --- + # Pipeline parallelism: first stage missing inputs + - exact: "You must provide either input_ids or inputs_embeds" + exception: InternalError + error_details: "Pipeline parallelism internal error: the first pipeline stage did not receive input data (input_ids or inputs_embeds). This is an internal configuration issue with how the model is split across pipeline stages, please reach out to the NeMo Platform team." + + # Pipeline parallelism: intermediate stage missing embeddings + - exact: "inputs_embeds must be provided for pipeline stages without embed_tokens" + exception: InternalError + error_details: "Pipeline parallelism internal error: an intermediate pipeline stage did not receive embeddings from the previous stage. In pipeline parallelism, each stage processes a portion of the model layers and passes activations to the next stage. This error indicates the inter-stage communication failed, and is an internal training configuration issue, please reach out to the NeMo Platform team." + + # --- Automodel MoE (Mixture of Experts) Errors --- + # MoE: only 1D mesh supported (occurs when TP+EP are both > 1) + - exact: "We only support 1D mesh for MoE" + exception: ParallelismConfigError + error_details: "MoE (Mixture of Experts) models do not support combining tensor parallelism with expert parallelism. When using expert_model_parallel_size > 1, you must set tensor_parallel_size=1. Please update your parallelism configuration to disable tensor parallelism for MoE training." + + # MoE: DTensor placement error (checkpoint/parallelism mismatch) + - contains: "has unsupported DTensor placement" + exception: ParallelismConfigError + error_details: "MoE (Mixture of Experts) model checkpoint has an incompatible tensor distribution for the current expert parallelism settings. This typically occurs when the base model checkpoint was saved with different expert_model_parallel_size than what you're using for training. Please ensure your expert_model_parallel_size matches how the base model was originally distributed, or use a checkpoint that was saved without expert parallelism (expert_model_parallel_size=1)." + + # --- Automodel Fused Optimization Errors --- + # FusedLinearCrossEntropy configuration + - contains: "FusedLinearCrossEntropy requires the model to output hidden states" + exception: InternalError + error_details: "The fused linear cross-entropy optimization requires the model to output hidden states, but the model is configured to only output logits. FusedLinearCrossEntropy is a memory optimization that combines the final linear projection and loss computation. This is an internal configuration issue, contact the NeMo Platform team." + + # --- NeMo-RL Async GRPO Errors --- + # Stale trajectories in replay buffer + - regex: "^Found \\d+ trajectories older than min_valid_version \\d+$" + exception: InternalError + error_details: "The async GRPO replay buffer contains stale trajectories that are older than the minimum valid version. In async GRPO, trajectories are generated asynchronously and stored in a replay buffer. Stale trajectories can cause training instability because they were generated by an outdated policy. This indicates a synchronization issue between generation and training workers. Please contact the administrator." + + # --- NeMo-RL Tensor Processing Errors --- + # Tensor dimension mismatch + - regex: "^tensors for .+ must have same number of dimensions" + exception: InternalError + error_details: "Tensors being processed have mismatched dimensions during internal batching. This is an internal data processing issue that should not occur with valid datasets. Please contact the NeMo Platform team with your dataset format details." + + # Tensor dtype mismatch + - contains: "expected consistent types but got:" + exception: InternalError + error_details: "Tensors being processed have inconsistent data types (dtypes) during internal batching. This is an internal data processing issue that should not occur with valid datasets. Please contact the NeMo Platform team." + + # Tensors on different devices + - contains: "expected tensors on the same device but got:" + exception: InternalError + error_details: "Tensors are located on different devices during internal processing. This is an internal distributed training issue. Please contact the NeMo Platform team." + + # --- Automodel Configuration Errors --- + # Config instantiation failure (from ConfigNode.instantiate()) + # This prints a detailed error with "Instantiation failed for `func_name`" + - contains: "Instantiation failed for" + exception: InternalError + error_details: "Failed to instantiate a training configuration component. The training system uses a configuration tree where each node can instantiate Python objects (like optimizers, schedulers, or model components). This error means one of these instantiations failed, possibly due to invalid parameters or missing dependencies. Please contact the administrator." + + # Model compilation failure + - contains: "Model compilation failed" + exception: InternalError + error_details: "PyTorch model compilation (torch.compile) failed. Model compilation is an optional optimization that can speed up training by compiling the model graph. Training will fall back to eager mode and continue without compilation. If this error persists, it may indicate an incompatibility between the model architecture and PyTorch's compiler. Please contact the administrator if training fails." + + # --- General Training Process Errors --- + # Training subprocess error (generic fallback when no specific error was parsed) + # Matches both parser ("Training failed with exit code: X") and train.py ("Training subprocess returned with error code: X") + - regex: "^Training (failed with exit code|subprocess returned with (?:error )?code):? \\d+.*" + exception: InternalError + error_details: "The training process exited with a non-zero exit code, but no specific error message could be extracted from the training logs. This is a generic failure that can have many causes. Please check the full training logs for more details, and contact the administrator if you cannot determine the cause." + diff --git a/services/automodel/src/nmp/automodel/tasks/training/errors/exceptions.py b/services/automodel/src/nmp/automodel/tasks/training/errors/exceptions.py new file mode 100644 index 0000000000..6a98527051 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/errors/exceptions.py @@ -0,0 +1,431 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Custom exceptions for Customizer training errors. + +These exceptions provide user-friendly error messages for errors that may occur +during training with various backends: +- Automodel +- NeMo-RL +- Megatron Bridge +""" + +from dataclasses import dataclass +from typing import TypedDict + + +def format_exception_string(exc: BaseException) -> str: + """Format an exception as ``TypeName: message`` matching Python's traceback style. + + This is the canonical format used throughout the error-handling pipeline: + - ``ray_bootstrap`` writes it into the driver output buffer so the parser + can extract exceptions that occurred outside the subprocess. + - ``default_exception_handler`` uses it for the ``detail`` field reported + to the Jobs service. + - The parser's ``_EXCEPTION_RE`` regex is designed to match this format + when reading subprocess output. + """ + return f"{type(exc).__name__}: {exc}" + + +class ErrorDetails(TypedDict): + """Error details dict for Jobs service reporting.""" + + message: str + type: str + detail: str | None + + +@dataclass +class CustomizerTrainingError(Exception): + """ + Base exception for Customizer training errors. + + Attributes: + message: User-friendly error message shown to the user. + detail: Technical details about the original error (for debugging). + user_message: Class-level default message used as fallback when the YAML rule + does not specify an `error_details` field. Subclasses override this. + """ + + message: str + detail: str | None = None + + # Default user-facing message - subclasses override this. + # Used as fallback when YAML rule omits `error_details` field. + # See default_exception_handler() for usage. + user_message: str = "An error occurred during training." + + def __post_init__(self): + # Call Exception.__init__ with the message + super().__init__(self.message) + + def __str__(self) -> str: + return self.message + + def to_error_details(self) -> ErrorDetails: + """Convert to error_details dict for Jobs service reporting.""" + return ErrorDetails( + message=self.message, + type=type(self).__name__, + detail=self.detail, + ) + + +# ============================================================================= +# CLIENT ERRORS (400) +# ============================================================================= + + +@dataclass +class DatasetFormatError(CustomizerTrainingError): + """ + Dataset has invalid format or schema. + + Raised when: + - Dataset sample has unsupported role (not system/user/assistant/tool) + - Dataset is empty or has zero valid samples + - Text input is not a string or list of strings + - Required field missing from dataset sample + - Prompt file does not exist + """ + + user_message: str = "Dataset format error. Please check your dataset matches the expected schema." + + +@dataclass +class TrainingConfigError(CustomizerTrainingError): + """ + Invalid training configuration. + + Raised when: + - Model incompatible with pipeline parallelism (tied embeddings, encoder-decoder) + - PP batch/microbatch configuration invalid + - Model doesn't support SDPA for context parallelism + - Triton not installed for optimized LoRA kernels + - LoRA adapter dimensions mismatch + - DPO with dynamic batching or sequence packing + - GRPO missing generation config or validation dataset + - Async GRPO configuration errors + - Batch size not divisible by data parallel size + - World size insufficient for parallelism configuration + """ + + user_message: str = ( + "Training configuration error. Please check your parallelism settings " + "(tensor_parallel_size, pipeline_parallel_size, expert_model_parallel_size), " + "batch settings (batch_size, micro_batch_size), or training type configuration." + ) + + +@dataclass +class TrainingEnvironmentError(CustomizerTrainingError): + """ + Invalid environment configuration for GRPO. + + Raised when: + - GRPO environment name is not recognized + - GRPO environment not configured + - No environment found for task type + """ + + user_message: str = "Environment configuration error. Please check your GRPO environment settings." + + +@dataclass +class ParallelismConfigError(CustomizerTrainingError): + """ + Invalid parallelism configuration for MoE models. + + Raised when: + - MoE model uses tensor parallelism with expert parallelism (only 1D mesh supported) + - DTensor placement incompatible with expert parallelism settings + - Checkpoint parallelism settings don't match training configuration + """ + + user_message: str = ( + "Parallelism configuration error for Mixture-of-Experts (MoE) model. " + "MoE models do not support combining tensor_parallel_size > 1 with expert_model_parallel_size > 1. " + "To fix: either set tensor_parallel_size=1 when using expert parallelism, " + "or set expert_model_parallel_size=1 when using tensor parallelism." + ) + + +# ============================================================================= +# NOT FOUND ERRORS (404) +# ============================================================================= + + +@dataclass +class ModelNotFoundError(CustomizerTrainingError): + """ + Model or checkpoint path doesn't exist. + + Raised when: + - The specified checkpoint path does not exist + - The checkpoint directory is empty when resuming + - Nemotron model missing required HF source code + """ + + user_message: str = ( + "Model or checkpoint not found. The specified model path does not exist or is inaccessible. " + "Please verify the model identifier is correct and the model was successfully downloaded." + ) + + +# ============================================================================= +# SERVER ERRORS (500) +# ============================================================================= + + +@dataclass +class ModelLoadError(CustomizerTrainingError): + """ + Failed to load or initialize model. + + Raised when: + - Model weights could not be applied to a layer (corruption) + - Model optimizations/patches failed + - Method signature mismatch during patching + - Missing lm_head.weight in model + - vLLM library not installed + - Shape mismatch for model parameters or buffers + - Generation output missing required fields + """ + + user_message: str = ( + "Failed to load the model. This can happen when: " + "1) The model checkpoint is corrupted or incomplete, " + "2) The model architecture is incompatible with the training configuration, " + "3) There is a version mismatch between the model and the training framework. " + "Please verify the model checkpoint is valid and complete." + ) + + +@dataclass +class CheckpointError(CustomizerTrainingError): + """ + Checkpoint save or load failure. + + Raised when: + - Checkpoint directory already exists + - Failed to validate global plan (distributed checkpoint corruption) + - Missing key in checkpoint state_dict + - Expert weights missing from MoE checkpoint + - Training interrupted during checkpoint save + - Parallelism settings don't match checkpoint + - Model export or upload failed + """ + + user_message: str = ( + "Checkpoint save or load failed. This can happen when: " + "1) The checkpoint is corrupted or was saved incompletely (e.g., training was interrupted), " + "2) Disk space is insufficient for saving checkpoints, " + "3) The base model checkpoint is incompatible with the current training configuration." + ) + + +@dataclass +class CudaError(CustomizerTrainingError): + """ + GPU/CUDA runtime error. + + Raised when: + - GPU out of memory (OOM) + - General CUDA runtime errors + """ + + user_message: str = ( + "GPU memory exhausted. To reduce memory usage: " + "1) Reduce batch_size or micro_batch_size, " + "2) Reduce max_seq_length, " + "3) Use LoRA fine-tuning instead of full fine-tuning, " + "4) Increase tensor_parallel_size to distribute the model across more GPUs." + ) + + +@dataclass +class DistributedError(CustomizerTrainingError): + """ + Distributed training or Ray cluster failure. + + Raised when: + - torch.distributed not available + - torch.distributed not initialized + - Distributed operation timeout + - NCCL communication errors + - Ray cluster resource insufficiency + - Placement group allocation failure + """ + + user_message: str = "Distributed training error. Please check cluster resources and try again." + + +@dataclass +class GenerationError(CustomizerTrainingError): + """ + vLLM generation/inference failure. + + Raised when: + - Failed to update vLLM weights from training policy + - Sync method called on async engine + - Error during rollout for a sample + - Async generation called without async engine + - Penguin requires async vLLM + """ + + user_message: str = ( + "Generation error during reinforcement learning training. " + "DPO and GRPO training generate model responses during the training loop to compute rewards. " + "This error indicates the generation step failed, which may be caused by vLLM backend issues " + "or incompatible generation settings." + ) + + +@dataclass +class TrainingTimeoutError(CustomizerTrainingError): + """ + Training exceeded time limit. + + Raised when: + - Training subprocess exceeded configured timeout + """ + + user_message: str = ( + "Training exceeded the maximum allowed time limit. " + "To reduce training time: reduce epochs or max_steps, use a smaller dataset, " + "use a smaller model, or use LoRA fine-tuning instead of full fine-tuning. " + "Contact your administrator if you need longer training time limits." + ) + + +@dataclass +class InternalError(CustomizerTrainingError): + """ + Unexpected internal error. + + Raised when: + - Pipeline stage missing input_ids or inputs_embeds + - MoE device mesh configuration error + - DTensor placement error for expert parallelism + - FusedLinearCrossEntropy configuration error + - Tensor dimension/dtype/device mismatch + - Logger misconfiguration + - Any unmatched error (fallback) + """ + + user_message: str = ( + "An unexpected internal error occurred during training. " + "This is typically caused by framework-level issues such as tensor misconfigurations, " + "device mesh errors, or internal pipeline failures. " + "Please try running your job again. If the issue persists, contact your administrator " + "with the job ID and error details for further investigation." + ) + + +@dataclass +class GenericTrainingError(CustomizerTrainingError): + """ + Fallback when error classification is ambiguous. + + Used when multiple error rules match the same exception, + making classification unreliable. + """ + + user_message: str = ( + "Training failed due to an error that could not be precisely categorized. " + "Please review the error details for more information. " + "If the issue persists, try adjusting your training configuration." + ) + + +# ============================================================================= +# EXCEPTION REGISTRY +# ============================================================================= + +# Maps exception class names (strings in YAML) to actual Python classes +EXCEPTION_REGISTRY: dict[str, type[Exception]] = { + # Base + "CustomizerTrainingError": CustomizerTrainingError, + # Client errors (400) + "DatasetFormatError": DatasetFormatError, + "TrainingConfigError": TrainingConfigError, + "TrainingEnvironmentError": TrainingEnvironmentError, + "ParallelismConfigError": ParallelismConfigError, + # Not found (404) + "ModelNotFoundError": ModelNotFoundError, + # Server errors (500) + "ModelLoadError": ModelLoadError, + "CheckpointError": CheckpointError, + "CudaError": CudaError, + "DistributedError": DistributedError, + "GenerationError": GenerationError, + "TrainingTimeoutError": TrainingTimeoutError, + "InternalError": InternalError, + "GenericTrainingError": GenericTrainingError, +} + + +# ============================================================================= +# DEFAULT EXCEPTION HANDLER +# ============================================================================= + + +def default_exception_handler( + exception_class: type[Exception], + original_exception: Exception, + error_details: str | None, +) -> Exception: + """ + Default handler for creating Customizer training exceptions. + + This handler is used by RulesLoader when: + 1. A rule matches but doesn't have a custom handler + 2. No rule matches and fallback_exception is set + + Args: + exception_class: The exception class to create (from EXCEPTION_REGISTRY) + original_exception: The original exception that was caught + error_details: User-friendly message from the rule's error_details field, + or None if not specified + + Returns: + A new instance of exception_class with appropriate message and detail + """ + # Get the default user message from the class if no error_details provided + if issubclass(exception_class, CustomizerTrainingError): + user_message = error_details or exception_class.user_message + # For InternalError fallback (no matching rule), include the original error + # in the message so users get actionable information instead of a vague message + if exception_class is InternalError and error_details is None: + user_message = f"{user_message} ({format_exception_string(original_exception)})" + return exception_class( + message=user_message, + detail=format_exception_string(original_exception), + ) + else: + # For non-CustomizerTrainingError classes (shouldn't happen, but be safe) + return exception_class(error_details or str(original_exception)) + + +__all__ = [ + "CheckpointError", + "CudaError", + "CustomizerTrainingError", + "DatasetFormatError", + "DistributedError", + "ErrorDetails", + "EXCEPTION_REGISTRY", + "format_exception_string", + "GenerationError", + "GenericTrainingError", + "InternalError", + "ModelLoadError", + "ModelNotFoundError", + "ParallelismConfigError", + "TrainingConfigError", + "TrainingEnvironmentError", + "TrainingTimeoutError", + "default_exception_handler", +] diff --git a/services/automodel/src/nmp/automodel/tasks/training/errors/parser.py b/services/automodel/src/nmp/automodel/tasks/training/errors/parser.py new file mode 100644 index 0000000000..e0a4ee2174 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/errors/parser.py @@ -0,0 +1,255 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Error parser for subprocess output. + +This module provides utilities to parse and extract meaningful error messages from +training subprocess output (stdout/stderr). It should be used by all training backends +(Automodel, NeMo-RL, Megatron Bridge) to capture errors for classification. + +The extracted error messages are then matched against YAML rules by the +error converter to produce user-friendly error messages. +""" + +import re +import subprocess +import sys +from collections import deque +from dataclasses import dataclass + +# Number of recent output lines to keep for error parsing +MAX_OUTPUT_LINES = 500 + +# Patterns that indicate an error line (case-insensitive search) +# These match Python exception types and common error patterns from training libraries +ERROR_INDICATORS = [ + # Python exception type names (appear as "ExceptionType: message") + "runtimeerror", + "valueerror", + "assertionerror", + "importerror", + "attributeerror", + "keyerror", + "typeerror", + "filenotfounderror", + "permissionerror", + "oserror", + "ioerror", + # Generic error patterns + "error:", + "exception:", + "traceback", + # Automodel-specific patterns + "instantiation failed", # From ConfigNode.instantiate() + "model compilation failed", # From compile_utils.py + # NeMo-RL patterns + "ray error", + "actor died", + "worker crashed", + # Megatron Bridge patterns + "nemo error", + "lightning error", + # CUDA/GPU patterns + "cuda out of memory", + "out of memory", + "oom", + "cuda error", + "cublas error", + "cudnn error", + # Distributed training patterns + "nccl", + "gloo", + "distributed", + "mpi error", + # General failure patterns + "failed", + "failure", + "abort", + "killed", + "segmentation fault", + "signal", +] + +# Regex to detect Python exception lines ("SomeError: message") and extract +# the type name (group 1) and message (group 2) as separate captures. +_EXCEPTION_RE = re.compile( + r"\b(\w*(?:Error|Exception)):\s*(.*)", + re.IGNORECASE, +) + +# Wrapper exceptions from distributed training - skip these to find root cause +WRAPPER_EXCEPTION_PATTERNS = [ + "childfailederror", # torch.distributed wrapper + "torch.distributed.elastic", # torch elastic wrapper + "multiprocessing.errors", # multiprocessing wrapper +] + + +@dataclass(frozen=True) +class ParsedError: + """Error extracted from subprocess output. + + Preserves both the original exception type name (as printed in the + traceback) and the message, so callers can reconstruct a typed + exception for the converter's type-based matchers. + """ + + exception_type: str + message: str + + def to_exception(self) -> Exception: + """Reconstruct an exception that preserves the original type name. + + Dynamically creates an exception class whose ``__name__`` matches + the original type (e.g. ``ValueError``, ``ResourceInsufficientError``) + so that ``type_name`` YAML matchers can match it. The class inherits + from ``RuntimeError`` so that standard ``except Exception`` handling + works without needing the real library class to be importable. + """ + exc_class = type(self.exception_type, (RuntimeError,), {}) + return exc_class(self.message) + + +def _clean_line(line: str) -> str: + """Remove common prefixes like [rank0]: from distributed output.""" + line = re.sub(r"^\[rank\d+\]:\s*", "", line.strip()) + return line.strip() + + +def _is_wrapper_exception(line: str) -> bool: + """Check if this is a wrapper exception that should be skipped.""" + line_lower = line.lower() + return any(pattern in line_lower for pattern in WRAPPER_EXCEPTION_PATTERNS) + + +def _extract_exception(line: str) -> ParsedError | None: + """ + Extract the exception type and message from a subprocess output line. + + Examples: + >>> _extract_exception("[rank0]: ValueError: invalid input") + ParsedError(exception_type='ValueError', message='invalid input') + >>> _extract_exception("torch.cuda.OutOfMemoryError: CUDA OOM") + ParsedError(exception_type='OutOfMemoryError', message='CUDA OOM') + >>> _extract_exception(" File 'train.py', line 42") + None + >>> _extract_exception("ChildFailedError: worker 0 failed") + None # Wrapper exception, skipped + + Returns None for non-exception lines and wrapper exceptions. + """ + if _is_wrapper_exception(line): + return None + + match = _EXCEPTION_RE.search(line) + if match: + exc_type = match.group(1).strip() + message = match.group(2).strip() if match.group(2) else "" + return ParsedError( + exception_type=exc_type, + message=message or exc_type, + ) + + return None + + +def parse_error_from_output(output_lines: deque, returncode: int) -> ParsedError: + """ + Parse subprocess output and extract a structured error. + + Searches the captured output for Python exception lines and returns a + ``ParsedError`` preserving both the exception type name and message. + Callers use ``result.to_exception()`` to reconstruct a typed exception + that works with both message-based *and* type-based YAML matchers. + + Strategy: + 1. Find the LAST Python exception line (e.g., "ValueError: message") + 2. Extract the type name and message separately + 3. Deduplicate across distributed ranks + + Args: + output_lines: Rolling buffer of recent output lines. + returncode: Process exit code. + + Returns: + ParsedError with exception_type and message. + """ + if not output_lines: + return ParsedError("RuntimeError", f"Training failed with exit code: {returncode}") + + lines = list(output_lines) + + # Search backwards for exception lines and collect unique ones + # (distributed training often prints the same error multiple times) + found: list[ParsedError] = [] + seen_messages: set[str] = set() + + for i in range(len(lines) - 1, -1, -1): + parsed = _extract_exception(lines[i]) + if parsed and parsed.message not in seen_messages: + seen_messages.add(parsed.message) + found.append(parsed) + if len(found) >= 3: + break + + if found: + return found[0] + + # Fallback: search for any error-related lines + error_lines: list[str] = [] + for line in reversed(lines): + line_lower = line.lower() + is_error_line = any(indicator in line_lower for indicator in ERROR_INDICATORS) + if is_error_line: + cleaned = _clean_line(line) + if cleaned and cleaned not in error_lines: + error_lines.insert(0, cleaned) + if len(error_lines) > 10: + break + + if error_lines: + return ParsedError("RuntimeError", "\n".join(error_lines[-10:])) + + # Last resort: return last N lines of output + last_lines = [_clean_line(line) for line in lines[-10:]] + message = f"Training failed with exit code {returncode}. Last output:\n" + "\n".join(last_lines) + return ParsedError("RuntimeError", message) + + +def read_subprocess_output(proc: subprocess.Popen, buffer: deque) -> None: + """ + Read subprocess output, stream to console, and capture in buffer. + + This function is designed to run in a daemon thread alongside a subprocess, + reading its stdout line-by-line, printing to console in real-time, and + storing lines in a rolling buffer for later error extraction. + + Args: + proc: The subprocess.Popen object with stdout=PIPE. + buffer: A deque with maxlen to store recent output lines. + """ + if proc.stdout is None: + return + + try: + for line in iter(proc.stdout.readline, ""): + if not line: + break + # Stream to console + sys.stdout.write(line) + sys.stdout.flush() + # Capture in rolling buffer + buffer.append(line.rstrip("\n")) + except (ValueError, OSError): + # Process closed or pipe broken + pass + + +__all__ = [ + "ERROR_INDICATORS", + "MAX_OUTPUT_LINES", + "ParsedError", + "parse_error_from_output", + "read_subprocess_output", +] diff --git a/services/automodel/src/nmp/automodel/tasks/training/integrations.py b/services/automodel/src/nmp/automodel/tasks/training/integrations.py new file mode 100644 index 0000000000..f3610c9d13 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/integrations.py @@ -0,0 +1,168 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual +# property and proprietary rights in and to this material, related +# documentation and any modifications thereto. Any use, reproduction, +# disclosure or distribution of this material and related documentation +# without an express license agreement from NVIDIA CORPORATION or +# its affiliates is strictly prohibited. + +"""WandB and MLflow config helpers for Automodel training.""" + +import logging +import os +from pathlib import Path +from typing import Any + +from nmp.automodel.app.jobs.context import NMPJobContext +from nmp.automodel.tasks.training.schemas import TrainingStepConfig + +logger = logging.getLogger(__name__) + + +def _resolve_with_fallback( + primary: str | None, + fallback: str | None, + default: str, + field_label: str | None = None, +) -> str: + """Pick the first truthy value from *primary* → *fallback* → *default*. + + When *field_label* is given and neither *primary* nor *fallback* is set, + a warning is logged so operators know a hardcoded default is in use. + """ + if field_label and not (primary or fallback): + logger.warning(f"{field_label} is not set; using fallback '{default}'.") + return primary or fallback or default + + +def build_mlflow_config( + customizer_config: TrainingStepConfig, + job_ctx: NMPJobContext, + framework: str, +) -> dict[str, Any] | None: + """Build MLflow config for Automodel training. + The resulting dict is passed to MLflow logging setup in the recipe config. + + Run naming strategy (same as WandB): + - run_name uses job_id (stable across pause/resume) + - task_id is added to tags for granular execution tracking + + Missing tracking URI disables integration with a warning. + """ + user_config = customizer_config.integrations.mlflow + if not user_config: + return None + + # User-provided tracking URI takes precedence over environment variable + tracking_uri = user_config.tracking_uri or os.environ.get("MLFLOW_TRACKING_URI") + if not tracking_uri: + logger.warning( + "MLflow integration is configured but no tracking URI is set " + "(MLFLOW_TRACKING_URI env var and integrations.mlflow.tracking_uri in job POST request are empty); " + "MLflow integration will be disabled." + ) + return None + + tags: dict[str, str] = { + "service": "customizer", + "framework": framework, + } + if job_ctx.workspace: + tags["workspace"] = job_ctx.workspace + if job_ctx.job_id: + tags["job"] = job_ctx.job_id + if job_ctx.task: + tags["task"] = job_ctx.task + if customizer_config.model.name: + tags["model_name"] = customizer_config.model.name + + # User-provided tags override defaults above + if user_config.tags: + tags.update(user_config.tags) + if user_config.description: + # MLflow run description is stored in the reserved `mlflow.note.content` tag. + # See: https://mlflow.org/docs/latest/ml/tracking/#how-to-include-additional-description-texts-about-the-run + tags["mlflow.note.content"] = user_config.description + + experiment_name = _resolve_with_fallback( + user_config.experiment_name, + customizer_config.output_model, + "default-experiment", + field_label="MLflow experiment_name", + ) + run_name = _resolve_with_fallback( + user_config.run_name, + job_ctx.job_id, + "default-run", + field_label="MLflow run_name", + ) + + mlflow_config: dict[str, Any] = { + "tracking_uri": tracking_uri, + "experiment_name": experiment_name, + "run_name": run_name, + "tags": tags, + } + + return mlflow_config + + +def build_wandb_config( + customizer_config: TrainingStepConfig, + job_ctx: NMPJobContext, + framework: str, +) -> dict[str, Any] | None: + """Build WandB config for Automodel training. + + The resulting dict is passed to wandb.init() as kwargs by automodel. + See: https://docs.wandb.ai/ref/python/init + + TODO: Add pause/resume support: + - 'name' and 'id' use job_id (stable across pause/resume) + - 'resume="allow"' enables continuing runs after pause/resume + """ + user_config = customizer_config.integrations.wandb + if not user_config: + return None + + wandb_api_key = os.environ.get("WANDB_API_KEY") + if not user_config.base_url and not wandb_api_key: + logger.warning("WandB API key is not set and no base_url is provided, skipping WandB integration") + return None + + # Note: This is semantically different from job_ctx.workspace. + # This is the workspace for training artifacts. + run_dir = Path(customizer_config.workspace_path) / "wandb" + + tags: list[str] = ["service:customizer", f"framework:{framework}"] + if job_ctx.workspace: + tags.append(f"workspace:{job_ctx.workspace}") + if job_ctx.job_id: + tags.append(f"job:{job_ctx.job_id}") + if job_ctx.task: + tags.append(f"task:{job_ctx.task}") + if customizer_config.model.name: + tags.append(f"model:{customizer_config.model.name}") + # User-provided tags are appended (can override tags above) + if user_config.tags: + tags.extend(user_config.tags) + + wandb_config: dict[str, Any] = { + "project": _resolve_with_fallback(user_config.project, customizer_config.output_model, "default-project"), + "name": _resolve_with_fallback(user_config.name, job_ctx.job_id, "default-run"), + "dir": str(run_dir), + "tags": tags, + } + if user_config.entity: + wandb_config["entity"] = user_config.entity + if user_config.notes: + wandb_config["notes"] = user_config.notes + if user_config.base_url: + # For self-hosted W&B servers, base_url is passed via the settings dict + # (wandb.init accepts settings as Union[Settings, Dict[str, Any], None]). + logger.info(f"Using self-hosted W&B server: {user_config.base_url}") + wandb_config["settings"] = {"base_url": user_config.base_url} + + return wandb_config diff --git a/services/automodel/src/nmp/automodel/tasks/training/model_utils/constants.py b/services/automodel/src/nmp/automodel/tasks/training/model_utils/constants.py new file mode 100644 index 0000000000..c784016b7d --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/model_utils/constants.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +ADAPTER_FILES = ["adapter_config.json", "adapter_model.safetensors"] diff --git a/services/automodel/src/nmp/automodel/tasks/training/model_utils/file_utils.py b/services/automodel/src/nmp/automodel/tasks/training/model_utils/file_utils.py new file mode 100644 index 0000000000..579fb5559e --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/model_utils/file_utils.py @@ -0,0 +1,172 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual +# property and proprietary rights in and to this material, related +# documentation and any modifications thereto. Any use, reproduction, +# disclosure or distribution of this material and related documentation +# without an express license agreement from NVIDIA CORPORATION or +# its affiliates is strictly prohibited. +import logging +from enum import Enum +from pathlib import Path +from typing import Dict, List, Optional + +from nmp.automodel.tasks.training.model_utils.constants import ADAPTER_FILES + + +class TargetCheckpointType(str, Enum): + """Target checkpoint format types for model conversion.""" + + NEMO = "NEMO" + HF = "HF" + HF_LORA = "HF_LORA" + + +logger = logging.getLogger(__name__) + + +def get_flat_files_list(parent_dir: str) -> List[str]: + """ + Get a list of files in a directory + """ + parent_path = Path(parent_dir).resolve() + if not parent_path.exists(): + raise ValueError(f"Path {parent_dir} does not exist") + if not parent_path.is_dir(): + raise ValueError(f"Path {parent_dir} is not a directory") + + return [str(path) for path in parent_path.rglob("*") if path.is_file()] + + +def is_adapter_file_present(files: List[str]) -> bool: + """ + Check if the any file is a LoRA adapter file + """ + for file in files: + if not file: + continue + if any(adapter_file in file.lower() for adapter_file in ADAPTER_FILES): + return True + return False + + +def check_directory_structure(path: Path | str, target: Dict[str, Optional[Dict]]) -> bool: + if isinstance(path, str): + path = Path(path) + + if not path.is_dir(): + logger.error(f"Provided path '{path}' is not a directory") + return False + + try: + got_files = {f.name for f in path.iterdir()} + except OSError: + logger.exception("Cannot read directory '%s'", path) + return False + + expected_files = set(target.keys()) + missing = expected_files - got_files + if missing: + logger.debug(f"Mismatch in '{path}': Missing items -> {missing}") + return False + + for name, _target in target.items(): + current_path = path / name + if isinstance(_target, dict): + # this is a directory + if not current_path.is_dir(): + return False + if not check_directory_structure(current_path, _target): + return False + elif _target is None: + if not current_path.is_file(): + logger.debug(f"Mismatch: '{current_path}' is expected to be a file but is a directory.") + return False + return True + + +def is_nemo_model_directory(model_path: Path | str) -> bool: + nemo_structure = { + "context": {"nemo_tokenizer": {}, "model.yaml": None}, + "weights": {"metadata.json": None}, + } + return check_directory_structure(model_path, nemo_structure) + + +def is_huggingface_model_directory(model_path: Path | str) -> bool: + """ + Checks if a directory contains the necessary files to be considered a + Hugging Face model directory. + + Args: + directory_path: The path to the directory to check. + + Returns: + True if the directory contains a config.json file and model weights, + False otherwise. + """ + if isinstance(model_path, str): + model_path = Path(model_path) + + # 1. Check for the mandatory config.json file + config_file = model_path / "config.json" + if not config_file.is_file(): + logger.debug(f"Missing {config_file}") + return False + + tokenizer_files = [ + model_path / "tokenizer.json", + model_path / "tokenizer_config.json", + model_path / "vocab.txt", + model_path / "merges.txt", + ] + if not any(tf.is_file() for tf in tokenizer_files): + logger.debug(f"Missing any tokenizer file: at least one of [{tokenizer_files}] is required") + return False + + # 2. Check for the presence of model weight files (either safetensors or pytorch bin) + safe_tensor_file = model_path / "model.safetensors" + has_safetensors = safe_tensor_file.is_file() or any(model_path.glob("model-*.safetensors")) + if has_safetensors: + return True + + logger.debug(f"Missing model weights files in the form of {safe_tensor_file} or {model_path}/model-*.safetensors") + pytorch_bin_file = model_path / "pytorch_model.bin" + has_pytorch_bin = pytorch_bin_file.is_file() or any(model_path.glob("pytorch_model-*.bin")) + if has_pytorch_bin: + return True + + logger.debug(f"Missing model weights files in the form of {pytorch_bin_file} or {model_path}/pytorch_model-*.bin") + return False + + +def determine_llm_model_type(model_dir: str | Path) -> TargetCheckpointType | None: + """ + Determines whether a model directory contains a HuggingFace or NVIDIA NeMo model. + """ + model_path = Path(model_dir).resolve() + + if not model_path.exists() or not model_path.is_dir(): + logger.error(f"Provided path {model_path} is not a directory") + return None + + logger.debug(f"Checking model in {model_path} for LoRA adapter format indicators") + if is_adapter_file_present(get_flat_files_list(str(model_path))): + logger.info(f"Huggingface LoRA adapter format detected in {model_path}") + return TargetCheckpointType.HF_LORA + + logger.debug(f"Checking model in {model_path} for NeMo format indicators") + if is_nemo_model_directory(model_path): + logger.info(f"NeMo format detected in {model_path}") + return TargetCheckpointType.NEMO + + logger.debug(f"Checking model in {model_path} for HugginFace format indicators") + if is_huggingface_model_directory(model_path): + logger.info(f"HuggingFace format detected in {model_path}") + return TargetCheckpointType.HF + + logger.warning(f"model at {model_path} is an unknown checkpoint format") + logger.warning(f"File List: {get_flat_files_list(str(model_path))}") + + return None diff --git a/services/automodel/src/nmp/automodel/tasks/training/progress.py b/services/automodel/src/nmp/automodel/tasks/training/progress.py new file mode 100644 index 0000000000..0ede065112 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/progress.py @@ -0,0 +1,173 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Progress reporting for training tasks. + +This module provides progress reporting to the Jobs service using +the NeMo Platform SDK. The `JobsServiceProgressReporter` class +handles high-level phase reporting for the training runner. + +For training-specific metrics (loss, validation, checkpoints), see +the `TrainingProgressCallback` in the automodel backend which composes +this reporter and provides training-specific methods. +""" + +import logging +import os +from typing import Any + +from nmp.automodel.app.constants import SERVICE_NAME +from nmp.automodel.app.jobs.context import NMPJobContext +from nmp.common.sdk_factory import get_task_sdk + +logger = logging.getLogger(__name__) + + +class JobsServiceProgressReporter: + """Reports high-level progress to the Jobs service. + + This class provides progress reporting for the training runner: + - configure_progress_tracking(max_steps, num_epochs) - Set bounds for percentage calculation + - report_running(phase, **details) - Report current phase (auto-calculates percentage_done) + - report_completed(message) - Report successful completion + - report_error(message) - Report failure + + For training backends that need to report detailed metrics, the + `update_task` method is exposed for direct use. See `TrainingProgressCallback` + in the automodel backend for an example. + """ + + def __init__(self, job_ctx: NMPJobContext): + """Initialize the progress reporter.""" + self._job_ctx = job_ctx + self._sdk = get_task_sdk(SERVICE_NAME) + self._is_main_rank = int(os.environ.get("RANK", "0")) == 0 + self._max_steps = 0 + self._num_epochs = 0 + + self._enabled = self._is_main_rank and all( + [self._job_ctx.job_id, self._job_ctx.step, self._job_ctx.normalized_task] + ) + + def configure_progress_tracking(self, max_steps: int, num_epochs: int) -> None: + """Configure progress tracking at the start of training. + + Args: + max_steps: Total number of training steps + num_epochs: Total number of epochs + """ + self._max_steps = max_steps + self._num_epochs = num_epochs + + def _calculate_percentage_done(self, step: int | None) -> int: + """Calculate percentage done based on current step and max_steps.""" + if step is None or self._max_steps <= 0: + return 0 + return int((step / self._max_steps) * 100) + + def update_task( + self, + status: str = "active", + status_details: dict[str, Any] | None = None, + error_details: dict[str, Any] | None = None, + ) -> None: + """Update task status via SDK. + + This is the low-level method exposed for composition by training + callbacks that need to report detailed metrics. + + Args: + status: Task status ("active", "completed", "error") + status_details: Details about the current status + error_details: Error information (for status="error") + """ + if not self._enabled: + return + + # Only report from rank 0 in distributed training + if not self._is_main_rank: + return + + try: + self._sdk.jobs.tasks.create_or_update( + name=self._job_ctx.normalized_task, + workspace=self._job_ctx.workspace, + job=self._job_ctx.job_id, + step=self._job_ctx.step, + status=status, + status_details=status_details or {}, + error_details=error_details or {}, + ) + except Exception as e: + logger.warning(f"Failed to update task progress: {e}") + + def fetch_current_metrics(self) -> dict[str, list[dict[str, float | int]]]: + """Fetch accumulated metrics from the server for the current task. + + Used to seed metric accumulators on startup so that metrics + survive pause/resume cycles. Returns empty lists on failure + or if no prior metrics exist. + """ + if not self._enabled: + return {"train_loss": [], "val_loss": []} + + try: + task = self._sdk.jobs.tasks.retrieve( + name=self._job_ctx.normalized_task, + workspace=self._job_ctx.workspace, + job=self._job_ctx.job_id, + step=self._job_ctx.step, + ) + metrics = (task.status_details or {}).get("metrics", {}) + return { + "train_loss": metrics.get("train_loss", []), + "val_loss": metrics.get("val_loss", []), + } + except Exception as e: + logger.info(f"No prior metrics to seed (expected on first run): {e}") + return {"train_loss": [], "val_loss": []} + + # --- High-level runner methods --- + + def report_running(self, phase: str, **details: Any) -> None: + """Report that a phase is running. + + If 'step' is provided and training schedule is set (via configure_progress_tracking), + percentage_done is automatically calculated unless explicitly provided. + + Args: + phase: The current phase (e.g., "compiling_config", "training") + **details: Additional context (e.g., step, epoch, loss, backend="automodel") + """ + # Auto-calculate percentage_done if step is provided and not already set + if "step" in details and "percentage_done" not in details and self._max_steps > 0: + details["percentage_done"] = self._calculate_percentage_done(details["step"]) + + status_details = {"phase": phase, **details} + self.update_task(status="active", status_details=status_details) + + def report_completed(self, message: str = "Completed") -> None: + """Report task completed successfully. + + Args: + message: Completion message + """ + self.update_task(status="completed", status_details={"message": message, "phase": "completed"}) + + def report_error(self, error: str | dict[str, Any]) -> None: + """Report task error. + + Args: + error: Error message (str) or error details dict with 'message', 'type', 'detail' keys. + The dict format is typically from create_error_details() in the errors module. + """ + if isinstance(error, str): + error_details = {"message": error} + else: + error_details = error + self.update_task(status="error", error_details=error_details) + + def close(self) -> None: + """Clean up SDK resources.""" + self._sdk.close() diff --git a/services/automodel/src/nmp/automodel/tasks/training/protocol.py b/services/automodel/src/nmp/automodel/tasks/training/protocol.py new file mode 100644 index 0000000000..59045ca59e --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/protocol.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import dataclass +from pathlib import Path +from typing import Any + + +@dataclass +class LibraryConfig: + """nemo-automodel recipe config written by the training runner.""" + + config_dict: dict[str, Any] + config_path: Path diff --git a/services/automodel/src/nmp/automodel/tasks/training/runner.py b/services/automodel/src/nmp/automodel/tasks/training/runner.py new file mode 100644 index 0000000000..2d893dde19 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/runner.py @@ -0,0 +1,190 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Training runner with distributed coordination support. + +Orchestrates Automodel training in single-node and multi-node environments, +using file-based barriers for cross-pod synchronization. +""" + +import json +import logging +import random +import time +from enum import Enum +from pathlib import Path +from types import TracebackType + +import yaml +from nmp.automodel.app.constants import DEFAULT_TRAINING_RESULT_FILE_NAME +from nmp.automodel.app.jobs.context import NMPJobContext + +from .backends.backend import AUTOMODEL_CONFIG_FILENAME, AutomodelBackend +from .distributed import DistributedContext +from .errors.converter import create_error_details +from .progress import JobsServiceProgressReporter +from .protocol import LibraryConfig +from .schemas import ( + GPUInfo, + TrainingMetrics, + TrainingResult, + TrainingStepConfig, +) +from .utils import get_gpu_info + + +# Custom YAML representer to serialize Enum values as their string values +def _enum_representer(dumper: yaml.Dumper, data: Enum) -> yaml.Node: + """Represent Enum as its value (string) rather than a Python object tag.""" + return dumper.represent_str(str(data.value)) + + +yaml.add_representer(Enum, _enum_representer) +yaml.add_multi_representer(Enum, _enum_representer) + +logger = logging.getLogger(__name__) + +BARRIER_CONFIG_READY = "config_ready" +BARRIER_TRAINING_COMPLETE = "training_complete" + + +class TrainingRunner: + """ + Orchestrates Automodel training across single-node and multi-node environments. + + Usage: + with TrainingRunner() as runner: + result = runner.run() + """ + + def __init__(self, backend: AutomodelBackend | None = None) -> None: + self._job_ctx = NMPJobContext.from_env() + self._config = self._load_config(self._job_ctx.config_path) + self._progress = JobsServiceProgressReporter(self._job_ctx) + self._dist_ctx = DistributedContext.from_env(self._get_barrier_dir()) + self._backend = backend or AutomodelBackend(self._job_ctx) + self._workspace_path = Path(self._config.workspace_path) + self._output_path = Path(self._config.output_path) + + def __enter__(self) -> "TrainingRunner": + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def close(self) -> None: + self._progress.close() + + def run(self) -> TrainingResult: + random.seed(self._config.seed) + logger.info(f"Global random seed set to {self._config.seed}") + + start_time = time.time() + gpu_info = get_gpu_info() + result = TrainingResult(success=False, error_message="No result") + + try: + library_config = self._compile_config_phase() + metrics = self._training_phase(library_config) + self._dist_ctx.sync_point(BARRIER_TRAINING_COMPLETE) + result = self._postprocess_phase(gpu_info, metrics, start_time, library_config) + + except Exception as e: + logger.exception(f"Training failed: {e}") + error_details = create_error_details(e) + result = TrainingResult( + success=False, + error_message=error_details.get("message", str(e)), + gpu_info=gpu_info, + training_duration_seconds=time.time() - start_time, + ) + if self._dist_ctx.is_coordinator: + self._progress.report_error(error_details) + finally: + self._write_result(result) + + return result + + def _get_barrier_dir(self) -> Path: + return self._job_ctx.storage_path / self._job_ctx.attempt_id / "distributed" / "barriers" + + def _load_config(self, config_path: Path) -> TrainingStepConfig: + with open(config_path) as f: + return TrainingStepConfig.model_validate(json.load(f)) + + def _get_library_config_path(self) -> Path: + return self._workspace_path / AUTOMODEL_CONFIG_FILENAME + + def _compile_config_phase(self) -> LibraryConfig: + config_path = self._get_library_config_path() + + if self._dist_ctx.is_coordinator: + self._progress.report_running("compiling_config") + config_dict = self._backend.compile_config(self._config, self._workspace_path) + config_path.parent.mkdir(parents=True, exist_ok=True) + with open(config_path, "w") as f: + yaml.dump(config_dict, f, default_flow_style=False) + logger.info(f"Library config written to: {config_path}") + self._dist_ctx.signal(BARRIER_CONFIG_READY) + return LibraryConfig(config_dict=config_dict, config_path=config_path) + + self._dist_ctx.wait_for_coordinator(BARRIER_CONFIG_READY) + return self._load_library_config(config_path) + + def _load_library_config(self, config_path: Path) -> LibraryConfig: + if not config_path.exists(): + raise FileNotFoundError( + f"Library config not found at {config_path}. Coordinator may not have written it yet." + ) + with open(config_path) as f: + config_dict = yaml.safe_load(f) + logger.info(f"Loaded library config from: {config_path}") + return LibraryConfig(config_dict=config_dict, config_path=config_path) + + def _training_phase(self, library_config: LibraryConfig) -> TrainingMetrics: + return self._backend.execute_training(self._config, library_config, self._progress) + + def _postprocess_phase( + self, + gpu_info: GPUInfo | None, + metrics: TrainingMetrics, + start_time: float, + library_config: LibraryConfig, + ) -> TrainingResult: + if not self._dist_ctx.is_coordinator: + return TrainingResult( + success=True, + gpu_info=gpu_info, + training_duration_seconds=time.time() - start_time, + ) + + self._progress.report_running("processing_checkpoint") + checkpoint_path = self._backend.find_best_checkpoint(self._workspace_path, self._config, library_config) + checkpoint_info = self._backend.process_checkpoint( + checkpoint_path, self._output_path, self._config, library_config + ) + + result = TrainingResult( + success=True, + checkpoint=checkpoint_info, + gpu_info=gpu_info, + metrics=metrics, + training_duration_seconds=time.time() - start_time, + ) + self._progress.report_completed("Training completed") + return result + + def _write_result(self, result: TrainingResult) -> None: + if not self._dist_ctx.is_coordinator: + return + result_path = self._workspace_path / DEFAULT_TRAINING_RESULT_FILE_NAME + result_path.parent.mkdir(parents=True, exist_ok=True) + with open(result_path, "w") as f: + f.write(result.model_dump_json(indent=2)) + logger.info(f"Result written to: {result_path}") diff --git a/services/automodel/src/nmp/automodel/tasks/training/schemas.py b/services/automodel/src/nmp/automodel/tasks/training/schemas.py new file mode 100644 index 0000000000..4a5d493ee5 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/schemas.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from nmp.automodel.app.jobs.training.schemas import ( + CheckpointInfo, + DistillationConfig, + EmbeddingConfig, + GPUInfo, + LoRAConfig, + MLflowConfig, + ModelConfig, + OptimizerType, + TrainingMetrics, + TrainingResult, + TrainingStepConfig, + WandBConfig, +) +from nmp.automodel.entities.values import ( + CheckpointFormat, + FinetuningType, + Precision, + TrainingType, +) + +__all__ = [ + "CheckpointFormat", + "FinetuningType", + "Precision", + "TrainingType", + "CheckpointInfo", + "DistillationConfig", + "EmbeddingConfig", + "GPUInfo", + "LoRAConfig", + "MLflowConfig", + "ModelConfig", + "OptimizerType", + "TrainingMetrics", + "TrainingResult", + "TrainingStepConfig", + "WandBConfig", +] diff --git a/services/automodel/src/nmp/automodel/tasks/training/sequence_packing.py b/services/automodel/src/nmp/automodel/tasks/training/sequence_packing.py new file mode 100644 index 0000000000..ae1948ee11 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/sequence_packing.py @@ -0,0 +1,349 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Sequence packing utilities for Automodel training. + +Sequence packing combines multiple shorter sequences into a single packed sequence +to improve GPU utilization during training. This module provides: + +1. Optimal pack size calculation based on dataset statistics +2. Dataset sequence length estimation via sampling + +The algorithm balances packing efficiency with training stability by: +- Calculating a target packing factor from global batch size and GPU count +- Ensuring pack size is at least the max sequence length in the dataset +- Clamping to the model's maximum sequence length + +Usage with Automodel: + The `packed_sequence_size` calculated here should be passed to Automodel's + config under `packed_sequence.packed_sequence_size`. Automodel automatically + handles step calculation based on the packed dataset size - no manual + adjustment of max_steps or global_batch_size is needed. + +Reference: + - NeMo docs: https://docs.nvidia.com/nemo-framework/user-guide/latest/sft_peft/packed_sequence.html + - Automodel docs: https://github.com/NVIDIA-NeMo/Automodel/blob/main/docs/guides/llm/dataset.md#packed-sequence-support-in-nemo-automodel +""" + +import json +import logging +import math +import random +from dataclasses import dataclass +from pathlib import Path + +from nmp.automodel.app.constants import DEFAULT_SEED +from nmp.automodel.tasks.training.schemas import TrainingStepConfig + +logger = logging.getLogger(__name__) + + +@dataclass +class PackingEstimate: + """Statistics from dataset sampling for sequence packing configuration. + + This dataclass holds the results of sampling a dataset to estimate + sequence length statistics, which are used to calculate optimal + pack sizes for sequence packing. + + Attributes: + pack_size: Recommended pack size in tokens for Automodel's + `packed_sequence.packed_sequence_size` config + avg_seq_length: Average sequence length in the sampled data + max_seq_length: Maximum sequence length in the sampled data + packing_factor: Estimated number of sequences per pack + (pack_size / avg_seq_length) + samples_analyzed: Number of samples successfully tokenized + """ + + pack_size: int + avg_seq_length: int + max_seq_length: int + packing_factor: float + samples_analyzed: int + + +def _ceil_even(num: int | float) -> int: + """Round up to the nearest even number. + + NeMo/Automodel prefer even sequence lengths for efficiency with + tensor parallelism and other optimizations. + + Examples: + >>> _ceil_even(3) + 4 + >>> _ceil_even(4) + 4 + >>> _ceil_even(5.5) + 6 + """ + return int(math.ceil(num / 2) * 2) + + +def calculate_optimal_pack_size( + config: TrainingStepConfig, + dataset_avg_seq_length: int | None = None, + dataset_max_seq_length: int | None = None, +) -> int: + """ + Calculate optimal pack size for sequence packing. + + This algorithm balances packing efficiency with training stability: + 1. Target packing_factor = global_batch_size / total_gpus + 2. target_pack_size = avg_seq_length * packing_factor (but at least max_seq_length) + 3. Clamp to model's max_seq_length + + The packing factor determines how many sequences fit into one packed sequence. + A higher packing factor means better GPU utilization but may affect convergence + if pack sizes become very large. + + If dataset statistics are not provided, uses model's max_seq_length as a + conservative default (which effectively disables the optimization). + + Args: + config: Training configuration containing parallelism, batch, and model settings + dataset_avg_seq_length: Average sequence length in the dataset (after tokenization) + dataset_max_seq_length: Maximum sequence length in the dataset + + Returns: + Optimal pack size in tokens + + Example: + For a setup with: + - global_batch_size = 32 + - 8 GPUs (num_nodes=1, num_gpus_per_node=8) + - avg_seq_length = 512 + - max_seq_length = 1024 + - model.max_seq_length = 4096 + + Calculation: + - packing_factor = 32 / 8 = 4 + - target_pack_size = ceil_even(512 * 4) = 2048 + - final = max(2048, 1024) = 2048 (clamped to 4096) = 2048 + """ + parallelism = config.parallelism + total_gpus = parallelism.num_nodes * parallelism.num_gpus_per_node + gbs = config.batch.global_batch_size + model_max_seq = config.model.max_seq_length + + # If no dataset stats provided, use model's max_seq_length (conservative) + if dataset_avg_seq_length is None or dataset_max_seq_length is None: + logger.info(f"No dataset statistics provided, using model max_seq_length: {model_max_seq}") + return model_max_seq + + # Calculate target packing factor (how many sequences can fit in one pack) + # This keeps the effective batch size close to the original gbs + target_packing_factor = max(gbs // total_gpus, 1) + + # Calculate pack size based on average sequence length + # Round to nearest even number for efficiency + target_pack_size = _ceil_even(round(dataset_avg_seq_length * target_packing_factor)) + + # Ensure pack size is at least the max sequence length in the dataset + # (so no sequence gets truncated due to packing) + target_pack_size = max(target_pack_size, dataset_max_seq_length) + + # Clamp to model's maximum sequence length + optimal_pack_size = min(target_pack_size, model_max_seq) + + logger.info( + f"Calculated optimal pack size: {optimal_pack_size} " + f"(avg_seq={dataset_avg_seq_length}, max_seq={dataset_max_seq_length}, " + f"packing_factor={target_packing_factor})" + ) + + return optimal_pack_size + + +def estimate_dataset_sequence_lengths( + config: TrainingStepConfig, + train_file: Path | None = None, + max_samples: int = 1000, + seed: int = DEFAULT_SEED, + trust_remote_code: bool = False, +) -> PackingEstimate | None: + """ + Estimate dataset sequence lengths by sampling and calculate optimal pack size. + + This is a lightweight alternative to full tokenization that uses reservoir + sampling to randomly select a subset of the dataset for sequence length + estimation. The sampling is unbiased regardless of dataset ordering. + + The function: + 1. Loads the model's tokenizer + 2. Randomly samples up to `max_samples` examples using reservoir sampling + 3. Tokenizes each example (using apply_chat_template for chat format) + 4. Calculates optimal pack size based on the statistics + + NOTE: Sampling may underestimate max_seq_length for datasets with rare + long sequences. The pack size calculation accounts for this by clamping + to the model's max_seq_length. + + Args: + config: Training configuration with dataset and model paths + train_file: Path to the prepared training JSONL file. When provided + this file is used directly; otherwise falls back to + ``config.dataset.path / "train.jsonl"``. + max_samples: Maximum number of samples to analyze (default: 1000) + seed: Random seed for reproducible sampling (default: 1111) + trust_remote_code: Whether to trust remote code (default: False) + + Returns: + PackingEstimate with pack_size and statistics, or None if estimation fails + """ + + try: + if train_file is None: + train_file = Path(config.dataset.path) / "train.jsonl" + + if not train_file.exists(): + logger.warning(f"Training file not found: {train_file}") + return None + + # Import here to avoid ModuleNotFoundError in environments where + # transformers is not installed (e.g., during test collection) + from transformers import AutoTokenizer + + # Load tokenizer from model + tokenizer = AutoTokenizer.from_pretrained( + config.model.path, + trust_remote_code=trust_remote_code, + ) + + random.seed(seed) + + # Sample examples to estimate lengths + lengths = _sample_sequence_lengths(train_file, tokenizer, max_samples) + + if not lengths: + logger.warning("Could not estimate sequence lengths from dataset") + return None + + avg_length = _ceil_even(int(sum(lengths) / len(lengths))) + max_length = _ceil_even(max(lengths)) + + # Calculate optimal pack size + pack_size = calculate_optimal_pack_size(config, avg_length, max_length) + packing_factor = pack_size / avg_length if avg_length > 0 else 1.0 + + estimate = PackingEstimate( + pack_size=pack_size, + avg_seq_length=avg_length, + max_seq_length=max_length, + packing_factor=round(packing_factor, 2), + samples_analyzed=len(lengths), + ) + + logger.info( + f"Packing estimate from {len(lengths)} samples: " + f"pack_size={pack_size}, avg_seq={avg_length}, max_seq={max_length}, " + f"packing_factor={estimate.packing_factor:.2f}" + ) + + return estimate + + except Exception as e: + logger.warning(f"Failed to estimate sequence lengths: {e}") + return None + + +def _sample_sequence_lengths( + train_file: Path, + tokenizer, + max_samples: int, +) -> list[int]: + """ + Sample sequences from a JSONL file and return their tokenized lengths. + + Uses reservoir sampling for unbiased random selection, then tokenizes + each sample to measure its length. For chat format, uses apply_chat_template + to get accurate lengths including role tokens and formatting. + + Args: + train_file: Path to training JSONL file + tokenizer: HuggingFace tokenizer + max_samples: Maximum samples to return + + Returns: + List of sequence lengths (in tokens) + """ + # Reservoir sampling to select samples + samples: list[str] = [] + with open(train_file, "r") as f: + for i, line in enumerate(f): + if i < max_samples: + samples.append(line) + else: + j = random.randint(0, i) + if j < max_samples: + samples[j] = line + + # Tokenize samples to get lengths + lengths = [] + for line in samples: + try: + obj = json.loads(line) + length = _get_sample_token_length(obj, tokenizer) + if length is not None: + lengths.append(length) + except Exception: + # Skip malformed lines + continue + + return lengths + + +def _get_sample_token_length(obj: dict, tokenizer) -> int | None: + """ + Get the tokenized length of a dataset sample. + + For chat format, uses apply_chat_template to accurately measure length + including role tokens, special tokens, and formatting. Falls back to + simple text concatenation for other formats or if chat template fails. + + Args: + obj: Parsed JSON object from dataset + tokenizer: HuggingFace tokenizer + + Returns: + Token count, or None if sample is empty/invalid + """ + # Chat format: use apply_chat_template for accurate length + if "messages" in obj: + messages = obj["messages"] + if messages and hasattr(tokenizer, "apply_chat_template"): + try: + tokens = tokenizer.apply_chat_template( + messages, + add_generation_prompt=False, + tokenize=True, + ) + return len(tokens) + except Exception: + # Fall back to text extraction if chat template fails + pass + + # Fallback: concatenate role + content + parts = [] + for m in messages: + if isinstance(m, dict): + role = m.get("role", "") + content = m.get("content", "") + if role or content: + parts.append(f"{role}: {content}") + text = "\n".join(parts) + if text: + return len(tokenizer.encode(text, add_special_tokens=True)) + return None + + # SFT format: prompt + completion + if "prompt" in obj and "completion" in obj: + text = str(obj["prompt"]) + " " + str(obj["completion"]) + return len(tokenizer.encode(text, add_special_tokens=True)) + + # Generic: concatenate all string values + text = " ".join(str(v) for v in obj.values() if isinstance(v, str)) + if text: + return len(tokenizer.encode(text, add_special_tokens=True)) + return None diff --git a/services/automodel/src/nmp/automodel/tasks/training/templates/llama-3.1-instruct.jinja b/services/automodel/src/nmp/automodel/tasks/training/templates/llama-3.1-instruct.jinja new file mode 100644 index 0000000000..e074cba578 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/templates/llama-3.1-instruct.jinja @@ -0,0 +1,61 @@ +{{- bos_token }} +{%- if not date_string is defined %} + {%- if strftime_now is defined %} + {%- set date_string = strftime_now("%d %b %Y") %} + {%- else %} + {%- set date_string = "26 Jul 2024" %} + {%- endif %} +{%- endif %} +{%- set loop_messages = messages %} +{%- if tools is not none and tool_choice is not none %} + {{- '<|start_header_id|>system<|end_header_id|>\n\n' }} + {{- "Environment: ipython\n\n" }} + {{- "Cutting Knowledge Date: December 2023\n" }} + {{- "Today Date: " + date_string + "\n\n" }} + {{- "You are a helpful assistant.\n" }} + {{- '<|eot_id|>' }} + {{- '<|start_header_id|>user<|end_header_id|>\n\n' }} + {{- 'You have access to the following functions to supplement your existing knowledge:\n\n' }} + {%- for t in tools %} + {%- set tname = t.function.name %} + {%- set tdesc = t.function.description %} + {%- set tparams = t.function.parameters | tojson %} + {{- "Use the function '" + tname + "' to '" + tdesc + "':\n" }} + {{- '{"name": "' + tname + '", "description": "' + tdesc + '", "parameters": ' + tparams + '}\n\n' }} + {%- endfor %} + {{- 'Think very carefully before calling functions.\n' }} + {{- 'Only call them if they are relevant to the prompt.\n' }} + {{- 'If you choose to call a function ONLY reply in the following format with no natural language surrounding it:\n\n' }} + {{- '{"example_name": "example_value"}\n\n' }} + {{- 'Reminder:\n' }} + {{- '- Function calls MUST follow the specified format, start with \n' }} + {{- '- Required parameters MUST be specified\n' }} + {{- '- Only call one function at a time\n' }} + {{- '- Put the entire function call reply on one line\n' }} + {{- '- Do not call functions if they are not relevant to the prompt' }} + {{- '<|eot_id|>' }} +{%- endif %} +{%- for message in loop_messages %} + {%- if message['role'] in ['ipython', 'tool'] %} + {{- "<|start_header_id|>ipython<|end_header_id|>\n\n" }} + {{- "[stdout]" + message['content'] | trim + "[/stdout]\n<|eot_id|>" }} + {%- elif message['role'] == 'assistant'%} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} + {%- if message.get('tool_calls') is not none %} + {%- set tool_call = message['tool_calls'][0] %} + {%- generation %} + {{- '<|python_tag|>' + tool_call.function.arguments | tojson + '\n<|eot_id|>' }} + {%- endgeneration %} + {%- else %} + {%- generation %} + {{- message['content'] | trim + '<|eot_id|>' }} + {%- endgeneration %} + {%- endif %} + {%- else %} + {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' }} + {{- message['content'] | trim + '<|eot_id|>' }} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} +{%- endif %} diff --git a/services/automodel/src/nmp/automodel/tasks/training/templates/llama-3.2-instruct.jinja b/services/automodel/src/nmp/automodel/tasks/training/templates/llama-3.2-instruct.jinja new file mode 100644 index 0000000000..e074cba578 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/templates/llama-3.2-instruct.jinja @@ -0,0 +1,61 @@ +{{- bos_token }} +{%- if not date_string is defined %} + {%- if strftime_now is defined %} + {%- set date_string = strftime_now("%d %b %Y") %} + {%- else %} + {%- set date_string = "26 Jul 2024" %} + {%- endif %} +{%- endif %} +{%- set loop_messages = messages %} +{%- if tools is not none and tool_choice is not none %} + {{- '<|start_header_id|>system<|end_header_id|>\n\n' }} + {{- "Environment: ipython\n\n" }} + {{- "Cutting Knowledge Date: December 2023\n" }} + {{- "Today Date: " + date_string + "\n\n" }} + {{- "You are a helpful assistant.\n" }} + {{- '<|eot_id|>' }} + {{- '<|start_header_id|>user<|end_header_id|>\n\n' }} + {{- 'You have access to the following functions to supplement your existing knowledge:\n\n' }} + {%- for t in tools %} + {%- set tname = t.function.name %} + {%- set tdesc = t.function.description %} + {%- set tparams = t.function.parameters | tojson %} + {{- "Use the function '" + tname + "' to '" + tdesc + "':\n" }} + {{- '{"name": "' + tname + '", "description": "' + tdesc + '", "parameters": ' + tparams + '}\n\n' }} + {%- endfor %} + {{- 'Think very carefully before calling functions.\n' }} + {{- 'Only call them if they are relevant to the prompt.\n' }} + {{- 'If you choose to call a function ONLY reply in the following format with no natural language surrounding it:\n\n' }} + {{- '{"example_name": "example_value"}\n\n' }} + {{- 'Reminder:\n' }} + {{- '- Function calls MUST follow the specified format, start with \n' }} + {{- '- Required parameters MUST be specified\n' }} + {{- '- Only call one function at a time\n' }} + {{- '- Put the entire function call reply on one line\n' }} + {{- '- Do not call functions if they are not relevant to the prompt' }} + {{- '<|eot_id|>' }} +{%- endif %} +{%- for message in loop_messages %} + {%- if message['role'] in ['ipython', 'tool'] %} + {{- "<|start_header_id|>ipython<|end_header_id|>\n\n" }} + {{- "[stdout]" + message['content'] | trim + "[/stdout]\n<|eot_id|>" }} + {%- elif message['role'] == 'assistant'%} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} + {%- if message.get('tool_calls') is not none %} + {%- set tool_call = message['tool_calls'][0] %} + {%- generation %} + {{- '<|python_tag|>' + tool_call.function.arguments | tojson + '\n<|eot_id|>' }} + {%- endgeneration %} + {%- else %} + {%- generation %} + {{- message['content'] | trim + '<|eot_id|>' }} + {%- endgeneration %} + {%- endif %} + {%- else %} + {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' }} + {{- message['content'] | trim + '<|eot_id|>' }} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} +{%- endif %} diff --git a/services/automodel/src/nmp/automodel/tasks/training/templates/llama-3.3-instruct.jinja b/services/automodel/src/nmp/automodel/tasks/training/templates/llama-3.3-instruct.jinja new file mode 100644 index 0000000000..a0ba6017e1 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/templates/llama-3.3-instruct.jinja @@ -0,0 +1,61 @@ +{{- bos_token }} +{%- if not date_string is defined %} + {%- if strftime_now is defined %} + {%- set date_string = strftime_now("%d %b %Y") %} + {%- else %} + {%- set date_string = "26 Jul 2024" %} + {%- endif %} +{%- endif %} +{%- set loop_messages = messages %} +{%- if tools is not none and tool_choice is not none %} + {{- '<|start_header_id|>system<|end_header_id|>\n\n' }} + {{- "Environment: ipython\n\n" }} + {{- "Cutting Knowledge Date: December 2023\n" }} + {{- "Today Date: " + date_string + "\n\n" }} + {{- "You are a helpful assistant.\n" }} + {{- '<|eot_id|>' }} + {{- '<|start_header_id|>user<|end_header_id|>\n\n' }} + {{- 'You have access to the following functions to supplement your existing knowledge:\n\n' }} + {%- for t in tools %} + {%- set tname = t.function.name %} + {%- set tdesc = t.function.description %} + {%- set tparams = t.function.parameters | tojson %} + {{- "Use the function '" + tname + "' to '" + tdesc + "':\n" }} + {{- '{"name": "' + tname + '", "description": "' + tdesc + '", "parameters": ' + tparams + '}\n\n' }} + {%- endfor %} + {{- 'Think very carefully before calling functions.\n' }} + {{- 'Only call them if they are relevant to the prompt.\n' }} + {{- 'If you choose to call a function ONLY reply in the following format with no natural language surrounding it:\n\n' }} + {{- '{"example_name": "example_value"}\n\n' }} + {{- 'Reminder:\n' }} + {{- '- Function calls MUST follow the specified format, start with \n' }} + {{- '- Required parameters MUST be specified\n' }} + {{- '- Only call one function at a time\n' }} + {{- '- Put the entire function call reply on one line\n' }} + {{- '- Do not call functions if they are not relevant to the prompt' }} + {{- '<|eot_id|>' }} +{%- endif %} +{%- for message in loop_messages %} + {%- if message['role'] in ['ipython', 'tool'] %} + {{- "<|start_header_id|>ipython<|end_header_id|>\n\n" }} + {{- "[stdout]" + message['content'] | trim + "[/stdout]\n<|eot_id|>" }} + {%- elif message['role'] == 'assistant'%} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} + {%- if message.get('tool_calls') is not none %} + {%- set tool_call = message['tool_calls'][0] %} + {%- generation %} + {{- '' + tool_call.function.arguments | tojson + '\n<|eot_id|>' }} + {%- endgeneration %} + {%- else %} + {%- generation %} + {{- message['content'] | trim + '<|eot_id|>' }} + {%- endgeneration %} + {%- endif %} + {%- else %} + {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' }} + {{- message['content'] | trim + '<|eot_id|>' }} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} +{%- endif %} diff --git a/services/automodel/src/nmp/automodel/tasks/training/templates/nemotron-3.1.jinja b/services/automodel/src/nmp/automodel/tasks/training/templates/nemotron-3.1.jinja new file mode 100644 index 0000000000..00cfd85e48 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/templates/nemotron-3.1.jinja @@ -0,0 +1,51 @@ +{%- if messages[0]['role'] == 'system' %} + {%- set system_message = messages[0]['content'] | trim %} + {%- set messages = messages[1:] %} +{%- else %} + {%- set system_message = '' %} +{%- endif %} +{%- if tools is not none %} + {{- '<|begin_of_text|><|start_header_id|>system<|end_header_id|>' + '\n\n' + system_message }} + {{- '\n\n' if system_message else '' }} + {{- '[' }} + {%- for t in tools %} + {{- (t.function if t.function is defined else t) | tojson() }} + {{- ', ' if not loop.last else '' }} + {%- endfor %} + {{- ']' }} + {{- '<|eot_id|>' }} +{%- else %} + {{- '<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n' + system_message + '<|eot_id|>' }} +{%- endif %} +{%- for message in messages %} + {%- if (message['role'] in ['user', 'tool']) != (loop.index0 % 2 == 0) %} + {{- raise_exception('Conversation roles must alternate between user/tool and assistant') }} + {%- elif message['role'] == 'user' %} + {{- '<|start_header_id|>user<|end_header_id|>\n\n' + message['content'] | trim + '<|eot_id|>' }} + {%- elif message['role'] == 'tool' %} + {%- set tool_response = '[' + message['content'] | trim + ']' %} + {{- '<|start_header_id|>user<|end_header_id|>\n\n' + tool_response + '<|eot_id|>' }} + {%- elif message['role'] == 'assistant' and message.get('tool_calls') is not none %} + {%- set tool_calls = message['tool_calls'] %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} + {%- generation %} + {{- '['}} + {%- for tool_call in tool_calls %} + {{- '{"name": "' + tool_call.function.name + '", "arguments": ' + tool_call.function.arguments | tojson + '}' }} + {%- if not loop.last %} + {{- ', ' }} + {%- else %} + {{- ']<|eot_id|>' }} + {%- endif %} + {%- endfor %} + {%- endgeneration %} + {%- elif message['role'] == 'assistant' %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} + {%- generation %} + {{- message['content'] | trim + '<|eot_id|>' }} + {%- endgeneration %} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} +{%- endif %} diff --git a/services/automodel/src/nmp/automodel/tasks/training/templates/nemotron-3.3.jinja b/services/automodel/src/nmp/automodel/tasks/training/templates/nemotron-3.3.jinja new file mode 100644 index 0000000000..7530a8c87e --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/templates/nemotron-3.3.jinja @@ -0,0 +1,21 @@ +{{- bos_token }} +{%- if messages[0]['role'] == 'system' %} + {%- set system_message = messages[0]['content']|trim %} + {%- set messages = messages[1:] %} +{%- else %} + {%- set system_message = '' %} +{%- endif %} +{{- '<|start_header_id|>system<|end_header_id|>\n\n' }} +{{- system_message }} +{{- '<|eot_id|>' }} +{%- for message in messages %} + {%- if message['role'] == 'assistant' and '' in message['content'] %} + {%- set content = message['content'].split('')[-1].lstrip() %} + {%- else %} + {%- set content = message['content'] %} + {%- endif %} + {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\\n\\n' + content | trim + '<|eot_id|>' }} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|start_header_id|>assistant<|end_header_id|>\\n\\n' }} +{%- endif %} diff --git a/services/automodel/src/nmp/automodel/tasks/training/templates/nemotron-super-3.3.jinja b/services/automodel/src/nmp/automodel/tasks/training/templates/nemotron-super-3.3.jinja new file mode 100644 index 0000000000..1deec26343 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/templates/nemotron-super-3.3.jinja @@ -0,0 +1,82 @@ +{{- bos_token }} +{%- set ns = namespace(p='', has_tools=False) %} +{%- if tools is not none and tool_choice is not none %} + {%- set ns.has_tools = True %} + {%- set ns.p = ns.p + 'You are an expert in composing functions. You are given a question and a set of possible functions. ' %} + {%- set ns.p = ns.p + 'Based on the question, you will need to make one or more function/tool calls to achieve the purpose. ' %} + {%- set ns.p = ns.p + 'If none of the function can be used, point it out. ' %} + {%- set ns.p = ns.p + 'If the given question lacks the parameters required by the function, also point it out. ' %} + {%- set ns.p = ns.p + 'You should only return the function call in tools call sections. ' %} + {%- set ns.p = ns.p + 'Here is a list of functions in JSON format that you can invoke.\n' %} + {%- set ns.p = ns.p + '[' %} + {%- for tool in tools %} + {%- set function = tool.function %} + {%- set keys = function.keys() | reject('equalto', 'return') | list %} + {%- set ns.p = ns.p + '{"type": "function", "function": {' %} + {%- for key in keys %} + {%- set val = function[key] %} + {%- if val is string %} + {%- set ns.p = ns.p + '"' + key + '": "' + val + '"' %} + {%- else %} + {%- set ns.p = ns.p + '"' + key + '": ' + val|tojson %} + {%- endif %} + {%- if not loop.last %} + {%- set ns.p = ns.p + ', ' %} + {%- endif %} + {%- endfor %} + {%- set ns.p = ns.p + '}}' %} + {%- if not loop.last %} + {%- set ns.p = ns.p + ', ' %} + {%- endif %} + {%- endfor %} + {%- set ns.p = ns.p + ']\n' %} + {%- set ns.p = ns.p + 'If you decide to invoke any of the function(s), put it in the JSON TOOL CALLING format of ' %} + {%- set ns.p = ns.p + '[{"name": "func_name1", "arguments": {"params_name1": "params_value1", "params_name2": "params_value2"}}, ' %} + {%- set ns.p = ns.p + '{"name": "func_name2", "arguments": {"params_name1": "params_value1", "params_name2": "params_value2"}}] ' %} + {%- set ns.p = ns.p + '\n' %} + {%- set ns.p = ns.p + 'You SHOULD NOT include any other information in the response. REMEMBER TO USE JSON TOOL CALLING FORMAT.\n\n' %} +{%- endif %} +{%- for message in messages %} + {%- if message['role'] == 'user' %} + {%- if ns.has_tools %} + {%- if add_generation_prompt and loop.index0 == ((messages | length) - 1) %} + {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' + ns.p + (message['content'] | trim) + '<|eot_id|>' }} + {%- elif not add_generation_prompt and loop.index0 == ((messages | length) - 2) %} + {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' + ns.p + (message['content'] | trim) + '<|eot_id|>' }} + {%- else %} + {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' + (message['content'] | trim) + '<|eot_id|>' }} + {%- endif %} + {%- else %} + {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' + (message['content'] | trim) + '<|eot_id|>' }} + {%- endif %} + {%- elif message['role'] in ['ipython', 'tool'] %} + {{- '<|start_header_id|>user<|end_header_id|>\n\n' }} + {{- 'Here are the results from the tool:' + (message['content'] | trim) + '<|eot_id|>' }} + {%- elif message['role'] == 'assistant' %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} + {%- generation %} + {%- if message.get('tool_calls') is not none %} + {{- '[' }} + {%- for tool_call in message['tool_calls'] %} + {{- '{"name": "' + tool_call.function.name + '", "arguments": ' + tool_call.function.arguments | tojson }} + {%- if tool_call.get('id') is not none %} + {{- ', "id": "' + tool_call.id + '"' }} + {%- endif %} + {{- '}' }} + {%- if not loop.last %} + {{- ', ' }} + {%- endif %} + {%- endfor %} + {{- ']' }} + {{- '<|eot_id|>' }} + {%- else %} + {{- message['content'] | trim + '<|eot_id|>' }} + {%- endif %} + {%- endgeneration %} + {%- else %} + {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' + (message['content'] | trim) + '<|eot_id|>' }} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} +{%- endif %} diff --git a/services/automodel/src/nmp/automodel/tasks/training/templates/phi-4.jinja b/services/automodel/src/nmp/automodel/tasks/training/templates/phi-4.jinja new file mode 100644 index 0000000000..33a466f884 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/templates/phi-4.jinja @@ -0,0 +1,15 @@ +{%- for message in messages %} + {%- if (message['role'] == 'system') %} + {{- '<|im_start|>system<|im_sep|>' + message['content'] + '<|im_end|>'}} + {%- elif (message['role'] == 'user') %} + {{-'<|im_start|>user<|im_sep|>' + message['content'] + '<|im_end|>'}} + {%- elif (message['role'] == 'assistant') %} + {{- '<|im_start|>assistant<|im_sep|>' }} + {%- generation %} + {{- message['content'] + '<|im_end|>'}} + {%- endgeneration %} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|im_start|>assistant<|im_sep|>' }} +{%- endif %} diff --git a/services/automodel/src/nmp/automodel/tasks/training/utils.py b/services/automodel/src/nmp/automodel/tasks/training/utils.py new file mode 100644 index 0000000000..afbf73f970 --- /dev/null +++ b/services/automodel/src/nmp/automodel/tasks/training/utils.py @@ -0,0 +1,93 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import logging +import os + +from nmp.common.jobs.constants import NEMO_JOB_ID_ENVVAR + +from .schemas import GPUInfo + +logger = logging.getLogger(__name__) + + +def _get_architecture_name(major: int, minor: int) -> str: + """Map CUDA compute capability to architecture name. + + https://developer.nvidia.com/cuda-gpus + """ + if major == 3: + return "Kepler" + if major == 5: + return "Maxwell" + if major == 6: + return "Pascal" + if major == 7: + # 7.0/7.2 = Volta, 7.5 = Turing + if minor >= 5: + return "Turing" + return "Volta" + if major == 8: + return "Ampere" + if major == 9: + return "Hopper" + if major == 10: + return "Blackwell" + return f"Unknown (sm_{major}{minor})" + + +def get_gpu_info() -> GPUInfo | None: + """Capture GPU architecture information.""" + try: + import torch + + if not torch.cuda.is_available(): + return None + + device_id = torch.cuda.current_device() + props = torch.cuda.get_device_properties(device_id) + major, minor = torch.cuda.get_device_capability(device_id) + + return GPUInfo( + architecture=_get_architecture_name(major, minor), + device_name=props.name, + memory_gb=props.total_memory / (1024**3), + cuda_version=str(torch.version.cuda), + ) + except Exception as e: + logger.warning(f"Failed to capture GPU info: {e}") + return None + + +def generate_torchrun_flags_from_env() -> list[str]: + """Generate torchrun flags for distributed training.""" + # These values are typically injected by the Volcano/PyTorch operator + # or the Core Jobs Service when using DistributedGPUExecutionProvider. + master_addr = os.environ.get("MASTER_ADDR", "localhost") + master_port = os.environ.get("MASTER_PORT", "23456") # Default to port from volcano_job.py + node_rank = os.environ.get("NODE_RANK", os.environ.get("RANK", "0")) + num_nodes = os.environ.get("WORLD_SIZE", "1") + gpus_per_node = os.environ.get("GPUS_PER_NODE") + if gpus_per_node is None: + try: + import torch + + gpus_per_node = str(torch.cuda.device_count()) + except Exception as e: + logger.warning(f"Failed to determine number of GPUs: {e}, using default of 1") + gpus_per_node = "1" + + return [ + "--nnodes", + num_nodes, + "--nproc_per_node", + gpus_per_node, + "--node_rank", + node_rank, + "--rdzv_id", + os.environ.get(NEMO_JOB_ID_ENVVAR, "customizer-rdzv"), + "--rdzv_backend", + "c10d", + "--rdzv_endpoint", + f"{master_addr}:{master_port}", + ] diff --git a/services/automodel/tests/test_adapter.py b/services/automodel/tests/test_adapter.py new file mode 100644 index 0000000000..dfd1f44b45 --- /dev/null +++ b/services/automodel/tests/test_adapter.py @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from nmp.automodel.adapter import automodel_spec_to_compiler_output +from nmp.automodel.api.v2.jobs.schemas import DistillationTraining, SFTTraining + + +def test_adapter_sft() -> None: + spec = automodel_spec_to_compiler_output( + { + "model": "meta/llama", + "dataset": {"training": "default/train"}, + "training": {"training_type": "sft", "finetuning_type": "lora"}, + "output": {"name": "out", "type": "adapter", "fileset": "out-fs"}, + }, + ) + assert isinstance(spec.training, SFTTraining) + assert spec.dataset == "default/train" + + +def test_adapter_distillation() -> None: + spec = automodel_spec_to_compiler_output( + { + "model": "meta/llama", + "dataset": {"training": "default/train"}, + "training": { + "training_type": "distillation", + "finetuning_type": "all_weights", + "teacher_model": "meta/teacher", + }, + "output": {"name": "out", "type": "model", "fileset": "out-fs"}, + }, + ) + assert isinstance(spec.training, DistillationTraining) + assert spec.training.teacher_model == "meta/teacher" diff --git a/services/automodel/tests/test_compiler.py b/services/automodel/tests/test_compiler.py new file mode 100644 index 0000000000..6805b61112 --- /dev/null +++ b/services/automodel/tests/test_compiler.py @@ -0,0 +1,151 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import json +from datetime import datetime +from pathlib import Path +from unittest.mock import AsyncMock, Mock + +import pytest +from nemo_platform import AsyncNeMoPlatform +from nemo_platform.types.models.model_entity import ModelEntity +from nmp.automodel.adapter import automodel_spec_to_compiler_output +from nmp.automodel.api.v2.jobs.schemas import CustomizationJobOutput, LoRAParams, OutputResponse, SFTTraining +from nmp.automodel.app.jobs.compiler import _build_file_download_config +from nmp.automodel.compile import platform_job_config_compiler +from nmp.automodel.images import DEFAULT_AUTOMODEL_IMAGE_REGISTRY, TASKS_IMAGE_NAME, TRAINING_IMAGE_NAME +from nmp.common.entities.utils import get_random_id +from nmp.common.jobs.exceptions import PlatformJobCompilationError + + +def _make_mock_model_entity( + workspace: str = "default", + name: str = "test-target", + fileset: str | None = "default/base-model", +) -> ModelEntity: + return ModelEntity( + id=get_random_id("model"), + workspace=workspace, + name=name, + fileset=fileset, + trust_remote_code=False, + finetuning_type=None, + created_at=datetime.now(), + updated_at=datetime.now(), + ) + + +@pytest.fixture +def mock_sdk(): + sdk = Mock(spec=AsyncNeMoPlatform) + sdk.models = Mock() + sdk.models.retrieve = AsyncMock( + side_effect=lambda name, workspace, verbose=True: _make_mock_model_entity(workspace=workspace, name=name), + ) + sdk.files = Mock() + sdk.files.filesets = Mock() + sdk.files.filesets.retrieve = AsyncMock(return_value=Mock()) + return sdk + + +def _make_job_output() -> CustomizationJobOutput: + return CustomizationJobOutput( + model="default/test-target", + dataset="default/my-dataset", + training=SFTTraining( + peft=LoRAParams(rank=8, alpha=32, merge=False), + learning_rate=1e-4, + batch_size=4, + micro_batch_size=1, + max_seq_length=2048, + ), + output=OutputResponse(name="out", type="adapter", fileset="out-fs"), + ) + + +def test_build_file_download_config_rejects_missing_model_fileset() -> None: + with pytest.raises(PlatformJobCompilationError, match="has no fileset"): + _build_file_download_config(_make_job_output(), _make_mock_model_entity(fileset=None)) + + +@pytest.mark.asyncio +async def test_platform_job_config_compiler_sft_lora(mock_sdk, monkeypatch): + monkeypatch.setattr( + "nmp.automodel.app.jobs.compiler.fetch_model_entity", + AsyncMock(return_value=_make_mock_model_entity()), + ) + contract_dir = Path(__file__).resolve().parents[3] / "tests" / "customizer-automodel-contract" / "input_configs" + input_path = contract_dir / "llama-3.2-1b" / "llama_3_2_1b_lora.json" + if not input_path.exists(): + pytest.skip("contract configs not present") + + raw = json.loads(input_path.read_text()) + plugin_shape = { + "model": raw["model"]["path"], + "dataset": {"training": "default/train-data"}, + "training": { + "training_type": "sft", + "finetuning_type": "lora", + "lora": { + "rank": raw["training"]["lora"]["rank"], + "alpha": raw["training"]["lora"]["alpha"], + "merge": False, + }, + "max_seq_length": raw["model"]["max_seq_length"], + }, + "schedule": { + "epochs": raw["schedule"]["epochs"], + "max_steps": raw["schedule"]["max_steps"], + }, + "batch": { + "global_batch_size": raw["batch"]["global_batch_size"], + "micro_batch_size": raw["batch"]["micro_batch_size"], + }, + "optimizer": {"learning_rate": raw["optimizer"]["learning_rate"]}, + "parallelism": { + "num_nodes": raw["parallelism"]["num_nodes"], + "num_gpus_per_node": raw["parallelism"]["num_gpus_per_node"], + "tensor_parallel_size": raw["parallelism"]["tensor_parallel_size"], + }, + "output": {"name": "test-out", "type": "adapter", "fileset": "test-out-fs"}, + } + compiler_spec = automodel_spec_to_compiler_output(plugin_shape) + spec = await platform_job_config_compiler(compiler_spec, "default", mock_sdk) + + steps = spec.steps if hasattr(spec, "steps") else spec["steps"] + assert len(steps) == 4 + training_step = steps[1] + training_name = training_step.name if hasattr(training_step, "name") else training_step["name"] + assert training_name == "customization-training-job" + training_cmd = ( + training_step.executor.container.command + if hasattr(training_step, "executor") + else training_step["executor"]["container"]["command"] + ) + assert "nmp.automodel.tasks.training" in " ".join(training_cmd) + download_cmd = ( + steps[0].executor.container.command + if hasattr(steps[0], "executor") + else steps[0]["executor"]["container"]["command"] + ) + assert download_cmd[-1] == "nmp.automodel.tasks.file_io" + download_entrypoint = ( + steps[0].executor.container.entrypoint + if hasattr(steps[0], "executor") + else steps[0]["executor"]["container"]["entrypoint"] + ) + assert download_entrypoint == ["/opt/venv/bin/python"] + + def _step_image(step) -> str: + if hasattr(step, "executor"): + return step.executor.container.image + return step["executor"]["container"]["image"] + + tasks_image = f"{DEFAULT_AUTOMODEL_IMAGE_REGISTRY}/{TASKS_IMAGE_NAME}" + training_image = f"{DEFAULT_AUTOMODEL_IMAGE_REGISTRY}/{TRAINING_IMAGE_NAME}" + assert _step_image(steps[0]).startswith(tasks_image) + assert _step_image(steps[1]).startswith(training_image) + assert _step_image(steps[2]).startswith(tasks_image) + assert _step_image(steps[3]).startswith(tasks_image) diff --git a/services/automodel/tests/test_contract_configs.py b/services/automodel/tests/test_contract_configs.py new file mode 100644 index 0000000000..0d808033fb --- /dev/null +++ b/services/automodel/tests/test_contract_configs.py @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Contract continuity: compile_automodel_config import path and optional snapshot check.""" + +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[3] +CONTRACT_DIR = REPO_ROOT / "tests" / "customizer-automodel-contract" +GENERATE_SCRIPT = CONTRACT_DIR / "generate_configs.py" + +# v1 excludes embedding SFT until product expands scope. +EMBEDDING_CONFIG_STEMS = {"embed_1b_lora", "embed_1b_full_sft"} + + +@pytest.mark.skipif(not CONTRACT_DIR.is_dir(), reason="contract fixtures not in tree") +def test_generate_configs_import_path() -> None: + """generate_configs.py must import compile_automodel_config from backends.config.""" + text = GENERATE_SCRIPT.read_text() + assert "backends.config import compile_automodel_config" in text + assert "backends.automodel.config" not in text + + +@pytest.mark.skipif(not CONTRACT_DIR.is_dir(), reason="contract fixtures not in tree") +@pytest.mark.parametrize( + "config_name", + [ + "llama_3_2_1b_lora", + "llama_3_2_1b_lora_packing", + "nemotron_nano_lora_packing", + ], +) +def test_contract_input_parses_as_training_step_config(config_name: str) -> None: + from nmp.automodel.tasks.training.schemas import TrainingStepConfig + + input_path = CONTRACT_DIR / "input_configs" / "llama-3.2-1b" / f"{config_name}.json" + if config_name.startswith("nemotron"): + input_path = CONTRACT_DIR / "input_configs" / "nemotron-nano" / f"{config_name}.json" + if not input_path.exists(): + pytest.skip(f"missing {input_path}") + + raw = json.loads(input_path.read_text()) + raw.pop("backend", None) + TrainingStepConfig.model_validate(raw) + + +@pytest.mark.skipif(not CONTRACT_DIR.is_dir(), reason="contract fixtures not in tree") +def test_contract_output_configs_up_to_date_excluding_embedding() -> None: + """Run generate_configs --check when nemo_automodel is available in the environment.""" + pytest.importorskip("nemo_automodel") + if not GENERATE_SCRIPT.is_file(): + pytest.skip("generate_configs.py missing") + + env = dict(**__import__("os").environ) + env["PYTHONPATH"] = str(REPO_ROOT / "services" / "automodel" / "src") + + result = subprocess.run( + [sys.executable, str(GENERATE_SCRIPT), "--check"], + cwd=CONTRACT_DIR, + env=env, + capture_output=True, + text=True, + ) + if result.returncode != 0: + combined = result.stdout + result.stderr + for stem in EMBEDDING_CONFIG_STEMS: + if stem in combined: + pytest.skip("contract check failed on embedding configs (excluded from v1)") + if "nemo_automodel" in combined and "ModuleNotFoundError" in combined: + pytest.skip("nemo_automodel not installed in test env (run in training image CI)") + pytest.fail(f"contract configs out of date:\n{combined}") diff --git a/services/automodel/tests/test_images.py b/services/automodel/tests/test_images.py new file mode 100644 index 0000000000..4a633491ec --- /dev/null +++ b/services/automodel/tests/test_images.py @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from nmp.automodel.config import AutomodelConfig +from nmp.automodel.images import ( + DEFAULT_AUTOMODEL_IMAGE_REGISTRY, + TASKS_IMAGE_NAME, + TRAINING_IMAGE_NAME, + get_automodel_qualified_image, + get_tasks_image, + get_training_image, +) + + +def test_default_automodel_images_use_nvcr_dev_registry(monkeypatch): + monkeypatch.setattr("nmp.automodel.images.config", AutomodelConfig()) + + tasks = get_tasks_image() + training = get_training_image() + + assert tasks == f"{DEFAULT_AUTOMODEL_IMAGE_REGISTRY}/{TASKS_IMAGE_NAME}:local" + assert training == f"{DEFAULT_AUTOMODEL_IMAGE_REGISTRY}/{TRAINING_IMAGE_NAME}:local" + assert TASKS_IMAGE_NAME.count("/") == 0 # NVCR: single repo segment, no nested paths + + +def test_automodel_image_registry_override(monkeypatch): + monkeypatch.setattr( + "nmp.automodel.images.config", + AutomodelConfig(image_registry="nvcr.io/0921617854601259/other-registry"), + ) + + assert ( + get_automodel_qualified_image(TASKS_IMAGE_NAME) + == "nvcr.io/0921617854601259/other-registry/nmp-automodel-tasks:local" + ) + + +def test_automodel_full_image_override(monkeypatch): + monkeypatch.setattr( + "nmp.automodel.images.config", + AutomodelConfig( + tasks_image="nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-tasks:dev", + ), + ) + + assert get_tasks_image() == "nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-tasks:dev" diff --git a/services/automodel/tests/test_job_context.py b/services/automodel/tests/test_job_context.py new file mode 100644 index 0000000000..55efea6a11 --- /dev/null +++ b/services/automodel/tests/test_job_context.py @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for NMPJobContext.""" + +from pathlib import Path + +import pytest +from nmp.automodel.app.constants import DEFAULT_JOB_STORAGE_PATH, NMP_FILES_URL_ENVVAR, NMP_JOBS_URL_ENVVAR +from nmp.automodel.app.jobs.context import ( + DEFAULT_ATTEMPT_ID, + DEFAULT_JOB_ID, + DEFAULT_STEP, + DEFAULT_TASK, + NMPJobContext, +) +from nmp.common.entities.constants import DEFAULT_WORKSPACE +from nmp.common.jobs.constants import ( + DEFAULT_NEMO_JOB_STEP_CONFIG_FILE_PATH, + NEMO_JOB_ATTEMPT_ID_ENVVAR, + NEMO_JOB_ID_ENVVAR, + NEMO_JOB_STEP_CONFIG_FILE_PATH_ENVVAR, + NEMO_JOB_STEP_ENVVAR, + NEMO_JOB_TASK_ENVVAR, + NEMO_JOB_WORKSPACE_ENVVAR, + PERSISTENT_JOB_STORAGE_PATH_ENVVAR, +) + + +class TestNMPJobContextFromEnv: + def test_uses_defaults_when_env_vars_not_set(self, monkeypatch: pytest.MonkeyPatch) -> None: + for var in ( + NEMO_JOB_WORKSPACE_ENVVAR, + NEMO_JOB_ID_ENVVAR, + NEMO_JOB_ATTEMPT_ID_ENVVAR, + NEMO_JOB_STEP_ENVVAR, + NEMO_JOB_TASK_ENVVAR, + NMP_JOBS_URL_ENVVAR, + NMP_FILES_URL_ENVVAR, + PERSISTENT_JOB_STORAGE_PATH_ENVVAR, + NEMO_JOB_STEP_CONFIG_FILE_PATH_ENVVAR, + ): + monkeypatch.delenv(var, raising=False) + + ctx = NMPJobContext.from_env() + + assert ctx.workspace == DEFAULT_WORKSPACE + assert ctx.job_id == DEFAULT_JOB_ID + assert ctx.attempt_id == DEFAULT_ATTEMPT_ID + assert ctx.step == DEFAULT_STEP + assert ctx.task == DEFAULT_TASK + assert ctx.jobs_url is None + assert ctx.files_url is None + assert ctx.storage_path == Path(DEFAULT_JOB_STORAGE_PATH) + assert ctx.config_path == Path(DEFAULT_NEMO_JOB_STEP_CONFIG_FILE_PATH) + + def test_uses_env_vars_when_set(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv(NEMO_JOB_WORKSPACE_ENVVAR, "test-workspace") + monkeypatch.setenv(NEMO_JOB_ID_ENVVAR, "job-123") + monkeypatch.setenv(NEMO_JOB_ATTEMPT_ID_ENVVAR, "attempt-5") + monkeypatch.setenv(NEMO_JOB_STEP_ENVVAR, "training") + monkeypatch.setenv(NEMO_JOB_TASK_ENVVAR, "train-model") + monkeypatch.setenv(NMP_JOBS_URL_ENVVAR, "http://jobs.example.com") + monkeypatch.setenv(NMP_FILES_URL_ENVVAR, "http://files.example.com") + monkeypatch.setenv(PERSISTENT_JOB_STORAGE_PATH_ENVVAR, "/custom/storage") + monkeypatch.setenv(NEMO_JOB_STEP_CONFIG_FILE_PATH_ENVVAR, "/custom/config.json") + + ctx = NMPJobContext.from_env() + + assert ctx.workspace == "test-workspace" + assert ctx.job_id == "job-123" + assert ctx.normalized_task == "task-train-model" + assert ctx.jobs_url == "http://jobs.example.com" diff --git a/services/automodel/tests/test_platform_client.py b/services/automodel/tests/test_platform_client.py new file mode 100644 index 0000000000..c33b52cab2 --- /dev/null +++ b/services/automodel/tests/test_platform_client.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from nmp.automodel.app.jobs.file_io.schemas import FileSetRef + + +def test_fileset_ref_parse() -> None: + ref = FileSetRef.model_validate("acme-corp/my-dataset") + assert ref.workspace == "acme-corp" + assert ref.name == "my-dataset" + + bare = FileSetRef.model_validate("my-dataset") + assert bare.workspace is None + assert bare.name == "my-dataset" diff --git a/services/automodel/tests/test_progress_reporter.py b/services/automodel/tests/test_progress_reporter.py new file mode 100644 index 0000000000..ab7fc77894 --- /dev/null +++ b/services/automodel/tests/test_progress_reporter.py @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import MagicMock + +from nemo_platform import omit +from nmp.automodel.app.jobs.context import NMPJobContext +from nmp.automodel.tasks.progress_reporter import JobsServiceProgressReporter +from nmp.common.jobs.schemas import PlatformJobStatus + + +def test_progress_reporter_calls_sdk_create_or_update() -> None: + sdk = MagicMock() + ctx = NMPJobContext( + workspace="ws-a", + job_id="job-1", + attempt_id="attempt-0", + step="training", + task="train-model", + jobs_url="http://jobs.example.com", + files_url=None, + storage_path=Path("/tmp/job"), + config_path=Path("/tmp/job/config.json"), + ) + reporter = JobsServiceProgressReporter(sdk, ctx.workspace, ctx.job_id, ctx.step, ctx.normalized_task) + reporter.update_progress(PlatformJobStatus.ACTIVE, status_details={"phase": "training"}) + + sdk.jobs.tasks.create_or_update.assert_called_once_with( + ctx.normalized_task, + workspace=ctx.workspace, + job=ctx.job_id, + step=ctx.step, + status=PlatformJobStatus.ACTIVE.value, + status_details={"phase": "training"}, + error_details=omit, + error_stack=omit, + ) diff --git a/services/automodel/tests/test_validators.py b/services/automodel/tests/test_validators.py new file mode 100644 index 0000000000..a3f904d89b --- /dev/null +++ b/services/automodel/tests/test_validators.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from nmp.automodel.entities.validators import validate_fileset_uri + + +def test_validate_fileset_workspace_name() -> None: + assert validate_fileset_uri("acme-corp/train-data") == "acme-corp/train-data" + + +def test_validate_fileset_bare_name() -> None: + assert validate_fileset_uri("train-data") == "train-data" + + +def test_validate_strips_legacy_fileset_prefix() -> None: + assert validate_fileset_uri("fileset://acme-corp/train-data") == "acme-corp/train-data" + + +def test_validate_rejects_hf_protocol() -> None: + with pytest.raises(ValueError, match="Unsupported"): + validate_fileset_uri("hf://org/dataset") diff --git a/services/core/entities/config/local.env b/services/core/entities/config/local.env index 7283e036f9..1fb53eda2e 100644 --- a/services/core/entities/config/local.env +++ b/services/core/entities/config/local.env @@ -1,9 +1,4 @@ NMP_IMAGE_REGISTRY=my-registry NMP_IMAGE_TAG=local -DATABASE_NAME=entities -DATABASE_DIALECT=postgresql -DATABASE_USER=nmp -DATABASE_PASSWORD=nmp -DATABASE_HOST=localhost -DATABASE_PORT=5432 - +DATABASE_DIALECT=sqlite +DATABASE_PATH="${HOME}/.local/share/nemo/nmp-platform.db" diff --git a/services/core/jobs/src/nmp/core/jobs/config.py b/services/core/jobs/src/nmp/core/jobs/config.py index ce2d30ee83..3c71a0fbd6 100644 --- a/services/core/jobs/src/nmp/core/jobs/config.py +++ b/services/core/jobs/src/nmp/core/jobs/config.py @@ -5,7 +5,7 @@ from typing import Self -from nmp.common.config import create_service_config_class, get_platform_config, get_service_config +from nmp.common.config import Runtime, create_service_config_class, get_platform_config, get_service_config from nmp.core.jobs.app.profiles import ExecutionProfileT from nmp.core.jobs.controllers.backends.config import ( DefaultExecutionProfileConfig, @@ -30,6 +30,19 @@ class JobsServiceConfig(create_service_config_class("jobs")): # type: ignore ) reconcile_interval_seconds: int = Field(default=2, description="Interval in seconds for the job reconciler to run") schedule_interval_seconds: int = Field(default=5, description="Interval in seconds for the job scheduler to run") + enable_subprocess_executor: bool | None = Field( + default=None, + description=( + "Register the subprocess/default execution profile. When unset, defaults to true for " + "docker/none runtimes and false for kubernetes." + ), + ) + + def resolved_enable_subprocess_executor(self) -> bool: + """Whether host subprocess execution is registered for default profiles.""" + if self.enable_subprocess_executor is not None: + return self.enable_subprocess_executor + return get_platform_config().runtime != Runtime.KUBERNETES @model_validator(mode="after") def validate_executors(self) -> Self: @@ -55,5 +68,6 @@ def validate_executors(self) -> Self: get_default_executor_profiles_for_runtime( runtime=get_platform_config().runtime, defaults=config.executor_defaults, + enable_subprocess_executor=config.resolved_enable_subprocess_executor(), ), ) diff --git a/services/core/jobs/src/nmp/core/jobs/controllers/backends/config.py b/services/core/jobs/src/nmp/core/jobs/controllers/backends/config.py index 0e137b8ad6..f5d4724fae 100644 --- a/services/core/jobs/src/nmp/core/jobs/controllers/backends/config.py +++ b/services/core/jobs/src/nmp/core/jobs/controllers/backends/config.py @@ -41,8 +41,14 @@ class DefaultExecutionProfileConfig(BaseModel): ) -def get_default_executor_profiles_for_runtime(runtime: Runtime, defaults: DefaultExecutionProfileConfig) -> list: +def get_default_executor_profiles_for_runtime( + runtime: Runtime, + defaults: DefaultExecutionProfileConfig, + enable_subprocess_executor: bool | None = None, +) -> list: """Returns a list of default executor profiles based on the deployment runtime.""" + if enable_subprocess_executor is None: + enable_subprocess_executor = runtime != Runtime.KUBERNETES logger.debug("Getting default executors for runtime: %s", runtime) executors = [] @@ -87,9 +93,7 @@ def get_default_executor_profiles_for_runtime(runtime: Runtime, defaults: Defaul ] ) - # Subprocess execution is available for single-host runtimes only. Kubernetes deployments must opt in - # explicitly so subprocess profiles do not appear on distributed service pods by default. - if runtime != Runtime.KUBERNETES: + if enable_subprocess_executor: executors.append( SubprocessJobExecutionProfile( provider="subprocess", diff --git a/services/core/models/pyproject.toml b/services/core/models/pyproject.toml index c58a342fcf..203ddb50e4 100644 --- a/services/core/models/pyproject.toml +++ b/services/core/models/pyproject.toml @@ -37,8 +37,8 @@ packages = ["src/nmp"] [dependency-groups] # No task deps for models — `nmp.core.models.parallelism` is the only consumer of # torch/transformers/accelerate, and it's only invoked from the `model_spec` batch -# task. That task runs in the `customizer-tasks` image, which installs torch -# itself; parallelism tests in this repo guard with `pytest.importorskip("torch")`. +# task. That task runs in the `nmp-automodel-tasks` image (PyTorch from +# nmp-automodel-base); parallelism tests in this repo guard with `pytest.importorskip("torch")`. dev = [ "pytest>=8.3.4", diff --git a/services/core/models/src/nmp/core/models/api/v2/models.py b/services/core/models/src/nmp/core/models/api/v2/models.py index 1dbbfe0e5f..4c59d3966c 100644 --- a/services/core/models/src/nmp/core/models/api/v2/models.py +++ b/services/core/models/src/nmp/core/models/api/v2/models.py @@ -270,8 +270,9 @@ async def start_update_model_spec_job(model_entity: ModelEntity): executor=CPUExecutionProviderSpec( provider="cpu", container=ContainerSpec( - image=get_qualified_image("customizer-tasks"), - command=["nemo-platform", "run", "task", "--task", "nmp.core.models.tasks.model_spec"], + image=get_qualified_image("nmp-automodel-tasks"), + entrypoint=["/opt/venv/bin/python"], + command=["-m", "nmp.core.models.tasks.model_spec"], ), resources=ResourcesSpec( requests=ResourcesRequestsSpec( diff --git a/tests/customizer-automodel-contract/generate_configs.py b/tests/customizer-automodel-contract/generate_configs.py index 49979217dd..65a8b9a567 100644 --- a/tests/customizer-automodel-contract/generate_configs.py +++ b/tests/customizer-automodel-contract/generate_configs.py @@ -48,17 +48,20 @@ SCRIPT_DIR = Path(__file__).resolve().parent REPO_ROOT = SCRIPT_DIR.parent.parent +AUTOMODEL_SRC = REPO_ROOT / "services" / "automodel" / "src" CUSTOMIZER_SRC = REPO_ROOT / "services" / "customizer" / "src" -if CUSTOMIZER_SRC.is_dir(): +if AUTOMODEL_SRC.is_dir(): + sys.path.insert(0, str(AUTOMODEL_SRC)) +elif CUSTOMIZER_SRC.is_dir(): sys.path.insert(0, str(CUSTOMIZER_SRC)) else: - sys.path.insert(0, "/app/services/customizer/src") + sys.path.insert(0, "/app/services/automodel/src") -from nmp.customizer.app.constants import V4_MODEL_FOR_CAUSAL_LM_MAPPING_NAMES # noqa: E402 -from nmp.customizer.app.jobs.context import NMPJobContext # noqa: E402 -from nmp.customizer.tasks.training.backends.automodel.config import compile_automodel_config # noqa: E402 -from nmp.customizer.tasks.training.schemas import TrainingStepConfig # noqa: E402 +from nmp.automodel.app.constants import V4_MODEL_FOR_CAUSAL_LM_MAPPING_NAMES # noqa: E402 +from nmp.automodel.app.jobs.context import NMPJobContext # noqa: E402 +from nmp.automodel.tasks.training.backends.config import compile_automodel_config # noqa: E402 +from nmp.automodel.tasks.training.schemas import TrainingStepConfig # noqa: E402 INPUT_DIR = SCRIPT_DIR / "input_configs" OUTPUT_DIR = SCRIPT_DIR / "output_configs" diff --git a/tests/smoke_gpu/conftest.py b/tests/smoke_gpu/conftest.py index ae32d9c193..6e0c602ba3 100644 --- a/tests/smoke_gpu/conftest.py +++ b/tests/smoke_gpu/conftest.py @@ -9,3 +9,9 @@ def pytest_configure(config): "markers", "smoke_customizer_automodel: Import smoke tests for the customizer-automodel image" ) config.addinivalue_line("markers", "smoke_customizer_rl: Import smoke tests for the customizer-rl image") + config.addinivalue_line( + "markers", "smoke_nmp_automodel_tasks: Import smoke tests for the nmp/automodel-tasks image" + ) + config.addinivalue_line( + "markers", "smoke_nmp_automodel_training: Import smoke tests for the nmp/automodel-training image" + ) diff --git a/tests/smoke_gpu/test_nemo_automodel.py b/tests/smoke_gpu/test_nemo_automodel.py new file mode 100644 index 0000000000..f9d8063874 --- /dev/null +++ b/tests/smoke_gpu/test_nemo_automodel.py @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""NeMo Automodel image import smoke tests. + +Built as part of the nmp-automodel docker bake group (smoke-test stage) and run +on a CPU runner - no GPU hardware required. +""" + +import pytest + + +def test_torch_importable(): + import torch # noqa: F401 + + +def test_transformers_importable(): + import transformers # noqa: F401 + + +def test_mamba_ssm_importable(): + import mamba_ssm # noqa: F401 + + +def test_causal_conv1d_importable(): + import causal_conv1d # noqa: F401 + + +def test_bitsandbytes_importable(): + import bitsandbytes # noqa: F401 + + +@pytest.mark.smoke_nmp_automodel_tasks +def test_nmp_automodel_tasks_importable(): + from nmp.automodel.tasks import file_io # noqa: F401 + from nmp.automodel.tasks.model_entity import __main__ as model_entity_main # noqa: F401 + from nmp.core.models.tasks.model_spec import __main__ as model_spec_main # noqa: F401 + + +@pytest.mark.smoke_nmp_automodel_training +def test_nmp_automodel_training_importable(): + import nemo_automodel # noqa: F401 + from nmp.automodel.tasks.training import __main__ as training_main # noqa: F401 diff --git a/third_party/licenses.jsonl b/third_party/licenses.jsonl index 04e4a3d3d8..bbb1612417 100644 --- a/third_party/licenses.jsonl +++ b/third_party/licenses.jsonl @@ -256,6 +256,7 @@ {"name": "ruff", "license": "MIT", "compatible": true} {"name": "s3transfer", "license": "APACHE-2.0", "compatible": true} {"name": "sacrebleu", "license": "APACHE-2.0", "compatible": true} +{"name": "safetensors", "license": "APACHE-2.0", "compatible": true} {"name": "scikit-network", "license": "BSD-3-CLAUSE", "compatible": true} {"name": "scipy", "license": "BSD-3-CLAUSE", "compatible": true} {"name": "secretstorage", "license": "BSD-3-CLAUSE", "compatible": true} @@ -285,6 +286,7 @@ {"name": "tomlkit", "license": "MIT", "compatible": true} {"name": "tornado", "license": "APACHE-2.0", "compatible": true} {"name": "tqdm", "license": "MIT", "compatible": true} +{"name": "transformers", "license": "APACHE-2.0", "compatible": true} {"name": "typer", "license": "MIT", "compatible": true} {"name": "types-aioboto3", "license": "MIT", "compatible": true} {"name": "types-aiobotocore", "license": "MIT", "compatible": true} diff --git a/third_party/osv-licenses.json b/third_party/osv-licenses.json index db54176cf9..3af1cc51f7 100644 --- a/third_party/osv-licenses.json +++ b/third_party/osv-licenses.json @@ -3013,6 +3013,233 @@ "version": "26.1.1", "ecosystem": "PyPI" }, + "vulnerabilities": [ + { + "modified": "2026-06-05T12:45:14Z", + "published": "2026-06-01T17:17:35Z", + "schema_version": "1.7.5", + "id": "PYSEC-2026-196", + "aliases": [ + "CVE-2026-8643" + ], + "details": "pip would treat console_scripts and gui_scripts as paths instead of file names without sanitizing the resolved absolute path to the installation directory, leading to entry points being installed outside the installation directory.", + "severity": [ + { + "type": "CVSS_V3", + "score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N" + } + ], + "affected": [ + { + "package": { + "ecosystem": "PyPI", + "name": "pip", + "purl": "pkg:pypi/pip" + }, + "ranges": [ + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "0" + }, + { + "fixed": "26.1.2" + } + ] + } + ], + "versions": [ + "0.2", + "0.2.1", + "0.3", + "0.3.1", + "0.4", + "0.5", + "0.5.1", + "0.6", + "0.6.1", + "0.6.2", + "0.6.3", + "0.7", + "0.7.1", + "0.7.2", + "0.8", + "0.8.1", + "0.8.2", + "0.8.3", + "1.0", + "1.0.1", + "1.0.2", + "1.1", + "1.2", + "1.2.1", + "1.3", + "1.3.1", + "1.4", + "1.4.1", + "1.5", + "1.5.1", + "1.5.2", + "1.5.3", + "1.5.4", + "1.5.5", + "1.5.6", + "10.0.0", + "10.0.0b1", + "10.0.0b2", + "10.0.1", + "18.0", + "18.1", + "19.0", + "19.0.1", + "19.0.2", + "19.0.3", + "19.1", + "19.1.1", + "19.2", + "19.2.1", + "19.2.2", + "19.2.3", + "19.3", + "19.3.1", + "20.0", + "20.0.1", + "20.0.2", + "20.1", + "20.1.1", + "20.1b1", + "20.2", + "20.2.1", + "20.2.2", + "20.2.3", + "20.2.4", + "20.2b1", + "20.3", + "20.3.1", + "20.3.2", + "20.3.3", + "20.3.4", + "20.3b1", + "21.0", + "21.0.1", + "21.1", + "21.1.1", + "21.1.2", + "21.1.3", + "21.2", + "21.2.1", + "21.2.2", + "21.2.3", + "21.2.4", + "21.3", + "21.3.1", + "22.0", + "22.0.1", + "22.0.2", + "22.0.3", + "22.0.4", + "22.1", + "22.1.1", + "22.1.2", + "22.1b1", + "22.2", + "22.2.1", + "22.2.2", + "22.3", + "22.3.1", + "23.0", + "23.0.1", + "23.1", + "23.1.1", + "23.1.2", + "23.2", + "23.2.1", + "23.3", + "23.3.1", + "23.3.2", + "24.0", + "24.1", + "24.1.1", + "24.1.2", + "24.1b1", + "24.1b2", + "24.2", + "24.3", + "24.3.1", + "25.0", + "25.0.1", + "25.1", + "25.1.1", + "25.2", + "25.3", + "26.0", + "26.0.1", + "26.1", + "26.1.1", + "6.0", + "6.0.1", + "6.0.2", + "6.0.3", + "6.0.4", + "6.0.5", + "6.0.6", + "6.0.7", + "6.0.8", + "6.1.0", + "6.1.1", + "7.0.0", + "7.0.1", + "7.0.2", + "7.0.3", + "7.1.0", + "7.1.1", + "7.1.2", + "8.0.0", + "8.0.1", + "8.0.2", + "8.0.3", + "8.1.0", + "8.1.1", + "8.1.2", + "9.0.0", + "9.0.1", + "9.0.2", + "9.0.3" + ], + "database_specific": { + "source": "https://github.com/pypa/advisory-database/blob/main/vulns/pip/PYSEC-2026-196.yaml" + } + } + ], + "references": [ + { + "type": "ADVISORY", + "url": "http://www.openwall.com/lists/oss-security/2026/06/01/5" + }, + { + "type": "ADVISORY", + "url": "https://mail.python.org/archives/list/security-announce@python.org/thread/YV63UET5D3OOJY7O4M5XCVYO2YM4NBYJ/" + }, + { + "type": "FIX", + "url": "https://github.com/pypa/pip/pull/14000" + } + ] + } + ], + "groups": [ + { + "ids": [ + "PYSEC-2026-196" + ], + "aliases": [ + "CVE-2026-8643", + "PYSEC-2026-196" + ], + "max_severity": "5.5" + } + ], "licenses": [ "MIT" ] @@ -3185,12 +3412,13 @@ }, "vulnerabilities": [ { - "modified": "2026-05-20T09:19:11Z", + "modified": "2026-06-05T21:56:07Z", "published": "2026-02-17T14:16:01Z", "schema_version": "1.7.5", "id": "PYSEC-2026-113", "aliases": [ - "CVE-2026-25087" + "CVE-2026-25087", + "GHSA-rgxp-2hwp-jwgg" ], "details": "Use After Free vulnerability in Apache Arrow C++.\n\nThis issue affects Apache Arrow C++ from 15.0.0 through 23.0.0. It can be triggered when reading an Arrow IPC file (but not an IPC stream) with pre-buffering enabled, if the IPC file contains data with variadic buffers (such as Binary View and String View data). Depending on the number of variadic buffers in a record batch column and on the temporal sequence of multi-threaded IO, a write to a dangling pointer could occur. The value (a `std::shared_ptr` object)\u00a0that is written to the dangling pointer is not under direct control of the attacker.\n\nPre-buffering is disabled by default but can be enabled using a specific C++ API call (`RecordBatchFileReader::PreBufferMetadata`). The functionality is not exposed in language bindings (Python, Ruby, C GLib), so these bindings are not vulnerable.\n\nThe most likely consequence of this issue would be random crashes or memory corruption when reading specific kinds of IPC files. If the application allows ingesting IPC files from untrusted sources, this could plausibly be exploited for denial of service. Inducing more targeted kinds of misbehavior (such as confidential data extraction from the running process) depends on memory allocation and multi-threaded IO temporal patterns that are unlikely to be easily controlled by an attacker.\n\nAdvice for users of Arrow C++:\n\n1. check whether you enable pre-buffering on the IPC file reader (using\u00a0`RecordBatchFileReader::PreBufferMetadata`)\n\n2. if so, either disable pre-buffering (which may have adverse performance consequences), or switch to Arrow 23.0.1 which is not vulnerable", "severity": [ @@ -3254,15 +3482,111 @@ "url": "https://github.com/apache/arrow/pull/48925" } ] + }, + { + "modified": "2026-06-05T21:56:07Z", + "published": "2026-02-17T15:31:35Z", + "schema_version": "1.7.5", + "id": "GHSA-rgxp-2hwp-jwgg", + "aliases": [ + "CVE-2026-25087", + "PYSEC-2026-113" + ], + "summary": "Apache Arrow: Potential use-after-free when reading IPC file with pre-buffering", + "details": "Use After Free vulnerability in Apache Arrow C++.\n\nThis issue affects Apache Arrow C++ from 15.0.0 through 23.0.0. It can be triggered when reading an Arrow IPC file (but not an IPC stream) with pre-buffering enabled, if the IPC file contains data with variadic buffers (such as Binary View and String View data). Depending on the number of variadic buffers in a record batch column and on the temporal sequence of multi-threaded IO, a write to a dangling pointer could occur. The value (a `std::shared_ptr` object)\u00a0that is written to the dangling pointer is not under direct control of the attacker.\n\nPre-buffering is disabled by default but can be enabled using a specific C++ API call (`RecordBatchFileReader::PreBufferMetadata`). The functionality is not exposed in language bindings (Python, Ruby, C GLib), so these bindings are not vulnerable.\n\nThe most likely consequence of this issue would be random crashes or memory corruption when reading specific kinds of IPC files. If the application allows ingesting IPC files from untrusted sources, this could plausibly be exploited for denial of service. Inducing more targeted kinds of misbehavior (such as confidential data extraction from the running process) depends on memory allocation and multi-threaded IO temporal patterns that are unlikely to be easily controlled by an attacker.\n\nAdvice for users of Arrow C++:\n\n1. check whether you enable pre-buffering on the IPC file reader (using\u00a0`RecordBatchFileReader::PreBufferMetadata`)\n\n2. if so, either disable pre-buffering (which may have adverse performance consequences), or switch to Arrow 23.0.1 which is not vulnerable", + "severity": [ + { + "type": "CVSS_V3", + "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:H" + } + ], + "affected": [ + { + "package": { + "ecosystem": "PyPI", + "name": "pyarrow", + "purl": "pkg:pypi/pyarrow" + }, + "ranges": [ + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "15.0.0" + }, + { + "fixed": "23.0.1" + } + ] + } + ], + "versions": [ + "15.0.0", + "15.0.1", + "15.0.2", + "16.0.0", + "16.1.0", + "17.0.0", + "18.0.0", + "18.1.0", + "19.0.0", + "19.0.1", + "20.0.0", + "21.0.0", + "22.0.0", + "23.0.0" + ], + "database_specific": { + "source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/02/GHSA-rgxp-2hwp-jwgg/GHSA-rgxp-2hwp-jwgg.json" + } + } + ], + "references": [ + { + "type": "ADVISORY", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-25087" + }, + { + "type": "WEB", + "url": "https://github.com/apache/arrow/pull/48925" + }, + { + "type": "PACKAGE", + "url": "https://github.com/apache/arrow" + }, + { + "type": "WEB", + "url": "https://github.com/pypa/advisory-database/tree/main/vulns/pyarrow/PYSEC-2026-113.yaml" + }, + { + "type": "WEB", + "url": "https://lists.apache.org/thread/mpm4ld1qony30tchfpjtk5b11tcyvmwh" + }, + { + "type": "WEB", + "url": "http://www.openwall.com/lists/oss-security/2026/02/17/4" + } + ], + "database_specific": { + "cwe_ids": [ + "CWE-416" + ], + "github_reviewed": true, + "github_reviewed_at": "2026-06-05T21:39:35Z", + "nvd_published_at": "2026-02-17T14:16:01Z", + "severity": "HIGH" + } } ], "groups": [ { "ids": [ - "PYSEC-2026-113" + "PYSEC-2026-113", + "GHSA-rgxp-2hwp-jwgg" ], "aliases": [ "CVE-2026-25087", + "GHSA-rgxp-2hwp-jwgg", "PYSEC-2026-113" ], "max_severity": "7.0" @@ -4170,6 +4494,16 @@ "Apache-2.0" ] }, + { + "package": { + "name": "safetensors", + "version": "0.8.0rc1", + "ecosystem": "PyPI" + }, + "licenses": [ + "non-standard" + ] + }, { "package": { "name": "scikit-network", @@ -5257,6 +5591,16 @@ "MIT AND MPL-2.0" ] }, + { + "package": { + "name": "transformers", + "version": "5.10.2", + "ecosystem": "PyPI" + }, + "licenses": [ + "Apache-2.0" + ] + }, { "package": { "name": "typer", @@ -5737,11 +6081,11 @@ }, { "name": "Apache-2.0", - "count": 80 + "count": 81 }, { "name": "non-standard", - "count": 42 + "count": 43 }, { "name": "BSD-3-Clause", diff --git a/third_party/requirements-main.txt b/third_party/requirements-main.txt index 869950db5d..64f3265a50 100644 --- a/third_party/requirements-main.txt +++ b/third_party/requirements-main.txt @@ -25,6 +25,8 @@ # nemo-agents-plugin # nemo-anonymizer-plugin # nemo-auditor-plugin + # nemo-automodel-plugin + # nemo-customizer-plugin # nemo-data-designer-plugin # nemo-guardrails-plugin # nemo-safe-synthesizer-plugin @@ -36,6 +38,8 @@ # nemo-agents-plugin # nemo-anonymizer-plugin # nemo-auditor-plugin + # nemo-automodel-plugin + # nemo-customizer-plugin # nemo-data-designer-plugin # nemo-evaluator-plugin # nemo-guardrails-plugin @@ -58,6 +62,7 @@ # nemo-safe-synthesizer-plugin # nemoplatform # nmp-auth + # nmp-automodel # nmp-core-mcp # nmp-customizer # nmp-entities @@ -85,6 +90,8 @@ # via nemo-agents-plugin -e ./plugins/nemo-anonymizer ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') -e ./plugins/nemo-auditor ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') +-e ./plugins/nemo-automodel ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') +-e ./plugins/nemo-customizer ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') -e ./plugins/nemo-data-designer ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') -e ./plugins/nemo-evaluator ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') -e ./plugins/nemo-guardrails ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') @@ -99,9 +106,12 @@ # nemo-platform # nemo-platform-ext # nemo-platform-plugin + # nmp-automodel # nmp-common # nmp-core-mcp # nmp-entities +-e ./services/automodel ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') + # via nemo-automodel-plugin -e ./services/core/auth ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') # via # nemoplatform @@ -137,9 +147,7 @@ -e ./services/customizer ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') # via nemoplatform -e ./services/evaluator ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') - # via - # nemoplatform - # nmp-platform-seed + # via nemoplatform -e ./services/guardrails ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') # via # nemoplatform @@ -175,6 +183,7 @@ aiofiles==25.1.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # aioboto3 # nemoguardrails # ngcsdk + # nmp-automodel # nmp-common # nmp-evaluator # nvidia-nat-core @@ -185,48 +194,24 @@ aiohappyeyeballs==2.6.1 ; (platform_machine == 'arm64' and sys_platform == 'darw # via aiohttp aiohttp==3.13.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9 \ - --hash=sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b \ - --hash=sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174 \ - --hash=sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8 \ --hash=sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c \ - --hash=sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe \ --hash=sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9 \ - --hash=sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286 \ --hash=sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc \ --hash=sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665 \ - --hash=sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9 \ --hash=sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090 \ --hash=sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49 \ - --hash=sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46 \ --hash=sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3 \ --hash=sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6 \ --hash=sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb \ - --hash=sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88 \ --hash=sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14 \ - --hash=sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c \ - --hash=sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d \ - --hash=sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8 \ --hash=sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1 \ --hash=sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb \ --hash=sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61 \ --hash=sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4 \ --hash=sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9 \ - --hash=sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6 \ --hash=sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2 \ --hash=sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1 \ - --hash=sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e \ - --hash=sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5 \ - --hash=sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a \ - --hash=sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13 \ - --hash=sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac \ - --hash=sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8 \ - --hash=sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3 \ - --hash=sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b \ - --hash=sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c \ - --hash=sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6 \ - --hash=sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d \ - --hash=sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6 \ - --hash=sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540 + --hash=sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c # via # aiobotocore # aiohttp-retry @@ -426,7 +411,6 @@ certifi==2026.2.25 ; (platform_machine == 'arm64' and sys_platform == 'darwin') # requests # sentry-sdk cffi==2.0.0 ; (platform_machine == 'arm64' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') \ - --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ @@ -434,20 +418,15 @@ cffi==2.0.0 ; (platform_machine == 'arm64' and platform_python_implementation != --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ - --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ - --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ - --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ - --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ - --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ - --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 + --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c # via cryptography chardet==5.2.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \ @@ -457,47 +436,23 @@ chardet==5.2.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or ( # diff-cover # sqlfluff charset-normalizer==3.4.6 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c \ --hash=sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5 \ --hash=sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f \ --hash=sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6 \ - --hash=sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2 \ - --hash=sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f \ --hash=sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab \ - --hash=sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a \ - --hash=sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84 \ --hash=sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f \ --hash=sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843 \ - --hash=sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0 \ --hash=sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9 \ --hash=sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd \ - --hash=sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e \ --hash=sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9 \ - --hash=sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc \ - --hash=sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d \ --hash=sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2 \ - --hash=sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398 \ - --hash=sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c \ --hash=sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e \ - --hash=sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2 \ - --hash=sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6 \ - --hash=sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5 \ - --hash=sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194 \ --hash=sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69 \ --hash=sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73 \ --hash=sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923 \ - --hash=sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88 \ --hash=sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21 \ --hash=sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2 \ - --hash=sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021 \ - --hash=sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de \ - --hash=sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc \ - --hash=sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8 \ - --hash=sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf \ - --hash=sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef \ - --hash=sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff \ - --hash=sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d \ - --hash=sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed + --hash=sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021 # via requests circuitbreaker==2.1.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:1a4baee510f7bea3c91b194dcce7c07805fe96c4423ed5594b75af438531d084 \ @@ -552,7 +507,6 @@ cryptography==46.0.7 ; (platform_machine == 'arm64' and sys_platform == 'darwin' --hash=sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65 \ --hash=sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832 \ --hash=sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067 \ - --hash=sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de \ --hash=sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0 \ --hash=sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968 \ --hash=sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef \ @@ -566,14 +520,9 @@ cryptography==46.0.7 ; (platform_machine == 'arm64' and sys_platform == 'darwin' --hash=sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7 \ --hash=sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83 \ --hash=sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85 \ - --hash=sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006 \ - --hash=sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb \ --hash=sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e \ --hash=sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba \ --hash=sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325 \ - --hash=sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1 \ - --hash=sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2 \ - --hash=sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0 \ --hash=sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455 \ --hash=sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15 \ --hash=sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5 \ @@ -626,6 +575,7 @@ datasets==4.0.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or --hash=sha256:7ef95e62025fd122882dbce6cb904c8cd3fbc829de6669a5eb939c77d50e203d \ --hash=sha256:9657e7140a9050db13443ba21cb5de185af8af944479b00e7ff1e00a61c8dbf1 # via + # nemo-customizer-plugin # nmp-evaluator # ragas diff-cover==10.2.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ @@ -763,44 +713,24 @@ fastar==0.9.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p --hash=sha256:108bb46c080ca152bb331f1e0576177d36e9badba51b1d5724d2823542e0dd1f \ --hash=sha256:17e2c3b46408193ea13c1e1177275ca7951e88bd3dce16baccb8de4f5e0dc2e8 \ --hash=sha256:2394980cc126a3263e115600bc4ff9e7320cddde83c99fc334ab530be5b7166e \ - --hash=sha256:24b13fc4ef3f1e3c9cc2dcf07ad9445900db9d3ce09b73021547a55994d0407f \ - --hash=sha256:3feede2d72ec0782b5ccc18568f36cbe33816be396551aa47b3e1b73c322cdd2 \ - --hash=sha256:40b8c08df809e5e58d1839ccb37bafe4485deb6ee56bb7c5f0cbb72d701eb965 \ - --hash=sha256:4a734506b071d2a8844771fe735fbd6d67dd0eec80eef5f189bbe763ebe7a0b8 \ - --hash=sha256:4d012644421d669d9746157193f4eafd371e8ae56ff7aef97612a4922418664c \ - --hash=sha256:52f96a3d4cfbe4f06b376706fa0562f3a1d2329bc37168119af0e47e1ac21cab \ - --hash=sha256:57e9b94e485713c79bb259f7ecff1213527d05e9aa43a157c3fbc88812cf163e \ --hash=sha256:59bc500d7b6bdaf2ffb2b632bc6b0f97ddfb3bb7d31b54d61ceb00b5698d6484 \ - --hash=sha256:59d860e82a531e9cc67e7f500a299bffbe6e93d80bbf48401fd8f452a0c58f28 \ --hash=sha256:5a67b061b1099cf3b8b6234dd3605fa16f5078ab6b51c8d77ad7a5d11c3cf834 \ --hash=sha256:5c03fad1ad9ac57cf03a4db9e18c7109c37416ff4eb9ebfca98fcd2b233a26c4 \ - --hash=sha256:75c70be3a7da3ff9342f64c15ec3749c13ef56bc28e69075d82d03768532a8d0 \ --hash=sha256:76be31936cabce31cbb6381128f851cf0a6da2d5c25357615cd1504b26dc31cf \ - --hash=sha256:7bf6958bb6f94e5ec522e4a255b8e940d3561ad973f0be5dde6115b5a0854af5 \ --hash=sha256:87006c8770dfc558aefe927590bbcdaf9648ca4472a9ee6d10dfb7c0bda4ce5b \ - --hash=sha256:8eac084ab215aaf65fa406c9b9da1ac4e697c3d3a1a183e09c488e555802f62d \ - --hash=sha256:912efe3121dc1f3c05940cfa1c6b09b8868d702d24566506aa1d0d96e429923a \ --hash=sha256:9ec841a69fea73361c6df6d9183915c09e9ce3bd96493763fa46019e79918400 \ - --hash=sha256:a79c53c3003958dca88a7ec3dd805bf9c2fb2a659110039f44571d57e329e3d4 \ --hash=sha256:acb62e2369834fb23d26327157f0a2dbec40b230c709fa85b1ce96cf010e6fbf \ --hash=sha256:b665c33afcd1d581b82235b690d999c5446ccc2c4d80c4a95f30df3b43d22494 \ --hash=sha256:c75e779f72d845037d4bf6692d01ac66f014eaef965c9231d41d5cc1276b89fc \ --hash=sha256:c8ac3e8aaee57dfc822b04f570f0a963c2381a9dc8990fe0c6e965efd23fd451 \ - --hash=sha256:c93bf4732d0dd6adae4a8b3bbebe19af76ee1072b7688bf39c5a1d120425a772 \ --hash=sha256:c9bd8879ebf05aa247e60e454bb7568cbdd44f016b8c58e31e5398039403e61d \ - --hash=sha256:d0aff74ea98642784c941d3cd8c35943258d4b9626157858901c5b181683339b \ - --hash=sha256:d17d311cfbb559154ba940972b6d07a3a7ac221a2a01208f119ad03495f01d32 \ - --hash=sha256:d2a9a49f9217f4f60f9ba23fdd1f7f3f04fed97391145eb9460ec83ca0b4bd33 \ - --hash=sha256:d2ef34e7088f308e73460e1b8d9b0479a743f679816782a80db6ae87ee68714a \ --hash=sha256:d49114d5f0b76c5cc242875d90fa4706de45e0456ddedf416608ecd0787fb410 \ --hash=sha256:d62a4fd86eda3bea7cc32efd64d43b6d0fcdbbec009558b750fc362f20142789 \ --hash=sha256:d9ac410d32cbb514e966c45f0fedd0f9447b0dea9e734af714648da503603df6 \ --hash=sha256:de264da9e8ef6407aa0b23c7c47ed4e34fde867e7c1f6e3cb98945a93e5f89f2 \ --hash=sha256:ec7852de506d022ad36ad56f4aefb10c259dd59e485bf87af827954d404ba9d5 \ --hash=sha256:f07c6bdeedfeb30ef459f21fa9ab06e2b6727f7e7653176d3abb7a85f447c400 \ - --hash=sha256:f2f399fffb74bcd9e9d4507e253ace2430b5ccf61000596bda41e90414bcf4f2 \ - --hash=sha256:fad70e257daefb42bab68dcd68beaf2e2a99da056d65f2c9f988449a4e869306 \ - --hash=sha256:fb06d0a0cc3cf52a9c07559bb16ab99eb75afe0b3d5ce68f5c299569460851ac + --hash=sha256:fad70e257daefb42bab68dcd68beaf2e2a99da056d65f2c9f988449a4e869306 # via fastapi-cloud-cli fastembed==0.8.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:40bee672657574a1009e35ec50030a55f2b426842cb011845379817641bbbbd0 \ @@ -847,53 +777,29 @@ flatbuffers==25.12.19 ; (platform_machine == 'arm64' and sys_platform == 'darwin --hash=sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4 # via onnxruntime frozenlist==1.8.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0 \ - --hash=sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121 \ --hash=sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84 \ --hash=sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d \ --hash=sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967 \ - --hash=sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd \ --hash=sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed \ --hash=sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f \ - --hash=sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143 \ - --hash=sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e \ - --hash=sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8 \ --hash=sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad \ - --hash=sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82 \ --hash=sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29 \ --hash=sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383 \ --hash=sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52 \ - --hash=sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d \ - --hash=sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1 \ - --hash=sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714 \ --hash=sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65 \ --hash=sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506 \ --hash=sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41 \ - --hash=sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608 \ --hash=sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa \ --hash=sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1 \ - --hash=sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52 \ --hash=sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a \ - --hash=sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3 \ --hash=sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695 \ --hash=sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4 \ --hash=sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51 \ - --hash=sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8 \ --hash=sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b \ - --hash=sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b \ - --hash=sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d \ - --hash=sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567 \ - --hash=sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e \ - --hash=sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52 \ - --hash=sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1 \ --hash=sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94 \ --hash=sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822 \ --hash=sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11 \ - --hash=sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581 \ - --hash=sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51 \ --hash=sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40 \ - --hash=sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92 \ - --hash=sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5 \ --hash=sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4 \ --hash=sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93 \ --hash=sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027 @@ -930,7 +836,6 @@ greenlet==3.3.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or --hash=sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f \ --hash=sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2 \ --hash=sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd \ - --hash=sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2 \ --hash=sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070 \ --hash=sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99 \ --hash=sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be \ @@ -938,16 +843,11 @@ greenlet==3.3.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or --hash=sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a \ --hash=sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395 \ --hash=sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358 \ - --hash=sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac \ - --hash=sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55 \ --hash=sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4 \ --hash=sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986 \ --hash=sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd \ - --hash=sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb \ --hash=sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab \ - --hash=sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86 \ - --hash=sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd \ - --hash=sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92 + --hash=sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86 # via # nmp-entities # sqlalchemy @@ -965,11 +865,8 @@ grpcio==1.80.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or ( --hash=sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0 \ --hash=sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f \ --hash=sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2 \ - --hash=sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0 \ --hash=sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6 \ --hash=sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de \ - --hash=sha256:dfab85db094068ff42e2a3563f60ab3dddcc9d6488a35abf0132daec13209c8a \ - --hash=sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad \ --hash=sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2 # via # opentelemetry-exporter-otlp-proto-grpc @@ -1055,6 +952,7 @@ httpx==0.28.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p # nemo-safe-synthesizer-plugin # nemoguardrails # nmp-auth + # nmp-automodel # nmp-guardrails # nvidia-nat-core # oci-openai @@ -1082,6 +980,7 @@ huggingface-hub==1.15.0 ; (platform_machine == 'arm64' and sys_platform == 'darw # nmp-common # nmp-evaluator # tokenizers + # transformers hvac==2.4.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:008db5efd8c2f77bd37d2368ea5f713edceae1c65f11fd608393179478649e0f \ --hash=sha256:e0056ad9064e7923e874e6769015b032580b639e29246f5ab1044f7959c1c7e0 @@ -1151,32 +1050,23 @@ jinja2==3.1.6 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p # sqlfluff jiter==0.10.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500 \ - --hash=sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959 \ --hash=sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605 \ - --hash=sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db \ --hash=sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070 \ --hash=sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2 \ --hash=sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca \ --hash=sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041 \ - --hash=sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026 \ --hash=sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a \ --hash=sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5 \ --hash=sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d \ - --hash=sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25 \ --hash=sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6 \ --hash=sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b \ - --hash=sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3 \ --hash=sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc \ - --hash=sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2 \ --hash=sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea \ - --hash=sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c \ --hash=sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744 \ --hash=sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5 \ --hash=sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4 \ - --hash=sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61 \ --hash=sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a \ - --hash=sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e \ - --hash=sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2 + --hash=sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e # via # anthropic # instructor @@ -1213,39 +1103,23 @@ jsonpath-ng==1.8.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') # nvidia-nat-core jsonpath-rust-bindings==1.1.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:0017af7054fb6bce55863a7065ae465a9c47fd93fb94f002ca98bb8adf15101a \ - --hash=sha256:02373d581a093d0640e60858884d67ec93259e7b6d6bd8e5874400ad99558e00 \ --hash=sha256:0ca169ac219bc141775fb19df8165d4d0162e6ed77102e1ab19a74a80c1f9051 \ --hash=sha256:13446ad021abe05d622a01eaa648c238ef3b98e9fc0bd837a589bafb246ca3bc \ - --hash=sha256:146b69ce20cb9869e05a6d369f4a10b52f98e1f8575f1ac5b49e285fa2032380 \ - --hash=sha256:1ff4cd052f733d5f270329c552a04e08a1520053355d35f0be886714dff46955 \ --hash=sha256:26955685acf0208b6061419cab4bd79fe869ebce57f3cec1e9b20f0e0af56b35 \ - --hash=sha256:330f457556d06abc1ea36b6738eb172288afff6bd251350eaba42bed2f459fd3 \ - --hash=sha256:366cba544c080c08530cef0cc19922f0380f0caab6e7e5a0ddfb70de288d5abc \ - --hash=sha256:36a40ed04d2db70897cde2ac92f6c9aae2ed1b426aa4c97a47f3e2be911ea4ba \ --hash=sha256:3c220c2d27ab6a0791e3af10e2a7c53ccd1dc2dfc8681999fed4458392aa0372 \ --hash=sha256:40c23781d28a8b126c8a2b337e4fe275cc8f35a149bda769e3ec2760dfb58b91 \ --hash=sha256:44de7464ad227028c36e8d713653b4bfe5eb7524ac1a4b0a71e8bcb3bd4f4f3a \ - --hash=sha256:4eacb98f80fff7d43956503ca7b42e491f7084c7b9bd8b5b6bad3f50d08480df \ - --hash=sha256:50f16c3dd6eb572dda74731508d2fca1abbb927ab4f6511fb65eeba6e59fd041 \ - --hash=sha256:6716caa0855dbf9d021509a3caa00a9fa7cc241930f40830c24e85d0e17a6246 \ --hash=sha256:734eee89754c829a0fb55a30467c8a33081976375b763c907f71f7018682c26c \ - --hash=sha256:7bf30e27a81d07c79cc58c86600687e5adfe0f7b1aaf8069a737085bebfaea71 \ --hash=sha256:7f2a526c87a245f708dc1d8d4988c471384c369a5909b8b730e63b6a7f0c2d60 \ --hash=sha256:8c390c33582cd268d35b86eb0f550229e0cf26f03bb06c470db4712d6fa4dc0f \ - --hash=sha256:9212d3746a57015fc3722488f61c4afc465d993f68371d864be8fa5b0c58d635 \ --hash=sha256:9d656507b5913f9515ff136797c5850df907c5040fa1368baa428f7e829e33f0 \ --hash=sha256:a239166bd1418897de327c952a9d9ff912d1fabc9da82e688204ccfcd7b22584 \ --hash=sha256:a43107f6efc4e66ee046c338741429a268fd972e887721b01bf0f32e47387e30 \ - --hash=sha256:aa7e9d25b00c227c51e7a916a13fbf22cf483df622699dbc3ef051861ec1de85 \ --hash=sha256:b06b24668085b2791acbfefdfe2f2824d36be539c7647c00aee33242b4d3385d \ - --hash=sha256:b9583e965fe5f8f21cd0d047244db9716a119e0e82a06f2336e6b14c9a9637af \ --hash=sha256:ce1c6804706012c3c7a194903ef20befafa3cc913a4ef553696bc837ac738a66 \ --hash=sha256:ce7039a2f497674785a423076e803a1fa547c2f9cf568b25e2ac83ff5890b98f \ --hash=sha256:d21101114514d34b21ab216eef1d7bb41155311fa61284e8f2dbdb93bde41c78 \ - --hash=sha256:dc0c3488f04dbd318fa876fb880e8cb7d1e53abcf8b0d9e697e10a0a15ac3158 \ --hash=sha256:ddbf025592bf88fc5395d9d023d7bcc8fab977898c406e0a5722925c3b887c71 \ - --hash=sha256:e423363b47080830bbb4d8257c0f26bda8ee655a18c4f934952bfe4c46e8d510 \ - --hash=sha256:ebb9a05a2b80195ac47aec0ce98d861c102459d16225fefb0f7e0158196c4a58 \ --hash=sha256:f55ee1e7fdb6bb2363c40a6d6ce0285e53bd52b4ecae7bef3909eeb11a9b4cd2 \ --hash=sha256:fbfeb05c7a6854104e97a0e3234f312004b3f4e678d14b68180a6a4f33f4d7c3 \ --hash=sha256:fe44737c6c72079ef30c85f975c19fa0114c13039fe538d8c5b259007a35a0ff @@ -1267,6 +1141,7 @@ jsonschema==4.23.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') # mcp # nemo-evaluator-sdk # nemo-safe-synthesizer + # nmp-automodel jsonschema-path==0.3.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001 \ --hash=sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8 @@ -1434,46 +1309,30 @@ loguru==0.7.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p --hash=sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c # via fastembed lxml==6.1.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad \ --hash=sha256:07f98f5496f96bf724b1e3c933c107f0cbf2745db18c03d2e13a291c3afd2635 \ - --hash=sha256:0d082495c5fcf426e425a6e28daaba1fcb6d8f854a4ff01effb1f1f381203eb9 \ - --hash=sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54 \ - --hash=sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69 \ - --hash=sha256:23a5dc68e08ed13331d61815c08f260f46b4a60fdd1640bbeb82cf89a9d90289 \ --hash=sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d \ --hash=sha256:363e47283bde87051b821826e71dde47f107e08614e1aa312ba0c5711e77738c \ --hash=sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491 \ --hash=sha256:3ae5d8d5427f3cc317e7950f2da7ad276df0cfa37b8de2f5658959e618ea8512 \ --hash=sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2 \ - --hash=sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88 \ --hash=sha256:4642e04449a1e164b5ff71ffd901ddb772dfabf5c9adf1b7be5dffe1212bc037 \ - --hash=sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485 \ --hash=sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105 \ --hash=sha256:5cfa1a34df366d9dc0d5eaf420f4cf2bb1e1bebe1066d1c2fc28c179f8a4004c \ - --hash=sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5 \ - --hash=sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d \ --hash=sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d \ - --hash=sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e \ --hash=sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33 \ - --hash=sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e \ --hash=sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a \ --hash=sha256:942454ff253da14218f972b23dc72fa4edf6c943f37edd19cd697618b626fac5 \ --hash=sha256:976a6b39b1b13e8c354ad8d3f261f3a4ac6609518af91bdb5094760a08f132c4 \ - --hash=sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495 \ --hash=sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45 \ --hash=sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc \ --hash=sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814 \ --hash=sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13 \ - --hash=sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe \ --hash=sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181 \ --hash=sha256:cec05be8c876f92a5aa07b01d60bbb4d11cfbdd654cad0561c0d7b5c043a61b9 \ --hash=sha256:d036ee7b99d5148072ac7c9b847193decdfeac633db350363f7bce4fff108f0e \ --hash=sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d \ - --hash=sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24 \ --hash=sha256:db88156fcf544cdbf0d95588051515cfdfd4c876fc66444eb98bceb5d6db76de \ - --hash=sha256:e3c4f84b24a1fcba435157d111c4b755099c6ff00a3daee1ad281817de75ed11 \ --hash=sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2 \ - --hash=sha256:f15401d8d3dbf239e23c818afc10c7207f7b95f9a307e092122b6f86dd43209a \ --hash=sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d \ --hash=sha256:fcf3da95e93349e0647d48d4b36a12783105bcc74cb0c416952f9988410846a3 # via @@ -1510,28 +1369,20 @@ markupsafe==3.0.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') o --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ - --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ - --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ - --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ - --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ - --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ - --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ - --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ - --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ @@ -1560,33 +1411,21 @@ mmh3==5.2.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (pla --hash=sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8 \ --hash=sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e \ --hash=sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825 \ - --hash=sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4 \ - --hash=sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f \ --hash=sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593 \ - --hash=sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a \ --hash=sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5 \ - --hash=sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1 \ - --hash=sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b \ - --hash=sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000 \ --hash=sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5 \ --hash=sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15 \ --hash=sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006 \ - --hash=sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211 \ - --hash=sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d \ --hash=sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38 \ --hash=sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f \ --hash=sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166 \ --hash=sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad \ --hash=sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03 \ - --hash=sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2 \ - --hash=sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4 \ --hash=sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6 \ - --hash=sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1 \ --hash=sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450 \ --hash=sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d \ --hash=sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6 \ --hash=sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7 \ - --hash=sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2 \ --hash=sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a # via fastembed more-itertools==10.8.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ @@ -1600,55 +1439,31 @@ mpmath==1.3.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c # via sympy multidict==6.7.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9 \ - --hash=sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43 \ - --hash=sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c \ --hash=sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa \ --hash=sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6 \ - --hash=sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd \ - --hash=sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d \ --hash=sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3 \ - --hash=sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0 \ --hash=sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292 \ - --hash=sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed \ --hash=sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23 \ --hash=sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e \ --hash=sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582 \ - --hash=sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0 \ - --hash=sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e \ - --hash=sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a \ - --hash=sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d \ --hash=sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108 \ --hash=sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144 \ - --hash=sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060 \ --hash=sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56 \ - --hash=sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84 \ - --hash=sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71 \ --hash=sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7 \ - --hash=sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8 \ --hash=sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49 \ --hash=sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d \ --hash=sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445 \ - --hash=sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a \ --hash=sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33 \ - --hash=sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca \ - --hash=sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733 \ --hash=sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429 \ - --hash=sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6 \ --hash=sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172 \ - --hash=sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52 \ --hash=sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7 \ --hash=sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961 \ - --hash=sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b \ --hash=sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1 \ - --hash=sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c \ - --hash=sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a \ --hash=sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23 \ --hash=sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34 \ --hash=sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75 \ --hash=sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d \ --hash=sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855 \ - --hash=sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4 \ --hash=sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba # via # aiobotocore @@ -1753,6 +1568,7 @@ numpy==2.4.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (pl # sacrebleu # scikit-network # scipy + # transformers nvidia-ml-py==13.595.45 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:b65a7977f503d56154b14d683710125ef93594adb63fbf7e559336e3318f1376 \ --hash=sha256:c9f34897fe0441ff35bc8f35baf80f830a20b0f4e6ce71e0a325bc0e66acf079 @@ -1994,20 +1810,11 @@ orjson==3.11.8 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or ( --hash=sha256:0022bb50f90da04b009ce32c512dc1885910daa7cb10b7b0cba4505b16db82a8 \ --hash=sha256:003646067cc48b7fcab2ae0c562491c9b5d2cbd43f1e5f16d98fd118c5522d34 \ --hash=sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25 \ - --hash=sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546 \ --hash=sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f \ - --hash=sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137 \ --hash=sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc \ - --hash=sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb \ - --hash=sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c \ --hash=sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f \ - --hash=sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813 \ - --hash=sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6 \ - --hash=sha256:58a4a208a6fbfdb7a7327b8f201c6014f189f721fd55d047cafc4157af1bc62a \ - --hash=sha256:5d8b5231de76c528a46b57010bbd83fb51e056aa0220a372fd5065e978406f1c \ --hash=sha256:5f8952d6d2505c003e8f0224ff7858d341fa4e33fef82b91c4ff0ef070f2393c \ --hash=sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6 \ - --hash=sha256:6eda5b8b6be91d3f26efb7dc6e5e68ee805bc5617f65a328587b35255f138bf4 \ --hash=sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53 \ --hash=sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b \ --hash=sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8 \ @@ -2016,12 +1823,9 @@ orjson==3.11.8 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or ( --hash=sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623 \ --hash=sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e \ --hash=sha256:a5c370674ebabe16c6ccac33ff80c62bf8a6e59439f5e9d40c1f5ab8fd2215b7 \ - --hash=sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506 \ --hash=sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559 \ --hash=sha256:ed193ce51d77a3830cad399a529cd4ef029968761f43ddc549e1bc62b40d88f8 \ - --hash=sha256:f30491bc4f862aa15744b9738517454f1e46e56c972a2be87d70d727d5b2a8f8 \ - --hash=sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55 \ - --hash=sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6 + --hash=sha256:f30491bc4f862aa15744b9738517454f1e46e56c972a2be87d70d727d5b2a8f8 # via # langgraph-sdk # langsmith @@ -2029,25 +1833,19 @@ orjson==3.11.8 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or ( # pymilvus ormsgpack==1.12.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d \ - --hash=sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172 \ --hash=sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a \ - --hash=sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5 \ --hash=sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d \ --hash=sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181 \ --hash=sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7 \ --hash=sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc \ - --hash=sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685 \ --hash=sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355 \ --hash=sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7 \ --hash=sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b \ --hash=sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e \ --hash=sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33 \ - --hash=sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e \ --hash=sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9 \ --hash=sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a \ --hash=sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258 \ - --hash=sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92 \ - --hash=sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6 \ --hash=sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1 \ --hash=sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd # via langgraph-checkpoint @@ -2069,6 +1867,7 @@ packaging==26.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # opentelemetry-instrumentation-sqlalchemy # optuna # pytest + # transformers pandas==2.3.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791 \ --hash=sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac \ @@ -2211,51 +2010,31 @@ prompt-toolkit==3.0.52 ; (platform_machine == 'arm64' and sys_platform == 'darwi # nemo-platform-sdk # nemoguardrails propcache==0.4.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4 \ --hash=sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3 \ --hash=sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf \ - --hash=sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393 \ - --hash=sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc \ --hash=sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe \ --hash=sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75 \ --hash=sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566 \ - --hash=sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367 \ - --hash=sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874 \ --hash=sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf \ --hash=sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1 \ - --hash=sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6 \ - --hash=sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61 \ --hash=sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af \ - --hash=sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa \ --hash=sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf \ - --hash=sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c \ - --hash=sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85 \ --hash=sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e \ --hash=sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1 \ --hash=sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b \ --hash=sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f \ --hash=sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66 \ --hash=sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0 \ - --hash=sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165 \ - --hash=sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778 \ - --hash=sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b \ --hash=sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237 \ - --hash=sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859 \ --hash=sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835 \ --hash=sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 \ - --hash=sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9 \ --hash=sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f \ --hash=sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2 \ - --hash=sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7 \ - --hash=sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757 \ --hash=sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72 \ - --hash=sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24 \ --hash=sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207 \ - --hash=sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e \ --hash=sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d \ --hash=sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e \ --hash=sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570 \ - --hash=sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af \ --hash=sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48 # via # aiohttp @@ -2264,7 +2043,6 @@ protobuf==6.33.6 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or --hash=sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901 \ --hash=sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a \ --hash=sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135 \ - --hash=sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3 \ --hash=sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2 \ --hash=sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593 # via @@ -2290,32 +2068,20 @@ psutil==7.2.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p # ngcsdk # opentelemetry-instrumentation-system-metrics psycopg2-binary==2.9.11 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f \ --hash=sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1 \ --hash=sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee \ --hash=sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4 \ - --hash=sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a \ --hash=sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee \ --hash=sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3 \ --hash=sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d \ --hash=sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a \ - --hash=sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f \ --hash=sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0 \ - --hash=sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757 \ - --hash=sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb \ --hash=sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a \ - --hash=sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c \ --hash=sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c \ --hash=sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4 \ - --hash=sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e \ - --hash=sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766 \ --hash=sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908 \ - --hash=sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60 \ - --hash=sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7 \ --hash=sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f \ - --hash=sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34 \ --hash=sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3 \ - --hash=sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94 \ --hash=sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc \ --hash=sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db # via @@ -2326,26 +2092,20 @@ py-key-value-aio==0.4.4 ; (platform_machine == 'arm64' and sys_platform == 'darw --hash=sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55 # via fastmcp py-rust-stemmers==0.1.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:07b3b8582313ef8a7f544acf2c887f27c3dd48c5ddca028fa0f498de7380e24f \ - --hash=sha256:0ae0540453843bc36937abb54fdbc0d5d60b51ef47aa9667afd05af9248e09eb \ --hash=sha256:191ea8bf922c984631ffa20bf02ef0ad7eec0465baeaed3852779e8f97c7e7a3 \ --hash=sha256:1c3593d895453fa06bf70a7b76d6f00d06def0f91fc253fe4260920650c5e078 \ --hash=sha256:1f9efc4da5e734bdd00612e7506de3d0c9b7abc4b89d192742a0569d0d1fe749 \ --hash=sha256:31ff4fb9417cec35907c18a6463e3d5a4941a5aa8401f77fbb4156b3ada69e3f \ - --hash=sha256:35d32f6e7bdf6fd90e981765e32293a8be74def807147dea9fdc1f65d6ce382f \ --hash=sha256:4d62410ada44a01e02974b85d45d82f4b4c511aae9121e5f3c1ba1d0bea9126b \ --hash=sha256:4e308fc7687901f0c73603203869908f3156fa9c17c4ba010a7fcc98a7a1c5f2 \ - --hash=sha256:541d4b5aa911381e3d37ec483abb6a2cf2351b4f16d5e8d77f9aa2722956662a \ --hash=sha256:5845709d48afc8b29e248f42f92431155a3d8df9ba30418301c49c6072b181b0 \ --hash=sha256:804944eeb5c5559443d81f30c34d6e83c6292d72423f299e42f9d71b9d240941 \ --hash=sha256:85944262c248ea30444155638c9e148a3adc61fe51cf9a3705b4055b564ec95d \ --hash=sha256:910d87d39ba75da1fe3d65df88b926b4b454ada8d73893cbd36e258a8a648158 \ --hash=sha256:96ccc7fd042ffc3f7f082f2223bb7082ed1423aa6b43d5d89ab23e321936c045 \ --hash=sha256:a231dc6f0b2a5f12a080dfc7abd9e6a4ea0909290b10fd0a4620e5a0f52c3d17 \ - --hash=sha256:a979c3f4ff7ad94a0d4cf566ca7bfecebb59e66488cc158e64485cf0c9a7879f \ --hash=sha256:b28ef729a4c83c7d9418be3c23c0372493fcccc67e86783ff04596ef8a208cdf \ --hash=sha256:c52c5c326de78c70cfc71813fa56818d1bd4894264820d037d2be0e805b477bd \ - --hash=sha256:cc2cc8d2b36bc05b8b06506199ac63d437360ae38caefd98cd19e479d35afd42 \ --hash=sha256:d8f374c0f26ef35fb87212686add8dff394bcd9a1364f14ce40fe11504e25e30 \ --hash=sha256:e48bfd5e3ce9d223bfb9e634dc1425cf93ee57eef6f56aa9a7120ada3990d4be \ --hash=sha256:e9c310cfb5c2470d7c7c8a0484725965e7cab8b1237e106a0863d5741da3e1f7 \ @@ -2405,6 +2165,8 @@ pydantic==2.12.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # mcp # nemo-anonymizer # nemo-auditor-plugin + # nemo-automodel-plugin + # nemo-customizer-plugin # nemo-evaluator-plugin # nemo-evaluator-sdk # nemo-platform-ext @@ -2415,6 +2177,7 @@ pydantic==2.12.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # nemoguardrails # nemoplatform # nmp-auth + # nmp-automodel # nmp-common # nmp-customizer # nmp-entities @@ -2437,40 +2200,27 @@ pydantic==2.12.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or pydantic-core==2.41.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740 \ --hash=sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84 \ - --hash=sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33 \ --hash=sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0 \ --hash=sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e \ --hash=sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0 \ --hash=sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34 \ --hash=sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808 \ - --hash=sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594 \ --hash=sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a \ --hash=sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284 \ --hash=sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586 \ - --hash=sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294 \ --hash=sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc \ --hash=sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c \ - --hash=sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e \ - --hash=sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05 \ - --hash=sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e \ --hash=sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b \ --hash=sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b \ - --hash=sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1 \ --hash=sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858 \ --hash=sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2 \ - --hash=sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2 \ - --hash=sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770 \ --hash=sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc \ --hash=sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1 \ --hash=sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56 \ --hash=sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c \ --hash=sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e \ - --hash=sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e \ - --hash=sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc \ - --hash=sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8 \ --hash=sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69 \ --hash=sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c \ - --hash=sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75 \ --hash=sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f \ --hash=sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad \ --hash=sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b @@ -2488,10 +2238,12 @@ pydantic-settings==2.8.1 ; (platform_machine == 'arm64' and sys_platform == 'dar # fastapi # langchain-community # mcp + # nemo-automodel-plugin # nemo-platform-plugin # nemo-safe-synthesizer # nemo-safe-synthesizer-plugin # nmp-auth + # nmp-automodel # nmp-common # nmp-customizer # nmp-entities @@ -2605,12 +2357,9 @@ pyyaml==6.0.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ - --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ - --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ - --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ @@ -2648,6 +2397,7 @@ pyyaml==6.0.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p # nvidia-nat-core # optuna # sqlfluff + # transformers # uvicorn ragas==0.3.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:164d5c0a96048d9c9373aa3e9123f0096649abbd2b58e747c2f0a454da6c2d6b \ @@ -2661,52 +2411,28 @@ referencing==0.36.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') # jsonschema-path # jsonschema-specifications regex==2026.5.9 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611 \ --hash=sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3 \ - --hash=sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf \ --hash=sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c \ - --hash=sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0 \ --hash=sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc \ --hash=sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c \ --hash=sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21 \ - --hash=sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d \ - --hash=sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c \ --hash=sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9 \ --hash=sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026 \ - --hash=sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2 \ - --hash=sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020 \ --hash=sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06 \ --hash=sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0 \ --hash=sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e \ - --hash=sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2 \ - --hash=sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178 \ - --hash=sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e \ --hash=sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88 \ - --hash=sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107 \ - --hash=sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309 \ - --hash=sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2 \ --hash=sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919 \ --hash=sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270 \ --hash=sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c \ --hash=sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed \ --hash=sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2 \ --hash=sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff \ - --hash=sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100 \ - --hash=sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451 \ --hash=sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48 \ - --hash=sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621 \ - --hash=sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f \ --hash=sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb \ - --hash=sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6 \ - --hash=sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66 \ --hash=sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8 \ - --hash=sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041 \ - --hash=sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8 \ --hash=sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081 \ - --hash=sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04 \ - --hash=sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962 \ --hash=sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555 \ - --hash=sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d \ --hash=sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225 \ --hash=sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce \ --hash=sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b @@ -2715,6 +2441,7 @@ regex==2026.5.9 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # sacrebleu # sqlfluff # tiktoken + # transformers requests==2.33.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517 \ --hash=sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a @@ -2806,99 +2533,57 @@ rignore==0.7.6 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or ( --hash=sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7 \ --hash=sha256:392dcabfecbe176c9ebbcb40d85a5e86a5989559c4f988c2741da7daf1b5be25 \ --hash=sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696 \ - --hash=sha256:53fb28882d2538cb2d231972146c4927a9d9455e62b209f85d634408c4103538 \ - --hash=sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5 \ - --hash=sha256:5991e46ab9b4868334c9e372ab0892b0150f3f586ff2b1e314272caeb38aaedb \ - --hash=sha256:62020dbb89a1dd4b84ab3d60547b3b2eb2723641d5fb198463643f71eaaed57d \ - --hash=sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e \ - --hash=sha256:684014e42e4341ab3ea23a203551857fcc03a7f8ae96ca3aefb824663f55db32 \ --hash=sha256:6e01cad2b0b92f6b1993f29fc01f23f2d78caf4bf93b11096d28e9d578eb08ce \ --hash=sha256:77356ebb01ba13f8a425c3d30fcad40e57719c0e37670d022d560884a30e4767 \ - --hash=sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b \ - --hash=sha256:87409f7eeb1103d6b77f3472a3a0d9a5953e3ae804a55080bdcb0120ee43995b \ --hash=sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010 \ - --hash=sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a \ --hash=sha256:aaf938530dcc0b47c4cfa52807aa2e5bfd5ca6d57a621125fe293098692f6345 \ - --hash=sha256:b34acd532769d5a6f153a52a98dcb81615c949ab11697ce26b2eb776af2e174d \ - --hash=sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e \ --hash=sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961 \ - --hash=sha256:ba5524f5178deca4d7695e936604ebc742acb8958f9395776e1fcb8133f8257a \ --hash=sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a \ - --hash=sha256:c081f17290d8a2b96052b79207622aa635686ea39d502b976836384ede3d303c \ --hash=sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c \ - --hash=sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2 \ --hash=sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd \ - --hash=sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0 \ --hash=sha256:d8955b57e42f2a5434670d5aa7b75eaf6e74602ccd8955dddf7045379cd762fb \ - --hash=sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360 \ - --hash=sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116 + --hash=sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360 # via fastapi-cloud-cli rouge-score==0.1.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:c7d4da2683e68c9abf0135ef915d63a46643666f848e558a1b9f7ead17ff0f04 # via nemo-evaluator-sdk rpds-py==0.30.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f \ - --hash=sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136 \ - --hash=sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4 \ - --hash=sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2 \ - --hash=sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c \ --hash=sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4 \ - --hash=sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6 \ --hash=sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89 \ --hash=sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85 \ - --hash=sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa \ --hash=sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb \ --hash=sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4 \ --hash=sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23 \ - --hash=sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db \ --hash=sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27 \ --hash=sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083 \ --hash=sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738 \ --hash=sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7 \ - --hash=sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2 \ --hash=sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05 \ --hash=sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5 \ - --hash=sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7 \ --hash=sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394 \ --hash=sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6 \ --hash=sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e \ --hash=sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95 \ - --hash=sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d \ - --hash=sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3 \ - --hash=sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5 \ - --hash=sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97 \ - --hash=sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e \ - --hash=sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd \ --hash=sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e \ --hash=sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94 \ --hash=sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28 \ --hash=sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000 \ - --hash=sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1 \ --hash=sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7 \ --hash=sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d \ --hash=sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84 \ - --hash=sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f \ --hash=sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a \ --hash=sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8 \ - --hash=sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9 \ - --hash=sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a \ - --hash=sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f + --hash=sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a # via # jsonschema # referencing ruff==0.15.7 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac \ --hash=sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580 \ - --hash=sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12 \ - --hash=sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036 \ --hash=sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c \ --hash=sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf \ --hash=sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e \ - --hash=sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85 \ - --hash=sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e \ - --hash=sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4 \ - --hash=sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5 \ - --hash=sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912 + --hash=sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5 # via data-designer-engine s3transfer==0.14.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456 \ @@ -2910,6 +2595,19 @@ sacrebleu==2.6.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # via # nemo-evaluator-sdk # nemoplatform +safetensors==0.8.0rc1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:3a72817e309ed17a6b805168bca500af711c8bf50cccf7bf790ad247788c676c \ + --hash=sha256:53f59971f435fb5c23bb7bfa3c00e6cdbb26486d41f3aaf04c6d9e2d91bc8e4c \ + --hash=sha256:6ea00be4f7066ba834064fd7b104ba4b10d2e42cd915c9ece31f0e2b42e6b6d2 \ + --hash=sha256:87aad206a0bb02fa3ddaeb2feb1c6f7f39a87bea1976750ba28a857fbfcd6b31 \ + --hash=sha256:a4bacbcd2ab9efe4eb5f1ea44afc9ac5f3b40e103ebde146e370e885ba46f2fc \ + --hash=sha256:b070ba9428e6b2b3b820152b1e1583931cfbd692cda595442d5fbc8b5a46e824 \ + --hash=sha256:ba0397739b71400eab5d1f492d68484595cf8b5d13dbfef274e3bcf518348bd8 \ + --hash=sha256:ba66ebb7eaa5914ff41cd2b2cd7ccd22c844854be2a5179289e748c70ffa90a4 \ + --hash=sha256:c945f1ec6fc5a04abc174e79bce69c4613ae536c5276a3812a2a89b62ae009d7 \ + --hash=sha256:cf949f6a37287572de1c47294b36131bf49528436724eec2f96015f75a3d0bc8 \ + --hash=sha256:f61b7d2c1babc6271d778138788c57c8a95898be6ad3b559971fce20ec1c9c23 + # via transformers scikit-network==0.33.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:2408d3f4c81256a3193d536aad4a6ffcfbb05d096abe6a9cc0b6b5e275df876d \ --hash=sha256:3615d073ba9ae1ae30dda2de747474cd23c86cededa82b317471ee9f9bebd1b2 \ @@ -3099,6 +2797,7 @@ tenacity==9.1.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # instructor # langchain-community # langchain-core + # nmp-automodel # nmp-models tiktoken==0.12.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa \ @@ -3129,20 +2828,17 @@ tiktoken==0.12.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # nemo-anonymizer # ragas tokenizers==0.22.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e \ --hash=sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001 \ --hash=sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7 \ - --hash=sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd \ --hash=sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4 \ --hash=sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67 \ - --hash=sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a \ --hash=sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5 \ - --hash=sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917 \ - --hash=sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b + --hash=sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917 # via # fastembed # langchain-huggingface # litellm + # transformers tomlkit==0.14.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680 \ --hash=sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064 @@ -3168,6 +2864,11 @@ tqdm==4.67.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (pl # optuna # ragas # sqlfluff + # transformers +transformers==5.10.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:8a669db546f82c7c3618cb46ceb0f0afd89292bc70f319c058f8332ec63e268d \ + --hash=sha256:f9a44b9c8ca9ab1156b467f574d832ea066284299c2fd0ed84641ccb592751fc + # via nemo-customizer-plugin typer==0.24.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e \ --hash=sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45 @@ -3178,6 +2879,8 @@ typer==0.24.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p # huggingface-hub # instructor # nemo-auditor-plugin + # nemo-automodel-plugin + # nemo-customizer-plugin # nemo-evaluator-plugin # nemo-platform-ext # nemo-platform-plugin @@ -3185,6 +2888,7 @@ typer==0.24.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p # nemo-safe-synthesizer-plugin # nemoguardrails # ragas + # transformers types-aioboto3==15.5.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:5769a1c3df7ca1abedf3656ddf0b970c9b0436d0f88cf4686040b55cd2a02925 \ --hash=sha256:8aed7c9b6fe9b59e6ce74f7a6db7b8a9912a34c8f80ed639fac1fa59d6b20aa1 @@ -3282,20 +2986,15 @@ urllib3==2.7.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or ( # requests # sentry-sdk uuid-utils==0.14.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3 \ --hash=sha256:0b5d2ad28063d422ccc2c28d46471d47b61a58de885d35113a8f18cb547e25bf \ - --hash=sha256:50fffc2827348c1e48972eed3d1c698959e63f9d030aa5dd82ba451113158a62 \ - --hash=sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae \ --hash=sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0 \ --hash=sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69 \ --hash=sha256:b197cd5424cf89fb019ca7f53641d05bfe34b1879614bed111c9c313b5574cd8 \ --hash=sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1 \ - --hash=sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96 \ --hash=sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43 \ --hash=sha256:c1dbe718765f70f5b7f9b7f66b6a937802941b1cc56bcf642ce0274169741e01 \ --hash=sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297 \ - --hash=sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862 \ - --hash=sha256:da2234387b45fde40b0fedfee64a0ba591caeea9c48c7698ab6e2d85c7991533 + --hash=sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862 # via # langchain-core # langsmith @@ -3360,25 +3059,19 @@ wasmtime==43.0.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # via nmp-auth watchdog==6.0.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2 \ - --hash=sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f \ - --hash=sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c \ --hash=sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c \ --hash=sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c \ --hash=sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0 \ --hash=sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13 \ - --hash=sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379 \ --hash=sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282 \ --hash=sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b \ --hash=sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c \ - --hash=sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948 \ - --hash=sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26 + --hash=sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948 # via nemoguardrails watchfiles==1.1.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219 \ --hash=sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803 \ --hash=sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94 \ - --hash=sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6 \ - --hash=sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae \ --hash=sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43 \ --hash=sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10 \ --hash=sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374 \ @@ -3387,28 +3080,18 @@ watchfiles==1.1.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') o --hash=sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77 \ --hash=sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741 \ --hash=sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a \ - --hash=sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d \ --hash=sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701 \ --hash=sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6 \ - --hash=sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620 \ --hash=sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef \ --hash=sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af \ --hash=sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336 \ - --hash=sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff \ --hash=sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2 \ --hash=sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606 \ - --hash=sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04 \ --hash=sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610 \ - --hash=sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150 \ --hash=sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b \ --hash=sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d \ - --hash=sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70 \ --hash=sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24 \ --hash=sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e \ - --hash=sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb \ - --hash=sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428 \ - --hash=sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b \ - --hash=sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa \ --hash=sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf # via # fastmcp @@ -3486,44 +3169,28 @@ xdg-base-dirs==6.0.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin' --hash=sha256:950504e14d27cf3c9cb37744680a43bf0ac42efefc4ef4acf98dc736cab2bced # via garak-api xxhash==3.6.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c \ - --hash=sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1 \ --hash=sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8 \ - --hash=sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa \ --hash=sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa \ - --hash=sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5 \ - --hash=sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11 \ --hash=sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae \ --hash=sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d \ --hash=sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2 \ - --hash=sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89 \ - --hash=sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e \ --hash=sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3 \ --hash=sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db \ --hash=sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033 \ - --hash=sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f \ - --hash=sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42 \ --hash=sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f \ --hash=sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd \ - --hash=sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d \ --hash=sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1 \ - --hash=sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374 \ --hash=sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263 \ --hash=sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13 \ --hash=sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62 \ - --hash=sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b \ --hash=sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2 \ --hash=sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204 \ - --hash=sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546 \ --hash=sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9 \ --hash=sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd \ - --hash=sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf \ --hash=sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0 \ - --hash=sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292 \ --hash=sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6 \ --hash=sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd \ --hash=sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7 \ - --hash=sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0 \ --hash=sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee # via # datasets @@ -3543,64 +3210,32 @@ yara-python==4.5.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') --hash=sha256:f533848781f0e46e44eda77055eae4ec934cf56c1f473e787704f1a348e90094 # via nmp-guardrails yarl==1.23.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc \ - --hash=sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222 \ - --hash=sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de \ --hash=sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25 \ --hash=sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e \ --hash=sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2 \ --hash=sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860 \ - --hash=sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760 \ - --hash=sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788 \ --hash=sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035 \ --hash=sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4 \ - --hash=sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a \ --hash=sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34 \ - --hash=sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb \ - --hash=sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f \ - --hash=sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8 \ - --hash=sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c \ --hash=sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512 \ --hash=sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9 \ - --hash=sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072 \ --hash=sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5 \ --hash=sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a \ - --hash=sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2 \ --hash=sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c \ --hash=sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5 \ --hash=sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b \ - --hash=sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7 \ - --hash=sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2 \ --hash=sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7 \ - --hash=sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123 \ - --hash=sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea \ --hash=sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598 \ --hash=sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8 \ - --hash=sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83 \ - --hash=sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6 \ --hash=sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f \ --hash=sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51 \ - --hash=sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1 \ - --hash=sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05 \ --hash=sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa \ --hash=sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a \ --hash=sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99 \ - --hash=sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d \ - --hash=sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86 \ - --hash=sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67 \ --hash=sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59 \ - --hash=sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4 \ - --hash=sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f \ --hash=sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432 \ - --hash=sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b \ --hash=sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543 \ - --hash=sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24 \ - --hash=sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5 \ - --hash=sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b \ - --hash=sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b \ - --hash=sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735 \ - --hash=sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401 \ - --hash=sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6 + --hash=sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24 # via aiohttp zipp==3.23.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ @@ -3608,39 +3243,27 @@ zipp==3.23.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (pl # via importlib-metadata zstandard==0.25.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a \ - --hash=sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6 \ - --hash=sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250 \ --hash=sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f \ --hash=sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3 \ --hash=sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6 \ - --hash=sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa \ --hash=sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611 \ --hash=sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b \ - --hash=sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e \ --hash=sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa \ --hash=sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf \ --hash=sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902 \ - --hash=sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc \ - --hash=sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98 \ - --hash=sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a \ --hash=sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097 \ --hash=sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea \ - --hash=sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb \ --hash=sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7 \ --hash=sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b \ --hash=sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a \ --hash=sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00 \ --hash=sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072 \ - --hash=sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065 \ - --hash=sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512 \ --hash=sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1 \ --hash=sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f \ --hash=sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b \ --hash=sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea \ --hash=sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277 \ - --hash=sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708 \ - --hash=sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313 \ - --hash=sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551 + --hash=sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313 # via # clickhouse-connect # langsmith diff --git a/uv.lock b/uv.lock index 6364dbc06c..259d87ffb3 100644 --- a/uv.lock +++ b/uv.lock @@ -28,6 +28,8 @@ members = [ "nemo-agents-plugin", "nemo-anonymizer-plugin", "nemo-auditor-plugin", + "nemo-automodel-plugin", + "nemo-customizer-plugin", "nemo-data-designer-plugin", "nemo-evaluator-plugin", "nemo-evaluator-sdk", @@ -42,6 +44,7 @@ members = [ "nemo-switchyard", "nemoplatform", "nmp-auth", + "nmp-automodel", "nmp-build-tools", "nmp-common", "nmp-core-mcp", @@ -3692,6 +3695,88 @@ dev = [ { name = "ruff", specifier = ">=0.11.8" }, ] +[[package]] +name = "nemo-automodel-plugin" +version = "0.1.0" +source = { editable = "plugins/nemo-automodel" } +dependencies = [ + { name = "nemo-platform", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nemo-platform-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nmp-automodel", 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 = "pydantic-settings", 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 = "typer", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[package.dev-dependencies] +dev = [ + { 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 = "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-customizer-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pytest", 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 = "pytest-asyncio", 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 = "ruff", 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 = "nemo-platform", editable = "packages/nemo_platform" }, + { name = "nemo-platform-plugin", editable = "packages/nemo_platform_plugin" }, + { name = "nmp-automodel", editable = "services/automodel" }, + { name = "pydantic", specifier = ">=2.10.6" }, + { name = "pydantic-settings", specifier = ">=2.6.1" }, + { name = "typer", specifier = ">=0.12.5" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "fastapi", specifier = ">=0.115.0" }, + { name = "httpx", specifier = ">=0.27.0" }, + { name = "nemo-customizer-plugin", editable = "plugins/nemo-customizer" }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-asyncio", specifier = ">=0.25.3" }, + { name = "ruff", specifier = ">=0.11.8" }, +] + +[[package]] +name = "nemo-customizer-plugin" +version = "0.1.0" +source = { editable = "plugins/nemo-customizer" } +dependencies = [ + { name = "datasets", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nemo-platform", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nemo-platform-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "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 = "transformers", 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 = "typer", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[package.dev-dependencies] +dev = [ + { 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 = "pytest", 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 = "pytest-asyncio", 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 = "ruff", 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 = "datasets", specifier = ">=3.3.1" }, + { name = "nemo-platform", editable = "packages/nemo_platform" }, + { name = "nemo-platform-plugin", editable = "packages/nemo_platform_plugin" }, + { name = "pydantic", specifier = ">=2.10.6" }, + { name = "transformers", specifier = ">=4.48.0" }, + { name = "typer", specifier = ">=0.12.5" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "fastapi", specifier = ">=0.115.0" }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-asyncio", specifier = ">=0.25.3" }, + { name = "ruff", specifier = ">=0.11.8" }, +] + [[package]] name = "nemo-data-designer-plugin" version = "0.1.0" @@ -5709,6 +5794,8 @@ core-services = [ { name = "nemo-agents-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-anonymizer-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-auditor-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nemo-automodel-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nemo-customizer-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-data-designer-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-evaluator-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-guardrails-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')" }, @@ -5804,6 +5891,8 @@ enabled-plugins = [ { name = "nemo-agents-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-anonymizer-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-auditor-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nemo-automodel-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nemo-customizer-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-data-designer-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-evaluator-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-guardrails-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')" }, @@ -5814,6 +5903,8 @@ functional-services = [ { name = "nemo-agents-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-anonymizer-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-auditor-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nemo-automodel-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nemo-customizer-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-data-designer-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-evaluator-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-guardrails-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')" }, @@ -5905,6 +5996,8 @@ core-services = [ { name = "nemo-agents-plugin", editable = "plugins/nemo-agents" }, { name = "nemo-anonymizer-plugin", editable = "plugins/nemo-anonymizer" }, { name = "nemo-auditor-plugin", editable = "plugins/nemo-auditor" }, + { name = "nemo-automodel-plugin", editable = "plugins/nemo-automodel" }, + { name = "nemo-customizer-plugin", editable = "plugins/nemo-customizer" }, { name = "nemo-data-designer-plugin", editable = "plugins/nemo-data-designer" }, { name = "nemo-evaluator-plugin", editable = "plugins/nemo-evaluator" }, { name = "nemo-guardrails-plugin", editable = "plugins/nemo-guardrails" }, @@ -6003,6 +6096,8 @@ enabled-plugins = [ { name = "nemo-agents-plugin", editable = "plugins/nemo-agents" }, { name = "nemo-anonymizer-plugin", editable = "plugins/nemo-anonymizer" }, { name = "nemo-auditor-plugin", editable = "plugins/nemo-auditor" }, + { name = "nemo-automodel-plugin", editable = "plugins/nemo-automodel" }, + { name = "nemo-customizer-plugin", editable = "plugins/nemo-customizer" }, { name = "nemo-data-designer-plugin", editable = "plugins/nemo-data-designer" }, { name = "nemo-evaluator-plugin", editable = "plugins/nemo-evaluator" }, { name = "nemo-guardrails-plugin", editable = "plugins/nemo-guardrails" }, @@ -6013,6 +6108,8 @@ functional-services = [ { name = "nemo-agents-plugin", editable = "plugins/nemo-agents" }, { name = "nemo-anonymizer-plugin", editable = "plugins/nemo-anonymizer" }, { name = "nemo-auditor-plugin", editable = "plugins/nemo-auditor" }, + { name = "nemo-automodel-plugin", editable = "plugins/nemo-automodel" }, + { name = "nemo-customizer-plugin", editable = "plugins/nemo-customizer" }, { name = "nemo-data-designer-plugin", editable = "plugins/nemo-data-designer" }, { name = "nemo-evaluator-plugin", editable = "plugins/nemo-evaluator" }, { name = "nemo-guardrails-plugin", editable = "plugins/nemo-guardrails" }, @@ -6175,6 +6272,44 @@ dev = [ { name = "typer", specifier = ">=0.9.0" }, ] +[[package]] +name = "nmp-automodel" +version = "0.1.0" +source = { editable = "services/automodel" } +dependencies = [ + { name = "aiofiles", 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 = "jsonschema", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nemo-platform-sdk", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nmp-common", 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 = "pydantic-settings", 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 = "tenacity", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[package.optional-dependencies] +dev = [ + { name = "pytest", 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 = "pytest-asyncio", 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 = "pytest-mock", 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 = "aiofiles", specifier = ">=24.1.0" }, + { name = "httpx", specifier = ">=0.27.0" }, + { name = "jsonschema", specifier = ">=4.23.0" }, + { name = "nemo-platform-sdk", editable = "sdk/python/nemo-platform" }, + { name = "nmp-common", editable = "packages/nmp_common" }, + { name = "pydantic", specifier = ">=2.10.6" }, + { name = "pydantic-settings", specifier = ">=2.6.1" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.4" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.25.3" }, + { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.14.0" }, + { name = "tenacity", specifier = ">=8.5.0" }, +] +provides-extras = ["dev"] + [[package]] name = "nmp-build-tools" version = "0.0.0" @@ -9189,6 +9324,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/06/f2/6c90ccf3ad1d09a7d662a405b274f3c93b92df59c8d6a025d26aaf34d302/sacrebleu-2.6.0-py3-none-any.whl", hash = "sha256:3edc1531575cfe4ad04ce53491a9307e234af1c3f805a1f491cbec844229a8a8", size = 100785, upload-time = "2026-01-12T17:17:18.868Z" }, ] +[[package]] +name = "safetensors" +version = "0.8.0rc1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/a1/d46f642820c00443e86343b9548647a82c78f8818857652084852757f805/safetensors-0.8.0rc1.tar.gz", hash = "sha256:a4bacbcd2ab9efe4eb5f1ea44afc9ac5f3b40e103ebde146e370e885ba46f2fc", size = 325883, upload-time = "2026-06-01T09:54:53.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/5d/2aa9139997bf1681778b96188522480c862af0b4efd978eaae5171296105/safetensors-0.8.0rc1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ba66ebb7eaa5914ff41cd2b2cd7ccd22c844854be2a5179289e748c70ffa90a4", size = 484485, upload-time = "2026-06-01T09:54:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/01/07/c78d1bb09f83885467d4472b14598efc933bd37e0de443f18d23758b0ff3/safetensors-0.8.0rc1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a72817e309ed17a6b805168bca500af711c8bf50cccf7bf790ad247788c676c", size = 503240, upload-time = "2026-06-01T09:54:37.393Z" }, + { url = "https://files.pythonhosted.org/packages/a8/1a/5ef0b186d4edda2ecdd1b4f4b384c03afa64ce17b4582c33329c32bad9ca/safetensors-0.8.0rc1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c945f1ec6fc5a04abc174e79bce69c4613ae536c5276a3812a2a89b62ae009d7", size = 511782, upload-time = "2026-06-01T09:54:38.903Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/699638f35a524de781ba16cc5c54f31430fd04387960b1d7dc0d5d5fa305/safetensors-0.8.0rc1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba0397739b71400eab5d1f492d68484595cf8b5d13dbfef274e3bcf518348bd8", size = 633524, upload-time = "2026-06-01T09:54:40.169Z" }, + { url = "https://files.pythonhosted.org/packages/4f/bc/12ecd32fec076e836c25e57ac991e09d26fd448d361228057d3d5b3e4d1b/safetensors-0.8.0rc1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f61b7d2c1babc6271d778138788c57c8a95898be6ad3b559971fce20ec1c9c23", size = 545342, upload-time = "2026-06-01T09:54:42.585Z" }, + { url = "https://files.pythonhosted.org/packages/0c/8f/23e0eca29cbaba9f89917e6ca2840f6a80bbcdf659a0e2cd59d311549b7b/safetensors-0.8.0rc1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53f59971f435fb5c23bb7bfa3c00e6cdbb26486d41f3aaf04c6d9e2d91bc8e4c", size = 516088, upload-time = "2026-06-01T09:54:45.356Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/490c76d05f9dd7041bdf29d72b43084ed1498dadebaca5955491e488f2be/safetensors-0.8.0rc1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:b070ba9428e6b2b3b820152b1e1583931cfbd692cda595442d5fbc8b5a46e824", size = 513718, upload-time = "2026-06-01T09:54:41.477Z" }, + { url = "https://files.pythonhosted.org/packages/73/37/1c2cd01fcf86703433bd2df7371f6d864449d31bfc0289d2575d9e4b001c/safetensors-0.8.0rc1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87aad206a0bb02fa3ddaeb2feb1c6f7f39a87bea1976750ba28a857fbfcd6b31", size = 678511, upload-time = "2026-06-01T09:54:48.861Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/15c17603d8b575253431a574af4ffff447550a9ad6023f2ea643dad7af47/safetensors-0.8.0rc1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:6ea00be4f7066ba834064fd7b104ba4b10d2e42cd915c9ece31f0e2b42e6b6d2", size = 786778, upload-time = "2026-06-01T09:54:50.231Z" }, + { url = "https://files.pythonhosted.org/packages/9b/03/53ecffb459f5c58b8712e9fde57e8e2a011274eb7ec1f8de3db4641039a2/safetensors-0.8.0rc1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cf949f6a37287572de1c47294b36131bf49528436724eec2f96015f75a3d0bc8", size = 722435, upload-time = "2026-06-01T09:54:52.767Z" }, +] + [[package]] name = "scikit-network" version = "0.33.5" @@ -9857,6 +10010,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] +[[package]] +name = "transformers" +version = "5.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", 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 = "numpy", 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 = "packaging", 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 = "regex", 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 = "safetensors", 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 = "tokenizers", 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 = "tqdm", 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 = "typer", 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')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/38/d5f978bd5091019e89aef29b9a831f5cd70f2598963a3ead8b9570cab592/transformers-5.10.2.tar.gz", hash = "sha256:f9a44b9c8ca9ab1156b467f574d832ea066284299c2fd0ed84641ccb592751fc", size = 8799687, upload-time = "2026-06-04T18:43:49.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/6f/e1564b0cc182afa05e219a8e09a8e770ffaab879b6b824b56c819bd221da/transformers-5.10.2-py3-none-any.whl", hash = "sha256:8a669db546f82c7c3618cb46ceb0f0afd89292bc70f319c058f8332ec63e268d", size = 11003830, upload-time = "2026-06-04T18:43:45.303Z" }, +] + [[package]] name = "trove-classifiers" version = "2026.1.14.14" From e0232ed6e33a7653068649970ca138c4b583f376 Mon Sep 17 00:00:00 2001 From: anubhutiv Date: Wed, 3 Jun 2026 09:19:40 -0700 Subject: [PATCH 03/26] feat(customizer): add unsloth backend plugin Signed-off-by: Sam Oluwalana --- .../src/nemo_customizer/router.py | 35 +- .../src/nemo_customizer/sdk/resources.py | 5 + .../skills/nemo-customizer/SKILL.md | 231 ++++- .../references/dataset-formats.md | 46 +- .../references/hf-conversion.md | 22 + .../references/hyperparameters.md | 310 +++++- .../references/troubleshooting.md | 63 +- .../skills/nemo-customizer/tests.json | 20 + plugins/nemo-customizer/tests/test_router.py | 41 + plugins/nemo-unsloth/README.md | 90 ++ plugins/nemo-unsloth/pyproject.toml | 56 ++ .../src/nemo_unsloth_plugin/__init__.py | 4 + .../src/nemo_unsloth_plugin/cli/__init__.py | 9 + .../src/nemo_unsloth_plugin/cli/inputs.py | 128 +++ .../src/nemo_unsloth_plugin/cli/main.py | 26 + .../src/nemo_unsloth_plugin/config.py | 34 + .../src/nemo_unsloth_plugin/contributor.py | 115 +++ .../src/nemo_unsloth_plugin/jobs/__init__.py | 2 + .../src/nemo_unsloth_plugin/jobs/jobs.py | 152 +++ .../src/nemo_unsloth_plugin/schema.py | 147 +++ .../src/nemo_unsloth_plugin/sdk/__init__.py | 18 + .../src/nemo_unsloth_plugin/sdk/http_utils.py | 64 ++ .../nemo_unsloth_plugin/sdk/job_resources.py | 91 ++ .../src/nemo_unsloth_plugin/sdk/resources.py | 180 ++++ .../src/nemo_unsloth_plugin/transform.py | 129 +++ .../tests/fixtures/minimal_unsloth_sft.json | 15 + plugins/nemo-unsloth/tests/test_cli.py | 212 +++++ .../nemo-unsloth/tests/test_contributor.py | 110 +++ plugins/nemo-unsloth/tests/test_jobs.py | 171 ++++ plugins/nemo-unsloth/tests/test_schema.py | 251 +++++ pyproject.toml | 6 + services/unsloth/README.md | 63 ++ .../docker/Dockerfile.nmp-unsloth-training | 87 ++ .../docker/Dockerfile.platform-workspace | 20 + services/unsloth/docker/README.md | 306 ++++++ services/unsloth/docker/docker-bake.hcl | 56 ++ .../unsloth/docker/pyproject.workspace.toml | 27 + services/unsloth/pyproject.toml | 60 ++ .../unsloth/src/nmp/unsloth/app/__init__.py | 2 + .../unsloth/src/nmp/unsloth/app/constants.py | 28 + .../src/nmp/unsloth/app/jobs/__init__.py | 2 + .../src/nmp/unsloth/app/jobs/compiler.py | 249 +++++ .../src/nmp/unsloth/app/jobs/context.py | 89 ++ .../nmp/unsloth/app/jobs/file_io/__init__.py | 2 + .../nmp/unsloth/app/jobs/file_io/schemas.py | 175 ++++ .../unsloth/app/jobs/model_entity/__init__.py | 2 + .../unsloth/app/jobs/model_entity/schemas.py | 110 +++ .../nmp/unsloth/app/jobs/training/__init__.py | 2 + .../nmp/unsloth/app/jobs/training/compiler.py | 82 ++ .../nmp/unsloth/app/jobs/training/schemas.py | 27 + services/unsloth/src/nmp/unsloth/compile.py | 46 + services/unsloth/src/nmp/unsloth/config.py | 47 + .../src/nmp/unsloth/entities/__init__.py | 2 + .../src/nmp/unsloth/entities/values.py | 40 + services/unsloth/src/nmp/unsloth/images.py | 65 ++ .../src/nmp/unsloth/platform_client.py | 80 ++ services/unsloth/src/nmp/unsloth/schemas.py | 326 +++++++ .../src/nmp/unsloth/tasks/file_io/__init__.py | 8 + .../src/nmp/unsloth/tasks/file_io/__main__.py | 9 + .../nmp/unsloth/tasks/file_io/callbacks.py | 481 ++++++++++ .../tasks/file_io/progress_reporter.py | 93 ++ .../src/nmp/unsloth/tasks/file_io/run.py | 507 ++++++++++ .../src/nmp/unsloth/tasks/file_io/utils.py | 125 +++ .../unsloth/tasks/model_entity/__init__.py | 8 + .../unsloth/tasks/model_entity/__main__.py | 15 + .../src/nmp/unsloth/tasks/model_entity/run.py | 437 +++++++++ .../nmp/unsloth/tasks/progress_reporter.py | 12 + .../nmp/unsloth/tasks/training/__main__.py | 131 +++ .../tasks/training/backends/unsloth_sft.py | 337 +++++++ services/unsloth/tests/test_compile.py | 67 ++ .../unsloth/tests/test_dataset_loading.py | 54 ++ services/unsloth/tests/test_file_io.py | 313 ++++++ services/unsloth/tests/test_main.py | 121 +++ services/unsloth/tests/test_model_entity.py | 493 ++++++++++ services/unsloth/tests/test_schemas.py | 105 ++ third_party/osv-licenses.json | 2 +- third_party/requirements-main.txt | 20 +- uv.lock | 897 +++++++++++++++++- 78 files changed, 8948 insertions(+), 65 deletions(-) create mode 100644 plugins/nemo-unsloth/README.md create mode 100644 plugins/nemo-unsloth/pyproject.toml create mode 100644 plugins/nemo-unsloth/src/nemo_unsloth_plugin/__init__.py create mode 100644 plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/__init__.py create mode 100644 plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/inputs.py create mode 100644 plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/main.py create mode 100644 plugins/nemo-unsloth/src/nemo_unsloth_plugin/config.py create mode 100644 plugins/nemo-unsloth/src/nemo_unsloth_plugin/contributor.py create mode 100644 plugins/nemo-unsloth/src/nemo_unsloth_plugin/jobs/__init__.py create mode 100644 plugins/nemo-unsloth/src/nemo_unsloth_plugin/jobs/jobs.py create mode 100644 plugins/nemo-unsloth/src/nemo_unsloth_plugin/schema.py create mode 100644 plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/__init__.py create mode 100644 plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/http_utils.py create mode 100644 plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/job_resources.py create mode 100644 plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/resources.py create mode 100644 plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py create mode 100644 plugins/nemo-unsloth/tests/fixtures/minimal_unsloth_sft.json create mode 100644 plugins/nemo-unsloth/tests/test_cli.py create mode 100644 plugins/nemo-unsloth/tests/test_contributor.py create mode 100644 plugins/nemo-unsloth/tests/test_jobs.py create mode 100644 plugins/nemo-unsloth/tests/test_schema.py create mode 100644 services/unsloth/README.md create mode 100644 services/unsloth/docker/Dockerfile.nmp-unsloth-training create mode 100644 services/unsloth/docker/Dockerfile.platform-workspace create mode 100644 services/unsloth/docker/README.md create mode 100644 services/unsloth/docker/docker-bake.hcl create mode 100644 services/unsloth/docker/pyproject.workspace.toml create mode 100644 services/unsloth/pyproject.toml create mode 100644 services/unsloth/src/nmp/unsloth/app/__init__.py create mode 100644 services/unsloth/src/nmp/unsloth/app/constants.py create mode 100644 services/unsloth/src/nmp/unsloth/app/jobs/__init__.py create mode 100644 services/unsloth/src/nmp/unsloth/app/jobs/compiler.py create mode 100644 services/unsloth/src/nmp/unsloth/app/jobs/context.py create mode 100644 services/unsloth/src/nmp/unsloth/app/jobs/file_io/__init__.py create mode 100644 services/unsloth/src/nmp/unsloth/app/jobs/file_io/schemas.py create mode 100644 services/unsloth/src/nmp/unsloth/app/jobs/model_entity/__init__.py create mode 100644 services/unsloth/src/nmp/unsloth/app/jobs/model_entity/schemas.py create mode 100644 services/unsloth/src/nmp/unsloth/app/jobs/training/__init__.py create mode 100644 services/unsloth/src/nmp/unsloth/app/jobs/training/compiler.py create mode 100644 services/unsloth/src/nmp/unsloth/app/jobs/training/schemas.py create mode 100644 services/unsloth/src/nmp/unsloth/compile.py create mode 100644 services/unsloth/src/nmp/unsloth/config.py create mode 100644 services/unsloth/src/nmp/unsloth/entities/__init__.py create mode 100644 services/unsloth/src/nmp/unsloth/entities/values.py create mode 100644 services/unsloth/src/nmp/unsloth/images.py create mode 100644 services/unsloth/src/nmp/unsloth/platform_client.py create mode 100644 services/unsloth/src/nmp/unsloth/schemas.py create mode 100644 services/unsloth/src/nmp/unsloth/tasks/file_io/__init__.py create mode 100644 services/unsloth/src/nmp/unsloth/tasks/file_io/__main__.py create mode 100644 services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py create mode 100644 services/unsloth/src/nmp/unsloth/tasks/file_io/progress_reporter.py create mode 100644 services/unsloth/src/nmp/unsloth/tasks/file_io/run.py create mode 100644 services/unsloth/src/nmp/unsloth/tasks/file_io/utils.py create mode 100644 services/unsloth/src/nmp/unsloth/tasks/model_entity/__init__.py create mode 100644 services/unsloth/src/nmp/unsloth/tasks/model_entity/__main__.py create mode 100644 services/unsloth/src/nmp/unsloth/tasks/model_entity/run.py create mode 100644 services/unsloth/src/nmp/unsloth/tasks/progress_reporter.py create mode 100644 services/unsloth/src/nmp/unsloth/tasks/training/__main__.py create mode 100644 services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py create mode 100644 services/unsloth/tests/test_compile.py create mode 100644 services/unsloth/tests/test_dataset_loading.py create mode 100644 services/unsloth/tests/test_file_io.py create mode 100644 services/unsloth/tests/test_main.py create mode 100644 services/unsloth/tests/test_model_entity.py create mode 100644 services/unsloth/tests/test_schemas.py diff --git a/plugins/nemo-customizer/src/nemo_customizer/router.py b/plugins/nemo-customizer/src/nemo_customizer/router.py index addc683c80..1f372138b2 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/router.py +++ b/plugins/nemo-customizer/src/nemo_customizer/router.py @@ -31,17 +31,32 @@ def merge_router_dependencies(contributors: dict[str, object]) -> list[str]: return sorted(deps) -def _assert_no_prefix_collisions(contributors: dict[str, object]) -> None: - prefixes: dict[str, str] = {} +def _assert_no_route_collisions(contributors: dict[str, object]) -> None: + """Catch contributors that would handle the same ``(METHOD, PATH)`` pair. + + Contributors are free to share a parent mount prefix — e.g. every backend's + jobs router is mounted under ``/v2/workspaces/{workspace}`` and adds its + own ``/{backend}/jobs/...`` paths via ``job_collection_path_for``. We only + error when two contributors would respond to the same HTTP method on the + same fully-qualified path. + """ + # Map (method, full_path) -> contributor key + seen: dict[tuple[str, str], str] = {} for key, contributor in contributors.items(): for spec in contributor.get_routers(): # type: ignore[union-attr] - prefix = spec.prefix.strip("/") - if prefix in prefixes: - raise CustomizationRouterError( - f"Route prefix collision: contributors {prefixes[prefix]!r} and {key!r} " - f"both use prefix {spec.prefix!r}", - ) - prefixes[prefix] = key + prefix = spec.prefix.rstrip("/") + for route in spec.router.routes: + methods = getattr(route, "methods", None) or {"*"} + path = getattr(route, "path", "") + full_path = f"{prefix}{path}" + for method in methods: + op = (method, full_path) + if op in seen: + raise CustomizationRouterError( + f"Route collision: contributors {seen[op]!r} and {key!r} " + f"both handle {method} {full_path}", + ) + seen[op] = key class CustomizationRouterService(NemoService): @@ -58,7 +73,7 @@ def __init__(self) -> None: "Install a backend plugin (e.g. nemo-automodel) and ensure " f"'{CUSTOMIZATION_CONTRIBUTORS_GROUP}' entry points are registered.", ) - _assert_no_prefix_collisions(self._contributors) + _assert_no_route_collisions(self._contributors) type(self).dependencies = merge_router_dependencies(self._contributors) def get_routers(self) -> list[RouterSpec]: diff --git a/plugins/nemo-customizer/src/nemo_customizer/sdk/resources.py b/plugins/nemo-customizer/src/nemo_customizer/sdk/resources.py index b8cc1fb996..d769619728 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/sdk/resources.py +++ b/plugins/nemo-customizer/src/nemo_customizer/sdk/resources.py @@ -22,6 +22,11 @@ "AutomodelCustomization", "AsyncAutomodelCustomization", ), + "unsloth": ( + "nemo_unsloth_plugin.sdk.resources", + "UnslothCustomization", + "AsyncUnslothCustomization", + ), } diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/SKILL.md b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/SKILL.md index 11e34a06b9..0805de6885 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/SKILL.md +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/SKILL.md @@ -1,10 +1,11 @@ --- name: nemo-customizer description: >- - Fine-tune models on NeMo Platform via `nemo customization automodel submit`: - HF dataset conversion, filesets, model entities, SFT/LoRA job JSON (hyperparameters, - batch, schedule, optimizer), and job polling. Use for train, fine-tune, customize, - SFT, LoRA, learning rate, epochs, or nemo customization. + Fine-tune models on NeMo Platform with `automodel` (`submit` → GPU containers on + the platform) or `unsloth` (`run --venv` → local in-process, BYO-venv): HF dataset + conversion, filesets, model entities, SFT/LoRA job JSON (hyperparameters, batch, + schedule, optimizer), and job polling. Use for train, fine-tune, customize, SFT, + LoRA, learning rate, epochs, or nemo customization. triggers: - nemo-customizer - nemo customizer @@ -37,39 +38,78 @@ allowed-tools: [Bash, Read, Grep] # NeMo Customizer -End-to-end **SFT + LoRA** on NeMo Platform. Default plugin: **automodel**. Batch shell work; reuse resources with `--exist-ok`; skip CLI `--help` unless a command fails. +End-to-end **SFT + LoRA** on NeMo Platform. Two backend plugins ship in this repo: + +| Backend | Verb | Where it runs | Pick when | +|---------|------|---------------|-----------| +| **`automodel`** (default) | `submit` | Platform-managed GPU containers (jobs service) | Platform exposes a GPU execution profile, or the user wants a persisted job they can poll/share | +| **`unsloth`** | `run --venv ` | **Locally**, in a BYO-venv re-exec on the caller's GPU | User asks for Unsloth, the platform has **no GPU execution profile**, or the user wants a quick single-GPU local SFT/LoRA | + +Decision rule below in **Plugin pick**. Batch shell work; reuse resources with `--exist-ok`; skip CLI `--help` unless a command fails. + +## Plugin pick + +1. After `nemo auth login`, run `uv run nemo jobs list-execution-profiles -f json` (see `references/troubleshooting.md` for parsing). +2. If the user explicitly asked for Unsloth → **`unsloth`**. +3. Else if any profile has `provider: gpu` or `gpu_distributed` → **`automodel`** (default). +4. Else if the caller machine has a usable local GPU → **`unsloth`** (after the `--venv` one-time setup below). +5. Else stop and tell the user remote GPU customization is unavailable. + +**Unsloth `--venv` requirement.** The base `nemo` install has no Unsloth/torch. Before the first `unsloth run`, set up a separate venv with the heavy ML extras (one-time): + +```bash +UNSLOTH_VENV=/workspace/.venv-unsloth +uv venv "$UNSLOTH_VENV" --python 3.11 +uv pip install --python "$UNSLOTH_VENV/bin/python" -e plugins/nemo-unsloth[unsloth] +``` + +Then every `unsloth run` passes `--venv "$UNSLOTH_VENV"`. The platform re-execs into that interpreter before importing `unsloth`. Without `--venv`, the run errors with an actionable setup hint. ## Gotchas - Run all `uv run` commands from the **nemo-platform** git root (top-level `pyproject.toml`), not a plugin subfolder. - Set `NEMO_BASE_URL` (or `NMP_BASE_URL`) only when the user gives a platform URL; default `http://127.0.0.1:8080`. -- **Never set `max_steps` together with `epochs`.** `max_steps` is a global cap and stops mid-epoch. Test fixtures include `max_steps` for smoke tests — do not copy into production jobs. -- **Job done = top-level `status`** in `completed` | `error` | `cancelled`. Steps can all be `completed` while the job is still `active` (upload, entity registration). `status_details.phase` may stay `training` with `progress_pct: 100` for a long time — keep polling. `poll_automodel_job.sh` exits **1** on `error` or `cancelled`. +- **Verb is backend-specific.** Automodel is **`submit` only** (no local `run`). Unsloth is **`run` only** (no `submit`); a stray `nemo customization unsloth submit ...` exits non-zero with a friendly hint. Do not improvise verbs. +- **Never set `max_steps` together with `epochs`** (both backends). `max_steps` is a global cap and stops mid-epoch. Test fixtures include `max_steps` for smoke tests — do not copy into production jobs. Unsloth's schema enforces this as a hard mutex; automodel allows both but the result is surprising. +- **Job done (automodel) = top-level `status`** in `completed` | `error` | `cancelled`. Steps can all be `completed` while the job is still `active` (upload, entity registration). `status_details.phase` may stay `training` with `progress_pct: 100` for a long time — keep polling. `poll_automodel_job.sh` exits **1** on `error` or `cancelled`. +- **Job done (unsloth) = `nemo customization unsloth run` returns.** It is synchronous and in-process — the result dict (`loss`, `output_path`, `output_fileset`, `output_model_entity`) prints to stdout. There is no remote job record to poll. - Model spec fills async: **submit without polling** `nemo models get` unless submit fails. - HF dataset id from the user → convert locally; do not ask for local paths first. - Dataset fileset name = HF dataset **name** only (`tau/commonsense_qa` → `commonsense_qa`), not the model name. -- Prefer **CHAT** JSONL when the model has a chat template; details in `references/dataset-formats.md`. -- User asks to tune **batch or parallelism** → **Batch sizing** / **Multi-GPU** below. Other fields (LR, epochs, LoRA rank, distillation) → `references/hyperparameters.md` (`nemo customization automodel explain` for schema). -- Skill **defaults** (`micro_batch_size` 1, `global_batch_size` 4) are safe on unknown VRAM. When the user has **≥48 GB** on one GPU, use **Batch sizing** instead of defaults. +- Prefer **CHAT** JSONL when the model has a chat template; details in `references/dataset-formats.md` (automodel auto-detects schema; unsloth needs `dataset.apply_chat_template: true` to consume `messages`). +- User asks to tune **batch or parallelism** (automodel) → **Batch sizing** / **Multi-GPU** below. Other fields (LR, epochs, LoRA rank, distillation) → `references/hyperparameters.md`. For unsloth, see **Batch sizing — unsloth** and the `Unsloth job JSON` section in `references/hyperparameters.md`. Run `nemo customization explain` for the live schema. +- Skill **defaults** (`micro_batch_size` 1, `global_batch_size` 4) are safe on unknown VRAM. When the user has **≥48 GB** on one GPU, use **Batch sizing** instead of defaults. Unsloth's analogues are `batch.per_device_train_batch_size` and `batch.gradient_accumulation_steps` (effective batch = product). +- **Unsloth is single-GPU**. `hardware.gpus` is **selection, not reservation** (sets `CUDA_VISIBLE_DEVICES` before `import torch`). No `parallelism`/TP/PP block exists. Multi-GPU sharding → use automodel. - **Do not use local `docker info`** to pick automodel vs unsloth. After auth, run `uv run nemo jobs list-execution-profiles -f json` against the user's platform (see `references/troubleshooting.md`). Default output is a table — **`-f json` is required** for scripting; parse **stdout only** (do not pipe `2>&1` into `json.load`). -- For submit/image/plugin errors, read `references/troubleshooting.md`. +- For submit/image/plugin errors (both backends) and the unsloth `--venv` install probe, read `references/troubleshooting.md`. ## Workflow +Common steps then **branch by plugin pick**: + ``` - [ ] export NEMO_BASE_URL (if user provided endpoint) - [ ] cd nemo-platform && uv run nemo auth login --unsigned-token --email -- [ ] uv run nemo jobs list-execution-profiles -f json — GPU profile → automodel; else see troubleshooting (no local docker check) +- [ ] uv run nemo jobs list-execution-profiles -f json — apply Plugin pick rules above - [ ] Convert HF dataset → /tmp/train-data/*.jsonl (see references/hf-conversion.md) - [ ] Create dataset fileset (--exist-ok), upload train.jsonl (+ validation.jsonl), nemo files list to verify - [ ] Create HF weights fileset + model entity if missing (--exist-ok) + +# automodel branch (remote, submit) - [ ] Write /tmp/job.json (batch sizing for ≥48 GB GPU; else Defaults table) - [ ] uv run nemo customization automodel submit /tmp/job.json --workspace default - [ ] Poll until top-level terminal (scripts/poll_automodel_job.sh or 60–120s manual polls) - [ ] Report using output template below + +# unsloth branch (local, run --venv) +- [ ] Ensure $UNSLOTH_VENV is set up (see Plugin pick — Unsloth --venv requirement) +- [ ] Write /tmp/job.json using the UnslothJobInput shape (see Fast path — unsloth) +- [ ] uv run nemo customization unsloth run /tmp/job.json --venv "$UNSLOTH_VENV" --workspace default +- [ ] Read result dict from stdout (loss / output_path / output_fileset / output_model_entity) +- [ ] Report using output template below ``` -## Fast path +## Fast path — automodel (remote) Substitute ``, ``, ``, ``, ``, ``. @@ -138,20 +178,97 @@ bash plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/scripts/ Or poll manually: `uv run nemo jobs get-status automodel-` every 60–120s. +## Fast path — unsloth (local) + +Same substitutions as automodel. Steps 1 (dataset) and 2 (model entity) are identical — the differences are the job JSON shape and the run verb. + +**0. One-time `--venv` setup** (skip if `$UNSLOTH_VENV` already passes the import probe): + +```bash +export UNSLOTH_VENV=/workspace/.venv-unsloth # any path you own +uv venv "$UNSLOTH_VENV" --python 3.11 +uv pip install --python "$UNSLOTH_VENV/bin/python" -e plugins/nemo-unsloth[unsloth] +# Probe: this must print nothing and exit 0. +"$UNSLOTH_VENV/bin/python" -c "import nemo_platform, nemo_unsloth_plugin, unsloth" +``` + +**1. Dataset** — same as automodel Fast path step 1. + +**2. Model** — same as automodel Fast path step 2. + +**3. Job JSON** — write `/tmp/job.json` using the **`UnslothJobInput`** shape (see `references/hyperparameters.md` → *Unsloth job JSON*). `model` is an **object** (not a string), `dataset.path` is a single fileset ref, `hardware.gpus` replaces the `parallelism` block (single GPU). `nemo customization unsloth explain` prints the live schema. + +```json +{ + "name": "", + "model": { + "name": "default/", + "max_seq_length": 2048, + "load_in_4bit": true, + "dtype": "auto" + }, + "dataset": { + "path": "default/", + "text_field": "text", + "apply_chat_template": true + }, + "training": { + "training_type": "sft", + "finetuning_type": "lora", + "lora": { "rank": 16, "alpha": 32 } + }, + "schedule": { "epochs": 1, "warmup_ratio": 0.1 }, + "batch": { "per_device_train_batch_size": 2, "gradient_accumulation_steps": 4 }, + "optimizer": { "learning_rate": 5e-5, "optim": "adamw_8bit" }, + "hardware": { "gpus": "0", "precision": "bf16" }, + "output": { "name": "", "save_method": "lora" } +} +``` + +If the model uses `messages` chat format (preferred when the tokenizer has a chat template), keep `dataset.apply_chat_template: true`. Otherwise emit a single `text` column from your converter and set `apply_chat_template: false`. + +**4. Run (no polling — synchronous)** + +```bash +uv run nemo customization unsloth run /tmp/job.json --venv "$UNSLOTH_VENV" --workspace default +``` + +The result dict prints to stdout — capture `output_path`, `output_fileset`, and `output_model_entity` from it. If the command exits with the "Backend 'unsloth' is not importable" message, the `--venv` probe failed: re-run the one-time setup above. + ## Defaults +Shared: + | Field | Value | |-------|-------| | Workspace | `default` | -| Plugin | `automodel` | +| Plugin | `automodel` (override per **Plugin pick**) | | Training | SFT + LoRA, `max_seq_length` 2048 | | Schedule | `epochs` ≥ 1; omit `max_steps` | +| Auth email | `admin@example.com` unless user specifies | + +Automodel-specific: + +| Field | Value | +|-------|-------| | Parallelism | 1 node, 1 GPU, TP=1 | | Batch | `global_batch_size` 4, `micro_batch_size` 1 (unknown VRAM; see **Batch sizing** for ≥48 GB) | | Optimizer | `learning_rate` 5e-5 | -| Auth email | `admin@example.com` unless user specifies | -## Batch sizing (≥48 GB VRAM) +Unsloth-specific: + +| Field | Value | +|-------|-------| +| Hardware | `hardware.gpus` `"0"`, `hardware.precision` `bf16` (selection only, single GPU) | +| Model load | `load_in_4bit: true`, `dtype: "auto"` | +| Batch | `batch.per_device_train_batch_size` 2, `batch.gradient_accumulation_steps` 4 (effective batch 8; see **Batch sizing — unsloth** for ≥48 GB ramp) | +| Optimizer | `learning_rate` 5e-5, `optim` `adamw_8bit` | +| Output | `save_method: "lora"` (adapter-only) unless user asks for merged checkpoint | +| Gradient checkpointing | `training.use_gradient_checkpointing: "unsloth"` | + +## Batch sizing — automodel (≥48 GB VRAM) + +Tables, multi-GPU rules, and the tuning loop below are **automodel-specific** (fields `global_batch_size` / `micro_batch_size` / `tensor_parallel_size` / `num_gpus_per_node`). For unsloth see **Batch sizing — unsloth** further down. Assume **one GPU with at least 48 GB** (e.g. RTX 5880 / A6000 / L40), `parallelism` = 1 node × 1 GPU, `tensor_parallel_size` 1, bf16, `training_type` `sft`, LoRA **rank 16** unless the user asks otherwise. @@ -255,12 +372,57 @@ Higher rank uses more VRAM. If OOM at rank 16, drop to rank 8 before lowering ba Field glossary, distillation/KD, and schema pointers: `references/hyperparameters.md` (batch/multi-GPU → **this file**, not hyperparameters). +## Batch sizing — unsloth (single GPU) + +Unsloth is single-GPU by design. The effective batch is the **product** of two fields, not a global/micro split: + +``` +effective_batch = batch.per_device_train_batch_size × batch.gradient_accumulation_steps +``` + +There is no `parallelism` block, no TP / PP / DP, no GBS divisibility math. Multi-GPU sharding → switch to automodel. + +**Field mapping from the automodel tables above:** + +| Automodel field | Unsloth analogue | Notes | +|-----------------|------------------|-------| +| `micro_batch_size` | `batch.per_device_train_batch_size` | Drives peak VRAM. | +| `global_batch_size` | `batch.per_device_train_batch_size × batch.gradient_accumulation_steps` | Set `gradient_accumulation_steps` so the product matches the GBS you'd pick on automodel. | +| `parallelism.num_gpus_per_node` | n/a — single GPU | Use `hardware.gpus: "0"` to pin to one GPU. | +| `tensor_parallel_size` | n/a | If the model doesn't fit on one GPU → use automodel. | + +**Starting points (LoRA, `max_seq_length` 2048, one ≥48 GB GPU):** + +| Model params | `per_device_train_batch_size` | `gradient_accumulation_steps` | Effective batch | `learning_rate` | +|--------------|------------------------------:|------------------------------:|----------------:|----------------:| +| ≤4B | 8 | 16 | 128 | `1e-4` | +| 4B–8B | 4 | 24 | 96 | `8e-5` | +| 8B–14B | 2 | 32 | 64 | `8e-5` | +| >14B | 1 | 32 | 32 | `5e-5` | + +`load_in_4bit: true` (default) keeps base weights in 4-bit, which is what makes the "smaller per-device batch on bigger models" rule milder than vanilla HF. If you raise `per_device_train_batch_size` and hit OOM (exit 137) or training crashes (exit 1), halve `per_device_train_batch_size` first and double `gradient_accumulation_steps` to keep the effective batch the same. + +**Save method.** Default `output.save_method: "lora"` (adapter only — small, fast, deploy-friendly). Use `"merged_16bit"` if the user wants a full-weight checkpoint to deploy without an adapter loader; `"merged_4bit"` only when storage is tight (lossy). Merged methods require `training.finetuning_type: "lora"`. + +**Tuning loop (unsloth):** + +| Symptom | Action | +|---------|--------| +| CUDA OOM | Halve `per_device_train_batch_size` (keep effective batch via `gradient_accumulation_steps`); then lower `model.max_seq_length`; then drop `lora.rank` to 8 | +| `torch.cuda.is_available()` is False | Host CUDA / venv mismatch — see `references/troubleshooting.md` | +| `Backend 'unsloth' is not importable` | `$UNSLOTH_VENV` setup never ran or `--venv` was omitted (see Plugin pick) | +| Loss not moving | Raise `learning_rate` one step (e.g. `5e-5` → `1e-4`); confirm `apply_chat_template` matches the data shape; check the LoRA `target_modules` covers the right layers (defaults are Unsloth's 7-module set) | + ## Worked example -`Qwen/Qwen3-1.7B` + `tau/commonsense_qa` → CHAT JSONL, fileset `commonsense_qa`, entity `qwen3-1.7b`, output `qwen3-1.7b-commonsense-qa-lora`, `epochs: 1` (no `max_steps`). On ≥48 GB GPU use LoRA ≤4B **default**: `micro` 32, GBS 128, `learning_rate` `1e-4` (high-util: 64 / 256). +**Automodel:** `Qwen/Qwen3-1.7B` + `tau/commonsense_qa` → CHAT JSONL, fileset `commonsense_qa`, entity `qwen3-1.7b`, output `qwen3-1.7b-commonsense-qa-lora`, `epochs: 1` (no `max_steps`). On ≥48 GB GPU use LoRA ≤4B **default**: `micro` 32, GBS 128, `learning_rate` `1e-4` (high-util: 64 / 256). + +**Unsloth:** same model + dataset + entity + fileset, but `nemo customization unsloth run /tmp/job.json --venv "$UNSLOTH_VENV" -w default`. Job JSON ≤4B row: `batch.per_device_train_batch_size` 8, `batch.gradient_accumulation_steps` 16 (effective 128), `learning_rate` `1e-4`, `hardware.gpus` `"0"`, `output.save_method` `"lora"`. Reference fixture: `plugins/nemo-unsloth/tests/fixtures/minimal_unsloth_sft.json` (ignore `max_steps` for real runs). ## Report to user +**Automodel (submit):** + ```markdown ## Fine-tune result @@ -271,17 +433,34 @@ Field glossary, distillation/KD, and schema pointers: `references/hyperparameter - **Notes:** ``` +**Unsloth (run):** the `run` command prints a result dict to stdout — pick fields from it. + +```markdown +## Fine-tune result + +- **Backend:** unsloth (local) +- **Model entity:** default/ +- **Output model entity:** / +- **Output fileset:** / +- **Local checkpoint:** +- **Final loss:** +- **Status:** completed (or `error: ` if the command exited non-zero) +``` + +If the dict contains `upload_error` or `model_entity_error`, surface those — training succeeded but registration failed; the local `output_path` is still usable. + ## Reference files | When | Read | |------|------| | HF conversion or MCQA shaping | `references/hf-conversion.md` | -| CHAT vs SFT vs CUSTOM | `references/dataset-formats.md` | -| Field glossary, distillation/KD, schema | `references/hyperparameters.md` (not batch sizing) | -| Batch sizing (≥48 GB), OOM / throughput | **Batch sizing** section above | -| Multi-GPU same node | **Multi-GPU (same node)** under batch sizing | -| Backend choice, execution profiles, submit failure, images, CLI | `references/troubleshooting.md` | -| Live JSON schema | `uv run nemo customization automodel explain` | -| Job JSON fixture | `plugins/nemo-automodel/tests/fixtures/qwen3_0.6b_sft_lora.json` (ignore `max_steps` for real runs) | - -Related: `plugins/nemo-automodel/README.md`, `plugins/nemo-customizer/docs/CUSTOMIZATION.md`, skills **`nemo-files`**, **`nemo-status`**. +| CHAT vs SFT vs CUSTOM (automodel); text vs messages (unsloth) | `references/dataset-formats.md` | +| Field glossary, distillation/KD, schema (both backends) | `references/hyperparameters.md` (not batch sizing) | +| Batch sizing (≥48 GB), OOM / throughput | **Batch sizing — automodel** / **Batch sizing — unsloth** above | +| Multi-GPU same node | **Multi-GPU (same node)** under automodel batch sizing (unsloth is single-GPU) | +| Backend choice, execution profiles, `--venv` setup, submit/run failure, images, CLI | `references/troubleshooting.md` | +| Live JSON schema | `uv run nemo customization automodel explain` / `uv run nemo customization unsloth explain` | +| Job JSON fixture (automodel) | `plugins/nemo-automodel/tests/fixtures/qwen3_0.6b_sft_lora.json` (ignore `max_steps` for real runs) | +| Job JSON fixture (unsloth) | `plugins/nemo-unsloth/tests/fixtures/minimal_unsloth_sft.json` (ignore `max_steps` for real runs) | + +Related: `plugins/nemo-automodel/README.md`, `plugins/nemo-unsloth/README.md`, `plugins/nemo-customizer/docs/CUSTOMIZATION.md`, skills **`nemo-files`**, **`nemo-status`**. diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/dataset-formats.md b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/dataset-formats.md index ad6fd62f83..b03b43e12c 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/dataset-formats.md +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/dataset-formats.md @@ -1,8 +1,12 @@ -# Dataset formats (automodel) +# Dataset formats -Automodel detects schema from the **first JSONL line** (`DatasetSchema` in `services/automodel/.../datasets/preparation.py`). +Both backends read JSONL from a platform fileset, but the **row shape and the job-JSON dataset block differ**. Pick the section that matches your plugin. + +Upload `train.jsonl` and optional `validation.jsonl` at the **fileset root**. For automodel use the same fileset for `dataset.training` and `dataset.validation`. For unsloth use `dataset.path` (and `dataset.validation_path`). -Upload `train.jsonl` and optional `validation.jsonl` at the **fileset root**. Use the same fileset for `dataset.training` and `dataset.validation` in job JSON. +## Automodel + +Automodel detects schema from the **first JSONL line** (`DatasetSchema` in `services/automodel/.../datasets/preparation.py`). | Schema | JSONL shape | Job JSON | |--------|-------------|----------| @@ -14,3 +18,39 @@ Upload `train.jsonl` and optional `validation.jsonl` at the **fileset root**. Us **Conversion preference:** CHAT if `AutoTokenizer(...).chat_template` or model `spec.is_chat` / `spec.chat_template` → else SFT. Use CUSTOM or EMBEDDING only when the user asks or the task requires it. For **CUSTOM**, placeholders in `prompt_template` must match column names exactly (two placeholders). + +## Unsloth + +Unsloth has no schema auto-detection — the row shape is controlled by two `dataset` fields in the job JSON. The training driver hands rows to `trl.SFTTrainer`, which only reads one column (`text_field`) per row. + +| Mode | `dataset.apply_chat_template` | Required JSONL shape | What the trainer sees | +|------|------------------------------|----------------------|----------------------| +| **Messages (preferred)** | `true` | `{"messages": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}` (same as automodel CHAT) | Each row's `messages` is rendered through `tokenizer.apply_chat_template(...)` at training time; the rendered string is written into `text_field` (default `"text"`). | +| **Pre-rendered text** | `false` (default) | `{"text": ""}` | The string in `text_field` is fed to SFTTrainer verbatim. | + +Job JSON snippets: + +```json +"dataset": { "path": "default/", "apply_chat_template": true } +``` + +```json +"dataset": { "path": "default/", "text_field": "text", "apply_chat_template": false } +``` + +Optional fields on the unsloth `dataset` block: + +| Field | Default | Notes | +|-------|---------|-------| +| `validation_path` | `null` | Same ref shape as `path` (`"name"` or `"workspace/name"`). | +| `text_field` | `"text"` | Column the trainer reads. In messages mode it's the column the rendered string is **written to** before training. | +| `apply_chat_template` | `false` | Set `true` only when each row has a `messages` array. | +| `packing` | `false` | `trl.SFTTrainer` packing — concatenates short rows up to `max_seq_length` for throughput. Needs short, compatible rows; safe to leave off. | + +**Conversion guidance:** + +- If the model has a chat template (`AutoTokenizer.from_pretrained(...).chat_template` is truthy), use the same `to_chat` converter from `references/hf-conversion.md` and set `apply_chat_template: true`. This is the recommended path for instruction-tuned models. +- If the model has **no** chat template, render each example to a single training string yourself (e.g. `f"{prompt}\n{completion}"`) and emit `{"text": "..."}` rows. Then set `apply_chat_template: false` and keep `text_field: "text"`. +- The automodel SFT format `{"prompt": "...", "completion": "..."}` is **not** directly consumable by unsloth — unsloth has no built-in `prompt`/`completion` concatenation. Convert to either messages or pre-rendered text before upload. + +EMBEDDING and CUSTOM (automodel-only schemas) are not supported by unsloth today. diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hf-conversion.md b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hf-conversion.md index e7521dc06b..642be8b9ca 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hf-conversion.md +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hf-conversion.md @@ -52,3 +52,25 @@ for split in ("train", "validation"): ``` Then upload (see main skill). Validate with `nemo files list --workspace default`. + +## Mapping to job JSON + +The same converted JSONL works for both backends, but the **dataset block in job JSON is shaped per backend**. + +| Backend | Row format used | Dataset block in job JSON | +|---------|----------------|---------------------------| +| automodel (CHAT) | `to_chat` output (`messages`) | `{ "training": "default/", "validation": "default/" }` — schema auto-detected from row 1 | +| automodel (SFT) | `to_sft` output (`prompt` / `completion`) | same as above (no `prompt_template`) | +| **unsloth (preferred)** | `to_chat` output (`messages`) | `{ "path": "default/", "apply_chat_template": true }` (+ `validation_path` if present) | +| unsloth (no chat template) | **Custom `to_text` rendering**: emit `{"text": "\n"}` rows (not the `to_sft` output directly) | `{ "path": "default/", "text_field": "text" }` | + +**Note:** Unsloth does **not** read the automodel SFT shape `{"prompt": ..., "completion": ...}`. If `has_chat` is False *and* the user picked unsloth, swap `to_sft` for a `to_text` that renders one `text` column. Sketch: + +```python +def to_text(ex): + row = to_chat(ex) + user, assistant = row["messages"][0]["content"], row["messages"][1]["content"] + return {"text": f"{user}\n{assistant}"} +``` + +For the chat path (`has_chat` True), the `to_chat` JSONL works unchanged across both backends — only the job-JSON dataset block differs. diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md index 3e275ef58d..c024701af0 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md @@ -1,8 +1,23 @@ -# Hyperparameters (automodel job JSON) +# Hyperparameters + +Two backend job schemas live in this skill. Pick by plugin: + +| Plugin | Schema class | Schema dump | Section below | +|--------|--------------|-------------|---------------| +| `automodel` | `AutomodelJobInput` (`plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py`) | `uv run nemo customization automodel explain` | **Automodel job JSON** (below) | +| `unsloth` | `UnslothJobInput` (`plugins/nemo-unsloth/src/nemo_unsloth_plugin/schema.py`) | `uv run nemo customization unsloth explain` | **Unsloth job JSON** (further down) | + +Both schemas use `extra="forbid"` — unknown keys raise validation errors. Field names are **not** interchangeable across backends (e.g. automodel uses `micro_batch_size` / `global_batch_size` / `parallelism`; unsloth uses `per_device_train_batch_size` / `gradient_accumulation_steps` / `hardware`). Use the right schema for the chosen plugin. + +**Batch sizing, 48 GB VRAM tables, multi-GPU (data parallel vs tensor parallel), and throughput tuning** live in **`SKILL.md`** (§ Batch sizing — automodel, § Batch sizing — unsloth, § Multi-GPU). This file is the **field glossary**, full JSON template per backend, distillation/KD, and schema pointers — not the place to pick batch sizes for production runs. + +--- + +# Automodel job JSON Job JSON for `nemo customization automodel submit` uses **`AutomodelJobInput`** (`plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py`). Only fields in that schema are accepted (`extra="forbid"`). -**Schema dump:** from nemo-platform root: +**Schema dump:** ```bash uv run nemo customization automodel explain @@ -10,10 +25,6 @@ uv run nemo customization automodel explain **Contract examples:** `tests/customizer-automodel-contract/input_configs/` (legacy shape; map `batch_size` → `global_batch_size` in submit JSON). -**Batch sizing, 48 GB VRAM tables, multi-GPU (data parallel vs tensor parallel), and throughput tuning** live in **`SKILL.md`** (§ Batch sizing, § Multi-GPU). This file is the **field glossary**, full JSON template, distillation/KD, and schema pointers — not the place to pick `micro_batch_size` / `global_batch_size` for production runs. - ---- - ## Job JSON layout | Section | Purpose | @@ -301,13 +312,286 @@ Verify: `nemo models get --workspace default`. Reuse an existin --- -## Source of truth +# Unsloth job JSON + +Job JSON for `nemo customization unsloth run` uses **`UnslothJobInput`** (`plugins/nemo-unsloth/src/nemo_unsloth_plugin/schema.py`). Only fields in that schema are accepted (`extra="forbid"`). The canonical post-transform shape lives in `services/unsloth/src/nmp/unsloth/schemas.py` (`UnslothJobOutput`) and is what the training driver consumes. + +**Schema dump:** + +```bash +uv run nemo customization unsloth explain +``` + +Unsloth is **single-GPU, local, in-process** (BYO-venv). There is no `parallelism` block, no `tensor_parallel_size`, and no platform `execution_profile` — `hardware.gpus` selects which CUDA device the local interpreter uses. Multi-GPU sharding → use automodel. + +## Job JSON layout (unsloth) + +| Section | Purpose | +|---------|---------| +| `name` | Optional job name (auto-generated if omitted, mostly cosmetic for local run) | +| `model` | **Object** — base model entity ref + how to load it (4-bit, dtype, max_seq_length) | +| `dataset` | Single fileset ref (`path`) + optional `validation_path`; row shape selector (`text_field`, `apply_chat_template`, `packing`) | +| `training` | Method (`sft`), adapter shape (`lora`/`full`), LoRA hyperparams, gradient checkpointing | +| `schedule` | `epochs` xor `max_steps`; `warmup_steps` xor `warmup_ratio`; logging / save / eval cadence; LR scheduler | +| `batch` | `per_device_train_batch_size` × `gradient_accumulation_steps` = effective batch | +| `optimizer` | LR, weight decay, optimizer choice (`adamw_8bit` default) | +| `hardware` | GPU selection (`CUDA_VISIBLE_DEVICES`) + mixed precision (`bf16` / `fp16`) | +| `integrations` | Optional W&B + `report_to` | +| `output` | Output entity name, optional description, **`save_method`** (controls what's persisted) | + +Full template (every section, defaults inline): + +```json +{ + "name": "", + "model": { + "name": "default/", + "max_seq_length": 2048, + "load_in_4bit": true, + "load_in_8bit": false, + "dtype": "auto", + "trust_remote_code": false + }, + "dataset": { + "path": "default/", + "validation_path": null, + "text_field": "text", + "apply_chat_template": true, + "packing": false + }, + "training": { + "training_type": "sft", + "finetuning_type": "lora", + "lora": { + "rank": 16, + "alpha": 16, + "dropout": 0.0, + "target_modules": ["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"], + "bias": "none", + "use_rslora": false, + "random_state": 3407 + }, + "use_gradient_checkpointing": "unsloth" + }, + "schedule": { + "epochs": 1, + "max_steps": null, + "warmup_steps": 0, + "warmup_ratio": null, + "lr_scheduler_type": "linear", + "logging_steps": 1, + "save_steps": null, + "eval_steps": null, + "seed": 3407 + }, + "batch": { + "per_device_train_batch_size": 2, + "gradient_accumulation_steps": 4 + }, + "optimizer": { + "learning_rate": 5e-5, + "weight_decay": 0.0, + "optim": "adamw_8bit" + }, + "hardware": { + "gpus": "0", + "precision": "bf16" + }, + "integrations": null, + "output": { + "name": "", + "description": null, + "save_method": "lora" + } +} +``` + +## Field reference (unsloth) + +### `model` + +`model` is an **object** (not a string). `name` is the platform model entity ref. + +| Field | Default | Notes | +|-------|---------|-------| +| `name` | — | Model entity ref: `"name"` (uses job workspace) or `"workspace/name"`. Plugin resolves to a local path before training. | +| `max_seq_length` | `2048` | Truncate / pack to this length; lower if VRAM tight. | +| `load_in_4bit` | `true` | bitsandbytes 4-bit. Mutex with `load_in_8bit`. Default for Unsloth's headline path; required to fit larger models on small GPUs. | +| `load_in_8bit` | `false` | bitsandbytes 8-bit. Mutex with `load_in_4bit`. | +| `dtype` | `"auto"` | One of `"auto"`, `"bfloat16"`, `"float16"`, `"float32"`. | +| `trust_remote_code` | `false` | HF `trust_remote_code` flag for custom model code. | + +**Mutex:** `load_in_4bit` xor `load_in_8bit`. Both quantization flags are also **incompatible with `training.finetuning_type: "full"`** — full SFT must use a non-quantized base. + +### `dataset` + +See `references/dataset-formats.md` § Unsloth for row-shape rules. + +| Field | Default | Notes | +|-------|---------|-------| +| `path` | — | Training fileset ref (`"name"` or `"workspace/name"`). | +| `validation_path` | `null` | Optional validation fileset ref. | +| `text_field` | `"text"` | Column SFTTrainer reads. In `apply_chat_template: true` mode, the rendered template string is written into this column. | +| `apply_chat_template` | `false` | Set `true` for rows with a `messages` array (preferred when the tokenizer has a chat template). | +| `packing` | `false` | trl.SFTTrainer packing for throughput on short rows. | + +### `training` + +| Field | Default | Notes | +|-------|---------|-------| +| `training_type` | `"sft"` | Only `"sft"` is implemented today. | +| `finetuning_type` | `"lora"` | `"lora"` (adapter; default) or `"full"` (full SFT — heavy, no quantization). | +| `lora` | auto-filled when `finetuning_type` is `lora` | See LoRA subsection below. | +| `use_gradient_checkpointing` | `"unsloth"` | `"unsloth"` (recommended), `"true"`, or `"false"`. Unsloth's variant is faster than HF's. | + +**LoRA block (`training.lora`):** + +| Field | Default | Notes | +|-------|---------|-------| +| `rank` | `16` | Higher → more capacity, more VRAM. Cap at 32 if the adapter will deploy via default NIM / vLLM. | +| `alpha` | `16` | LoRA scaling; common rule of thumb `alpha ≈ rank` or `2× rank`. | +| `dropout` | `0.0` | LoRA dropout (0.0–<1.0). | +| `target_modules` | Unsloth 7-module set: `q_proj`, `k_proj`, `v_proj`, `o_proj`, `gate_proj`, `up_proj`, `down_proj` | Full attention + MLP. Override with a subset like `["q_proj","v_proj"]` for a lighter touch. | +| `bias` | `"none"` | `"none"` / `"all"` / `"lora_only"`. | +| `use_rslora` | `false` | Rank-stabilized LoRA. | +| `random_state` | `3407` | Reproducibility seed for the LoRA init. | + +`lora` is auto-filled with these defaults when `finetuning_type: "lora"` and the user omits the block. Must be `null` / omitted when `finetuning_type: "full"`. + +### `schedule` + +| Field | Default | Notes | +|-------|---------|-------| +| `epochs` | `null` | Full passes. **`epochs` xor `max_steps`** — exactly one is required. | +| `max_steps` | `null` | Global step cap. Use alone for smoke tests; do not combine with `epochs`. | +| `warmup_steps` | `0` | Linear warmup. Mutex with `warmup_ratio`. | +| `warmup_ratio` | `null` | Fractional warmup over total steps. Mutex with `warmup_steps`. | +| `lr_scheduler_type` | `"linear"` | `"linear"`, `"cosine"`, `"constant"`, `"constant_with_warmup"`, `"cosine_with_restarts"`. | +| `logging_steps` | `1` | Loss-log cadence. | +| `save_steps` | `null` | If set, save checkpoint every N steps. | +| `eval_steps` | `null` | If set with `validation_path`, eval every N steps. | +| `seed` | `3407` | Trainer seed (`TrainingArguments.seed`). | + +**Hard mutex enforced by the schema:** `epochs` xor `max_steps`; `warmup_steps` xor `warmup_ratio`. Validation errors surface at submit time. + +### `batch` + +| Field | Default | Notes | +|-------|---------|-------| +| `per_device_train_batch_size` | `1` | Forwarded verbatim to `TrainingArguments`. Drives peak VRAM. | +| `gradient_accumulation_steps` | `1` | Multiplies effective batch without raising VRAM. | + +`effective_batch = per_device_train_batch_size × gradient_accumulation_steps`. No GBS divisibility math (single GPU). Starting points by model size are in `SKILL.md` § Batch sizing — unsloth. + +### `optimizer` + +| Field | Default | Notes | +|-------|---------|-------| +| `learning_rate` | `2e-4` (schema default; skill uses `5e-5` for LoRA SFT) | See LR table below. | +| `weight_decay` | `0.0` | L2-style regularization. | +| `optim` | `"adamw_8bit"` | `"adamw_torch"`, `"adamw_torch_fused"` (Hopper+), `"adamw_8bit"`, `"paged_adamw_8bit"`, `"sgd"`. `adamw_8bit` has the smallest optimizer state and is Unsloth's notebook default. | + +`warmup_steps` is on `schedule`, not on `optimizer` (different from the automodel schema). + +### `hardware` + +| Field | Default | Notes | +|-------|---------|-------| +| `gpus` | `null` | Comma-separated CUDA indices: `"0"` or `"0,1"` (Unsloth only uses one). Sets `CUDA_VISIBLE_DEVICES` **before** `import torch`. **Selection, not reservation.** Leave unset to inherit the caller's env. | +| `precision` | `"bf16"` | `"bf16"` (Ampere+) or `"fp16"`. | + +### `integrations` + +```json +"integrations": { + "wandb": { "enabled": true, "project": "my-project", "run_name": "qwen3-1.7b-lora" }, + "report_to": ["wandb"] +} +``` + +| Field | Notes | +|-------|-------| +| `wandb.enabled` | Toggle. | +| `wandb.project` | Sets `WANDB_PROJECT` env var. | +| `wandb.run_name` | Becomes `TrainingArguments.run_name`. | +| `report_to` | List of `"wandb"`, `"tensorboard"`, `"mlflow"`, `"none"`. Empty default = `["none"]`. | + +The user sets `WANDB_API_KEY` themselves in `$UNSLOTH_VENV` (or shell) — the plugin does **not** manage that secret. No `api_key_secret` field today. + +### `output` + +| Field | Default | Notes | +|-------|---------|-------| +| `name` | auto-derived from `--` | The output model entity / fileset name. | +| `description` | `null` | Free-form description carried onto the entity and fileset. | +| `save_method` | `"lora"` | `"lora"` (adapter — small, deploy via NIM/vLLM with adapter loader), `"merged_16bit"` (merged checkpoint, deploy without adapter), `"merged_4bit"` (lossy, storage-tight). `merged_*` requires `training.finetuning_type: "lora"`. | + +After `to_spec`, the canonical `OutputResponse` also carries `type` (`"adapter"` for `save_method: "lora"`, `"model"` otherwise) and `fileset` (defaults to `name`); both are derived — submitter doesn't set them. + +## Tuning guide (unsloth) + +VRAM / batch tuning is in **`SKILL.md` § Batch sizing — unsloth**. Below covers non-batch fields. + +### Learning rate (LoRA SFT, starting points) + +Same scale as automodel (the underlying optimizer math is the same): + +| Model scale | Suggested `learning_rate` | +|-------------|---------------------------| +| ≤ 3B | `5e-5` – `1e-4` | +| 3B – 8B | `2e-5` – `5e-5` | +| > 8B | `1e-5` – `2e-5` | + +Schema default is `2e-4` (Unsloth notebook default — works for small adapters with `adamw_8bit`). Skill defaults are conservative `5e-5`. + +### LoRA rank / alpha + +| Use case | `rank` | `alpha` | +|----------|--------|---------| +| Default / balanced | 16 | 16 | +| Lighter touch | 8 | 16 | +| More capacity (inference-safe max on default NIM/vLLM) | 32 | 32 or 64 | + +Drop `rank` before lowering batch when OOM. Higher `alpha/rank` ratios amplify adapter influence; Unsloth's defaults keep `alpha == rank`. + +### Save-method picker + +| User wants | `save_method` | +|------------|---------------| +| Smallest artefact, deploy via adapter loader (default NIM / vLLM) | `lora` | +| Full-weight checkpoint to deploy without an adapter | `merged_16bit` | +| Disk-tight merged checkpoint (lossy) | `merged_4bit` | +| Full SFT (no LoRA) | `lora` is invalid here; output is always a full model — leave `save_method` at default and ignore the merged options | + +`merged_*` require `training.finetuning_type: "lora"`. The schema validator surfaces a clear error if violated. + +### Smoke test (unsloth) + +```json +"schedule": { "max_steps": 50 } +``` + +(omit `epochs`). + +### Distillation + +Not supported by unsloth today (`training_type` is `Literal["sft"]`). Use automodel for distillation. + +--- + +# Source of truth | Resource | Path | Use for | |----------|------|---------| -| **Batch / multi-GPU / 48 GB LoRA** | `SKILL.md` (§ Batch sizing, § Multi-GPU) | Choosing `micro`, GBS, LR, TP vs data parallel | -| Submit schema | `plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py` | Allowed JSON fields | -| Schema → compiler mapping | `services/automodel/src/nmp/automodel/adapter.py` | `dataset.training` → compiler `dataset` string | -| API field descriptions | `services/automodel/src/nmp/automodel/api/v2/jobs/schemas.py` | Compiler-internal shape (not submit JSON) | -| JSON examples | `plugins/nemo-automodel/tests/fixtures/*.json` | Copy-paste templates (ignore fixture `max_steps` in prod) | -| Full spec doc | `plugins/nemo-automodel/SCOPE.md` (simplified JSON section) | Design notes | +| **Batch / multi-GPU / 48 GB LoRA (automodel)** | `SKILL.md` § Batch sizing — automodel, § Multi-GPU | Choosing `micro`, GBS, LR, TP vs data parallel | +| **Batch (unsloth, single GPU)** | `SKILL.md` § Batch sizing — unsloth | `per_device_train_batch_size` × `gradient_accumulation_steps` starting points | +| Submit schema (automodel) | `plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py` | Allowed JSON fields | +| Schema → compiler mapping (automodel) | `services/automodel/src/nmp/automodel/adapter.py` | `dataset.training` → compiler `dataset` string | +| API field descriptions (automodel) | `services/automodel/src/nmp/automodel/api/v2/jobs/schemas.py` | Compiler-internal shape (not submit JSON) | +| Submit schema (unsloth) | `plugins/nemo-unsloth/src/nemo_unsloth_plugin/schema.py` | Allowed JSON fields (`UnslothJobInput`) | +| Canonical schema (unsloth) | `services/unsloth/src/nmp/unsloth/schemas.py` | Post-`to_spec` shape; what `train_sft` consumes | +| Training driver (unsloth) | `services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py` | Field → call-site mapping (FastLanguageModel.from_pretrained, SFTTrainer, save_pretrained{,_merged}) | +| JSON examples (automodel) | `plugins/nemo-automodel/tests/fixtures/*.json` | Copy-paste templates (ignore fixture `max_steps` in prod) | +| JSON example (unsloth) | `plugins/nemo-unsloth/tests/fixtures/minimal_unsloth_sft.json` | Smoke-test template (ignore `max_steps` for real runs) | +| Full spec doc (automodel) | `plugins/nemo-automodel/SCOPE.md` (simplified JSON section) | Design notes | +| Plugin README (unsloth) | `plugins/nemo-unsloth/README.md` | `--venv` setup, CUDA caveat, container-submit roadmap | diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/troubleshooting.md b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/troubleshooting.md index 15242573ab..e9021e6976 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/troubleshooting.md +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/troubleshooting.md @@ -18,11 +18,13 @@ Each entry has `provider`, `profile` (name), and `backend` (e.g. `docker`, `kube | Condition | Plugin | |-----------|--------| -| User asks for Unsloth | `unsloth` (if installed) | +| User explicitly asks for Unsloth | `unsloth` (verify `--venv` setup — see *Unsloth `--venv` setup and probe* below) | +| User explicitly asks for Automodel | `automodel` (verify a GPU execution profile exists) | | Response includes **`provider`: `gpu` or `gpu_distributed`** | **`automodel`** (default) | -| No GPU profiles (only `subprocess` and/or CPU `provider`) | Platform cannot schedule GPU container training → use **`unsloth`** locally if the user has a GPU, or report that remote automodel is unavailable | +| No GPU profiles (only `subprocess` and/or CPU `provider`), caller has a usable local GPU | **`unsloth`** locally (one-time `--venv` setup required) | +| No GPU profiles **and** no local GPU | Report that GPU customization is unavailable; offer to set up a remote platform with a GPU profile | -Automodel training steps need a **GPU execution profile** on the platform. `subprocess` profiles run host commands and are not a substitute for automodel’s GPU container step. +Automodel training steps need a **GPU execution profile** on the platform. `subprocess` profiles run host commands and are not a substitute for automodel's GPU container step. Unsloth ignores execution profiles entirely — it runs in-process inside `$UNSLOTH_VENV` on the caller's machine and reads `hardware.gpus` (selection via `CUDA_VISIBLE_DEVICES`, not platform reservation). ### Pick `training.execution_profile` @@ -41,9 +43,39 @@ for p in json.load(sys.stdin): Do not run `nemo customization --help` unless submit returns unknown plugin. -Automodel uses **`submit` only** (no `run`). Dataset refs in job JSON: `default/`. +## Verb is backend-specific -## Missing training images +- **Automodel** uses **`submit` only** (no local `run`). Dataset refs in job JSON: `default/`. +- **Unsloth** uses **`run` only** (no `submit`). The CLI exits non-zero with a friendly hint if you try `nemo customization unsloth submit ...`. Dataset refs in job JSON: `default/` (single `dataset.path`). + +## Unsloth `--venv` setup and probe + +Unsloth requires a **separate Python venv** containing the heavy ML extras (`unsloth`, `torch`, `transformers`, `trl`, `peft`, `accelerate`, `bitsandbytes`). The base `nemo` install does **not** pull these. The platform's `--venv` flag re-execs `nemo customization unsloth run` inside the supplied interpreter before importing anything torch-related. + +One-time setup (from nemo-platform root): + +```bash +export UNSLOTH_VENV=/workspace/.venv-unsloth # any path the user owns +uv venv "$UNSLOTH_VENV" --python 3.11 +uv pip install --python "$UNSLOTH_VENV/bin/python" -e plugins/nemo-unsloth[unsloth] +``` + +Probe (must print nothing and exit 0): + +```bash +"$UNSLOTH_VENV/bin/python" -c "import nemo_platform, nemo_unsloth_plugin, unsloth" +``` + +| Error from `unsloth run` | Cause | Fix | +|--------------------------|-------|-----| +| `Backend 'unsloth' is not importable in the current interpreter` | `--venv` omitted, or the venv doesn't have the `[unsloth]` extra | Pass `--venv "$UNSLOTH_VENV"`; if it still fails, re-run the install above and the probe | +| `torch.cuda.is_available()` returns False inside training | Host CUDA / driver mismatch with the torch wheel installed into `$UNSLOTH_VENV` | Verify `nvidia-smi` works on the host, then reinstall torch in the venv against a compatible CUDA wheel (`uv pip install --python "$UNSLOTH_VENV/bin/python" torch --index-url https://download.pytorch.org/whl/cu121` or matching cu-tag) | +| `ImportError: cannot import name 'FastLanguageModel'` | Old `unsloth` version | `uv pip install --python "$UNSLOTH_VENV/bin/python" -U unsloth` | +| `OSError: Could not find a suitable CUDA install` | No GPU on the caller machine | Switch to automodel (remote GPU); unsloth is single-GPU on the local box | + +The plugin does **not** auto-create venvs and does **not** manage CUDA versions — that's the BYO-venv contract. See `plugins/nemo-unsloth/README.md` for the canonical install snippet. + +## Missing training images (automodel only) Set **before** starting the platform (not per job): @@ -53,10 +85,12 @@ export NMP_IMAGE_TAG= export NMP_AUTOMODEL_IMAGE_REGISTRY=$NMP_IMAGE_REGISTRY ``` -Pull automodel images only when the job error mentions a missing image. +Pull automodel images only when the job error mentions a missing image. Unsloth does **not** consume platform-managed training images — it runs in-process inside `$UNSLOTH_VENV`, so registry env vars are irrelevant to it. ## CLI quick reference +Shared: + | Action | Command | |--------|---------| | Execution profiles | `nemo jobs list-execution-profiles -f json` | @@ -65,5 +99,22 @@ Pull automodel images only when the job error mentions a missing image. | Upload | `nemo files upload --workspace default --remote-path train.jsonl` | | List files | `nemo files list --workspace default` | | Create model | `nemo models create --workspace default --exist-ok --input-data ''` | + +Automodel (remote): + +| Action | Command | +|--------|---------| | Submit | `nemo customization automodel submit --workspace default` | | Status | `nemo jobs get-status automodel-` | +| Live schema | `nemo customization automodel explain` | + +Unsloth (local): + +| Action | Command | +|--------|---------| +| One-time venv setup | `uv venv "$UNSLOTH_VENV" --python 3.11 && uv pip install --python "$UNSLOTH_VENV/bin/python" -e plugins/nemo-unsloth[unsloth]` | +| Venv probe | `"$UNSLOTH_VENV/bin/python" -c "import nemo_platform, nemo_unsloth_plugin, unsloth"` | +| Run | `nemo customization unsloth run --venv "$UNSLOTH_VENV" --workspace default` | +| Live schema | `nemo customization unsloth explain` | + +There is **no** `nemo jobs get-status` for unsloth — `run` is synchronous and prints the result dict to stdout. There is no `submit` verb either (it is intentionally disabled). diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/tests.json b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/tests.json index f564074615..a57c37314d 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/tests.json +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/tests.json @@ -31,6 +31,26 @@ "prompt": "Train a small chat model on a dataset I have locally and register the output on the platform.", "expected_skill": "nemo-customizer" }, + { + "type": "explicit", + "prompt": "Use nemo customization unsloth run to do a LoRA SFT on my workstation GPU.", + "expected_skill": "nemo-customizer" + }, + { + "type": "explicit", + "prompt": "Help me set up the unsloth --venv and run a local fine-tune via nemo customization.", + "expected_skill": "nemo-customizer" + }, + { + "type": "implicit", + "prompt": "My platform has no GPU profile but my laptop has an RTX 5880 — fine-tune Qwen3-1.7B with LoRA locally.", + "expected_skill": "nemo-customizer" + }, + { + "type": "implicit", + "prompt": "I want a quick LoRA adapter on Qwen with bitsandbytes 4-bit loading, single GPU, in-process.", + "expected_skill": "nemo-customizer" + }, { "type": "contextual", "prompt": "NeMo Platform is running. Before any customization training, help me explore what my support agent should do.", diff --git a/plugins/nemo-customizer/tests/test_router.py b/plugins/nemo-customizer/tests/test_router.py index 6d9a954620..43cd3ca157 100644 --- a/plugins/nemo-customizer/tests/test_router.py +++ b/plugins/nemo-customizer/tests/test_router.py @@ -101,3 +101,44 @@ class _DupB(_FakeContributor): ) with pytest.raises(CustomizationRouterError, match="collision"): CustomizationRouterService() + + +def test_shared_parent_prefix_with_disjoint_routes_is_ok(monkeypatch: pytest.MonkeyPatch) -> None: + """Two contributors mounting at the same parent prefix is fine as long as their + actual routes underneath don't collide. This is the automodel + unsloth case: + both register their jobs router at ``/v2/workspaces/{workspace}`` and add a + backend-scoped collection path via ``job_collection_path_for``. + """ + + def _make_contributor(backend_name: str) -> object: + class _Contributor: + name: ClassVar[str] = backend_name + dependencies: ClassVar[list[str]] = [] + + def get_routers(self) -> list[RouterSpec]: + router = APIRouter() + + @router.post(f"/{backend_name}/jobs") + async def submit() -> dict[str, str]: + return {"backend": backend_name} + + return [ + RouterSpec( + router=router, + prefix="/v2/workspaces/{workspace}", + tag=backend_name.title(), + ), + ] + + def get_cli(self) -> typer.Typer: + return typer.Typer() + + return _Contributor() + + monkeypatch.setattr( + "nemo_customizer.router.discover_customization_contributors", + lambda: {"automodel": _make_contributor("automodel"), "unsloth": _make_contributor("unsloth")}, + ) + # Should not raise. + service = CustomizationRouterService() + assert sorted(service._contributors.keys()) == ["automodel", "unsloth"] diff --git a/plugins/nemo-unsloth/README.md b/plugins/nemo-unsloth/README.md new file mode 100644 index 0000000000..b4cb5ae872 --- /dev/null +++ b/plugins/nemo-unsloth/README.md @@ -0,0 +1,90 @@ +# nemo-unsloth-plugin + +Unsloth GPU fine-tuning **customization contributor** for NeMo Platform. + +Registered under `nemo.customization.contributors` (key `unsloth`) — the `nemo-customizer-plugin` hub composes it under `/apis/customization/v2/workspaces/{workspace}/unsloth/` (HTTP) and `client.customization.unsloth.*` (SDK), and mounts the CLI at `nemo customization unsloth ...`. + +Unsloth is **submit-only**: training executes remotely on the platform's GPU cluster as a 4-step container job (download → train → upload → model-entity), mirroring `nemo-automodel-plugin`. The plugin itself stays lightweight — heavy ML deps (`unsloth`, `trl`, `transformers`, `peft`, `accelerate`, `bitsandbytes`, `torch`) live only inside the `nmp-unsloth-training` container image. `run` is hard-disabled — use `submit`. + +## Install + +The plugin is part of `enabled-plugins` once `uv sync` runs. No GPU / ML deps are installed locally; only the container image needs them. + +Container image build / push instructions live in [`services/unsloth/docker/README.md`](../../services/unsloth/docker/README.md). + +## Submit a training job + +```bash +nemo customization unsloth submit /path/to/job.json -w default +``` + +Job JSON uses the `UnslothJobInput` schema (see `nemo_unsloth_plugin/schema.py`). Minimal example: + +```json +{ + "name": "qwen-tutorial-smoke", + "model": {"name": "unsloth/Qwen2.5-0.5B-Instruct", "max_seq_length": 2048}, + "dataset": {"path": "default/my-dataset", "text_field": "text"}, + "schedule": {"max_steps": 60, "warmup_ratio": 0.1} +} +``` + +What happens after submit: + +1. The plugin's `to_spec` validates the model entity + dataset fileset against the live platform. +2. `UnslothJob.compile` produces a 4-step `PlatformJobSpec`: + 1. **`model-and-dataset-download`** — CPU step, `nmp.unsloth.tasks.file_io` pulls the model entity's fileset + the dataset fileset to the shared PVC. + 2. **`training`** — GPU step, `nmp.unsloth.tasks.training` runs `train_sft` against the local paths. + 3. **`model-upload`** — CPU step, `nmp.unsloth.tasks.file_io` uploads the saved checkpoint to a new fileset (named after `output.fileset`). + 4. **`model-entity-creation`** — CPU step, `nmp.unsloth.tasks.model_entity` registers the output entity (adapter for LoRA, full model entity otherwise). +3. The platform Jobs runner schedules each step; tail logs with the standard jobs API. + +## CLI surface + +``` +nemo customization unsloth --help +nemo customization unsloth submit JOB_JSON -w WORKSPACE [--profile P] [--cluster C] [-o k=v] +nemo customization unsloth run ... # hard-fails: Unsloth is submit-only +nemo customization unsloth explain # prints schemas +``` + +`submit`'s positional `JOB_JSON` replaces the `--spec` / `--spec-file` shape used by some other backends. + +## GPU selection + +Set `hardware.gpus = "0"` (or `"0,1"`) in the job JSON. The training container picks the value up via `CUDA_VISIBLE_DEVICES` *before* importing `unsloth` / `torch` so the var is observed at torch-init time. Selection, not reservation — Unsloth picks one GPU per process. + +The container image targets the same compute capabilities NVIDIA's stock `pytorch` base supports (Ampere+). Pre-Ampere users should set `hardware.precision = "fp16"` in the job JSON. + +## Schema reference + +- `model: ModelLoadSpec` — `name`, `max_seq_length`, `load_in_4bit`, `load_in_8bit`, `dtype`, `trust_remote_code`. +- `dataset: DatasetSpec` — `path` (required), `text_field`, `apply_chat_template`, `validation_path`, `packing`. +- `training: TrainingSpec` — `training_type`, `finetuning_type` (`lora` or `full`), `lora: LoRAParams`, `use_gradient_checkpointing`. +- `schedule: ScheduleSpec` — `epochs` xor `max_steps`, `warmup_steps` xor `warmup_ratio`, `lr_scheduler_type`, `logging_steps`, `save_steps`, `eval_steps`, `seed`. +- `batch: BatchSpec` — `per_device_train_batch_size`, `gradient_accumulation_steps`. +- `optimizer: OptimizerSpec` — `learning_rate`, `weight_decay`, `optim`. +- `hardware: HardwareSpec` — `gpus`, `precision` (`bf16` / `fp16`). +- `integrations: IntegrationsSpec | None` — `wandb` (`enabled`/`project`/`run_name`; WANDB_API_KEY pulled from platform Secrets), `report_to`. +- `output: OutputRequest | None` — `name`, `description`, `save_method` (`lora` / `merged_16bit` / `merged_4bit`). + +`UnslothJobOutput` is the canonical post-`to_spec` form: same as the input plus a resolved `output: OutputResponse` carrying the auto-generated name, inferred type (adapter vs model), and the destination fileset name. + +## Architecture (plugin ↔ service split) + +This plugin is the **thin contributor wrapper**. The heavy code lives in `services/unsloth/` (`nmp-unsloth`): + +- **Plugin** (`plugins/nemo-unsloth/`, `nemo_unsloth_plugin`) — `UnslothContributor`, `UnslothJob` (lifecycle + `compile()`), submitter-facing schema (`UnslothJobInput`), CLI overrides, SDK shapes, contributor wiring. +- **Service** (`services/unsloth/`, `nmp.unsloth`) — canonical schemas (`UnslothJobOutput` and shared sub-shapes), the `train_sft` training driver, the three container task entrypoints (`tasks/file_io`, `tasks/model_entity`, `tasks/training`), and the `platform_job_config_compiler`. + +The plugin imports two things from the service: + +- `nmp.unsloth.compile.platform_job_config_compiler` — invoked from `UnslothJob.compile()` to build the 4-step `PlatformJobSpec`. +- `nmp.unsloth.config.config` — for the default execution profile. + +## See also + +- `plugins/nemo-customizer/` — the customization router hub. Owns `/apis/customization`, `nemo customization`, `client.customization`. +- `services/unsloth/` — the heavy code this plugin delegates to. +- `services/unsloth/docker/` — Dockerfile + build instructions for the `nmp-unsloth-training` image. +- `plugins/nemo-automodel/` — sibling plugin with the same submit shape. diff --git a/plugins/nemo-unsloth/pyproject.toml b/plugins/nemo-unsloth/pyproject.toml new file mode 100644 index 0000000000..c5969f1c93 --- /dev/null +++ b/plugins/nemo-unsloth/pyproject.toml @@ -0,0 +1,56 @@ +[project] +name = "nemo-unsloth-plugin" +version = "0.1.0" +description = "Unsloth GPU fine-tuning customization contributor for NeMo Platform (container submit)." +readme = "README.md" +requires-python = ">=3.11,<3.14" +dependencies = [ + "nemo-platform-plugin", + "nemo-platform", + "nmp-unsloth", + "pydantic>=2.10.6", + "pydantic-settings>=2.6.1", + "typer>=0.12.5", +] + +# Heavy ML deps no longer live in any plugin extra — they are baked +# into the nmp-unsloth-training container image. The plugin (and the +# nmp-unsloth task package) only need the lightweight compile-side +# imports in this process. Users do not install unsloth/torch locally. + +[project.entry-points."nemo.customization.contributors"] +unsloth = "nemo_unsloth_plugin.contributor:UnslothContributor" + +[project.entry-points."nemo.jobs"] +"customization.unsloth.jobs" = "nemo_unsloth_plugin.jobs.jobs:UnslothJob" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/nemo_unsloth_plugin"] + +[tool.uv.sources] +nemo-platform-plugin = { workspace = true } +nemo-platform = { workspace = true } +nemo-customizer-plugin = { workspace = true } +nmp-unsloth = { workspace = true } + +[dependency-groups] +dev = [ + "pytest>=8.3.4", + "pytest-asyncio>=0.25.3", + "ruff>=0.11.8", + "fastapi>=0.115.0", + "httpx>=0.27.0", + "nemo-customizer-plugin", +] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +pythonpath = ["src"] +testpaths = ["tests"] + +[tool.pyright] +extraPaths = ["src"] diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/__init__.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/__init__.py new file mode 100644 index 0000000000..1f3bcd4291 --- /dev/null +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""NeMo Unsloth customization contributor — local fine-tuning via BYO-venv.""" diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/__init__.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/__init__.py new file mode 100644 index 0000000000..0bc88630f7 --- /dev/null +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/__init__.py @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Unsloth contributor CLI helpers.""" + +from nemo_unsloth_plugin.cli.inputs import apply_unsloth_job_cli_overrides, load_job_json +from nemo_unsloth_plugin.cli.main import UnslothContributorCLI + +__all__ = ["UnslothContributorCLI", "apply_unsloth_job_cli_overrides", "load_job_json"] diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/inputs.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/inputs.py new file mode 100644 index 0000000000..27ef692a81 --- /dev/null +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/inputs.py @@ -0,0 +1,128 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""CLI overrides for the Unsloth contributor. + +After the platform's :func:`_add_run_command` / :func:`_add_submit_command` +register the default verbs on the contributor's Typer group, this module +swaps in: + +- ``submit`` → positional ``JOB_JSON`` argument plus ``--workspace``, + ``--profile``, ``--cluster``, ``--base-url``, ``-o`` overrides. Loads + the JSON, validates against :class:`UnslothJobInput`, then delegates + to the original ``submit`` callback with ``--spec`` set to the + validated JSON string. +- ``run`` → hard-fails with a "submit-only" message pointing at the new + verb (Unsloth migrated from local BYO-venv runs to container submit + in 2026). +- ``explain`` → unchanged (the original schema dump is useful as-is). +""" + +from __future__ import annotations + +import json +from collections.abc import Callable +from pathlib import Path + +import typer + +from nemo_unsloth_plugin.schema import UnslothJobInput + +_JOB_JSON_HELP = "Path to Unsloth job JSON (UnslothJobInput schema)." + + +def load_job_json(path: Path) -> str: + """Load and validate job JSON; return canonical JSON string for ``--spec``.""" + data = json.loads(path.read_text()) + validated = UnslothJobInput.model_validate(data) + return validated.model_dump_json() + + +def apply_unsloth_job_cli_overrides(group: typer.Typer) -> None: + """Flat ``unsloth`` CLI: ``submit JOB.json``; ``run`` is disabled. + + Order matters: drop the original verbs first, then re-register the + overrides. Typer iterates ``registered_commands`` in insertion order + so leaving stale entries behind would route users back to the + auto-generated shapes. + """ + _replace_job_run_disabled(group) + _replace_job_submit(group) + + +def _pluck_callback(group: typer.Typer, verb: str) -> Callable[..., None]: + callback = next(c for c in group.registered_commands if c.name == verb).callback + if callback is None: + raise RuntimeError(f"missing {verb!r} callback to override") + return callback + + +def _drop_command(group: typer.Typer, name: str) -> None: + group.registered_commands = [c for c in group.registered_commands if c.name != name] + + +def _replace_job_run_disabled(group: typer.Typer) -> None: + """Replace ``run`` with a hard-fail explainer. + + Unsloth is submit-only (container-execution); local run attempts + would either return ``NotImplementedError`` from :class:`NemoJob.run` + or require the unsloth/torch stack in the CLI interpreter. Surface + the intended workflow up front instead. + """ + _drop_command(group, "run") + + @group.command("run") + def run( + _typer_ctx: typer.Context, + _job_json: Path | None = typer.Argument( + None, + metavar="JOB_JSON", + help=_JOB_JSON_HELP, + ), + ) -> None: + typer.secho( + "Unsloth does not support local run. Submit to the platform API instead:\n" + " nemo customization unsloth submit -w ", + err=True, + fg=typer.colors.RED, + ) + raise typer.Exit(code=1) + + +def _replace_job_submit(group: typer.Typer) -> None: + """Replace ``submit`` with a ``JOB_JSON`` positional + standard submit flags.""" + original = _pluck_callback(group, "submit") + _drop_command(group, "submit") + + @group.command("submit") + def submit( + typer_ctx: typer.Context, + job_json: Path = typer.Argument(..., metavar="JOB_JSON", help=_JOB_JSON_HELP), + workspace: str = typer.Option("default", "--workspace", "-w", help="Target workspace."), + profile: str | None = typer.Option(None, "--profile"), + cluster: str | None = typer.Option(None, "--cluster"), + base_url: str | None = typer.Option( + None, + "--base-url", + help=( + "Override platform API host. If omitted: --cluster, then CLI context, " + "then $NMP_BASE_URL, then http://localhost:8080." + ), + ), + options: list[str] = typer.Option([], "-o", help="Backend option override, 'backend.key=value'."), + options_file: Path | None = typer.Option(None, "--options-file"), + ) -> None: + spec_json = load_job_json(job_json) + original( + typer_ctx, + spec=spec_json, + spec_file=None, + options=options, + options_file=options_file, + profile=profile, + cluster=cluster, + base_url=base_url, + workspace=workspace, + config=None, + config_file=None, + ) diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/main.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/main.py new file mode 100644 index 0000000000..606a0cf70b --- /dev/null +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/main.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""CLI hooks for the Unsloth customization contributor. + +The plugin's CLI surface is auto-mounted by the customization hub via +:meth:`UnslothContributor.get_cli`. This class provides the +``add_job_commands`` integration hook for any caller that builds the +CLI through that helper instead — both shapes apply the same overrides. +""" + +from __future__ import annotations + +import typer +from nemo_platform_plugin.job import NemoJob + +from nemo_unsloth_plugin.cli.inputs import apply_unsloth_job_cli_overrides +from nemo_unsloth_plugin.jobs.jobs import UnslothJob + + +class UnslothContributorCLI: + """Passed to ``add_job_commands`` to override run/submit with job-file args.""" + + def update_job_cli(self, job_cls: type[NemoJob], group: typer.Typer) -> None: + if job_cls is UnslothJob: + apply_unsloth_job_cli_overrides(group) diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/config.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/config.py new file mode 100644 index 0000000000..eac7b6c4a3 --- /dev/null +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/config.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Plugin configuration for Unsloth local training.""" + +from __future__ import annotations + +import uuid + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class UnslothPluginConfig(BaseSettings): + """Environment-driven Unsloth plugin settings. + + All fields are optional; defaults match the in-process / BYO-venv + posture documented in the plugin README. The only knob the contributor + actually consumes today is ``default_training_execution_profile`` — + forwarded into ``add_job_routes`` so the platform's job collection + routes have a sensible profile when the submitter omits one. + """ + + model_config = SettingsConfigDict(env_prefix="NMP_UNSLOTH_", extra="ignore") + + default_training_execution_profile: str = "gpu" + + +def get_config() -> UnslothPluginConfig: + return UnslothPluginConfig() + + +def generate_unsloth_id() -> str: + """Generate a job name when the submitter omits ``name``.""" + return f"unsloth-{uuid.uuid4().hex[:12]}" diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/contributor.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/contributor.py new file mode 100644 index 0000000000..2a23b7f4d4 --- /dev/null +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/contributor.py @@ -0,0 +1,115 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Unsloth customization contributor. + +Registered under ``nemo.customization.contributors`` (key ``unsloth``). +The customization router hub (``nemo-customizer-plugin``) discovers this +class at startup and: + +- merges :meth:`get_routers` into ``/apis/customization/...`` +- adds :meth:`get_cli` under ``nemo customization unsloth`` +- merges :meth:`get_authz_contribution` into the platform authz policy +- composes ``nemo-unsloth-plugin.sdk.resources.UnslothCustomization`` + under ``client.customization.unsloth`` (via the hub's + ``_CONTRIBUTOR_SDK`` map) +""" + +from __future__ import annotations + +from typing import ClassVar + +import typer +from fastapi import APIRouter +from nemo_platform_plugin.authz import AuthzContribution, authz_for_workspace_job_collection +from nemo_platform_plugin.jobs.api_factory import JobRouteOption +from nemo_platform_plugin.jobs.routes import add_job_routes +from nemo_platform_plugin.service import RouterSpec + +from nemo_unsloth_plugin.config import generate_unsloth_id, get_config +from nemo_unsloth_plugin.jobs.jobs import UnslothJob + + +class UnslothContributor: + """Registers Unsloth routes/CLI under the customization router.""" + + name: ClassVar[str] = "unsloth" + # Remote container submit needs the same set of platform services as + # automodel: workspace lookups, auth, jobs API, secrets passthrough, + # files for the model + dataset filesets, and models for entity creation. + dependencies: ClassVar[list[str]] = ["entities", "auth", "jobs", "secrets", "files", "models"] + + def get_routers(self) -> list[RouterSpec]: + """Health endpoint + ``add_job_routes`` for the Unsloth job collection. + + Submit-only: a POST that reaches ``compile()`` builds a 4-step + container job (download → train → upload → model-entity) the + platform Jobs runner executes on the cluster. + """ + config = get_config() + router = APIRouter() + + @router.get("/healthz") + async def healthz() -> dict[str, str]: + return {"backend": self.name, "status": "ok"} + + jobs_router = add_job_routes( + UnslothJob, + service_name="customization", + generate_job_name=generate_unsloth_id, + route_options=[JobRouteOption.CORE], + default_profile=config.default_training_execution_profile, + ) + + return [ + RouterSpec( + router=router, + prefix="/v2/workspaces/{workspace}/unsloth", + tag="Unsloth", + description="Unsloth contributor health.", + ), + RouterSpec( + router=jobs_router, + prefix="/v2/workspaces/{workspace}", + tag="Unsloth Jobs", + description="Unsloth GPU fine-tuning jobs (container submit).", + ), + ] + + def get_cli(self) -> typer.Typer: + """Compose run/submit/explain verbs, then apply Unsloth-specific overrides. + + :func:`apply_unsloth_job_cli_overrides` reshapes ``submit`` to + accept a positional ``JOB_JSON`` and hard-disables ``run`` (since + Unsloth now runs remotely in a container, not locally). + """ + from nemo_platform_plugin.commands import ( + _add_explain_command, + _add_run_command, + _add_submit_command, + ) + from nemo_platform_plugin.scheduler import NemoJobScheduler + + from nemo_unsloth_plugin.cli.inputs import apply_unsloth_job_cli_overrides + + app = typer.Typer( + name=self.name, + help="Unsloth GPU fine-tuning (container submit). SFT only.", + no_args_is_help=True, + ) + scheduler = NemoJobScheduler() + _add_run_command(app, UnslothJob, scheduler) + _add_submit_command(app, UnslothJob, scheduler) + _add_explain_command(app, UnslothJob, scheduler) + apply_unsloth_job_cli_overrides(app) + return app + + def get_authz_contribution(self) -> AuthzContribution: + """Register Unsloth job routes with the platform authorization policy.""" + return authz_for_workspace_job_collection( + api_area="customization", + collection_suffix="/unsloth/jobs", + permission_prefix="customization.unsloth.jobs", + include_healthz=True, + healthz_suffix="/unsloth/healthz", + ) diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/jobs/__init__.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/jobs/__init__.py new file mode 100644 index 0000000000..e5725ea5a4 --- /dev/null +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/jobs/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/jobs/jobs.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/jobs/jobs.py new file mode 100644 index 0000000000..bbe7769ed7 --- /dev/null +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/jobs/jobs.py @@ -0,0 +1,152 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Unsloth remote-submit training job (NemoJob). + +Submit-only — Unsloth executes as a 4-step ``PlatformJobSpec`` (download +→ train → upload → model-entity) on the platform's GPU cluster, mirroring +:class:`nemo_automodel_plugin.jobs.jobs.AutomodelJob`. + +The plugin's CLI hard-fails ``run`` with a friendlier message (see +:mod:`nemo_unsloth_plugin.cli.inputs`); a stray local-run would otherwise +need the unsloth/torch stack in the parent interpreter, which we no +longer support after the 2026 container-submit migration. + +Two responsibilities: + +1. ``to_spec`` — async; validates the model entity + dataset fileset + against the live SDK, resolves the output naming and fileset, and + returns a canonical :class:`UnslothJobOutput`. +2. ``compile`` — async; delegates to + :func:`nmp.unsloth.compile.platform_job_config_compiler`, which builds + the 4-step container job spec the platform Jobs runner executes. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, ClassVar, cast + +from nemo_platform_plugin.config import NemoPlatformConfig, Runtime +from nemo_platform_plugin.job import NemoJob +from nemo_platform_plugin.jobs.api_factory import PlatformJobSpec +from nemo_platform_plugin.jobs.docker import validate_gpu_available_for_docker +from nemo_platform_plugin.jobs.exceptions import PlatformJobCompilationError +from nmp.unsloth.compile import platform_job_config_compiler +from nmp.unsloth.config import config as unsloth_config +from nmp.unsloth.schemas import UnslothJobOutput +from pydantic import BaseModel + +from nemo_unsloth_plugin.schema import UnslothJobInput +from nemo_unsloth_plugin.transform import transform_input_to_output + +if TYPE_CHECKING: + from nemo_platform import AsyncNeMoPlatform + + +def _require_docker_runtime() -> None: + """Refuse to compile when the platform isn't configured for Docker. + + Mirrors :func:`nemo_automodel_plugin.jobs.jobs._require_docker_runtime`. + The compile step builds Docker container specs; surface the + misconfiguration before the Jobs API rejects the spec. + """ + platform_config = NemoPlatformConfig.get() + if platform_config.runtime != Runtime.DOCKER: + raise PlatformJobCompilationError( + "Unsloth training requires platform.runtime: docker with GPU-backed container execution.", + ) + from nemo_platform_plugin.config import validate_docker_available + + if not validate_docker_available(): + raise PlatformJobCompilationError( + "Unsloth training requires a reachable Docker daemon (platform.runtime: docker).", + ) + + +class UnslothJob(NemoJob): + """GPU Unsloth fine-tuning job under the customization router. + + Submit-only: ``run`` is intentionally not implemented. The plugin's + CLI replaces ``run`` with a hard-fail message; reaching the + platform's default ``run`` would raise ``NotImplementedError`` from + :class:`NemoJob`. + """ + + name: ClassVar[str] = "unsloth.jobs" + description: ClassVar[str] = "Unsloth SFT (LoRA / full / merged) training jobs on the platform GPU cluster." + job_collection_path: ClassVar[str | None] = "/unsloth/jobs" + input_spec_schema: ClassVar[type[BaseModel] | None] = UnslothJobInput + spec_schema: ClassVar[type[BaseModel] | None] = UnslothJobOutput + dependencies: ClassVar[list[str]] = ["entities", "auth", "jobs", "secrets", "files", "models"] + + @classmethod + async def to_spec( + cls, + input_spec: BaseModel, + workspace: str, + entity_client: object, + async_sdk: object, + is_local: bool, + ) -> UnslothJobOutput: + """Validate platform refs, resolve naming, return canonical spec.""" + del entity_client, is_local + job_input = ( + input_spec + if isinstance(input_spec, UnslothJobInput) + else UnslothJobInput.model_validate(input_spec.model_dump()) + ) + return await transform_input_to_output( + job_input, + workspace, + cast("AsyncNeMoPlatform", async_sdk), + ) + + @classmethod + async def compile( + cls, + workspace: str, + spec: BaseModel, + entity_client: object, + job_name: str | None, + async_sdk: object, + profile: str | None = None, + options: dict | None = None, + ) -> PlatformJobSpec: + """Compile a validated :class:`UnslothJobOutput` into a 4-step container job. + + Args: + workspace: Submitter's workspace; passed through to compile + for fileset/entity resolution. + spec: Canonical job spec (``UnslothJobOutput`` or anything + with a compatible ``model_dump``). + entity_client: Unused; kept for the + :class:`NemoJob.compile` interface contract. + job_name: Platform-assigned job name (used for logging / + future scheduling decisions inside the compiler). + async_sdk: Async platform SDK for validating model + dataset + refs against live state at compile time. + profile: Caller-supplied execution profile override. + Resolution order: this arg → + ``unsloth_config.default_training_execution_profile``. + Unsloth's :class:`HardwareSpec` does not (yet) expose an + ``execution_profile`` field; expose it on the schema if + callers need per-job overrides. + options: Unused; reserved for backend-specific compile + options the platform may forward later. + """ + del entity_client, options + _require_docker_runtime() + canonical = spec if isinstance(spec, UnslothJobOutput) else UnslothJobOutput.model_validate(spec.model_dump()) + + execution_profile = profile or unsloth_config.default_training_execution_profile + + platform_spec = await platform_job_config_compiler( + workspace=workspace, + spec=canonical, + sdk=cast("AsyncNeMoPlatform", async_sdk), + job_name=job_name, + profile=execution_profile, + ) + + validate_gpu_available_for_docker(platform_spec) + return platform_spec diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/schema.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/schema.py new file mode 100644 index 0000000000..81c805acf2 --- /dev/null +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/schema.py @@ -0,0 +1,147 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Submitter-facing Unsloth schemas. + +The **canonical** types (``UnslothJobOutput``, ``OutputResponse``, and +all shared sub-shapes) live in :mod:`nmp.unsloth.schemas`. They are +re-exported from this module for backward compatibility and to keep +caller imports concise (``from nemo_unsloth_plugin.schema import +UnslothJobInput, ModelLoadSpec`` still works). + +Only two types are defined here: + +- :class:`OutputRequest` — submitter-facing output preferences. The + plugin's :func:`~nemo_unsloth_plugin.transform.transform_input_to_output` + resolves it into the canonical :class:`~nmp.unsloth.schemas.OutputResponse`. +- :class:`UnslothJobInput` — the POST body / CLI JSON shape and the + validators that mediate between input and canonical (mutexes, + defaulting, etc.). +""" + +from __future__ import annotations + +from typing import Literal, Self + +from nmp.unsloth.schemas import ( + BatchSpec, + DatasetSpec, + DeploymentParams, + HardwareSpec, + IntegrationsSpec, + LoRAParams, + ModelLoadSpec, + OptimizerSpec, + OutputResponse, + ScheduleSpec, + ToolCallParams, + TrainingSpec, + UnslothJobOutput, + WandbIntegration, +) +from pydantic import BaseModel, ConfigDict, Field, model_validator + +__all__ = [ + "BatchSpec", + "DatasetSpec", + "DeploymentParams", + "HardwareSpec", + "IntegrationsSpec", + "LoRAParams", + "ModelLoadSpec", + "OptimizerSpec", + "OutputRequest", + "OutputResponse", + "ScheduleSpec", + "ToolCallParams", + "TrainingSpec", + "UnslothJobInput", + "UnslothJobOutput", + "WandbIntegration", +] + + +class OutputRequest(BaseModel): + """Submitter-facing output preferences. ``name`` is auto-derived if omitted.""" + + model_config = ConfigDict(extra="forbid") + + name: str | None = None + description: str | None = None + save_method: Literal["lora", "merged_16bit", "merged_4bit"] = "lora" + + +class UnslothJobInput(BaseModel): + """POST body / CLI JSON for ``nemo customization unsloth run``.""" + + model_config = ConfigDict(extra="forbid") + + name: str | None = None + model: ModelLoadSpec + dataset: DatasetSpec + training: TrainingSpec = Field(default_factory=TrainingSpec) + schedule: ScheduleSpec = Field(default_factory=ScheduleSpec) + batch: BatchSpec = Field(default_factory=BatchSpec) + optimizer: OptimizerSpec = Field(default_factory=OptimizerSpec) + hardware: HardwareSpec = Field(default_factory=HardwareSpec) + integrations: IntegrationsSpec | None = None + output: OutputRequest | None = None + deployment_config: str | DeploymentParams | None = Field( + default=None, + description=( + "Deployment configuration for auto-deploying the model after training. " + "Pass a string to reference an existing ModelDeploymentConfig by name " + "('my-config' or 'workspace/my-config'). An object provides inline NIM " + "deployment parameters. Omit to skip deployment." + ), + ) + + @model_validator(mode="after") + def _validate(self) -> Self: + # exactly one of epochs / max_steps + if self.schedule.epochs is None and self.schedule.max_steps is None: + raise ValueError("schedule.epochs or schedule.max_steps must be set") + if self.schedule.epochs is not None and self.schedule.max_steps is not None: + raise ValueError("schedule.epochs and schedule.max_steps are mutually exclusive") + # 4bit / 8bit mutex (bitsandbytes — they really are exclusive) + if self.model.load_in_4bit and self.model.load_in_8bit: + raise ValueError("model.load_in_4bit and model.load_in_8bit are mutually exclusive") + # full FT cannot quantize + if self.training.finetuning_type == "full": + if self.model.load_in_4bit or self.model.load_in_8bit: + raise ValueError( + "training.finetuning_type='full' is incompatible with 4-bit/8-bit loading; " + "set model.load_in_4bit=false and model.load_in_8bit=false" + ) + if self.training.lora is not None: + raise ValueError("training.lora must be unset when training.finetuning_type='full'") + # auto-fill LoRA when implied but not provided + if self.training.finetuning_type == "lora" and self.training.lora is None: + self.training.lora = LoRAParams() + # warmup_steps and warmup_ratio mutex (transformers also enforces this + # at runtime; we surface it earlier with a clearer message) + if self.schedule.warmup_steps and self.schedule.warmup_ratio is not None: + raise ValueError("schedule.warmup_steps and schedule.warmup_ratio are mutually exclusive") + # merged_* save methods only make sense with LoRA training + if self.output is not None and self.output.save_method != "lora": + if self.training.finetuning_type != "lora": + raise ValueError( + f"output.save_method={self.output.save_method!r} is only valid for training.finetuning_type='lora'" + ) + # LoRA adapters cannot be deployed against a base model with lora_enabled=false — + # the deployed base would refuse to serve the adapter. Surface this at submit time + # rather than failing after training completes. + is_lora_adapter = self.training.finetuning_type == "lora" and ( + self.output is None or self.output.save_method == "lora" + ) + if ( + is_lora_adapter + and isinstance(self.deployment_config, DeploymentParams) + and not self.deployment_config.lora_enabled + ): + raise ValueError( + "deployment_config.lora_enabled must be true (or omitted) when training a LoRA adapter. " + "Setting lora_enabled=false would deploy the base model without LoRA support, " + "making the trained adapter unservable." + ) + return self diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/__init__.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/__init__.py new file mode 100644 index 0000000000..92e06251e5 --- /dev/null +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/__init__.py @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Unsloth contributor SDK (mounted under ``client.customization`` by nemo-customizer).""" + +from nemo_unsloth_plugin.sdk.resources import ( + AsyncUnslothCustomization, + AsyncUnslothJobsResource, + UnslothCustomization, + UnslothJobsResource, +) + +__all__ = [ + "AsyncUnslothCustomization", + "AsyncUnslothJobsResource", + "UnslothCustomization", + "UnslothJobsResource", +] diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/http_utils.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/http_utils.py new file mode 100644 index 0000000000..7cb5f99491 --- /dev/null +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/http_utils.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Shared HTTP helpers for Unsloth customization SDK resources.""" + +from __future__ import annotations + +from typing import Any +from urllib.parse import quote, urljoin + +from nemo_platform import AsyncNeMoPlatform, NeMoPlatform + +from nemo_unsloth_plugin.schema import UnslothJobInput + +PlatformClient = NeMoPlatform | AsyncNeMoPlatform + +_API_PREFIX = "/apis/customization" +_JOBS_COLLECTION = "v2/workspaces/{workspace}/unsloth/jobs" + + +def base_url(source: str) -> str: + """Return the normalized base URL for a raw URL string.""" + return source.rstrip("/") + + +def resolve_workspace(platform: PlatformClient, workspace: str | None, strict: bool = False) -> str: + """Return the explicit, platform, or default workspace for customization routes.""" + resolved = workspace or platform.workspace + if resolved is None: + if strict: + raise ValueError("workspace must be provided when the client has no default workspace") + return "default" + return resolved + + +def url(platform: PlatformClient, path: str, workspace: str | None = None) -> str: + """Build a full customization plugin API URL for the provided route path.""" + resolved_path = path.format(workspace=quote(resolve_workspace(platform, workspace), safe="")) + return _join_url(str(platform.base_url), f"{_API_PREFIX}/{resolved_path}") + + +def jobs_collection_url(platform: PlatformClient, workspace: str | None = None) -> str: + """URL for the Unsloth jobs collection in a workspace.""" + return url(platform, _JOBS_COLLECTION, workspace) + + +def job_url(platform: PlatformClient, job_name: str, workspace: str | None = None) -> str: + """URL for a single Unsloth job.""" + return _join_url(jobs_collection_url(platform, workspace), quote(job_name, safe="")) + + +def platform_default_headers(platform: PlatformClient) -> dict[str, str]: + """Return string-valued default platform headers for direct HTTP calls.""" + return {str(key): value for key, value in platform.default_headers.items() if isinstance(value, str)} + + +def create_job_payload(spec: UnslothJobInput) -> dict[str, dict[str, Any]]: + """Serialize an Unsloth job creation request body.""" + return {"spec": spec.model_dump(mode="json")} + + +def _join_url(root: str, relative_path: str) -> str: + """Join a root URL and a relative path using URL parsing rules.""" + return urljoin(f"{base_url(root)}/", relative_path.lstrip("/")) diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/job_resources.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/job_resources.py new file mode 100644 index 0000000000..6bdbd8d050 --- /dev/null +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/job_resources.py @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Unsloth job resources for status polling via the customization plugin API. + +Local-only training executes inside the user's venv, but we still expose +an SDK so callers can ``get_status`` against any record the +customization router persisted. +""" + +from __future__ import annotations + +from typing import Any +from urllib.parse import quote + +from nemo_platform_plugin.jobs.schemas import PlatformJobStatusResponse +from pydantic import BaseModel + +from nemo_unsloth_plugin.sdk import http_utils + + +class UnslothJobRecord(BaseModel): + """Minimal job record returned by the customization Unsloth jobs API.""" + + name: str + workspace: str + status: str | None = None + spec: dict[str, Any] | None = None + + +class UnslothJobResource: + """Sync handle for one submitted Unsloth job.""" + + def __init__( + self, + job: UnslothJobRecord, + http_client: Any, + base_url: str, + workspace: str, + headers: dict[str, str], + ) -> None: + self.job = job + self._http_client = http_client + self._base_url = base_url + self._workspace = workspace + self._headers = headers + + def get_status(self) -> PlatformJobStatusResponse: + """Fetch current job status.""" + response = self._http_client.get( + _job_status_path(self._base_url, self._workspace, self.job.name), + headers=self._headers, + ) + response.raise_for_status() + return PlatformJobStatusResponse.model_validate(response.json()) + + +class AsyncUnslothJobResource: + """Async handle for one submitted Unsloth job.""" + + def __init__( + self, + job: UnslothJobRecord, + http_client: Any, + base_url: str, + workspace: str, + headers: dict[str, str], + ) -> None: + self.job = job + self._http_client = http_client + self._base_url = base_url + self._workspace = workspace + self._headers = headers + + async def get_status(self) -> PlatformJobStatusResponse: + """Fetch current job status.""" + response = await self._http_client.get( + _job_status_path(self._base_url, self._workspace, self.job.name), + headers=self._headers, + ) + response.raise_for_status() + return PlatformJobStatusResponse.model_validate(response.json()) + + +def _job_status_path(base_url: str, workspace: str, job_name: str) -> str: + encoded_workspace = quote(workspace, safe="") + encoded_job = quote(job_name, safe="") + return ( + f"{http_utils.base_url(base_url)}/apis/customization/v2/workspaces/" + f"{encoded_workspace}/unsloth/jobs/{encoded_job}" + ) diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/resources.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/resources.py new file mode 100644 index 0000000000..836c8215e0 --- /dev/null +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/resources.py @@ -0,0 +1,180 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Unsloth contributor SDK resources (composed by ``nemo-customizer-plugin``). + +A job-collection resource exposing ``plugin_status``, ``create``, and +``get_job_resource``, plus a ``UnslothCustomization`` namespace that +the hub mounts under ``client.customization.unsloth``. + +Even though Unsloth runs locally, we keep the SDK because the +customization router still persists job records (the jobs collection +route is exposed). ``create`` will succeed at the HTTP layer up to the +point ``compile()`` runs; production deployments should prefer the +local CLI path. +""" + +from __future__ import annotations + +from typing import Any + +from nemo_platform import AsyncNeMoPlatform, NeMoPlatform + +from nemo_unsloth_plugin.schema import UnslothJobInput +from nemo_unsloth_plugin.sdk import http_utils +from nemo_unsloth_plugin.sdk.job_resources import ( + AsyncUnslothJobResource, + UnslothJobRecord, + UnslothJobResource, +) + + +class UnslothJobsResource: + """Sync SDK namespace at ``client.customization.unsloth.jobs``.""" + + def __init__(self, platform: NeMoPlatform) -> None: + self._platform = platform + self._http_client = platform._client + + def plugin_status(self) -> dict[str, object]: + """Return Unsloth contributor health from the customization service.""" + response = self._http_client.get( + http_utils.url( + self._platform, + "v2/workspaces/{workspace}/unsloth/healthz", + self._platform.workspace, + ), + headers=http_utils.platform_default_headers(self._platform), + ) + response.raise_for_status() + payload = response.json() + if not isinstance(payload, dict): + raise TypeError("Unsloth health response must be a JSON object.") + return {str(key): value for key, value in payload.items()} + + def create( + self, + spec: UnslothJobInput, + workspace: str | None = None, + name: str | None = None, + ) -> UnslothJobResource: + """Submit an Unsloth training job record. + + Note: + Unsloth jobs execute *locally* — the platform-side compile + path raises ``NotImplementedError`` if the request lands + against an environment that tries to run the canonical job + container. Use ``nemo customization unsloth run`` for the + common case. + """ + body: dict[str, Any] = http_utils.create_job_payload(spec) + if name is not None: + body["name"] = name + response = self._http_client.post( + http_utils.jobs_collection_url(self._platform, workspace), + json=body, + headers=http_utils.platform_default_headers(self._platform), + ) + response.raise_for_status() + record = UnslothJobRecord.model_validate(response.json()) + resolved_ws = http_utils.resolve_workspace(self._platform, workspace) + return UnslothJobResource( + job=record, + http_client=self._http_client, + base_url=http_utils.base_url(str(self._platform.base_url)), + workspace=resolved_ws, + headers=http_utils.platform_default_headers(self._platform), + ) + + def get_job_resource(self, job_name: str, workspace: str | None = None) -> UnslothJobResource: + """Get a resource handle for an existing Unsloth job.""" + resolved_ws = http_utils.resolve_workspace(self._platform, workspace) + response = self._http_client.get( + http_utils.job_url(self._platform, job_name, resolved_ws), + headers=http_utils.platform_default_headers(self._platform), + ) + response.raise_for_status() + return UnslothJobResource( + job=UnslothJobRecord.model_validate(response.json()), + http_client=self._http_client, + base_url=http_utils.base_url(str(self._platform.base_url)), + workspace=resolved_ws, + headers=http_utils.platform_default_headers(self._platform), + ) + + +class AsyncUnslothJobsResource: + """Async SDK namespace at ``client.customization.unsloth.jobs``.""" + + def __init__(self, platform: AsyncNeMoPlatform) -> None: + self._platform = platform + self._http_client = platform._client + + async def plugin_status(self) -> dict[str, object]: + response = await self._http_client.get( + http_utils.url( + self._platform, + "v2/workspaces/{workspace}/unsloth/healthz", + self._platform.workspace, + ), + headers=http_utils.platform_default_headers(self._platform), + ) + response.raise_for_status() + payload = response.json() + if not isinstance(payload, dict): + raise TypeError("Unsloth health response must be a JSON object.") + return {str(key): value for key, value in payload.items()} + + async def create( + self, + spec: UnslothJobInput, + workspace: str | None = None, + name: str | None = None, + ) -> AsyncUnslothJobResource: + body: dict[str, Any] = http_utils.create_job_payload(spec) + if name is not None: + body["name"] = name + response = await self._http_client.post( + http_utils.jobs_collection_url(self._platform, workspace), + json=body, + headers=http_utils.platform_default_headers(self._platform), + ) + response.raise_for_status() + record = UnslothJobRecord.model_validate(response.json()) + resolved_ws = http_utils.resolve_workspace(self._platform, workspace) + return AsyncUnslothJobResource( + job=record, + http_client=self._http_client, + base_url=http_utils.base_url(str(self._platform.base_url)), + workspace=resolved_ws, + headers=http_utils.platform_default_headers(self._platform), + ) + + async def get_job_resource(self, job_name: str, workspace: str | None = None) -> AsyncUnslothJobResource: + resolved_ws = http_utils.resolve_workspace(self._platform, workspace) + response = await self._http_client.get( + http_utils.job_url(self._platform, job_name, resolved_ws), + headers=http_utils.platform_default_headers(self._platform), + ) + response.raise_for_status() + return AsyncUnslothJobResource( + job=UnslothJobRecord.model_validate(response.json()), + http_client=self._http_client, + base_url=http_utils.base_url(str(self._platform.base_url)), + workspace=resolved_ws, + headers=http_utils.platform_default_headers(self._platform), + ) + + +class UnslothCustomization: + """Sync SDK namespace at ``client.customization.unsloth``.""" + + def __init__(self, platform: NeMoPlatform) -> None: + self.jobs = UnslothJobsResource(platform) + + +class AsyncUnslothCustomization: + """Async SDK namespace at ``client.customization.unsloth``.""" + + def __init__(self, platform: AsyncNeMoPlatform) -> None: + self.jobs = AsyncUnslothJobsResource(platform) diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py new file mode 100644 index 0000000000..4dd4fedf50 --- /dev/null +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py @@ -0,0 +1,129 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Input → canonical spec transformation. + +Mirrors the automodel pattern: validates the platform refs (model +entity + dataset fileset) against the live SDK, then resolves output +naming and the fileset name. The plugin's local ``run`` orchestrates +the actual download → train → upload → model_entity steps later. + +Only platform refs are accepted today (per the strict-refs design +choice). Bare HF ids and arbitrary local paths are rejected before the +job runs because the run path expects a real fileset to download from. +""" + +from __future__ import annotations + +import re +import uuid +from typing import TYPE_CHECKING + +from nmp.common.entities.utils import parse_entity_ref +from nmp.unsloth.platform_client import check_dataset_access, fetch_model_entity +from nmp.unsloth.schemas import OutputResponse, UnslothJobOutput + +from nemo_unsloth_plugin.schema import OutputRequest, UnslothJobInput + +if TYPE_CHECKING: + from nemo_platform import AsyncNeMoPlatform + +_MAX_PREFIX_LEN = 50 +_HEX_LEN = 12 +_NAME_SAFE_RE = re.compile(r"[^a-zA-Z0-9_-]+") + + +def _slugify(token: str) -> str: + cleaned = _NAME_SAFE_RE.sub("-", token).strip("-") + return cleaned or "x" + + +def _model_basename(model_ref: str, workspace: str) -> str: + """Last segment of a model entity ref (handles 'workspace/name' and 'name').""" + return _slugify(parse_entity_ref(model_ref, workspace).name) + + +def _dataset_basename(uri: str) -> str: + """Last segment of a fileset ref (handles 'workspace/name' and 'name').""" + cleaned = uri.split("://", 1)[-1] + last = cleaned.rsplit("/", 1)[-1] or cleaned + return _slugify(last) + + +def _random_suffix(prefix: str) -> str: + truncated = prefix[:_MAX_PREFIX_LEN].rstrip("-") + return f"{truncated}-{uuid.uuid4().hex[:_HEX_LEN]}" + + +def _infer_output_type(output_request: OutputRequest) -> str: + """Adapter when saving the LoRA, model otherwise (merged or full).""" + if output_request.save_method == "lora": + return "adapter" + return "model" + + +async def transform_input_to_output( + input_spec: UnslothJobInput, + workspace: str, + sdk: "AsyncNeMoPlatform", +) -> UnslothJobOutput: + """Enrich submitter input into a canonical :class:`UnslothJobOutput`. + + Args: + input_spec: Submitter-facing input shape. + workspace: The job's workspace; used as the default for any bare + entity / fileset refs. + sdk: Async platform SDK handle for validation. + + Returns: + Canonical :class:`UnslothJobOutput` with ``output.fileset`` + populated. + + Raises: + ValueError: When the model entity or dataset fileset cannot be + resolved. + PermissionError: When access to the model or dataset is denied. + """ + # Strict refs: both calls error if the entity / fileset is missing. + model_entity = await fetch_model_entity(input_spec.model.name, workspace, sdk) + await check_dataset_access(sdk, input_spec.dataset.path, workspace) + if input_spec.dataset.validation_path: + await check_dataset_access(sdk, input_spec.dataset.validation_path, workspace) + + is_embedding = bool( + model_entity.spec and getattr(model_entity.spec, "is_embedding_model", False), + ) + if is_embedding: + raise ValueError( + "Embedding-model SFT is not supported by the unsloth backend. Use a causal LM model entity instead.", + ) + + output_request = input_spec.output or OutputRequest() + if output_request.name is None: + model_part = _model_basename(input_spec.model.name, workspace) + dataset_part = _dataset_basename(input_spec.dataset.path) + out_name = _random_suffix(f"{model_part}-{dataset_part}") + else: + out_name = output_request.name + + output = OutputResponse( + name=out_name, + type=_infer_output_type(output_request), # type: ignore[arg-type] + save_method=output_request.save_method, + fileset=out_name, # default the fileset to the entity name (mirrors automodel) + description=output_request.description, + ) + + return UnslothJobOutput( + name=input_spec.name, + model=input_spec.model, + dataset=input_spec.dataset, + training=input_spec.training, + schedule=input_spec.schedule, + batch=input_spec.batch, + optimizer=input_spec.optimizer, + hardware=input_spec.hardware, + integrations=input_spec.integrations, + output=output, + deployment_config=input_spec.deployment_config, + ) diff --git a/plugins/nemo-unsloth/tests/fixtures/minimal_unsloth_sft.json b/plugins/nemo-unsloth/tests/fixtures/minimal_unsloth_sft.json new file mode 100644 index 0000000000..f4dd5143e5 --- /dev/null +++ b/plugins/nemo-unsloth/tests/fixtures/minimal_unsloth_sft.json @@ -0,0 +1,15 @@ +{ + "name": "qwen-tutorial-smoke", + "model": { + "name": "unsloth/Qwen2.5-0.5B-Instruct", + "max_seq_length": 2048 + }, + "dataset": { + "path": "/data/sample.jsonl", + "text_field": "text" + }, + "schedule": { + "max_steps": 60, + "warmup_ratio": 0.1 + } +} diff --git a/plugins/nemo-unsloth/tests/test_cli.py b/plugins/nemo-unsloth/tests/test_cli.py new file mode 100644 index 0000000000..4d8d59677e --- /dev/null +++ b/plugins/nemo-unsloth/tests/test_cli.py @@ -0,0 +1,212 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the Unsloth CLI overrides (``apply_unsloth_job_cli_overrides``). + +Pins the post-2026 submit-only contract: ``submit`` accepts a positional +``JOB_JSON`` and delegates to the auto-generated callback with ``--spec`` +set to the validated JSON; ``run`` hard-fails with an "use submit" +message. +""" + +from __future__ import annotations + +import json +import re +from pathlib import Path +from unittest.mock import patch + +import httpx +import pytest +import typer +from nemo_platform_plugin.scheduler import NemoJobScheduler, submit_path_for +from nemo_unsloth_plugin.cli.inputs import apply_unsloth_job_cli_overrides, load_job_json +from nemo_unsloth_plugin.contributor import UnslothContributor +from nemo_unsloth_plugin.jobs.jobs import UnslothJob +from nemo_unsloth_plugin.schema import UnslothJobInput +from typer.testing import CliRunner + +FIXTURES = Path(__file__).parent / "fixtures" +_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m") + + +def _plain(text: str) -> str: + return _ANSI_RE.sub("", text) + + +def _build_app() -> typer.Typer: + """Build a Typer app with the contributor's overridden run/submit/explain.""" + from nemo_platform_plugin.commands import ( + _add_explain_command, + _add_run_command, + _add_submit_command, + ) + + app = typer.Typer(no_args_is_help=True) + scheduler = NemoJobScheduler() + _add_run_command(app, UnslothJob, scheduler) + _add_submit_command(app, UnslothJob, scheduler) + _add_explain_command(app, UnslothJob, scheduler) + apply_unsloth_job_cli_overrides(app) + return app + + +def _minimal_payload() -> dict[str, object]: + return { + "model": {"name": "unsloth/Qwen2.5-0.5B-Instruct"}, + "dataset": {"path": "default/my-dataset"}, + "schedule": {"max_steps": 60}, + } + + +class TestLoadJobJson: + def test_validates_and_returns_canonical_json(self, tmp_path: Path) -> None: + path = tmp_path / "job.json" + path.write_text(json.dumps(_minimal_payload())) + out = load_job_json(path) + UnslothJobInput.model_validate(json.loads(out)) + + def test_invalid_payload_raises(self, tmp_path: Path) -> None: + path = tmp_path / "job.json" + path.write_text(json.dumps({"model": {"name": "x"}, "schedule": {"max_steps": 1}})) + with pytest.raises(Exception): + load_job_json(path) + + def test_validates_fixture(self) -> None: + spec = json.loads(load_job_json(FIXTURES / "minimal_unsloth_sft.json")) + assert spec["training"]["training_type"] == "sft" + + +class TestSubmitPath: + def test_submit_path_includes_workspace(self) -> None: + path = submit_path_for(UnslothJob, workspace="acme-corp") + assert path == "/apis/customization/v2/workspaces/acme-corp/unsloth/jobs" + + +class TestRunHardFail: + def test_run_exits_1_with_submit_pointer(self, tmp_path: Path) -> None: + path = tmp_path / "job.json" + path.write_text(json.dumps(_minimal_payload())) + + app = _build_app() + runner = CliRunner() + result = runner.invoke(app, ["run", str(path)]) + assert result.exit_code == 1 + plain = _plain(result.output) + assert "submit" in plain + assert "does not support local run" in plain + + +class TestSubmitOverride: + def test_help_lists_job_json_workspace_and_profile(self) -> None: + app = _build_app() + runner = CliRunner() + result = runner.invoke(app, ["submit", "--help"]) + assert result.exit_code == 0, result.output + plain = _plain(result.output) + assert "JOB_JSON" in plain + assert "--workspace" in plain or "-w " in plain + assert "--profile" in plain + assert "--base-url" in plain + + def test_submit_delegates_with_validated_spec(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """``submit JOB.json -w ws`` forwards workspace + base-url to submit_remote.""" + submitted: dict[str, object] = {} + + def fake_submit_remote( + _scheduler, + _job_cls: type, + spec_data: dict, + base_url: str | None, + workspace: str, + profile: str | None = None, + options: dict | None = None, + metadata: dict | None = None, + http_client: httpx.Client | None = None, + headers: dict[str, str] | None = None, + ) -> dict: + submitted["workspace"] = workspace + submitted["spec"] = spec_data + submitted["base_url"] = base_url + return {"id": "job-99"} + + monkeypatch.setattr( + "nemo_platform_plugin.commands.NemoJobScheduler.submit_remote", + fake_submit_remote, + ) + monkeypatch.setattr( + "nemo_platform_plugin.discovery.discover_jobs", + lambda: {"customization.unsloth.jobs": UnslothJob}, + ) + + path = tmp_path / "job.json" + path.write_text(json.dumps(_minimal_payload())) + + unsloth_cli = UnslothContributor().get_cli() + runner = CliRunner() + result = runner.invoke( + unsloth_cli, + [ + "submit", + str(path), + "--workspace", + "acme-corp", + "--base-url", + "https://nmp.test", + ], + ) + + assert result.exit_code == 0, result.stdout + result.stderr + assert submitted["workspace"] == "acme-corp" + assert submitted["base_url"] == "https://nmp.test" + # Raw input shape — to_spec runs inside the (mocked-out) submit_remote. + assert submitted["spec"]["model"]["name"] == "unsloth/Qwen2.5-0.5B-Instruct" + + +class TestExplain: + def test_explain_exposes_input_and_output_schemas(self) -> None: + unsloth_cli = UnslothContributor().get_cli() + runner = CliRunner() + result = runner.invoke(unsloth_cli, ["explain"]) + assert result.exit_code == 0 + payload = json.loads(result.stdout) + assert "input_spec_schema" in payload + assert "spec_schema" in payload + assert "/unsloth/jobs" in payload["endpoint"] + + +class TestJobsSubmitWire: + def test_submit_remote_posts_to_unsloth_collection( + self, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + ) -> None: + """Job collection is /apis/customization/v2/workspaces//unsloth/jobs.""" + capture: dict = {} + + def handler(request: httpx.Request) -> httpx.Response: + capture["method"] = request.method + capture["url"] = str(request.url) + capture["body"] = json.loads(request.content) + return httpx.Response(200, json={"id": "job-1", "status": "queued"}) + + monkeypatch.setattr( + "nemo_platform_plugin.discovery.discover_jobs", + lambda: {"customization.unsloth.jobs": UnslothJob}, + ) + + path = tmp_path / "job.json" + path.write_text(json.dumps(_minimal_payload())) + + scheduler = NemoJobScheduler() + scheduler.submit_remote( + UnslothJob, + json.loads(load_job_json(path)), + base_url="https://nmp.test", + workspace="ws-a", + http_client=httpx.Client(transport=httpx.MockTransport(handler)), + ) + + assert capture["method"] == "POST" + assert capture["url"] == "https://nmp.test/apis/customization/v2/workspaces/ws-a/unsloth/jobs" + assert capture["body"]["spec"]["model"]["name"] == "unsloth/Qwen2.5-0.5B-Instruct" diff --git a/plugins/nemo-unsloth/tests/test_contributor.py b/plugins/nemo-unsloth/tests/test_contributor.py new file mode 100644 index 0000000000..7d9572242a --- /dev/null +++ b/plugins/nemo-unsloth/tests/test_contributor.py @@ -0,0 +1,110 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for UnslothContributor. + +Pin the contract the customization-router hub depends on: + +- ``name`` and ``dependencies`` (used by the hub's authz / dep merger). +- ``get_authz_contribution`` produces an authz block for the unsloth jobs + collection + healthz. +- ``get_routers`` returns the healthz + jobs routers under the right prefix. +- ``get_cli`` exposes ``run`` / ``submit`` / ``explain`` and the submit + group accepts the ``JOB_JSON`` positional. ``run`` hard-fails. +""" + +from __future__ import annotations + +import re + +import pytest +from typer.testing import CliRunner + +_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m") + + +def _plain(text: str) -> str: + return _ANSI_RE.sub("", text) + + +@pytest.fixture +def contributor() -> object: + from nemo_unsloth_plugin.contributor import UnslothContributor + + return UnslothContributor() + + +class TestIdentity: + def test_name(self, contributor: object) -> None: + assert contributor.name == "unsloth" # type: ignore[attr-defined] + + def test_dependencies_match_submit_path(self, contributor: object) -> None: + # Remote container submit needs the same set of platform services + # automodel needs: workspace/auth, jobs API, secrets, files + models. + for required in ("entities", "auth", "jobs", "files", "secrets", "models"): + assert required in contributor.dependencies, ( # type: ignore[attr-defined] + f"{required!r} missing from {contributor.dependencies!r}" # type: ignore[attr-defined] + ) + + +class TestAuthz: + def test_authz_contribution_targets_unsloth_collection(self, contributor: object) -> None: + ac = contributor.get_authz_contribution() # type: ignore[attr-defined] + repr_ = repr(ac) + assert "unsloth" in repr_ + + +class TestRouters: + def test_returns_two_router_specs(self, contributor: object) -> None: + try: + specs = contributor.get_routers() # type: ignore[attr-defined] + except ImportError as exc: + pytest.skip(f"router deps unavailable in this env: {exc}") + assert len(specs) == 2 + prefixes = {s.prefix for s in specs} + assert "/v2/workspaces/{workspace}/unsloth" in prefixes + # The jobs router is mounted at the workspace prefix; add_job_routes + # adds the /unsloth/jobs suffix internally based on + # UnslothJob.job_collection_path. + assert "/v2/workspaces/{workspace}" in prefixes + + +class TestCLI: + def test_cli_root_help_lists_three_verbs(self, contributor: object) -> None: + try: + cli = contributor.get_cli() # type: ignore[attr-defined] + except ImportError as exc: + pytest.skip(f"CLI deps unavailable in this env: {exc}") + runner = CliRunner() + result = runner.invoke(cli, ["--help"]) + assert result.exit_code == 0 + plain = _plain(result.output) + assert "run" in plain + assert "submit" in plain + assert "explain" in plain + + def test_run_hard_fails(self, contributor: object) -> None: + try: + cli = contributor.get_cli() # type: ignore[attr-defined] + except ImportError as exc: + pytest.skip(f"CLI deps unavailable in this env: {exc}") + runner = CliRunner() + result = runner.invoke(cli, ["run"]) + assert result.exit_code == 1 + plain = _plain(result.output) + assert "does not support local run" in plain + assert "submit" in plain + + def test_submit_help_shows_job_json_positional(self, contributor: object) -> None: + try: + cli = contributor.get_cli() # type: ignore[attr-defined] + except ImportError as exc: + pytest.skip(f"CLI deps unavailable in this env: {exc}") + runner = CliRunner() + result = runner.invoke(cli, ["submit", "--help"]) + assert result.exit_code == 0, result.output + plain = _plain(result.output) + assert "JOB_JSON" in plain + assert "--workspace" in plain or "-w" in plain + assert "--profile" in plain + assert "--base-url" in plain diff --git a/plugins/nemo-unsloth/tests/test_jobs.py b/plugins/nemo-unsloth/tests/test_jobs.py new file mode 100644 index 0000000000..49d117b5bc --- /dev/null +++ b/plugins/nemo-unsloth/tests/test_jobs.py @@ -0,0 +1,171 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for UnslothJob lifecycle (to_spec + compile). + +After the 2026 migration from local run to container submit we no longer +exercise ``train_sft`` from these tests — that lives in the +``nmp-unsloth-training`` container's smoke test. Here we just pin: + +- ``to_spec`` resolves output naming + fileset against a stub SDK. +- ``compile`` delegates to the service-side compiler (we patch it out) + and returns the resulting ``PlatformJobSpec`` after the Docker + runtime check. +- The Docker runtime check fires when the platform isn't configured for + Docker. +""" + +from __future__ import annotations + +import asyncio +from types import SimpleNamespace +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from nemo_platform_plugin.jobs.exceptions import PlatformJobCompilationError +from nemo_unsloth_plugin.jobs.jobs import UnslothJob +from nemo_unsloth_plugin.schema import UnslothJobInput +from nmp.unsloth.schemas import UnslothJobOutput + + +def _input_dict(**overrides: Any) -> dict[str, Any]: + base: dict[str, Any] = { + "model": {"name": "default/base"}, + "dataset": {"path": "default/training"}, + "schedule": {"max_steps": 60}, + } + base.update(overrides) + return base + + +def _stub_async_sdk() -> SimpleNamespace: + """Async SDK used by ``to_spec`` (validates refs).""" + me = SimpleNamespace( + name="base", + workspace="default", + spec=None, + fileset="base-fs", + trust_remote_code=False, + ) + return SimpleNamespace( + models=SimpleNamespace(retrieve=AsyncMock(return_value=me)), + files=SimpleNamespace( + filesets=SimpleNamespace(retrieve=AsyncMock(return_value=SimpleNamespace())), + ), + ) + + +def _make_canonical(workspace: str = "default", **overrides: Any) -> UnslothJobOutput: + spec = UnslothJobInput.model_validate(_input_dict(**overrides)) + return asyncio.run( + UnslothJob.to_spec( + spec, + workspace=workspace, + entity_client=object(), + async_sdk=_stub_async_sdk(), + is_local=False, + ), + ) + + +class TestToSpec: + def test_to_spec_resolves_output(self) -> None: + out = _make_canonical() + assert isinstance(out, UnslothJobOutput) + assert out.output.type == "adapter" + assert out.output.save_method == "lora" + # Fileset defaults to the entity name (mirrors automodel). + assert out.output.fileset == out.output.name + + +class TestCompile: + def test_compile_delegates_to_service_compiler(self) -> None: + """When the runtime check passes, ``compile`` returns whatever the service builds.""" + canonical = _make_canonical() + fake_spec = SimpleNamespace(steps=["model-and-dataset-download", "training", "model-upload", "model-entity-creation"]) + + with ( + patch("nemo_unsloth_plugin.jobs.jobs._require_docker_runtime"), + patch( + "nemo_unsloth_plugin.jobs.jobs.platform_job_config_compiler", + new=AsyncMock(return_value=fake_spec), + ) as compile_mock, + patch( + "nemo_unsloth_plugin.jobs.jobs.validate_gpu_available_for_docker", + new=MagicMock(), + ) as validate_mock, + ): + result = asyncio.run( + UnslothJob.compile( + workspace="default", + spec=canonical, + entity_client=object(), + job_name="my-unsloth-job", + async_sdk=object(), + profile=None, + ), + ) + + assert result is fake_spec + compile_mock.assert_awaited_once() + validate_mock.assert_called_once_with(fake_spec) + kwargs = compile_mock.await_args.kwargs + assert kwargs["workspace"] == "default" + assert kwargs["job_name"] == "my-unsloth-job" + # Profile falls through to the unsloth config default (`gpu`). + assert kwargs["profile"] == "gpu" + + def test_compile_passes_caller_profile_override(self) -> None: + canonical = _make_canonical() + with ( + patch("nemo_unsloth_plugin.jobs.jobs._require_docker_runtime"), + patch( + "nemo_unsloth_plugin.jobs.jobs.platform_job_config_compiler", + new=AsyncMock(return_value=SimpleNamespace(steps=[])), + ) as compile_mock, + patch("nemo_unsloth_plugin.jobs.jobs.validate_gpu_available_for_docker"), + ): + asyncio.run( + UnslothJob.compile( + workspace="default", + spec=canonical, + entity_client=object(), + job_name=None, + async_sdk=object(), + profile="gpu_distributed", + ), + ) + + assert compile_mock.await_args.kwargs["profile"] == "gpu_distributed" + + def test_compile_rejects_non_docker_runtime(self) -> None: + canonical = _make_canonical() + # Force the runtime check to raise so we don't need a Docker daemon + # in CI. The check is what runs first; the rest never executes. + with patch( + "nemo_unsloth_plugin.jobs.jobs._require_docker_runtime", + side_effect=PlatformJobCompilationError("not docker"), + ): + with pytest.raises(PlatformJobCompilationError, match="not docker"): + asyncio.run( + UnslothJob.compile( + workspace="default", + spec=canonical, + entity_client=object(), + job_name=None, + async_sdk=object(), + ), + ) + + +class TestNoRun: + def test_unsloth_job_is_abstract_because_run_is_not_implemented(self) -> None: + """``NemoJob.run`` is ``@abstractmethod`` and we deliberately don't override it. + + Pin so a future override doesn't silently re-enable local run — + Unsloth migrated to container submit in 2026. ``run`` lives in + the ``nmp-unsloth-training`` container's ``__main__`` now. + """ + with pytest.raises(TypeError, match="abstract"): + UnslothJob() diff --git a/plugins/nemo-unsloth/tests/test_schema.py b/plugins/nemo-unsloth/tests/test_schema.py new file mode 100644 index 0000000000..56a960a54f --- /dev/null +++ b/plugins/nemo-unsloth/tests/test_schema.py @@ -0,0 +1,251 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Schema validation tests for UnslothJobInput / UnslothJobOutput.""" + +from __future__ import annotations + +import asyncio +import json +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import AsyncMock + +import pytest +from nemo_unsloth_plugin.schema import ( + DatasetSpec, + LoRAParams, + ModelLoadSpec, + OutputRequest, + ScheduleSpec, + TrainingSpec, + UnslothJobInput, + UnslothJobOutput, +) +from nemo_unsloth_plugin.transform import transform_input_to_output +from pydantic import ValidationError + + +def _stub_sdk(*, is_embedding: bool = False) -> SimpleNamespace: + """Build a minimal async SDK that resolves model + dataset refs.""" + spec = SimpleNamespace(is_embedding_model=is_embedding) if is_embedding else None + model_entity = SimpleNamespace( + name="m", + workspace="default", + spec=spec, + fileset="m", + trust_remote_code=False, + ) + return SimpleNamespace( + models=SimpleNamespace(retrieve=AsyncMock(return_value=model_entity)), + files=SimpleNamespace( + filesets=SimpleNamespace(retrieve=AsyncMock(return_value=SimpleNamespace())), + ), + ) + + +def _run_transform(spec: UnslothJobInput) -> UnslothJobOutput: + return asyncio.run(transform_input_to_output(spec, "default", _stub_sdk())) + + +class TestCanonicalReexport: + """Pin that the canonical types come from the service package.""" + + def test_unsloth_job_output_lives_in_service(self) -> None: + # Re-exported from the plugin for caller convenience, but the + # source of truth is the service. Keeps the dependency direction + # plugin → service. + assert UnslothJobOutput.__module__ == "nmp.unsloth.schemas" + + +def _minimal_payload() -> dict[str, object]: + return { + "model": {"name": "unsloth/Qwen2.5-0.5B-Instruct", "max_seq_length": 2048}, + "dataset": {"path": "/data/sample.jsonl"}, + "schedule": {"max_steps": 60}, + } + + +class TestMinimalShape: + def test_minimal_payload_validates(self) -> None: + spec = UnslothJobInput.model_validate(_minimal_payload()) + # Defaults applied + assert spec.training.finetuning_type == "lora" + assert spec.training.lora is not None + assert spec.training.lora.rank == 16 + # Unsloth's recommended 7-module set + assert spec.training.lora.target_modules == [ + "q_proj", + "k_proj", + "v_proj", + "o_proj", + "gate_proj", + "up_proj", + "down_proj", + ] + assert spec.optimizer.optim == "adamw_8bit" + assert spec.hardware.precision == "bf16" + + def test_fixture_minimal_unsloth_sft_loads(self) -> None: + fixture = Path(__file__).parent / "fixtures" / "minimal_unsloth_sft.json" + UnslothJobInput.model_validate(json.loads(fixture.read_text())) + + +class TestRequiredFields: + def test_dataset_path_required(self) -> None: + payload = _minimal_payload() + del payload["dataset"] + with pytest.raises(ValidationError): + UnslothJobInput.model_validate(payload) + + def test_model_required(self) -> None: + payload = _minimal_payload() + del payload["model"] + with pytest.raises(ValidationError): + UnslothJobInput.model_validate(payload) + + +class TestScheduleMutex: + def test_neither_epochs_nor_max_steps_rejected(self) -> None: + payload = _minimal_payload() + payload["schedule"] = {} + with pytest.raises(ValidationError, match="schedule.epochs or schedule.max_steps"): + UnslothJobInput.model_validate(payload) + + def test_both_epochs_and_max_steps_rejected(self) -> None: + payload = _minimal_payload() + payload["schedule"] = {"epochs": 1, "max_steps": 60} + with pytest.raises(ValidationError, match="mutually exclusive"): + UnslothJobInput.model_validate(payload) + + def test_either_one_is_fine(self) -> None: + for sched in ({"epochs": 3}, {"max_steps": 60}): + payload = _minimal_payload() + payload["schedule"] = sched + UnslothJobInput.model_validate(payload) + + +class TestQuantizationMutex: + def test_4bit_and_8bit_rejected(self) -> None: + payload = _minimal_payload() + payload["model"] = { + "name": "x", + "max_seq_length": 1024, + "load_in_4bit": True, + "load_in_8bit": True, + } + with pytest.raises(ValidationError, match="load_in_4bit and model.load_in_8bit"): + UnslothJobInput.model_validate(payload) + + +class TestFullFinetuneRules: + def test_full_ft_rejects_4bit(self) -> None: + payload = _minimal_payload() + payload["training"] = {"finetuning_type": "full"} + # default load_in_4bit=True + with pytest.raises(ValidationError, match="incompatible with 4-bit/8-bit"): + UnslothJobInput.model_validate(payload) + + def test_full_ft_rejects_lora_block(self) -> None: + payload = _minimal_payload() + payload["model"]["load_in_4bit"] = False # type: ignore[index] + payload["training"] = {"finetuning_type": "full", "lora": {"rank": 8}} + with pytest.raises(ValidationError, match="training.lora must be unset"): + UnslothJobInput.model_validate(payload) + + def test_full_ft_clean(self) -> None: + payload = _minimal_payload() + payload["model"]["load_in_4bit"] = False # type: ignore[index] + payload["training"] = {"finetuning_type": "full"} + spec = UnslothJobInput.model_validate(payload) + assert spec.training.lora is None + + +class TestWarmupMutex: + def test_warmup_steps_and_ratio_rejected(self) -> None: + payload = _minimal_payload() + payload["schedule"] = {"max_steps": 60, "warmup_steps": 10, "warmup_ratio": 0.1} + with pytest.raises(ValidationError, match="warmup_steps and schedule.warmup_ratio"): + UnslothJobInput.model_validate(payload) + + +class TestSaveMethodCompatibility: + def test_merged_save_with_lora_ok(self) -> None: + payload = _minimal_payload() + payload["output"] = {"save_method": "merged_16bit"} + UnslothJobInput.model_validate(payload) + + def test_merged_save_with_full_rejected(self) -> None: + payload = _minimal_payload() + payload["model"]["load_in_4bit"] = False # type: ignore[index] + payload["training"] = {"finetuning_type": "full"} + payload["output"] = {"save_method": "merged_16bit"} + with pytest.raises(ValidationError, match="only valid for training.finetuning_type='lora'"): + UnslothJobInput.model_validate(payload) + + +class TestExtraForbidden: + def test_unknown_top_level_rejected(self) -> None: + payload = _minimal_payload() + payload["mystery_field"] = "boom" + with pytest.raises(ValidationError): + UnslothJobInput.model_validate(payload) + + +class TestTransformOutput: + def test_auto_name_when_output_omitted(self) -> None: + spec = UnslothJobInput.model_validate(_minimal_payload()) + out = _run_transform(spec) + # Auto-name draws from the model basename + dataset basename. + # "Qwen2.5-0.5B-Instruct" → "Qwen2-5-0-5B-Instruct" (dots → hyphens). + assert out.output.name.startswith("Qwen2-5-0-5B-Instruct-sample-") + assert out.output.type == "adapter" + assert out.output.save_method == "lora" + # Fileset defaults to the entity name (mirrors automodel). + assert out.output.fileset == out.output.name + + def test_explicit_name_preserved(self) -> None: + payload = _minimal_payload() + payload["output"] = {"name": "my-run", "save_method": "lora"} + out = _run_transform(UnslothJobInput.model_validate(payload)) + assert out.output.name == "my-run" + assert out.output.fileset == "my-run" + + def test_merged_inferred_as_model_type(self) -> None: + payload = _minimal_payload() + payload["output"] = {"save_method": "merged_4bit"} + out = _run_transform(UnslothJobInput.model_validate(payload)) + assert out.output.type == "model" + assert out.output.save_method == "merged_4bit" + + def test_embedding_model_rejected(self) -> None: + sdk = _stub_sdk(is_embedding=True) + spec = UnslothJobInput.model_validate(_minimal_payload()) + with pytest.raises(ValueError, match="Embedding-model SFT"): + asyncio.run(transform_input_to_output(spec, "default", sdk)) + + +class TestSubSpecExtras: + def test_dataset_extra_field_rejected(self) -> None: + with pytest.raises(ValidationError): + DatasetSpec.model_validate({"path": "/x", "junk": 1}) + + def test_lora_extra_field_rejected(self) -> None: + with pytest.raises(ValidationError): + LoRAParams.model_validate({"rank": 8, "junk": 1}) + + def test_model_extra_field_rejected(self) -> None: + with pytest.raises(ValidationError): + ModelLoadSpec.model_validate({"name": "x", "junk": 1}) + + def test_schedule_extra_field_rejected(self) -> None: + with pytest.raises(ValidationError): + ScheduleSpec.model_validate({"max_steps": 1, "junk": 1}) + + def test_training_extra_field_rejected(self) -> None: + with pytest.raises(ValidationError): + TrainingSpec.model_validate({"junk": 1}) + + def test_output_extra_field_rejected(self) -> None: + with pytest.raises(ValidationError): + OutputRequest.model_validate({"junk": 1}) diff --git a/pyproject.toml b/pyproject.toml index fdca6e8f63..71dfdc9f59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -173,6 +173,8 @@ enabled-plugins = [ "nemo-agents-plugin", "nemo-customizer-plugin", "nemo-automodel-plugin", + "nemo-unsloth-plugin", + "nmp-unsloth", ] # Legacy runtime needed specifically for task images that still invoke @@ -370,7 +372,9 @@ nemo-agents-plugin = { workspace = true } nemo-agents-example-calculator = { workspace = true } nemo-customizer-plugin = { workspace = true } nemo-automodel-plugin = { workspace = true } +nemo-unsloth-plugin = { workspace = true } nmp-automodel = { workspace = true } +nmp-unsloth = { workspace = true } [tool.uv.workspace] @@ -421,7 +425,9 @@ members = [ "plugins/nemo-agents/examples/calculator-agent", "plugins/nemo-customizer", "plugins/nemo-automodel", + "plugins/nemo-unsloth", "services/automodel", + "services/unsloth", ] diff --git a/services/unsloth/README.md b/services/unsloth/README.md new file mode 100644 index 0000000000..f2e94ba1cf --- /dev/null +++ b/services/unsloth/README.md @@ -0,0 +1,63 @@ +# nmp-unsloth + +Task package for **container-submit Unsloth SFT** under the NeMo Platform customization router. + +This package owns the heavy code that runs *inside* the platform's GPU containers: + +- **Canonical schemas** (`nmp.unsloth.schemas`) — `UnslothJobOutput` and shared sub-shapes consumed by both compile-time and runtime code. +- **Training driver** (`nmp.unsloth.tasks.training.backends.unsloth_sft.train_sft`) — runs SFT inside the training container's baked venv (`unsloth` + `torch` + `transformers` + `trl` + `peft` + `bitsandbytes`). Heavy imports are localized to the function body so the parent process can import this module without dragging in the ML stack. +- **Container entrypoints**: + - `nmp.unsloth.tasks.file_io` — handles model + dataset download (pre-train) and checkpoint upload (post-train). + - `nmp.unsloth.tasks.training` — runs `train_sft` against the paths the file_io step populated. + - `nmp.unsloth.tasks.model_entity` — registers the output model entity / adapter. +- **Compile glue** (`nmp.unsloth.compile.platform_job_config_compiler`) — turns a canonical `UnslothJobOutput` into a 4-step `PlatformJobSpec`. Invoked by the plugin's `UnslothJob.compile`. + +The thin contributor wrapper that registers Unsloth with the customization hub lives in `plugins/nemo-unsloth/`. That plugin owns submitter-facing schema (`UnslothJobInput`), the `UnslothContributor`, the `UnslothJob` lifecycle (`to_spec` + `compile`), the SDK shapes, and CLI overrides (`submit` reshaped, `run` disabled). + +## Layout + +``` +services/unsloth/ +├── pyproject.toml # nmp-unsloth + [unsloth] extra for container image +├── README.md # this file +├── docker/ # Dockerfile for nmp-unsloth-training +└── src/nmp/unsloth/ + ├── schemas.py # canonical UnslothJobOutput + sub-shapes + ├── compile.py # public compile entry + ├── config.py # NMP_UNSLOTH_* env-var configuration + ├── images.py # image-ref resolution (nmp-unsloth-training etc) + ├── download.py # fileset download helper (sync) + ├── upload.py # fileset upload helper (sync) + ├── model_entity.py # output entity / adapter creation + ├── platform_client.py # async helpers for to_spec validation + ├── app/ + │ ├── constants.py + │ └── jobs/ + │ ├── compiler.py # 4-step PlatformJobSpec builder + │ ├── context.py # NMPJobContext (env-var-driven) + │ ├── file_io/schemas.py # FileIOTaskConfig + │ ├── model_entity/schemas.py # ModelEntityTaskConfig + │ └── training/ + │ ├── compiler.py # GPU training PlatformJobStep + │ └── schemas.py # TrainingStepConfig + └── tasks/ + ├── file_io/__main__.py + run.py + ├── model_entity/__main__.py + run.py + └── training/ + ├── __main__.py (entrypoint: reads step config, calls train_sft) + └── backends/ + └── unsloth_sft.py (train_sft) +``` + +## Why a service package, not just a plugin module? + +Two reasons: + +1. **Container-process boundary.** The training driver and the file_io/model_entity tasks run inside containers built from this package. The plugin (compile-time) and the containers (runtime) need to share schemas (`UnslothJobOutput`, `FileIOTaskConfig`, `ModelEntityTaskConfig`, `TrainingStepConfig`) — co-locating them with the runtime code avoids a circular dep where the plugin owns canonical schemas the container needs to import. +2. **Image isolation.** The plugin process stays lightweight (no `unsloth` / `torch`). Heavy ML deps are installed only inside `nmp-unsloth-training` (see `docker/`). + +## Status + +Container submit is the **only** supported execution path. The GPU image (`docker/Dockerfile.nmp-unsloth-training`) installs the ML stack via the canonical `uv pip install unsloth --torch-backend=auto`, then layers the platform glue on top. The `[unsloth]` extra in `pyproject.toml` is now a thin alias for `unsloth[huggingface]` for any caller that wants to install the same ML stack outside the image. + +See `docker/README.md` for build / push / GPU smoke-test instructions. diff --git a/services/unsloth/docker/Dockerfile.nmp-unsloth-training b/services/unsloth/docker/Dockerfile.nmp-unsloth-training new file mode 100644 index 0000000000..7247980328 --- /dev/null +++ b/services/unsloth/docker/Dockerfile.nmp-unsloth-training @@ -0,0 +1,87 @@ +# syntax=docker/dockerfile:1 +# nmp-unsloth training image — single GPU image used for all four steps of +# an unsloth customization job (file_io download, training, file_io upload, +# model_entity). +# +# Two install steps: +# 1. `uv pip install unsloth --torch-backend=auto`. This is unsloth's +# canonical install command (per their README). It pulls unsloth + +# unsloth_zoo + the entire HF stack (transformers, trl, peft, accelerate, +# datasets, bitsandbytes, xformers, etc.) at the versions unsloth's own +# pyproject.toml has constrained — including explicit !=X.Y.Z blocklists +# for known-broken transformers/trl releases. We deliberately don't +# second-guess these pins; they're tested upstream. +# 2. Editable installs of the platform glue (nemo-platform SDK, plugin, +# nmp-common, nmp-unsloth) from the in-repo workspace slice. +# +# Publish target: nmp-unsloth-training +# Default tag: `local` (override via IMAGE_TAG at build time). + +# NGC PyTorch base. 25.04-py3 ships PyTorch 2.7 + CUDA 12.8 + Python 3.12, +# which is the newest combination with first-class bitsandbytes wheel support. +# Bumping to 25.06-py3 (CUDA 12.9) or 26.02-py3 (CUDA 13.x) loses bitsandbytes +# because its published wheels currently top out at CUDA 12.8. +ARG PYTORCH_BASE=nvcr.io/nvidia/pytorch:25.04-py3 + +FROM ${PYTORCH_BASE} AS base + +WORKDIR /opt + +COPY --from=ghcr.io/astral-sh/uv:0.9.14 /uv /bin/uv +ENV PATH="/bin:${PATH}" + +ENV VIRTUAL_ENV=/opt/venv \ + UV_PROJECT_ENVIRONMENT=/opt/venv \ + UV_LINK_MODE=copy \ + UV_COMPILE_BYTECODE=1 \ + HF_HUB_ENABLE_HF_TRANSFER=1 \ + OTEL_PYTHON_EXCLUDED_URLS="health" +ENV PATH="/opt/venv/bin:/root/.local/bin:${PATH}" + +# --system-site-packages lets the venv inherit the NGC base's pre-built torch +# (CUDA 12.8 build). Without this, `uv pip install unsloth --torch-backend=auto` +# would have to download a fresh torch wheel from PyPI, ballooning image size. +RUN uv venv ${UV_PROJECT_ENVIRONMENT} --system-site-packages + +# ────────────────────────────────────────────────────────────────────────── +# Stage 2 — install unsloth's ML stack + our platform glue. +# ────────────────────────────────────────────────────────────────────────── +FROM base AS runtime + +ARG USERNAME=ubuntu +ARG USER_UID=1000 +ARG USER_GID=1000 + +COPY --from=platform-workspace / /app +WORKDIR /app + +RUN mkdir -p /home/${USERNAME}/.cache && \ + chown -R ${USER_UID}:${USER_GID} /home/${USERNAME} /app/services/unsloth + +# Step 1: install unsloth via its own resolver. --torch-backend=auto tells uv +# to detect the existing torch's CUDA build (from --system-site-packages +# inheritance) and pick the matching xformers / bitsandbytes wheels. +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install --python ${VIRTUAL_ENV}/bin/python --no-cache \ + --torch-backend=auto \ + unsloth + +# Step 2: install the platform glue editably. No [unsloth] extra here — that's +# what we just installed in step 1, and re-triggering it would force the +# resolver to re-evaluate the whole HF stack. +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install --python ${VIRTUAL_ENV}/bin/python --no-cache \ + -e /app/sdk/python/nemo-platform \ + -e /app/packages/nemo_platform_plugin \ + -e /app/packages/nmp_common \ + -e /app/services/unsloth + +# Re-pin hf-transfer (used by HF_HUB_ENABLE_HF_TRANSFER above). +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install --python ${VIRTUAL_ENV}/bin/python --no-cache \ + "hf-transfer>=0.1.8,<0.2" + +ENTRYPOINT ["/opt/venv/bin/python"] +CMD ["-m", "nmp.unsloth.tasks.training", "--help"] + +USER ${USER_UID}:${USER_GID} diff --git a/services/unsloth/docker/Dockerfile.platform-workspace b/services/unsloth/docker/Dockerfile.platform-workspace new file mode 100644 index 0000000000..96bc0181dd --- /dev/null +++ b/services/unsloth/docker/Dockerfile.platform-workspace @@ -0,0 +1,20 @@ +# syntax=docker/dockerfile:1 +# Minimal Platform workspace slice for nmp-unsloth container installs. +# Used as a named build context (platform-workspace). +# Keep in sync with services/unsloth/docker/pyproject.workspace.toml members. + +FROM scratch AS platform-workspace +# Do not copy repo-root pyproject.toml/uv.lock — they reference the full monorepo workspace. +COPY services/unsloth/docker/pyproject.workspace.toml pyproject.toml +# nemo-platform-sdk's hatch build force-includes docs/ and mkdocs.yml from the +# repo root (see sdk/python/nemo-platform/pyproject.toml [tool.hatch.build.targets.wheel.force-include]). +# docs/api/openapi.yaml is a symlink to ../../openapi/openapi.yaml — copy both +# trees so the symlink resolves at build time. +COPY docs docs +COPY openapi openapi +COPY mkdocs.yml mkdocs.yml +COPY packages/nmp_build_tools packages/nmp_build_tools +COPY packages/nmp_common packages/nmp_common +COPY packages/nemo_platform_plugin packages/nemo_platform_plugin +COPY sdk/python/nemo-platform sdk/python/nemo-platform +COPY services/unsloth services/unsloth diff --git a/services/unsloth/docker/README.md b/services/unsloth/docker/README.md new file mode 100644 index 0000000000..abb4db11f1 --- /dev/null +++ b/services/unsloth/docker/README.md @@ -0,0 +1,306 @@ +# nmp-unsloth container image + +Single image — `nmp-unsloth-training` — used for all four steps of an +Unsloth customization job (file_io download, training, file_io upload, +model_entity). + +| Image | Dockerfile | Role | +|-------|------------|------| +| `nmp-unsloth-training` | `Dockerfile.nmp-unsloth-training` | NGC PyTorch base + Unsloth ML stack + platform glue. ENTRYPOINT is `/opt/venv/bin/python`. | + +Default tag is `nmp-unsloth-training:local` (no registry prefix) — suitable for +in-daemon builds via `--load`. Set `IMAGE_REGISTRY` to add a prefix when you +want to push: + +- Local: `nmp-unsloth-training:local` +- Pushed: `${IMAGE_REGISTRY}/nmp-unsloth-training:${IMAGE_TAG}` + +Future-proofing: a leaner CPU image (`nmp-unsloth-tasks`) can be added +later for the file_io / model_entity steps. The compiler already routes +those steps through `get_tasks_image()`, which falls back to the training +image when `NMP_UNSLOTH_TASKS_IMAGE` is not set. + +--- + +## Build & push (from Platform repo root) + +```bash +cd /path/to/Platform + +# --- Option A: local build (loads into the local daemon, no registry needed) --- +docker buildx bake \ + -f services/unsloth/docker/docker-bake.hcl \ + nmp-unsloth-training \ + --load \ + --set "*.platform=linux/amd64" +# Result: `nmp-unsloth-training:local` in `docker images`. + +# --- Option B: push to a registry --- +docker login nvcr.io +export IMAGE_REGISTRY="nvcr.io/0921617854601259/nemo-platform-dev" +export IMAGE_TAG="$(git rev-parse --short HEAD)" +docker buildx bake \ + -f services/unsloth/docker/docker-bake.hcl \ + nmp-unsloth-training \ + --push \ + --set "*.platform=linux/amd64" +# Result: `${IMAGE_REGISTRY}/nmp-unsloth-training:${IMAGE_TAG}` in the registry. +``` + +The build pulls the NGC PyTorch base, then runs unsloth's canonical install +in two steps: + +1. `uv pip install unsloth --torch-backend=auto` — this is the command + straight from unsloth's README. It pulls `unsloth`, `unsloth_zoo`, and the + full HF stack (transformers, trl, peft, accelerate, datasets, bitsandbytes, + xformers) at versions tested upstream — we deliberately don't pin any of + them on our end, because unsloth's pyproject already has precise + `!=X.Y.Z` blocklists for known-broken releases. +2. Editable install of the platform glue: `nemo-platform-sdk`, + `nemo-platform-plugin`, `nmp-common`, `nmp-unsloth`. + +We considered using the official `unsloth/unsloth` image as a base. We +didn't because it's 13 GB, bundles Jupyter Lab + SSH + Unsloth Studio +(which we don't need for a non-interactive job runner), and we'd still +need to layer the platform glue on top. Building on NGC PyTorch keeps +the image smaller and the entrypoint/user-config under our control. + +--- + +## Local smoke test (no platform, no GPU) + +```bash +# Use the same tag the bake produced. For local builds that's the bare name. +IMAGE=nmp-unsloth-training:local + +# CMD prints the training help banner — proves entrypoint + the ML stack import cleanly. +docker run --rm "$IMAGE" + +# Extra args replace CMD; include `-m nmp.unsloth.tasks.training` or you get plain `python --help`. +docker run --rm "$IMAGE" \ + -c "import unsloth, trl, peft, bitsandbytes; print('ok')" +``` + +--- + +## GPU pod runbook — end-to-end + +The same runbook works whether the GPU pod is a developer's interactive +node, a CI cluster runner, or a customer's air-gapped lab. + +### 0. Prereqs on the host + +- NVIDIA driver compatible with the NGC PyTorch base (CUDA 13.1 at the + time of writing; check `Dockerfile.nmp-automodel-base` for the latest + pin if unsure). +- `nvidia-container-toolkit` installed so Docker can mount GPUs. +- Network access to your image registry (`nvcr.io` by default). +- A running NeMo Platform install (`make bootstrap` + `nemo services run`) + with `platform.runtime: docker` configured. See top-level `AGENTS.md` for setup. + +### 1. Build the image + +From the Platform repo root, on a machine with Docker buildx. + +Pick **one** of the following depending on where the GPU host will pull from: + +**A) Same host builds and runs the platform** (e.g. the DinD GPU pod under +`services/unsloth/scripts/gpu-test/`). The bare local tag is enough: + +```bash +docker buildx bake \ + -f services/unsloth/docker/docker-bake.hcl \ + nmp-unsloth-training \ + --load \ + --set "*.platform=linux/amd64" +# → nmp-unsloth-training:local in the local daemon. +``` + +**B) Push to a registry the GPU host will pull from**: + +```bash +export IMAGE_REGISTRY="nvcr.io/0921617854601259/nemo-platform-dev" +export IMAGE_TAG="$(git rev-parse --short HEAD)" + +docker buildx bake \ + -f services/unsloth/docker/docker-bake.hcl \ + nmp-unsloth-training \ + --push \ + --set "*.platform=linux/amd64" +# → ${IMAGE_REGISTRY}/nmp-unsloth-training:${IMAGE_TAG} in the registry. +``` + +**C) Air-gapped GPU host** (save + `scp` + `docker load`): + +```bash +export IMAGE_TAG="$(git rev-parse --short HEAD)" +docker buildx build \ + -f services/unsloth/docker/Dockerfile.nmp-unsloth-training \ + --output type=docker,dest=/tmp/nmp-unsloth-training.tar \ + --target runtime \ + -t "nmp-unsloth-training:${IMAGE_TAG}" \ + --build-context "platform-workspace=path-to-platform-workspace" \ + . + +scp /tmp/nmp-unsloth-training.tar gpu-pod:/tmp/ +ssh gpu-pod docker load -i /tmp/nmp-unsloth-training.tar +``` + +### 2. Point the platform at your tag + +On the host running `nemo services run`, set the full image ref. For a local +build this is just the bare name: + +```bash +# Local build: +export NMP_UNSLOTH_TRAINING_IMAGE="nmp-unsloth-training:local" + +# Or, when you pushed (Option B above): +export NMP_UNSLOTH_TRAINING_IMAGE="${IMAGE_REGISTRY}/nmp-unsloth-training:${IMAGE_TAG}" + +# Restart so the env var takes effect. +nemo services restart +``` + +Or persist in `~/.nemo/config.yaml`: + +```yaml +unsloth: + training_image: nmp-unsloth-training:local +``` + +### 3. Prepare model + dataset filesets + +```bash +# 0.5B Qwen — fast enough for a smoke test on a single GPU. +nemo files filesets create base-qwen-05b -w default +# Push your model weights (or use an existing entity that already points at a fileset). +nemo files upload --fileset default/base-qwen-05b -w default + +nemo models create base-qwen-05b \ + -w default \ + --fileset default/base-qwen-05b \ + --finetuning_type all_weights + +# Tiny chat dataset. +nemo files filesets create unsloth-smoke-dataset -w default +nemo files upload ./smoke-dataset.jsonl --fileset default/unsloth-smoke-dataset -w default +``` + +### 4. Write a smoke job + +Save as `unsloth-smoke.json`: + +```json +{ + "name": "qwen-unsloth-smoke", + "model": { + "name": "default/base-qwen-05b", + "max_seq_length": 1024, + "load_in_4bit": true, + "dtype": "auto" + }, + "dataset": { + "path": "default/unsloth-smoke-dataset", + "text_field": "text" + }, + "training": { + "finetuning_type": "lora", + "lora": {"rank": 16, "alpha": 16} + }, + "schedule": { + "max_steps": 20, + "warmup_steps": 2, + "lr_scheduler_type": "linear", + "logging_steps": 1 + }, + "batch": { + "per_device_train_batch_size": 1, + "gradient_accumulation_steps": 4 + }, + "optimizer": { + "learning_rate": 2e-4, + "optim": "adamw_8bit" + }, + "hardware": { + "gpus": "0", + "precision": "bf16" + }, + "output": { + "name": "qwen-unsloth-smoke-out", + "save_method": "lora" + } +} +``` + +### 5. Submit and watch + +```bash +# Sanity-check schema + planned compilation locally. +nemo customization unsloth explain +cat unsloth-smoke.json | jq . + +# Submit. Captures the job id from the JSON response. +JOB_ID=$( + nemo customization unsloth submit unsloth-smoke.json -w default --json \ + | jq -r '.id' +) +echo "Submitted: $JOB_ID" + +# Tail the status — should go PENDING → ACTIVE → COMPLETED across the four steps. +nemo jobs status "$JOB_ID" -w default + +# Stream logs from each step. +nemo jobs logs "$JOB_ID" -w default --step model-and-dataset-download --follow +nemo jobs logs "$JOB_ID" -w default --step training --follow +nemo jobs logs "$JOB_ID" -w default --step model-upload --follow +nemo jobs logs "$JOB_ID" -w default --step model-entity-creation --follow + +# Verify the output entity (adapter, since save_method=lora). +nemo models adapters list -w default --model_name base-qwen-05b +nemo models adapters retrieve qwen-unsloth-smoke-out \ + --model_name base-qwen-05b -w default +``` + +### 6. Common gotchas + +| Symptom | Fix | +|---|---| +| `compile()` errors with "platform.runtime: docker" | Set `platform.runtime: docker` in `~/.nemo/config.yaml` and restart services. | +| `compile()` errors with "Docker daemon unreachable" | Confirm `docker info` works as the user running `nemo services`. | +| First job step errors with `Model 'X' has no fileset attached` | Attach a fileset to the model entity (`nemo models update --fileset ...`). | +| `training` step errors with `bitsandbytes`/CUDA mismatch | Rebuild the image — the base NGC PyTorch tag may have moved. | +| `training` step OOMs on a small GPU | Reduce `model.max_seq_length` and / or set `model.load_in_4bit: true`. | +| `model-entity-creation` errors with "Adapter already exists" | Pick a fresh `output.name` (the unsloth compiler is "always create"; no overwrite). | +| Step config not picked up (`NEMO_JOB_STEP_CONFIG_FILE_PATH is not set`) | The container was started outside the Jobs runner — only platform-driven submit populates this. | + +### 7. Cleanup + +```bash +# Remove the smoke adapter + fileset. +nemo models adapters delete qwen-unsloth-smoke-out --model_name base-qwen-05b -w default +nemo files filesets delete qwen-unsloth-smoke-out -w default +``` + +--- + +## Architecture notes + +- **Step layout** mirrors `services/automodel/docker/`. The compiler in + `nmp.unsloth.app.jobs.compiler` emits the same 4-step + `PlatformJobSpec` shape automodel emits; the only difference is the + image and the training entrypoint module. +- **`nemo_automodel` is intentionally not in this image** — unsloth is a + separate ML stack. If you need both backends on the same cluster, run + both images side by side; jobs from each backend route to their own + `nmp-{backend}-training` image via env-var overrides. +- **Why we don't pin transformers / trl / peft / bitsandbytes** — unsloth's + own pyproject already constrains them tightly (e.g. + `transformers>=4.51.3,!=4.52.0..3,!=4.53.0,!=4.54.0,!=4.55.0..1,!=4.57.0, + !=4.57.4..5,!=5.0.0,!=5.1.0,<=5.5.0`). Our `[unsloth]` extra in + `services/unsloth/pyproject.toml` is just `["unsloth[huggingface]"]` — + delegating everything to upstream so we don't ship our own subtly-wrong + constraints. +- **No CUDA wheels are pre-built** — `bitsandbytes` ships PyPI wheels + (Ampere+; for older arches, swap to a source build or pin a compatible + release). diff --git a/services/unsloth/docker/docker-bake.hcl b/services/unsloth/docker/docker-bake.hcl new file mode 100644 index 0000000000..30764d2c9e --- /dev/null +++ b/services/unsloth/docker/docker-bake.hcl @@ -0,0 +1,56 @@ +# nmp-unsloth bake — single image target. +# +# Run from the Platform repo root: +# +# # Local build (no registry prefix, --load into local daemon): +# docker buildx bake -f services/unsloth/docker/docker-bake.hcl nmp-unsloth-training --load +# +# # Push to a registry: +# IMAGE_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev \ +# docker buildx bake -f services/unsloth/docker/docker-bake.hcl nmp-unsloth-training --push +# +# Override via env vars: +# IMAGE_REGISTRY = registry+repo prefix (default: empty → bare `nmp-unsloth-training:TAG`) +# IMAGE_TAG = image tag (default: local) +# BUILD_PLATFORM = OCI platform (default: linux/amd64) + +variable "IMAGE_REGISTRY" { + default = "" +} + +variable "IMAGE_TAG" { + default = "local" +} + +variable "BUILD_PLATFORM" { + default = "linux/amd64" +} + +# Named platform-workspace build context (built from the repo root). Each +# target consumes it via `contexts.platform-workspace`. +target "platform-workspace" { + context = "." + dockerfile = "services/unsloth/docker/Dockerfile.platform-workspace" + target = "platform-workspace" + output = ["type=cacheonly"] +} + +target "nmp-unsloth-training" { + context = "." + dockerfile = "services/unsloth/docker/Dockerfile.nmp-unsloth-training" + target = "runtime" + contexts = { + platform-workspace = "target:platform-workspace" + } + # Tag with the registry prefix only when one is supplied; otherwise emit the + # bare local name so the image lands as `nmp-unsloth-training:TAG` in the + # daemon's image store. + tags = [ + IMAGE_REGISTRY != "" ? "${IMAGE_REGISTRY}/nmp-unsloth-training:${IMAGE_TAG}" : "nmp-unsloth-training:${IMAGE_TAG}", + ] + platforms = ["${BUILD_PLATFORM}"] +} + +group "default" { + targets = ["nmp-unsloth-training"] +} diff --git a/services/unsloth/docker/pyproject.workspace.toml b/services/unsloth/docker/pyproject.workspace.toml new file mode 100644 index 0000000000..b8ff159aff --- /dev/null +++ b/services/unsloth/docker/pyproject.workspace.toml @@ -0,0 +1,27 @@ +# Minimal uv workspace for nmp-unsloth container image builds only. +# Replaces the repo-root pyproject.toml in Dockerfile.platform-workspace so +# partial COPY trees are not validated against the full monorepo workspace. + +[project] +name = "nemo-platform-unsloth-image" +version = "0.0.0" +requires-python = ">=3.11,<3.14" + +[tool.uv] +required-version = ">=0.9.14,<0.10.0" + +[tool.uv.workspace] +members = [ + "packages/nmp_build_tools", + "sdk/python/nemo-platform", + "packages/nemo_platform_plugin", + "packages/nmp_common", + "services/unsloth", +] + +[tool.uv.sources] +nmp-build-tools = { workspace = true } +nemo-platform-sdk = { workspace = true } +nemo-platform-plugin = { workspace = true } +nmp-common = { workspace = true } +nmp-unsloth = { workspace = true } diff --git a/services/unsloth/pyproject.toml b/services/unsloth/pyproject.toml new file mode 100644 index 0000000000..5aa2aca095 --- /dev/null +++ b/services/unsloth/pyproject.toml @@ -0,0 +1,60 @@ +[project] +name = "nmp-unsloth" +version = "0.1.0" +description = "NeMo Unsloth task package — local SFT driver and container compile glue. No HTTP server." +readme = "README.md" +requires-python = ">=3.11,<3.14" +dependencies = [ + "nmp-common", + "nemo-platform-sdk", + "pydantic>=2.10.6", + "pydantic-settings>=2.6.1", + "httpx>=0.27.0", + "tenacity>=8.5.0", +] + +# Heavy ML deps live behind an optional extra. The package itself is +# importable without these (e.g. compile.py + schemas), so the API +# process / plugin discovery doesn't pay the install cost. BYO-venv +# users install nmp-unsloth[unsloth] (or nemo-unsloth-plugin[unsloth] +# which delegates here) into a separate, GPU-capable venv. +[project.optional-dependencies] +# We deliberately don't pin transformers / trl / peft / accelerate / bitsandbytes +# here. Unsloth's own pyproject.toml has *precise* constraints for these +# (including explicit !=X.Y.Z blocklists for known-broken releases of +# transformers and trl), and the only reliable way to stay in step with them +# is to let the unsloth extra drive resolution. `unsloth[huggingface]` pulls +# `unsloth_zoo`, transformers, trl, peft, accelerate, datasets, sentence- +# transformers, torchvision, and triton automatically. The Dockerfile installs +# unsloth via its canonical `uv pip install unsloth --torch-backend=auto` +# command (per unsloth's README), so this extra is mainly for non-container +# callers that want a single-package handle. +unsloth = ["unsloth[huggingface]"] + +[project.scripts] +# Container entrypoints. Each script matches the automodel name pattern for parity. +nmp-unsloth-training = "nmp.unsloth.tasks.training.__main__:main" +nmp-unsloth-file-io = "nmp.unsloth.tasks.file_io.run:run" +nmp-unsloth-model-entity = "nmp.unsloth.tasks.model_entity.__main__:run" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/nmp"] + +[tool.uv.sources] +nmp-common = { workspace = true } +nemo-platform-sdk = { workspace = true } + +[dependency-groups] +dev = [ + "pytest>=8.3.4", + "pytest-asyncio>=0.25.3", +] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +pythonpath = ["src"] +testpaths = ["tests"] diff --git a/services/unsloth/src/nmp/unsloth/app/__init__.py b/services/unsloth/src/nmp/unsloth/app/__init__.py new file mode 100644 index 0000000000..e5725ea5a4 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/app/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/services/unsloth/src/nmp/unsloth/app/constants.py b/services/unsloth/src/nmp/unsloth/app/constants.py new file mode 100644 index 0000000000..fdeaf64ee3 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/app/constants.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Shared constants for the unsloth service. + +These mirror the ``services/automodel`` constants so the unsloth service +exposes the same path layout to a future container submit pipeline (and to +the plugin's local ``run`` orchestration today). +""" + +from nmp.common.jobs.constants import DEFAULT_JOB_STORAGE_PATH + +SERVICE_NAME = "unsloth" + +# Subdirectory names under the job's persistent storage root. +DEFAULT_MODEL_OUTPUT_DIR_NAME = "model" +DEFAULT_DATASET_OUTPUT_DIR_NAME = "dataset" +DEFAULT_OUTPUT_MODEL_DIR_NAME = "output_model" + +# Absolute paths used by the compiler when wiring step-to-step file sharing. +# The plugin's local ``run`` re-derives equivalents under +# ``ctx.storage.persistent`` so it does not depend on these values. +DEFAULT_MODEL_PATH = f"{DEFAULT_JOB_STORAGE_PATH}/{DEFAULT_MODEL_OUTPUT_DIR_NAME}" +DEFAULT_DATASET_PATH = f"{DEFAULT_JOB_STORAGE_PATH}/{DEFAULT_DATASET_OUTPUT_DIR_NAME}" +DEFAULT_OUTPUT_MODEL_PATH = f"{DEFAULT_JOB_STORAGE_PATH}/{DEFAULT_OUTPUT_MODEL_DIR_NAME}" + +NMP_JOBS_URL_ENVVAR = "NMP_JOBS_URL" +NMP_FILES_URL_ENVVAR = "NMP_FILES_URL" diff --git a/services/unsloth/src/nmp/unsloth/app/jobs/__init__.py b/services/unsloth/src/nmp/unsloth/app/jobs/__init__.py new file mode 100644 index 0000000000..e5725ea5a4 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/app/jobs/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/services/unsloth/src/nmp/unsloth/app/jobs/compiler.py b/services/unsloth/src/nmp/unsloth/app/jobs/compiler.py new file mode 100644 index 0000000000..6b3dc33038 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/app/jobs/compiler.py @@ -0,0 +1,249 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Job compiler — transforms ``UnslothJobOutput`` into a 4-step ``PlatformJobSpec``. + +Invoked from :meth:`UnslothJob.compile` via :mod:`nmp.unsloth.compile`. +The four steps mirror automodel: + +1. file_io download — pull model fileset + dataset fileset to the PVC +2. training — GPU step running ``train_sft`` +3. file_io upload — push the saved checkpoint to a new fileset +4. model_entity — create the output ``ModelEntity`` referencing it +""" + +from __future__ import annotations + +import logging + +from nemo_platform import AsyncNeMoPlatform +from nemo_platform.types.models.model_entity import ModelEntity +from nemo_platform_plugin.jobs.api_factory import ( + ContainerSpec, + CPUExecutionProviderSpec, + EnvironmentVariable, + PlatformJobSpec, + PlatformJobStep, + ResourcesLimitsSpec, + ResourcesRequestsSpec, + ResourcesSpec, +) +from nemo_platform_plugin.jobs.exceptions import PlatformJobCompilationError +from nmp.common.jobs.constants import DEFAULT_JOB_STORAGE_PATH, PERSISTENT_JOB_STORAGE_PATH_ENVVAR +from nmp.unsloth.app.constants import ( + DEFAULT_DATASET_PATH, + DEFAULT_MODEL_PATH, + DEFAULT_OUTPUT_MODEL_PATH, +) +from nmp.unsloth.app.jobs.file_io.schemas import ( + DownloadItem, + FileIOTaskConfig, + FileSetRef, + UploadItem, +) +from nmp.unsloth.app.jobs.model_entity.schemas import ( + DeploymentParameters as ModelEntityDeploymentParameters, +) +from nmp.unsloth.app.jobs.model_entity.schemas import ModelEntityTaskConfig, PEFTConfig +from nmp.unsloth.app.jobs.training.compiler import compile_training_step +from nmp.unsloth.config import config +from nmp.unsloth.entities.values import FinetuningType +from nmp.unsloth.images import UNSLOTH_PYTHON_ENTRYPOINT, get_tasks_image +from nmp.unsloth.platform_client import fetch_model_entity +from nmp.unsloth.schemas import UnslothJobOutput + +logger = logging.getLogger(__name__) + + +def _get_cpu_resources() -> ResourcesSpec: + return ResourcesSpec( + limits=ResourcesLimitsSpec( + cpu=config.default_job_resource_cpu_limit, + memory=config.default_job_resource_memory_limit, + ), + requests=ResourcesRequestsSpec( + cpu=config.default_job_resource_cpu_request, + memory=config.default_job_resource_memory_request, + ), + ) + + +def _get_base_environment() -> list[EnvironmentVariable]: + return [ + EnvironmentVariable( + name=PERSISTENT_JOB_STORAGE_PATH_ENVVAR, + value=DEFAULT_JOB_STORAGE_PATH, + ), + ] + + +def _resolve_finetuning_type(spec: UnslothJobOutput) -> FinetuningType: + """Map the plugin's flat ``finetuning_type`` + ``save_method`` onto the enum.""" + if spec.training.finetuning_type == "lora": + if spec.output.save_method in {"merged_16bit", "merged_4bit"}: + return FinetuningType.LORA_MERGED + return FinetuningType.LORA + return FinetuningType.ALL_WEIGHTS + + +def _build_peft_config(spec: UnslothJobOutput) -> PEFTConfig | None: + if spec.training.finetuning_type != "lora": + return None + assert spec.training.lora is not None # validated by UnslothJobInput + return PEFTConfig( + type=_resolve_finetuning_type(spec), + rank=spec.training.lora.rank, + alpha=spec.training.lora.alpha, + ) + + +def _require_fileset(name: str | None, *, label: str) -> str: + if not name or not str(name).strip(): + raise PlatformJobCompilationError( + f"{label} has no fileset attached. Attach a platform FileSet " + "(workspace/name) with model weights before running training.", + ) + return str(name) + + +def _build_file_download_config( + job_spec: UnslothJobOutput, + me: ModelEntity, +) -> FileIOTaskConfig: + """Compile the download step: model fileset + dataset fileset.""" + model_fileset = _require_fileset( + me.fileset, + label=f"Model '{me.workspace}/{me.name}'", + ) + return FileIOTaskConfig( + download=[ + DownloadItem( + src=FileSetRef.model_validate(model_fileset), + dest=DEFAULT_MODEL_PATH, + ), + DownloadItem( + src=FileSetRef.model_validate(job_spec.dataset.path), + dest=DEFAULT_DATASET_PATH, + ), + ], + ) + + +def _build_file_upload_config(output_fileset_name: str) -> FileIOTaskConfig: + """Compile the upload step. + + ``workspace=None`` tells the file_io task to use the job's workspace + from its :class:`NMPJobContext`. + """ + return FileIOTaskConfig( + upload=[ + UploadItem( + src=DEFAULT_OUTPUT_MODEL_PATH, + dest=FileSetRef(workspace=None, name=output_fileset_name), + ), + ], + ) + + +def _build_model_entity_config( + workspace: str, + job_spec: UnslothJobOutput, + *, + trust_remote_code: bool, +) -> ModelEntityTaskConfig: + # Forward the user-supplied deployment_config from the job spec. + # String refs are passed through as-is; inline DeploymentParams are + # converted from the user-facing shape to the task-side shape via + # model_validate(model_dump()). + deployment_config: str | ModelEntityDeploymentParameters | None = None + if isinstance(job_spec.deployment_config, str): + deployment_config = job_spec.deployment_config + elif job_spec.deployment_config is not None: + deployment_config = ModelEntityDeploymentParameters.model_validate(job_spec.deployment_config.model_dump()) + + return ModelEntityTaskConfig( + name=job_spec.output.name, + workspace=workspace, + description=job_spec.output.description or "Customized model from unsloth job", + fileset=FileSetRef(workspace=None, name=job_spec.output.fileset), + model_entity=job_spec.model.name, + base_model=job_spec.model.name, + peft=_build_peft_config(job_spec), + trust_remote_code=trust_remote_code, + deployment_config=deployment_config, + ) + + +async def platform_job_config_compiler( + workspace: str, + job_spec: UnslothJobOutput, + sdk: AsyncNeMoPlatform, + *, + job_name: str | None = None, + profile: str | None = None, +) -> PlatformJobSpec: + """Compile a canonical unsloth job spec into a 4-step ``PlatformJobSpec``.""" + del job_name # reserved for future scheduling decisions (e.g. naming jobs) + + logger.info(f"Compiling Unsloth job to PlatformJobSpec: {job_spec.model_dump_json(indent=2)}") + + me = await fetch_model_entity(job_spec.model.name, workspace, sdk) + + cpu_resources = _get_cpu_resources() + base_env = _get_base_environment() + + download_config = _build_file_download_config(job_spec, me) + upload_config = _build_file_upload_config(job_spec.output.fileset) + model_entity_config = _build_model_entity_config( + workspace, + job_spec, + trust_remote_code=me.trust_remote_code or False, + ) + + steps: list[PlatformJobStep] = [ + PlatformJobStep( + name="model-and-dataset-download", + executor=CPUExecutionProviderSpec( + provider="cpu", + container=ContainerSpec( + image=get_tasks_image(), + entrypoint=UNSLOTH_PYTHON_ENTRYPOINT, + command=["-m", "nmp.unsloth.tasks.file_io"], + ), + resources=cpu_resources, + ), + environment=base_env, + config=download_config.model_dump(mode="json"), + ), + compile_training_step(job_spec, base_env, profile=profile), + PlatformJobStep( + name="model-upload", + executor=CPUExecutionProviderSpec( + provider="cpu", + container=ContainerSpec( + image=get_tasks_image(), + entrypoint=UNSLOTH_PYTHON_ENTRYPOINT, + command=["-m", "nmp.unsloth.tasks.file_io"], + ), + resources=cpu_resources, + ), + environment=base_env, + config=upload_config.model_dump(mode="json"), + ), + PlatformJobStep( + name="model-entity-creation", + executor=CPUExecutionProviderSpec( + provider="cpu", + container=ContainerSpec( + image=get_tasks_image(), + entrypoint=UNSLOTH_PYTHON_ENTRYPOINT, + command=["-m", "nmp.unsloth.tasks.model_entity"], + ), + resources=cpu_resources, + ), + environment=base_env, + config=model_entity_config.model_dump(mode="json"), + ), + ] + + return PlatformJobSpec(steps=steps) diff --git a/services/unsloth/src/nmp/unsloth/app/jobs/context.py b/services/unsloth/src/nmp/unsloth/app/jobs/context.py new file mode 100644 index 0000000000..388366fd48 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/app/jobs/context.py @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Job context for unsloth container task entrypoints. + +Mirror of :mod:`nmp.automodel.app.jobs.context`. Each service owns its +own context type so the task entrypoints stay decoupled — even though +the env-var shape is platform-wide. +""" + +from __future__ import annotations + +import os +from dataclasses import dataclass +from pathlib import Path +from typing import Self + +from nmp.common.entities.constants import DEFAULT_WORKSPACE +from nmp.common.jobs.constants import ( + DEFAULT_NEMO_JOB_STEP_CONFIG_FILE_PATH, + NEMO_JOB_ATTEMPT_ID_ENVVAR, + NEMO_JOB_ID_ENVVAR, + NEMO_JOB_STEP_CONFIG_FILE_PATH_ENVVAR, + NEMO_JOB_STEP_ENVVAR, + NEMO_JOB_TASK_ENVVAR, + NEMO_JOB_WORKSPACE_ENVVAR, + PERSISTENT_JOB_STORAGE_PATH_ENVVAR, +) +from nmp.unsloth.app.constants import ( + DEFAULT_JOB_STORAGE_PATH, + NMP_FILES_URL_ENVVAR, + NMP_JOBS_URL_ENVVAR, +) + +DEFAULT_JOB_ID = "unknown-job-id" +DEFAULT_ATTEMPT_ID = "attempt-0" +DEFAULT_STEP = "unknown-step" +DEFAULT_TASK = "unknown-task" + + +def _normalize_task_name(task: str) -> str: + """Ensure task name uses the expected Jobs prefix. + + Generated tasks in k8s don't start with a lowercase letter per + ``NAME_PATTERN``, so we normalize by adding ``task-`` when missing. + Matches the Docker backend's ``task_id = f"task-{uuid.uuid4().hex}"``. + """ + if task.startswith("task-"): + return task + return f"task-{task}" + + +@dataclass(frozen=True) +class NMPJobContext: + """NeMo Platform Job context populated from Job Controller environment variables.""" + + workspace: str + job_id: str + attempt_id: str + step: str + task: str + + jobs_url: str | None + files_url: str | None + + storage_path: Path + config_path: Path + + @property + def normalized_task(self) -> str: + """Task normalized for Jobs API compatibility.""" + return _normalize_task_name(self.task) + + @classmethod + def from_env(cls) -> Self: + """Create a :class:`NMPJobContext` from environment variables.""" + return cls( + workspace=os.environ.get(NEMO_JOB_WORKSPACE_ENVVAR, DEFAULT_WORKSPACE), + job_id=os.environ.get(NEMO_JOB_ID_ENVVAR, DEFAULT_JOB_ID), + attempt_id=os.environ.get(NEMO_JOB_ATTEMPT_ID_ENVVAR, DEFAULT_ATTEMPT_ID), + step=os.environ.get(NEMO_JOB_STEP_ENVVAR, DEFAULT_STEP), + task=os.environ.get(NEMO_JOB_TASK_ENVVAR, DEFAULT_TASK), + jobs_url=os.environ.get(NMP_JOBS_URL_ENVVAR), + files_url=os.environ.get(NMP_FILES_URL_ENVVAR), + storage_path=Path(os.environ.get(PERSISTENT_JOB_STORAGE_PATH_ENVVAR, DEFAULT_JOB_STORAGE_PATH)), + config_path=Path( + os.environ.get(NEMO_JOB_STEP_CONFIG_FILE_PATH_ENVVAR, DEFAULT_NEMO_JOB_STEP_CONFIG_FILE_PATH) + ), + ) diff --git a/services/unsloth/src/nmp/unsloth/app/jobs/file_io/__init__.py b/services/unsloth/src/nmp/unsloth/app/jobs/file_io/__init__.py new file mode 100644 index 0000000000..e5725ea5a4 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/app/jobs/file_io/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/services/unsloth/src/nmp/unsloth/app/jobs/file_io/schemas.py b/services/unsloth/src/nmp/unsloth/app/jobs/file_io/schemas.py new file mode 100644 index 0000000000..641a9ea69e --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/app/jobs/file_io/schemas.py @@ -0,0 +1,175 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Schemas for the unsloth file_io task configuration. + +Mirrors :mod:`nmp.automodel.app.jobs.file_io.schemas`. The duplication +is intentional — automodel's docstrings explicitly warn against importing +its task schemas back into other services. Each service owns its own +copy so the container task surfaces stay decoupled. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import StrEnum +from typing import Optional + +from pydantic import BaseModel, Field, model_validator + +FILESET_PROTOCOL = "fileset://" + + +class TaskStatus(StrEnum): + """Status of a file I/O task.""" + + RUNNING = "running" + COMPLETED = "completed" + ERROR = "error" + + +class TaskPhase(StrEnum): + """Phase of a file I/O task.""" + + DOWNLOADING = "downloading" + UPLOADING = "uploading" + COMPLETED = "completed" + + +class FileSetRef(BaseModel): + """Reference to a FileSet.""" + + # workspace is optional because at compile time, the workspace is not known. + # None tells the file_io task to use the job's workspace from the JobContext. + workspace: Optional[str] = None + name: str + + def __str__(self) -> str: + if self.workspace is None: + return self.name + return f"{self.workspace}/{self.name}" + + def __repr__(self) -> str: + return f"FileSetRef(workspace={self.workspace}, name={self.name})" + + @classmethod + def _parse_string_parts(cls, ref: str) -> tuple[Optional[str], str] | None: + if len(ref) == 0: + return None + if ref.startswith(FILESET_PROTOCOL): + ref = ref[len(FILESET_PROTOCOL) :] + parts = ref.split("/", 1) + if len(parts) == 1: + return None, parts[0] + if len(parts) == 2: + return parts[0], parts[1] + return None + + @classmethod + def extract_name(cls, ref: str) -> str: + """Extract the fileset/entity name from a reference string.""" + return cls.model_validate(ref).name + + @model_validator(mode="before") + @classmethod + def _convert_string_input(cls, v: object) -> object: + if isinstance(v, str): + result = cls._parse_string_parts(v) + if result is None: + raise ValueError(f"Invalid FileSet reference: {v!r}. Expected format: 'workspace/name' or 'name'.") + workspace, name = result + return {"workspace": workspace, "name": name} + return v + + +class DownloadItem(BaseModel): + """Configures a single download: fileset -> local path.""" + + src: FileSetRef = Field( + description=( + "FileSet reference for the source files. " + "Accepts 'workspace/name' or 'name' (job workspace used when omitted)." + ), + ) + dest: str = Field( + default=".", + description="Absolute destination path for downloaded files.", + ) + + +class UploadItem(BaseModel): + """Configures a single upload: local path -> fileset.""" + + src: str = Field(description="Absolute source path for files to upload.") + dest: FileSetRef = Field( + description=( + "FileSet reference for the destination. " + "Accepts 'workspace/name' or 'name' (job workspace used when omitted)." + ), + ) + metadata: Optional[dict] = Field( + default=None, + description="Optional metadata to set on the created fileset.", + ) + + +class FileIOTaskConfig(BaseModel): + """Configuration for the file_io task. + + Used when running ``python -m nmp.unsloth.tasks.file_io``. + """ + + download: list[DownloadItem] = Field( + default_factory=list, + description="List of FileSets to download.", + ) + upload: list[UploadItem] = Field( + default_factory=list, + description="List of paths to upload to FileSets.", + ) + + +class TaskCompilationError(Exception): + """Error compiling a task configuration.""" + + +class FileDownloadError(Exception): + """Error downloading files from the Files service.""" + + +class FileUploadError(Exception): + """Error uploading files to the Files service.""" + + +class ProgressReportError(Exception): + """Error reporting progress to the Jobs service.""" + + +class PathTraversalError(ValueError): + """Error when a path attempts to escape the allowed base directory. + + Raised when user-provided paths like '../..' would resolve outside + the designated storage directory. + """ + + +@dataclass +class FileStats: + """Statistics for a file operation.""" + + total_bytes: int = 0 + failed_files: int = 0 + + +@dataclass +class DownloadStats(FileStats): + """Statistics for a download operation.""" + + files_downloaded: int = 0 + + +@dataclass +class UploadStats(FileStats): + """Statistics for an upload operation.""" + + files_uploaded: int = 0 diff --git a/services/unsloth/src/nmp/unsloth/app/jobs/model_entity/__init__.py b/services/unsloth/src/nmp/unsloth/app/jobs/model_entity/__init__.py new file mode 100644 index 0000000000..e5725ea5a4 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/app/jobs/model_entity/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/services/unsloth/src/nmp/unsloth/app/jobs/model_entity/schemas.py b/services/unsloth/src/nmp/unsloth/app/jobs/model_entity/schemas.py new file mode 100644 index 0000000000..cd7f7f52f9 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/app/jobs/model_entity/schemas.py @@ -0,0 +1,110 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Schemas for the unsloth model_entity task configuration. + +Mirrors :mod:`nmp.automodel.app.jobs.model_entity.schemas`. Each service +owns its own copy so the container task surfaces stay decoupled. +""" + +from __future__ import annotations + +from typing import Optional + +from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef +from nmp.unsloth.entities.values import FinetuningType +from pydantic import BaseModel, Field + + +class ToolCallConfig(BaseModel): + """Tool calling configuration for NIM deployments.""" + + tool_call_parser: Optional[str] = Field(default=None, description="Name of the tool call parser to use.") + tool_call_plugin: Optional[str] = Field( + default=None, + pattern=r"^[\w\-.]+/[\w\-.]+$", + description=( + "Reference to a fileset containing the custom tool call plugin Python file. " + "Expected format: '{workspace}/{fileset_name}'." + ), + ) + auto_tool_choice: Optional[bool] = Field(default=None, description="Whether to enable automatic tool choice.") + + +class DeploymentParameters(BaseModel): + """Inline deployment parameters for creating a new ModelDeploymentConfig.""" + + gpu: int = Field(default=1, description="Number of GPUs required for deployment") + additional_envs: Optional[dict[str, str]] = Field( + default=None, + description="Additional environment variables for deployment", + ) + disk_size: Optional[str] = Field(default=None, description="Disk size for deployment") + image_name: Optional[str] = Field( + default=None, + description="Container image name from NGC. Defaults to multi-llm when unset", + ) + image_tag: Optional[str] = Field(default=None, description="Container image tag from NGC") + lora_enabled: bool = Field( + default=True, + description=( + "When auto-deploying full SFT training, setting this true allows " + "subsequent LoRA adapters to be deployed against the model." + ), + ) + tool_call_config: Optional[ToolCallConfig] = Field( + default=None, + description="Tool calling configuration override for the NIM deployment.", + ) + + +class PEFTConfig(BaseModel): + """PEFT configuration for LoRA / LoRA-merged fine-tuning.""" + + type: FinetuningType + rank: int + alpha: int + + +class ModelEntityTaskConfig(BaseModel): + """Configuration for the unsloth model_entity task. + + Used when running ``python -m nmp.unsloth.tasks.model_entity``. + """ + + name: str = Field(description="Name of the model entity to create.") + workspace: str = Field(description="Workspace of the model entity to create.") + description: Optional[str] = Field( + default=None, + description="Optional description of the model.", + ) + fileset: FileSetRef = Field( + description="FileSet reference containing the customized model artifacts.", + ) + model_entity: str = Field( + description="The model entity (workspace/name) this model was based on.", + ) + base_model: Optional[str] = Field( + default=None, + description="Link to the base model used for customization.", + ) + peft: Optional[PEFTConfig] = Field( + default=None, + description="PEFT configuration. Set for LoRA / LoRA-merged, None for full SFT.", + ) + trust_remote_code: bool = Field( + default=False, + description="Whether to trust remote code for the checkpoint.", + ) + deployment_config: Optional[str | DeploymentParameters] = Field( + default=None, + description=( + "Deployment configuration. A string references an existing ModelDeploymentConfig " + "by name. An object provides inline NIM deployment parameters. " + "Omit to skip deployment." + ), + ) + + +class ModelEntityCreationError(Exception): + """Error creating the output model entity.""" diff --git a/services/unsloth/src/nmp/unsloth/app/jobs/training/__init__.py b/services/unsloth/src/nmp/unsloth/app/jobs/training/__init__.py new file mode 100644 index 0000000000..e5725ea5a4 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/app/jobs/training/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/services/unsloth/src/nmp/unsloth/app/jobs/training/compiler.py b/services/unsloth/src/nmp/unsloth/app/jobs/training/compiler.py new file mode 100644 index 0000000000..e0be00c9a0 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/app/jobs/training/compiler.py @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Compile the training step of an unsloth job. + +This is the GPU step that container submit will execute. The plugin's +local ``run`` does **not** call this code path — it invokes +:func:`~nmp.unsloth.tasks.training.backends.unsloth_sft.train_sft` +directly inside the BYO-venv. Wiring the compiler step here today keeps +a future submit one-file-per-step away. +""" + +from __future__ import annotations + +import logging + +from nemo_platform_plugin.jobs.api_factory import ( + ContainerSpec, + EnvironmentVariable, + GPUExecutionProviderSpec, + PlatformJobStep, + ResourcesSpec, +) +from nmp.unsloth.app.constants import ( + DEFAULT_DATASET_PATH, + DEFAULT_MODEL_PATH, + DEFAULT_OUTPUT_MODEL_PATH, +) +from nmp.unsloth.app.jobs.training.schemas import TrainingStepConfig +from nmp.unsloth.images import UNSLOTH_PYTHON_ENTRYPOINT, get_training_image +from nmp.unsloth.schemas import UnslothJobOutput + +logger = logging.getLogger(__name__) + + +def compile_training_step( + job_spec: UnslothJobOutput, + base_env: list[EnvironmentVariable], + *, + profile: str | None = None, +) -> PlatformJobStep: + """Build the GPU training :class:`PlatformJobStep`. + + The container reads the step config (a serialized + :class:`TrainingStepConfig`) and runs ``train_sft`` against the + paths populated by the file_io download step. + + Args: + job_spec: Canonical job spec to serialize into the step config. + base_env: Environment variables carried into every step + (e.g. ``PERSISTENT_JOB_STORAGE_PATH``). + profile: GPU execution profile (e.g. ``gpu`` or + ``gpu_distributed``). When ``None`` the executor's default + is used. + """ + step_config = TrainingStepConfig( + spec=job_spec, + model_path=DEFAULT_MODEL_PATH, + dataset_path=DEFAULT_DATASET_PATH, + output_path=DEFAULT_OUTPUT_MODEL_PATH, + ) + + executor_kwargs: dict = { + "provider": "gpu", + "container": ContainerSpec( + image=get_training_image(), + entrypoint=UNSLOTH_PYTHON_ENTRYPOINT, + command=["-m", "nmp.unsloth.tasks.training"], + ), + # Resources are provider-decided; the platform's GPU executor + # selects a node based on the profile. + "resources": ResourcesSpec(), + } + if profile is not None: + executor_kwargs["profile"] = profile + + return PlatformJobStep( + name="training", + executor=GPUExecutionProviderSpec(**executor_kwargs), + environment=base_env, + config=step_config.model_dump(mode="json"), + ) diff --git a/services/unsloth/src/nmp/unsloth/app/jobs/training/schemas.py b/services/unsloth/src/nmp/unsloth/app/jobs/training/schemas.py new file mode 100644 index 0000000000..5417ff83d4 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/app/jobs/training/schemas.py @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Schemas for the unsloth training task configuration. + +The training step config is a thin wrapper around the canonical +:class:`~nmp.unsloth.schemas.UnslothJobOutput` plus the resolved +filesystem paths the file_io download step writes to. The container +runner reads this from the platform Jobs envelope, validates it, and +hands the job spec + paths to :func:`~nmp.unsloth.tasks.training.backends.unsloth_sft.train_sft`. +""" + +from __future__ import annotations + +from nmp.unsloth.schemas import UnslothJobOutput +from pydantic import BaseModel, ConfigDict, Field + + +class TrainingStepConfig(BaseModel): + """Configuration handed to ``python -m nmp.unsloth.tasks.training``.""" + + model_config = ConfigDict(extra="forbid") + + spec: UnslothJobOutput = Field(description="Canonical job spec for the training run.") + model_path: str = Field(description="Local filesystem path where the model weights were downloaded.") + dataset_path: str = Field(description="Local filesystem path where the training dataset was downloaded.") + output_path: str = Field(description="Local filesystem path the training driver should save the checkpoint to.") diff --git a/services/unsloth/src/nmp/unsloth/compile.py b/services/unsloth/src/nmp/unsloth/compile.py new file mode 100644 index 0000000000..48fbaeb9ca --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/compile.py @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Public compile entry for unsloth jobs. + +Mirror of :mod:`nmp.automodel.compile`. Invoked by the plugin's +:meth:`UnslothJob.compile` to turn a validated +:class:`~nmp.unsloth.schemas.UnslothJobOutput` into a 4-step +:class:`PlatformJobSpec` (download → train → upload → model-entity). +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from nmp.unsloth.app.jobs.compiler import platform_job_config_compiler as _compile_canonical + +if TYPE_CHECKING: + from nemo_platform import AsyncNeMoPlatform + from nemo_platform_plugin.jobs.api_factory import PlatformJobSpec + from nmp.unsloth.schemas import UnslothJobOutput + + +async def platform_job_config_compiler( + *, + workspace: str, + spec: "UnslothJobOutput", + sdk: "AsyncNeMoPlatform", + job_name: str | None = None, + profile: str | None = None, +) -> "PlatformJobSpec": + """Compile a canonical unsloth job spec to a ``PlatformJobSpec``. + + Used by :meth:`UnslothJob.compile`. Container submit only — Unsloth + no longer supports local run. + """ + return await _compile_canonical( + workspace, + spec, + sdk, + job_name=job_name, + profile=profile, + ) + + +__all__ = ["platform_job_config_compiler"] diff --git a/services/unsloth/src/nmp/unsloth/config.py b/services/unsloth/src/nmp/unsloth/config.py new file mode 100644 index 0000000000..d839c989a8 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/config.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Configuration for the nmp-unsloth compiler and tasks. + +Modeled after :mod:`nmp.automodel.config`. Environment variables use +the ``NMP_UNSLOTH_`` prefix. None of these settings are consumed by the +plugin's local ``run`` path today — they exist so a future container +submit (4-step PlatformJobSpec) lands without restructuring config. +""" + +from nmp.common.config import create_service_config_class, get_platform_config, get_service_config +from pydantic import Field + + +class UnslothConfig(create_service_config_class("unsloth")): # type: ignore[misc] + """Environment variables use the ``NMP_UNSLOTH_`` prefix.""" + + image_registry: str = Field( + default="nvcr.io/0921617854601259/nemo-platform-dev", + description=( + "Registry host/path prefix for nmp-unsloth-tasks and nmp-unsloth-training. " + "Override via NMP_UNSLOTH_IMAGE_REGISTRY for other environments." + ), + ) + training_image: str | None = Field( + default=None, + description="Override GPU training image (default: nmp-unsloth-training under image_registry).", + ) + tasks_image: str | None = Field( + default=None, + description="Override CPU tasks image (default: nmp-unsloth-tasks under image_registry).", + ) + + default_job_resource_cpu_request: str = Field(default="1") + default_job_resource_memory_request: str = Field(default="8Gi") + default_job_resource_cpu_limit: str = Field(default="4") + default_job_resource_memory_limit: str = Field(default="16Gi") + + default_training_execution_profile: str = Field( + default="gpu", + description="Default GPU execution profile when the job spec omits training.execution_profile.", + ) + + +config = get_service_config(UnslothConfig) +platform_config = get_platform_config() diff --git a/services/unsloth/src/nmp/unsloth/entities/__init__.py b/services/unsloth/src/nmp/unsloth/entities/__init__.py new file mode 100644 index 0000000000..e5725ea5a4 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/entities/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/services/unsloth/src/nmp/unsloth/entities/values.py b/services/unsloth/src/nmp/unsloth/entities/values.py new file mode 100644 index 0000000000..4ade32a64b --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/entities/values.py @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Value types for the unsloth service. + +A reduced subset of automodel's ``entities/values.py`` — the unsloth +backend supports SFT only, with two finetuning shapes (LoRA / full). +The plugin's input schema enforces this; this module exists so compile +and model-entity code can speak the same enum values as automodel. +""" + +from enum import Enum, StrEnum + + +class TrainingType(str, Enum): + """Training algorithm type. Unsloth backend supports SFT only.""" + + SFT = "sft" + + +class FinetuningType(str, Enum): + """Finetuning strategy. + + The plugin's ``UnslothJobInput.training.finetuning_type`` accepts + ``"lora"`` and ``"full"``; ``"full"`` maps onto :attr:`ALL_WEIGHTS`, + matching automodel's compiler vocabulary. ``"lora_merged"`` is + derived from ``output.save_method`` at compile time when the user + asks for a merged checkpoint. + """ + + ALL_WEIGHTS = "all_weights" + LORA = "lora" + LORA_MERGED = "lora_merged" + + +class OutputNameType(StrEnum): + """Output artifact type — adapter (LoRA only) or model (merged / full).""" + + ADAPTER = "adapter" + MODEL = "model" diff --git a/services/unsloth/src/nmp/unsloth/images.py b/services/unsloth/src/nmp/unsloth/images.py new file mode 100644 index 0000000000..e35e52bf6b --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/images.py @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Docker image resolution for nmp-unsloth job steps. + +Mirrors :mod:`nmp.automodel.images`. Consumed by the compiler in +:mod:`nmp.unsloth.app.jobs.compiler` (and its training sub-compiler) to +stamp the right image refs onto each container step. + +Today we ship **one** image, ``nmp-unsloth-training``, used by all four +steps (file_io, model_entity, training). Production environments that +want a leaner CPU image for file_io / model_entity can publish a separate +``nmp-unsloth-tasks`` and point ``NMP_UNSLOTH_TASKS_IMAGE`` at it. +""" + +from __future__ import annotations + +from nmp.common.jobs.image import get_qualified_image +from nmp.unsloth.config import config + +DEFAULT_UNSLOTH_IMAGE_REGISTRY = "nvcr.io/0921617854601259/nemo-platform-dev" + +BASE_IMAGE_NAME = "nmp-unsloth-base" +TASKS_IMAGE_NAME = "nmp-unsloth-tasks" +TRAINING_IMAGE_NAME = "nmp-unsloth-training" + +# Must match ENTRYPOINT in Dockerfile.nmp-unsloth-{tasks,training}. +# Job specs must set this explicitly: Docker API ``create()`` replaces the +# image entrypoint when the platform passes ``entrypoint=[]``. +UNSLOTH_PYTHON_ENTRYPOINT = ["/opt/venv/bin/python"] + + +def get_unsloth_qualified_image(name: str, override: str | None = None) -> str: + """Resolve a job step image reference. + + Args: + name: Image repository name under the registry (e.g. ``nmp-unsloth-tasks``). + override: Full image ref from ``NMP_UNSLOTH_TASKS_IMAGE`` / + ``NMP_UNSLOTH_TRAINING_IMAGE``. + + Returns: + Fully qualified image (``{registry}/{name}:{tag}``) unless ``override`` is set. + """ + if override: + return override + registry = config.image_registry or DEFAULT_UNSLOTH_IMAGE_REGISTRY + return get_qualified_image(name, registry=registry) + + +def get_tasks_image() -> str: + """CPU task steps (file_io, model_entity). + + When no explicit ``NMP_UNSLOTH_TASKS_IMAGE`` is set we reuse the + training image — it has the platform glue (``nmp-common`` SDK + + ``nemo-platform``) needed by file_io / model_entity in addition to + the ML stack. Override at deploy time once a leaner image exists. + """ + if config.tasks_image: + return get_unsloth_qualified_image(TASKS_IMAGE_NAME, config.tasks_image) + return get_training_image() + + +def get_training_image() -> str: + """GPU training step.""" + return get_unsloth_qualified_image(TRAINING_IMAGE_NAME, config.training_image) diff --git a/services/unsloth/src/nmp/unsloth/platform_client.py b/services/unsloth/src/nmp/unsloth/platform_client.py new file mode 100644 index 0000000000..0e1284ca77 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/platform_client.py @@ -0,0 +1,80 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Async helpers for resolving model/dataset references against the platform. + +Mirrors :mod:`nmp.automodel.platform_client`. Used by the plugin's +``transform.py`` (async, runs inside the FastAPI request handler / +``to_spec`` flow) to validate that the submitter's ``model`` and +``dataset.path`` exist before the job moves on to compile / run. + +Sync download/upload helpers live in :mod:`nmp.unsloth.file_io` — +those are consumed by the plugin's ``run()`` and by future container +tasks, both of which are sync. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from nemo_platform import AsyncNeMoPlatform +from nemo_platform._exceptions import NotFoundError, PermissionDeniedError +from nmp.common.entities.utils import parse_entity_ref +from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + +if TYPE_CHECKING: + from nemo_platform.types.models import ModelEntity + + +async def check_dataset_access( + sdk: AsyncNeMoPlatform, + dataset_uri: str, + default_workspace: str, +) -> None: + """Verify the caller can access the dataset fileset. + + Raises: + ValueError: If the fileset is not found. + PermissionError: If access is denied. + """ + ref = FileSetRef.model_validate(dataset_uri) + workspace = ref.workspace or default_workspace + try: + await sdk.files.filesets.retrieve(workspace=workspace, name=ref.name) + except PermissionDeniedError: + raise PermissionError(f"Access denied to dataset fileset '{workspace}/{ref.name}'") from None + except NotFoundError: + raise ValueError( + f"Dataset fileset '{ref.name}' not found in workspace '{workspace}'. Verify the dataset exists." + ) from None + + +async def fetch_model_entity( + model_ref: str, + default_workspace: str, + sdk: AsyncNeMoPlatform, +) -> "ModelEntity": + """Retrieve a model entity by reference string. + + Args: + model_ref: ``"name"`` (uses ``default_workspace``) or ``"workspace/name"``. + default_workspace: Workspace to use when the ref is bare. + sdk: Async platform SDK handle. + + Raises: + ValueError: If the model entity is not found. + PermissionError: If access is denied. + """ + resolved_ref = parse_entity_ref(model_ref, default_workspace) + try: + return await sdk.models.retrieve( + name=resolved_ref.name, + workspace=resolved_ref.workspace, + verbose=True, + ) + except PermissionDeniedError: + raise PermissionError(f"Access denied to model '{resolved_ref.workspace}/{resolved_ref.name}'") from None + except NotFoundError: + raise ValueError( + f"Model entity not found: '{resolved_ref.workspace}/{resolved_ref.name}'. Verify the model entity exists." + ) from None diff --git a/services/unsloth/src/nmp/unsloth/schemas.py b/services/unsloth/src/nmp/unsloth/schemas.py new file mode 100644 index 0000000000..bcc419eccc --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/schemas.py @@ -0,0 +1,326 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Canonical Unsloth schemas — consumed by ``train_sft`` (and ``compile`` later). + +Why these live in the service, not the plugin: + +- Both compile-time (``compile.platform_job_config_compiler``, when wired + later) and runtime (``train_sft``) consume the canonical shape. The + container entrypoint reads it from the platform Jobs envelope. +- The plugin's ``transform.py`` produces ``UnslothJobOutput`` from + ``UnslothJobInput`` (which lives in the plugin) — but the *output* + type is what flows downstream, so it belongs with the downstream + consumers. + +Each field maps onto a real argument of one of three call-sites the +training driver hits: + +- ``unsloth.FastLanguageModel.from_pretrained(...)`` → :class:`ModelLoadSpec` +- ``unsloth.FastLanguageModel.get_peft_model(...)`` → :class:`LoRAParams` +- ``transformers.TrainingArguments`` + ``trl.SFTTrainer(...)`` → + :class:`ScheduleSpec`, :class:`BatchSpec`, :class:`OptimizerSpec`, + :class:`HardwareSpec`, :class:`IntegrationsSpec` +- Output saving (``model.save_pretrained{,_merged}``) → :class:`OutputResponse` + +``extra="forbid"`` everywhere — typos in the JSON shape become +validation errors, not silently-ignored fields. +""" + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field + + +class ModelLoadSpec(BaseModel): + """Args to ``FastLanguageModel.from_pretrained``. + + ``name`` is a NeMo Platform model entity reference (``"name"`` or + ``"workspace/name"``). The plugin's run orchestration resolves the + entity, downloads its fileset to a local path, and hands that path + to :func:`train_sft`. + """ + + model_config = ConfigDict(extra="forbid") + + name: str = Field( + description=( + "Model entity reference. Accepts 'name' (uses the job's workspace) " + "or 'workspace/name'. The plugin's run resolves this to a local " + "path before training." + ), + ) + max_seq_length: int = Field(default=2048, gt=0) + load_in_4bit: bool = Field( + default=True, + description="bitsandbytes 4-bit. Mutex with load_in_8bit. Default for Unsloth's headline path.", + ) + load_in_8bit: bool = False + dtype: Literal["auto", "bfloat16", "float16", "float32"] = "auto" + trust_remote_code: bool = False + + +class LoRAParams(BaseModel): + """Args to ``FastLanguageModel.get_peft_model``.""" + + model_config = ConfigDict(extra="forbid") + + rank: int = Field(default=16, gt=0, description="LoRA rank.") + alpha: int = Field(default=16, gt=0, description="LoRA scaling factor (alpha).") + dropout: float = Field(default=0.0, ge=0.0, lt=1.0) + target_modules: list[str] = Field( + # Unsloth's recommended 7-module set: full attention + MLP. + default_factory=lambda: [ + "q_proj", + "k_proj", + "v_proj", + "o_proj", + "gate_proj", + "up_proj", + "down_proj", + ], + ) + bias: Literal["none", "all", "lora_only"] = "none" + use_rslora: bool = False + random_state: int = 3407 + + +class TrainingSpec(BaseModel): + """Algorithm + adapter shape selectors.""" + + model_config = ConfigDict(extra="forbid") + + training_type: Literal["sft"] = "sft" + finetuning_type: Literal["lora", "full"] = "lora" + lora: LoRAParams | None = Field( + default=None, + description="Required when finetuning_type='lora'. Auto-filled with defaults if omitted.", + ) + use_gradient_checkpointing: Literal["unsloth", "true", "false"] = "unsloth" + + +class DatasetSpec(BaseModel): + """Training data location + shape. + + ``path`` and ``validation_path`` are platform fileset references + (``"name"`` or ``"workspace/name"``). The plugin's run downloads + each fileset before training; ``train_sft`` only ever sees a local + filesystem path. + """ + + model_config = ConfigDict(extra="forbid") + + path: str = Field( + description=( + "Training fileset reference: 'name' (uses the job's workspace) " + "or 'workspace/name'. Resolved to a local path by the plugin run." + ), + ) + text_field: str = Field(default="text", description="Row field consumed by SFTTrainer.") + apply_chat_template: bool = Field( + default=False, + description=( + "If True, expects rows with a 'messages' field and applies tokenizer.apply_chat_template at training time." + ), + ) + validation_path: str | None = Field( + default=None, + description=( + "Optional validation fileset reference (same format as 'path'). Downloaded under the same scheme." + ), + ) + packing: bool = Field(default=False, description="trl.SFTTrainer packing flag.") + + +class ScheduleSpec(BaseModel): + """Training schedule, scheduler, logging cadence.""" + + model_config = ConfigDict(extra="forbid") + + epochs: int | None = Field(default=None, gt=0) + max_steps: int | None = Field(default=None, gt=0) + warmup_steps: int = Field(default=0, ge=0) + warmup_ratio: float | None = Field(default=None, ge=0.0, le=1.0) + lr_scheduler_type: Literal[ + "linear", + "cosine", + "constant", + "constant_with_warmup", + "cosine_with_restarts", + ] = "linear" + logging_steps: int = Field(default=1, gt=0) + save_steps: int | None = Field(default=None, gt=0) + eval_steps: int | None = Field(default=None, gt=0) + seed: int = 3407 + + +class BatchSpec(BaseModel): + model_config = ConfigDict(extra="forbid") + + per_device_train_batch_size: int = Field(default=1, gt=0) + gradient_accumulation_steps: int = Field(default=1, gt=0) + + +class OptimizerSpec(BaseModel): + model_config = ConfigDict(extra="forbid") + + learning_rate: float = Field(default=2e-4, gt=0.0) + weight_decay: float = Field(default=0.0, ge=0.0) + # Unsloth's notebooks default to adamw_8bit (bitsandbytes-backed) — much + # smaller optimizer state than adamw_torch, which lets users fit larger + # adapters on the same GPU. Users on Hopper+ may prefer adamw_torch_fused. + optim: Literal[ + "adamw_torch", + "adamw_torch_fused", + "adamw_8bit", + "paged_adamw_8bit", + "sgd", + ] = "adamw_8bit" + + +class HardwareSpec(BaseModel): + model_config = ConfigDict(extra="forbid") + + gpus: str | None = Field( + default=None, + description=( + "Comma-separated GPU indices ('0' or '0,1') for CUDA_VISIBLE_DEVICES. Selection, not reservation." + ), + ) + precision: Literal["bf16", "fp16"] = Field( + default="bf16", + description="Mixed-precision dtype for training. bf16 recommended for Ampere+.", + ) + + +class WandbIntegration(BaseModel): + model_config = ConfigDict(extra="forbid") + + enabled: bool = False + project: str | None = None + run_name: str | None = None + # No api_key_secret for now — users export WANDB_API_KEY in the venv + # environment themselves. Matches the BYO-venv stance. + + +class IntegrationsSpec(BaseModel): + model_config = ConfigDict(extra="forbid") + + wandb: WandbIntegration | None = None + report_to: list[Literal["wandb", "tensorboard", "mlflow", "none"]] = Field( + default_factory=lambda: ["none"], + ) + + +class OutputResponse(BaseModel): + """Stored on the canonical UnslothJobOutput. Path is resolved at run() time. + + ``type`` is the high-level shape (``adapter`` for a saved LoRA, ``model`` + for a merged checkpoint). ``save_method`` keeps the original Unsloth + save verb so the training driver can dispatch correctly without + re-deriving it. ``fileset`` is the platform fileset name the trained + artefacts will be uploaded to (the plugin's ``transform`` defaults + this to ``name``). + """ + + model_config = ConfigDict(extra="forbid") + + name: str + type: Literal["adapter", "model"] + save_method: Literal["lora", "merged_16bit", "merged_4bit"] + fileset: str = Field( + description=("Platform fileset name the trained checkpoint will be uploaded to. Defaults to the entity name."), + ) + description: str | None = None + + +class ToolCallParams(BaseModel): + """Tool calling configuration for NIM deployments.""" + + model_config = ConfigDict(extra="forbid") + + tool_call_parser: str | None = Field( + default=None, + description=( + "Name of the tool call parser to use (e.g., 'openai', 'hermes', 'pythonic', 'llama3_json', 'mistral')." + ), + ) + tool_call_plugin: str | None = Field( + default=None, + pattern=r"^[\w\-.]+/[\w\-.]+$", + description=( + "Reference to a fileset containing the custom tool call plugin Python file. " + "Expected format: '{workspace}/{fileset_name}'." + ), + ) + auto_tool_choice: bool | None = Field( + default=None, + description="Whether to enable automatic tool choice.", + ) + + +class DeploymentParams(BaseModel): + """Inline deployment parameters for auto-deploying a trained model. + + Used in :class:`UnslothJobInput.deployment_config` and passed through to + the model_entity task at compile time. When unset, no deployment is launched. + """ + + model_config = ConfigDict(extra="forbid") + + gpu: int = Field(default=1, description="Number of GPUs required for the deployment.") + additional_envs: dict[str, str] | None = Field( + default=None, + description="Additional environment variables for the deployment.", + ) + disk_size: str | None = Field(default=None, description="Disk size for the deployment.") + image_name: str | None = Field( + default=None, + description="Container image name from NGC. If not specified, defaults to multi-llm.", + ) + image_tag: str | None = Field(default=None, description="Container image tag from NGC.") + lora_enabled: bool = Field( + default=True, + description=( + "When auto-deploying a full SFT training, setting this true allows subsequent " + "LoRA adapters to be deployed against it." + ), + ) + tool_call_config: ToolCallParams | None = Field( + default=None, + description="Tool calling configuration override for the NIM deployment.", + ) + + +class UnslothJobOutput(BaseModel): + """Canonical spec stored after the plugin's ``to_spec()`` resolves output naming. + + Defaults match :class:`~nemo_unsloth_plugin.schema.UnslothJobInput` so SDK + callers and tests can construct :class:`UnslothJobOutput` directly without + restating every sub-section. The plugin's ``to_spec`` always passes the + resolved input values through, so these defaults never override real input. + """ + + model_config = ConfigDict(extra="forbid") + + name: str | None = None + model: ModelLoadSpec + dataset: DatasetSpec + training: TrainingSpec = Field(default_factory=TrainingSpec) + schedule: ScheduleSpec = Field(default_factory=ScheduleSpec) + batch: BatchSpec = Field(default_factory=BatchSpec) + optimizer: OptimizerSpec = Field(default_factory=OptimizerSpec) + hardware: HardwareSpec = Field(default_factory=HardwareSpec) + integrations: IntegrationsSpec | None = None + output: OutputResponse + deployment_config: str | DeploymentParams | None = Field( + default=None, + description=( + "Deployment configuration for auto-deploying the model after training. " + "Pass a string to reference an existing ModelDeploymentConfig by name " + "('my-config' or 'workspace/my-config'). An object provides inline NIM " + "deployment parameters. Omit to skip deployment." + ), + ) diff --git a/services/unsloth/src/nmp/unsloth/tasks/file_io/__init__.py b/services/unsloth/src/nmp/unsloth/tasks/file_io/__init__.py new file mode 100644 index 0000000000..182d33c009 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/tasks/file_io/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""File I/O task for unsloth customization jobs.""" + +from nmp.unsloth.tasks.file_io.run import run + +__all__ = ["run"] diff --git a/services/unsloth/src/nmp/unsloth/tasks/file_io/__main__.py b/services/unsloth/src/nmp/unsloth/tasks/file_io/__main__.py new file mode 100644 index 0000000000..cba1d22b48 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/tasks/file_io/__main__.py @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import sys + +from nmp.unsloth.tasks.file_io.run import run + +if __name__ == "__main__": + sys.exit(run()) diff --git a/services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py b/services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py new file mode 100644 index 0000000000..0ef1a32f10 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py @@ -0,0 +1,481 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Custom fsspec callbacks for progress reporting during file I/O operations.""" + +import logging +import os +import threading +from abc import abstractmethod +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from fsspec.callbacks import Callback, TqdmCallback +from nmp.common.jobs.schemas import PlatformJobStatus +from nmp.unsloth.app.jobs.file_io.schemas import DownloadStats, TaskPhase, UploadStats +from nmp.unsloth.tasks.file_io.progress_reporter import ProgressReporter + +logger = logging.getLogger(__name__) + + +def get_percentage(current: int, total: int) -> int: + """Get integer percentage 0-100. + + Raises: + ValueError: If current > total or either value is negative. + """ + if current > total: + raise ValueError( + f"Unexpected value of the current and total values: current={current} cannot be greater than total={total}", + ) + if total < 0: + raise ValueError(f"Unexpected negative value of the total value: total={total}, current={current}") + if current < 0: + raise ValueError(f"Unexpected negative value of the current value: current={current}, total={total}") + + if total == 0: + return 0 + return int((current / total) * 100) + + +@dataclass +class FileInfo: + """A dataclass for file information.""" + + path: str + size: int + + +class TqdmPerFileUploadCallback(Callback): + """A callback that creates a separate tqdm progress bar for each file upload.""" + + def __init__(self, src_path: Path, **kwargs: Any): + self.src_path = src_path + super().__init__(**kwargs) + + def branched(self, full_src_path: str, full_dest_path: str, **kwargs: Any) -> TqdmCallback: + if self.src_path.is_file(): + relative_path = self.src_path.name + else: + relative_path = Path(full_src_path).relative_to(self.src_path) + return TqdmCallback( + tqdm_kwargs={ + "desc": f"Uploading {relative_path!s}", + "unit": "B", + "unit_scale": True, + "unit_divisor": 1024, + "miniters": 1, + }, + ) + + +class TqdmPerFileDownloadCallback(Callback): + """A callback that creates a separate tqdm progress bar for each file download. + + Accepts a ``file_sizes`` dict (relative path -> byte size) so each + progress bar can show percent-complete even when the SDK streams the + file without a Content-Length header. + """ + + def __init__(self, dest_path: Path, fileset_path: str, file_sizes: dict[str, int] | None = None, **kwargs: Any): + self.dest_path = dest_path + self.fileset_path = fileset_path.rstrip("/") + self.file_sizes = file_sizes or {} + super().__init__(**kwargs) + + def branched(self, full_src_path: str, full_dest_path: str, **kwargs: Any) -> TqdmCallback: + dest_full_path = Path(full_dest_path) + if self.dest_path.is_file(): + relative_path = dest_full_path.name + else: + try: + relative_path = dest_full_path.relative_to(self.dest_path) + except ValueError: + relative_path = dest_full_path.name + + # full_src_path looks like "workspace/fileset/relative/path/file.txt". + # Strip the prefix to look up the size by relative path. + relative_file_path = full_src_path + if full_src_path.startswith(self.fileset_path): + relative_file_path = full_src_path[len(self.fileset_path) :].lstrip("/") + + file_size = self.file_sizes.get(relative_file_path) + + callback = TqdmCallback( + tqdm_kwargs={ + "desc": f"Downloading {relative_path!s}", + "unit": "B", + "unit_scale": True, + "unit_divisor": 1024, + "miniters": 1, + }, + ) + + # set_size() rather than tqdm_kwargs["total"] so the SDK can also + # call set_size() from a Content-Length header without conflict. + if file_size is not None: + callback.set_size(file_size) + + return callback + + +class BaseProgressCallback(Callback): + """Base class for file upload/download progress callbacks. + + Tracks file transfer progress and reports to the Jobs service. + Subclasses implement upload-vs-download behavior. + + Thread Safety: + Uses ``threading.Lock`` to protect stats updates because + FilesetFileSystem transfers files concurrently. + """ + + progress_reporter: ProgressReporter + fileset_name: str + total_files: int + total_size: int + stats: UploadStats | DownloadStats + _lock: threading.Lock + + def __init__( + self, + progress_reporter: ProgressReporter, + fileset_name: str, + total_files: int, + total_size: int, + stats: UploadStats | DownloadStats, + **kwargs: Any, + ): + super().__init__(**kwargs) + self.progress_reporter = progress_reporter + self.fileset_name = str(fileset_name) + self.total_files = total_files + self.total_size = total_size + self.stats = stats + self._lock = threading.Lock() + + @staticmethod + def list_local_files(src_path: Path) -> list[FileInfo]: + """List all files under *src_path*. + + If ``src_path`` is a file, returns a single ``FileInfo``. If it + is a directory, recursively lists all files. Mirrors the shape + returned by ``sdk.files.list``. + """ + if not src_path.exists(): + logger.warning(f"Failed to list local files. Source path does not exist: {src_path}") + return [] + + try: + if src_path.is_file(): + logger.info(f"Found 1 file: {src_path.name}") + return [FileInfo(path=src_path.name, size=src_path.stat().st_size)] + + files = [] + for root, _, filenames in os.walk(src_path): + for filename in filenames: + full_path = Path(root) / filename + relative_path = full_path.relative_to(src_path) + files.append(FileInfo(path=str(relative_path), size=full_path.stat().st_size)) + logger.info(f"Found {len(files)} files in {src_path}") + return files + except Exception as e: + logger.warning(f"Failed to list local files. Source path: {src_path}. Error: {e}") + return [] + + @abstractmethod + def branched(self, source_path: str, dest_path: str, **kwargs: Any) -> "BaseSingleFileCallback": + """Create a child callback for a single file transfer.""" + ... + + +class BaseSingleFileCallback(Callback): + """Base class for per-file callbacks within a batch operation. + + Uses the template-method pattern: ``close()`` runs the shared + state-update + progress-report sequence, while subclasses customize + via ``_get_phase``, ``_get_file_display_path``, ``_update_stats``, + ``_get_files_count``, and ``_build_status_details``. + """ + + parent: BaseProgressCallback + source_path: str + dest_path: str + _completed: bool + + def __init__( + self, + parent: BaseProgressCallback, + source_path: str, + dest_path: str, + **kwargs: Any, + ): + super().__init__(**kwargs) + self.parent = parent + self.source_path = source_path + self.dest_path = dest_path + self._completed = False + + @abstractmethod + def _get_phase(self) -> str: + """Return the TaskPhase for this operation.""" + ... + + @abstractmethod + def _get_file_display_path(self) -> str: + """Return the path to use for display/logging.""" + ... + + @abstractmethod + def _update_stats(self) -> None: + """Update the parent's stats for this operation (called within lock).""" + ... + + @abstractmethod + def _get_files_count(self) -> int: + """Return the current files count from stats (called within lock).""" + ... + + @abstractmethod + def _build_status_details(self, files_count: int, total_bytes: int, current_file: str) -> dict[str, Any]: + """Build the status_details dict for progress reporting.""" + ... + + def close(self) -> None: + """Called when the file transfer completes.""" + if self._completed: + return + + self._completed = True + parent = self.parent + current_file = self._get_file_display_path() + + with parent._lock: + self._update_stats() + files_count = self._get_files_count() + total_bytes = parent.stats.total_bytes + + logger.debug(f"File transferred: {current_file} ({files_count}/{parent.total_files})") + + # Report outside the lock — don't block other threads on the network call. + parent.progress_reporter.update_progress( + status=PlatformJobStatus.ACTIVE, + status_details=self._build_status_details(files_count, total_bytes, current_file), + ) + + def __enter__(self) -> "BaseSingleFileCallback": + return self + + def __exit__(self, *exc_args: object) -> None: + self.close() + + +class FileUploadProgressCallback(BaseProgressCallback): + """Callback for tracking file upload progress and reporting to the Jobs service.""" + + stats: UploadStats + + def __init__( + self, + progress_reporter: ProgressReporter, + src_path: Path, + fileset_name: str, + stats: UploadStats, + **kwargs: Any, + ): + files = self.list_local_files(src_path) + if not files: + logger.warning(f"Source path {src_path} contains no files") + total_files = len(files) + total_size = sum(f.size for f in files) + + super().__init__( + progress_reporter=progress_reporter, + fileset_name=fileset_name, + total_files=total_files, + total_size=total_size, + stats=stats, + **kwargs, + ) + + logger.info(f"Uploading {total_files} files ({total_size} bytes) to {self.fileset_name}") + + progress_reporter.update_progress( + status=PlatformJobStatus.ACTIVE, + status_details={ + "phase": TaskPhase.UPLOADING, + "fileset": self.fileset_name, + "total_files": total_files, + "total_size": total_size, + "uploaded_files": 0, + "uploaded_bytes": 0, + }, + ) + + def branched(self, source_path: str, dest_path: str, **kwargs: Any) -> "SingleFileUploadCallback": + return SingleFileUploadCallback( + parent=self, + source_path=source_path, + dest_path=dest_path, + **kwargs, + ) + + +class SingleFileUploadCallback(BaseSingleFileCallback): + """Per-file upload callback. Notifies parent on completion.""" + + parent: FileUploadProgressCallback + + def _get_phase(self) -> str: + return TaskPhase.UPLOADING + + def _get_file_display_path(self) -> str: + return self.dest_path.split("/")[-1] if "/" in self.dest_path else self.dest_path + + def _update_stats(self) -> None: + self.parent.stats.files_uploaded += 1 + if self.size is not None: + self.parent.stats.total_bytes += self.size + + def _get_files_count(self) -> int: + return self.parent.stats.files_uploaded + + def _build_status_details(self, files_count: int, total_bytes: int, current_file: str) -> dict[str, Any]: + return { + "phase": TaskPhase.UPLOADING, + "fileset": self.parent.fileset_name, + "total_files": self.parent.total_files, + "total_size": self.parent.total_size, + "uploaded_files": files_count, + "uploaded_bytes": total_bytes, + "current_file": current_file, + "progress_pct": get_percentage(files_count, self.parent.total_files), + } + + +class FileDownloadProgressCallback(BaseProgressCallback): + """Callback for tracking file download progress and reporting to the Jobs service.""" + + stats: DownloadStats + + def __init__( + self, + progress_reporter: ProgressReporter, + fileset_name: str, + total_files: int, + total_size: int, + stats: DownloadStats, + **kwargs: Any, + ): + super().__init__( + progress_reporter=progress_reporter, + fileset_name=fileset_name, + total_files=total_files, + total_size=total_size, + stats=stats, + **kwargs, + ) + + logger.info(f"Downloading {total_files} files ({total_size} bytes) from {self.fileset_name}") + + progress_reporter.update_progress( + status=PlatformJobStatus.ACTIVE, + status_details={ + "phase": TaskPhase.DOWNLOADING, + "fileset": self.fileset_name, + "total_files": total_files, + "total_size": total_size, + "downloaded_files": 0, + "downloaded_bytes": 0, + }, + ) + + def branched(self, source_path: str, dest_path: str, **kwargs: Any) -> "SingleFileDownloadCallback": + return SingleFileDownloadCallback( + parent=self, + source_path=source_path, + dest_path=dest_path, + **kwargs, + ) + + +class SingleFileDownloadCallback(BaseSingleFileCallback): + """Per-file download callback. Notifies parent on completion.""" + + parent: FileDownloadProgressCallback + + def _get_phase(self) -> str: + return TaskPhase.DOWNLOADING + + def _get_file_display_path(self) -> str: + return self.source_path.split("/")[-1] if "/" in self.source_path else self.source_path + + def _update_stats(self) -> None: + self.parent.stats.files_downloaded += 1 + if self.size is not None: + self.parent.stats.total_bytes += self.size + + def _get_files_count(self) -> int: + return self.parent.stats.files_downloaded + + def _build_status_details(self, files_count: int, total_bytes: int, current_file: str) -> dict[str, Any]: + return { + "phase": TaskPhase.DOWNLOADING, + "fileset": self.parent.fileset_name, + "total_files": self.parent.total_files, + "total_size": self.parent.total_size, + "downloaded_files": files_count, + "downloaded_bytes": total_bytes, + "current_file": current_file, + "progress_pct": get_percentage(files_count, self.parent.total_files), + } + + +class CompositeCallback(Callback): + """A callback that delegates to multiple child callbacks. + + Lets us combine console-side ``TqdmCallback`` and Jobs-service + ``File{Upload,Download}ProgressCallback`` into one callback object + passed to fsspec operations. + """ + + def __init__(self, *callbacks: Callback, **kwargs: Any): + super().__init__(**kwargs) + self.callbacks = list(callbacks) + + def set_size(self, size: int) -> None: + self.size = size + for cb in self.callbacks: + cb.set_size(size) + + def absolute_update(self, value: int) -> None: + self.value = value + for cb in self.callbacks: + cb.absolute_update(value) + + def relative_update(self, inc: int = 1) -> None: + self.value += inc + for cb in self.callbacks: + cb.relative_update(inc) + + def branched(self, source_path: str, dest_path: str, **kwargs: Any) -> "CompositeCallback": + child_callbacks = [cb.branched(source_path, dest_path, **kwargs) for cb in self.callbacks] + return CompositeCallback(*child_callbacks) + + def call(self, hook_name: str | None = None, **kwargs: Any) -> None: + for cb in self.callbacks: + cb.call(hook_name, **kwargs) + + def close(self) -> None: + for cb in self.callbacks: + cb.close() + + def __enter__(self) -> "CompositeCallback": + for cb in self.callbacks: + cb.__enter__() + return self + + def __exit__(self, *exc_args: object) -> None: + for cb in self.callbacks: + cb.__exit__(*exc_args) diff --git a/services/unsloth/src/nmp/unsloth/tasks/file_io/progress_reporter.py b/services/unsloth/src/nmp/unsloth/tasks/file_io/progress_reporter.py new file mode 100644 index 0000000000..3536b470d3 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/tasks/file_io/progress_reporter.py @@ -0,0 +1,93 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import logging +from typing import Any, Protocol + +from nemo_platform import NeMoPlatform, omit +from nemo_platform._exceptions import APIError +from nmp.common.jobs.schemas import PlatformJobStatus +from nmp.unsloth.app.jobs.context import NMPJobContext +from nmp.unsloth.app.jobs.file_io.schemas import ProgressReportError +from nmp.unsloth.tasks.file_io.utils import sdk_error_handler + +logger = logging.getLogger(__name__) + + +class ProgressReporter(Protocol): + """Interface for reporting task progress.""" + + def update_progress( + self, + status: PlatformJobStatus, + status_details: dict[str, Any] | None = None, + error_details: dict[str, Any] | None = None, + error_stack: str | None = None, + ) -> None: + """Update task progress.""" + ... + + +class NoOpProgressReporter: + """Progress reporter that does nothing. Used when Jobs service is not configured.""" + + def update_progress( + self, + status: PlatformJobStatus, + status_details: dict[str, Any] | None = None, + error_details: dict[str, Any] | None = None, + error_stack: str | None = None, + ) -> None: + """No-op: silently ignore progress updates.""" + + +class JobsServiceProgressReporter: + """Reports progress to the Jobs service via SDK.""" + + def __init__(self, sdk: NeMoPlatform, workspace: str, job_id: str, step_name: str, task_id: str): + self.sdk = sdk + self.workspace = workspace + self.job_id = job_id + self.step_name = step_name + self.task_id = task_id + + def update_progress( + self, + status: PlatformJobStatus, + status_details: dict[str, object] | None = None, + error_details: dict[str, object] | None = None, + error_stack: str | None = None, + ) -> None: + """Update task progress via SDK.""" + try: + with sdk_error_handler( + ProgressReportError, + f"update progress for task: {self.task_id}, job: {self.job_id}, step: {self.step_name}", + passthrough=(APIError,), + ): + self.sdk.jobs.tasks.create_or_update( + self.task_id, + workspace=self.workspace, + job=self.job_id, + step=self.step_name, + status=status.value, + status_details=status_details if status_details else omit, + error_details=error_details if error_details else omit, + error_stack=error_stack if error_stack else omit, + ) + logger.debug(f"Progress updated: {status} - {status_details}") + except Exception as e: + logger.warning( + f"Failed to report progress for task {self.task_id}, job {self.job_id}, step {self.step_name}: {e}", + ) + + @staticmethod + def create_progress_reporter(sdk: NeMoPlatform, job_ctx: NMPJobContext) -> ProgressReporter: + """Build a JobsServiceProgressReporter when jobs_url is set, else NoOpProgressReporter.""" + if job_ctx.jobs_url: + logger.info(f"Progress reporting enabled: {job_ctx.jobs_url}") + return JobsServiceProgressReporter( + sdk, job_ctx.workspace, job_ctx.job_id, job_ctx.step, job_ctx.normalized_task + ) + logger.info("Progress reporting disabled: jobs_url not configured") + return NoOpProgressReporter() diff --git a/services/unsloth/src/nmp/unsloth/tasks/file_io/run.py b/services/unsloth/src/nmp/unsloth/tasks/file_io/run.py new file mode 100644 index 0000000000..052af33194 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/tasks/file_io/run.py @@ -0,0 +1,507 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""File I/O task entry point. + +Handles file operations between NeMo Platform Files Service and the job's shared PVC. + +The task reads configuration and performs: +- Downloads: If config.download is non-empty, download files from FileSets to local paths +- Uploads: If config.upload is non-empty, upload files from local paths to FileSets + +Usage: + export NEMO_JOB_STEP_CONFIG_FILE_PATH= + python -m nmp.unsloth.tasks.file_io +""" + +import logging +from pathlib import Path + +import httpx + +# https://docs.nvidia.com/nemo/microservices/latest/pysdk/index.html#handling-errors +from nemo_platform import ( + APIConnectionError, + APITimeoutError, + ConflictError, + InternalServerError, + NeMoPlatform, + NotFoundError, +) +from nemo_platform.types.files.fileset_file import FilesetFile +from nmp.common.jobs.schemas import PlatformJobStatus +from nmp.common.sdk_factory import get_task_sdk +from nmp.unsloth.app.constants import SERVICE_NAME +from nmp.unsloth.app.jobs.context import NMPJobContext +from nmp.unsloth.app.jobs.file_io.schemas import ( + DownloadItem, + DownloadStats, + FileDownloadError, + FileSetRef, + FileUploadError, + PathTraversalError, + TaskPhase, + UploadItem, + UploadStats, +) +from nmp.unsloth.tasks.file_io.callbacks import ( + CompositeCallback, + FileDownloadProgressCallback, + FileUploadProgressCallback, + TqdmPerFileDownloadCallback, + TqdmPerFileUploadCallback, +) +from nmp.unsloth.tasks.file_io.progress_reporter import JobsServiceProgressReporter, ProgressReporter +from nmp.unsloth.tasks.file_io.utils import ( + filesystem_sdk_error_handler, + get_config, + sdk_error_handler, + validate_safe_path, + validate_storage_path, +) +from tenacity import before_sleep_log, retry, retry_if_exception_type, stop_after_attempt, wait_exponential + +logger = logging.getLogger(__name__) + +# Service-source tag stamped onto every upload-created fileset. Lets operators +# filter filesets by training backend. +SERVICE_SOURCE = "unsloth" + +# Timeout configurations for SDK operations (httpx.Timeout for API calls). +CREATE_FILESET_TIMEOUT = httpx.Timeout(10.0, connect=10.0) +LIST_FILES_TIMEOUT = httpx.Timeout(10.0, connect=10.0) + +# Timeout configurations for FilesetFileSystem operations. Passed via +# sdk.with_options(timeout=...). httpx.Timeout(read=...) is per-chunk +# (the SDK chunks at 16MB), NOT total transfer time — it's a socket-level +# timeout. SDK defaults are httpx.Timeout(timeout=60, connect=5.0). +DOWNLOAD_TIMEOUT = httpx.Timeout(30.0, read=5 * 60) +UPLOAD_TIMEOUT = httpx.Timeout(30.0, write=10 * 60, read=5 * 60) + +# Retry configuration. +MAX_RETRIES = 3 +INITIAL_BACKOFF_SECONDS = 1.0 +MAX_BACKOFF_SECONDS = 30.0 + +# Transient exceptions that should trigger retries for filesystem operations. +# FilesetFileSystem uses httpx under the hood, so we retry httpx transients +# in addition to SDK-level transients. +TRANSIENT_FILESYSTEM_EXCEPTIONS = ( + httpx.TimeoutException, + httpx.ConnectError, + httpx.ReadTimeout, +) + + +class FileIORunner: + """Runner for file I/O operations against the Files service.""" + + def __init__( + self, + sdk: NeMoPlatform, + progress_reporter: ProgressReporter, + job_ctx: NMPJobContext, + ): + self.sdk = sdk + self.progress_reporter = progress_reporter + self.job_ctx = job_ctx + + def list_fileset_files(self, fileset: FileSetRef) -> list[FilesetFile]: + """List files in a FileSet. Returns a list of ``FilesetFile`` objects.""" + try: + with sdk_error_handler(FileDownloadError, f"list files in fileset {fileset}", passthrough=(NotFoundError,)): + response = self.sdk.with_options(timeout=LIST_FILES_TIMEOUT).files.list( + fileset=fileset.name, + workspace=fileset.workspace, + ) + logger.info(f"Found {len(response.data)} files in FileSet {fileset!s}") + return response.data + except NotFoundError as e: + raise FileDownloadError( + f"FileSet {fileset!s} not found. Please ensure the FileSet exists and contains the expected files.", + ) from e + + def download_fileset(self, fileset: FileSetRef, dest_dir: Path) -> DownloadStats: + """Download all files from a FileSet to a destination directory. + + Uses ``FilesetFileSystem.get()`` with ``recursive=True`` for efficient batch + downloads. Progress is tracked via two callbacks combined in a + ``CompositeCallback``: + + - ``TqdmPerFileDownloadCallback`` — separate console progress bar per file + - ``FileDownloadProgressCallback`` — reports to Jobs service after each file + + Raises: + FileDownloadError: If the download fails. + """ + stats = DownloadStats() + fileset_name = str(fileset) + + files = self.list_fileset_files(fileset) + + if not files: + logger.warning(f"FileSet {fileset_name} contains no files") + return stats + + total_files = len(files) + total_size = sum(f.size for f in files) + + dest_dir.mkdir(parents=True, exist_ok=True) + + # Maps relative file paths to byte sizes for tqdm percent display. + file_sizes = {f.path.lstrip("/"): f.size for f in files} + + tqdm_callback = TqdmPerFileDownloadCallback( + dest_path=dest_dir, + fileset_path=fileset_name, + file_sizes=file_sizes, + ) + jobs_callback = FileDownloadProgressCallback( + progress_reporter=self.progress_reporter, + fileset_name=fileset_name, + total_files=total_files, + total_size=total_size, + stats=stats, + ) + composite_callback = CompositeCallback(tqdm_callback, jobs_callback) + + with filesystem_sdk_error_handler( + FileDownloadError, + f"download from '{fileset_name}' to '{dest_dir}'", + ): + self._download_with_retry( + fileset_name=fileset.name, + fileset_workspace=fileset.workspace, + dest_dir=str(dest_dir), + callback=composite_callback, + ) + + logger.info(f"Download complete: {stats.files_downloaded} files, {stats.total_bytes} bytes") + return stats + + @retry( + stop=stop_after_attempt(MAX_RETRIES), + wait=wait_exponential(multiplier=2, min=INITIAL_BACKOFF_SECONDS, max=MAX_BACKOFF_SECONDS), + retry=retry_if_exception_type(TRANSIENT_FILESYSTEM_EXCEPTIONS), + reraise=True, + before_sleep=before_sleep_log(logger, logging.WARNING), + ) + def _download_with_retry( + self, + fileset_name: str, + fileset_workspace: str | None, + dest_dir: str, + callback: CompositeCallback, + ) -> None: + """Internal method with retry logic for downloading from FilesetFileSystem.""" + self.sdk.with_options(timeout=DOWNLOAD_TIMEOUT).files.download( + fileset=fileset_name, + workspace=fileset_workspace, + local_path=dest_dir, + callback=callback, # type: ignore[arg-type] + ) + + def upload_fileset(self, fileset: FileSetRef, src_path: Path) -> UploadStats: + """Upload all files from a source path (file or directory) to a FileSet. + + Uses ``FilesetFileSystem.put()`` with ``recursive=True`` for efficient batch + uploads. Progress is tracked via the same composite-callback pattern as + downloads. + + Raises: + FileUploadError: If the upload fails. + """ + stats = UploadStats() + fileset_name = str(fileset) + + tqdm_callback = TqdmPerFileUploadCallback(src_path=src_path) + jobs_callback = FileUploadProgressCallback( + progress_reporter=self.progress_reporter, + src_path=src_path, + fileset_name=fileset_name, + stats=stats, + ) + composite_callback = CompositeCallback(tqdm_callback, jobs_callback) + + # Build local and remote paths for upload. ``remote_path`` is relative within + # the fileset ("" for root, "filename" for single file). Trailing slash on + # ``local_path`` follows rsync/scp convention: "dir/" copies contents, + # "dir" copies the directory itself. + if src_path.is_dir(): + local_path = f"{src_path}/" + remote_path = "" + else: + local_path = str(src_path) + remote_path = src_path.name + + with filesystem_sdk_error_handler( + FileUploadError, + f"upload from '{src_path}' to '{fileset_name}'", + ): + self._upload_with_retry( + local_path=local_path, + remote_path=remote_path, + fileset_name=fileset.name, + fileset_workspace=fileset.workspace, + callback=composite_callback, + ) + + logger.info(f"Upload complete: {stats.files_uploaded} files, {stats.total_bytes} bytes") + return stats + + @retry( + stop=stop_after_attempt(MAX_RETRIES), + wait=wait_exponential(multiplier=2, min=INITIAL_BACKOFF_SECONDS, max=MAX_BACKOFF_SECONDS), + retry=retry_if_exception_type(TRANSIENT_FILESYSTEM_EXCEPTIONS), + reraise=True, + before_sleep=before_sleep_log(logger, logging.WARNING, exc_info=True), + ) + def _upload_with_retry( + self, + local_path: str, + remote_path: str, + fileset_name: str, + fileset_workspace: str | None, + callback: CompositeCallback, + ) -> None: + """Internal method with retry logic for uploading to FilesetFileSystem.""" + self.sdk.with_options(timeout=UPLOAD_TIMEOUT).files.upload( + local_path=local_path, + remote_path=remote_path, + fileset=fileset_name, + workspace=fileset_workspace, + callback=callback, # type: ignore[arg-type] + ) + + def create_fileset(self, fileset: FileSetRef, metadata: dict | None = None) -> None: + """Create a FileSet. Skip if it already exists. + + Wraps the retry with ``sdk_error_handler`` to convert exceptions after + all retries exhaust. + """ + with sdk_error_handler(FileUploadError, f"create fileset {fileset}", passthrough=(ConflictError,)): + self._create_fileset_with_retry(fileset, metadata) + + # We don't use the SDK's built-in retry: it would retry on ConflictError, + # which is expected here and would just waste calls. + @retry( + stop=stop_after_attempt(MAX_RETRIES), + wait=wait_exponential(multiplier=2, min=INITIAL_BACKOFF_SECONDS, max=MAX_BACKOFF_SECONDS), + retry=retry_if_exception_type((InternalServerError, APITimeoutError, APIConnectionError)), + reraise=True, + ) + def _create_fileset_with_retry(self, fileset: FileSetRef, metadata: dict | None = None) -> None: + """Internal method with retry logic for creating a FileSet.""" + try: + create_kwargs: dict = { + "workspace": fileset.workspace, + "name": fileset.name, + "timeout": CREATE_FILESET_TIMEOUT, + "custom_fields": {"service_source": SERVICE_SOURCE}, + } + if metadata is not None: + create_kwargs["metadata"] = metadata + result = self.sdk.with_options(max_retries=0).files.filesets.create(**create_kwargs) + logger.info(f"Created FileSet: {result.workspace}/{result.name}") + except ConflictError: + # Fileset already exists — patch metadata so tool_calling etc. aren't lost. + workspace = fileset.workspace or self.job_ctx.workspace + if metadata is not None: + update_kwargs: dict = { + "name": fileset.name, + "workspace": workspace, + "metadata": metadata, + "timeout": CREATE_FILESET_TIMEOUT, + } + try: + self.sdk.with_options(max_retries=0).files.filesets.update(**update_kwargs) + logger.info(f"Patched existing FileSet metadata: {workspace}/{fileset.name}") + except Exception as e: + logger.warning( + f"Could not patch metadata on existing fileset {workspace}/{fileset.name}: {e}. " + "Upload will continue; downstream consumers may lack the latest metadata.", + ) + + def run_download(self, downloads: list[DownloadItem]) -> None: + """Execute download operations.""" + if not downloads: + logger.info("No downloads configured, skipping download operation") + return + + storage_path = validate_storage_path(self.job_ctx.storage_path) + + logger.info(f"Starting download operation: {len(downloads)} fileset(s) to download") + + self.progress_reporter.update_progress( + status=PlatformJobStatus.ACTIVE, + status_details={ + "phase": TaskPhase.DOWNLOADING, + "total_filesets": len(downloads), + "completed_filesets": 0, + }, + ) + + total_stats = DownloadStats() + + for idx, item in enumerate(downloads): + fileset = item.src + dest_dir = validate_safe_path(storage_path, item.dest) + + logger.info(f"[{idx + 1}/{len(downloads)}] Downloading from {fileset!s} to {dest_dir}") + + self.progress_reporter.update_progress( + status=PlatformJobStatus.ACTIVE, + status_details={ + "phase": TaskPhase.DOWNLOADING, + "total_filesets": len(downloads), + "completed_filesets": idx, + "current_fileset": f"{fileset!s}", + }, + ) + + stats = self.download_fileset(fileset, dest_dir) + total_stats.files_downloaded += stats.files_downloaded + total_stats.total_bytes += stats.total_bytes + + logger.info(f"FileSet download complete: {stats.files_downloaded} files, {stats.total_bytes} bytes") + + logger.info( + f"All downloads complete: {total_stats.files_downloaded} files, {total_stats.total_bytes} bytes total", + ) + + def run_upload(self, uploads: list[UploadItem]) -> None: + """Execute upload operations.""" + if not uploads: + logger.info("No uploads configured, skipping upload operation") + return + + storage_path = validate_storage_path(self.job_ctx.storage_path) + + logger.info(f"Starting upload operation: {len(uploads)} fileset(s) to upload") + + self.progress_reporter.update_progress( + status=PlatformJobStatus.ACTIVE, + status_details={ + "phase": TaskPhase.UPLOADING, + "total_filesets": len(uploads), + "completed_filesets": 0, + }, + ) + + total_stats = UploadStats() + + for idx, item in enumerate(uploads): + if item.dest.workspace is None: + item.dest.workspace = self.job_ctx.workspace + fileset = item.dest + src_path = validate_safe_path(storage_path, item.src) + if not src_path.exists(): + raise FileUploadError(f"Source path does not exist: {src_path}. Ensure the source path exists.") + if not src_path.is_dir() and not src_path.is_file(): + raise FileUploadError( + f"Source path is not a file or directory: {src_path}. " + "Ensure the source path is a file or directory.", + ) + + logger.info(f"[{idx + 1}/{len(uploads)}] Uploading from {src_path} to {fileset!s}") + + self.progress_reporter.update_progress( + status=PlatformJobStatus.ACTIVE, + status_details={ + "phase": TaskPhase.UPLOADING, + "total_filesets": len(uploads), + "completed_filesets": idx, + "current_fileset": str(fileset), + }, + ) + + self.create_fileset(fileset, metadata=item.metadata) + + stats = self.upload_fileset(fileset, src_path) + total_stats.files_uploaded += stats.files_uploaded + total_stats.total_bytes += stats.total_bytes + + logger.info(f"FileSet upload complete: {stats.files_uploaded} files, {stats.total_bytes} bytes") + + logger.info(f"All uploads complete: {total_stats.files_uploaded} files, {total_stats.total_bytes} bytes total") + + +def run(sdk: NeMoPlatform | None = None, job_ctx: NMPJobContext | None = None) -> int: + """Execute the file I/O task. + + Args: + sdk: Optional SDK instance for dependency injection (for testing). + If None, creates one via get_task_sdk(). + job_ctx: Optional job context for dependency injection (for testing). + If None, creates one via NMPJobContext.from_env(). + + Returns: + Exit code (0 for success, non-zero for failure). + """ + job_ctx = job_ctx or NMPJobContext.from_env() + validate_storage_path(job_ctx.storage_path) + + sdk_owned = sdk is None + progress_reporter: ProgressReporter | None = None + try: + sdk = sdk or get_task_sdk(SERVICE_NAME) + progress_reporter = JobsServiceProgressReporter.create_progress_reporter(sdk, job_ctx) + runner = FileIORunner(sdk=sdk, progress_reporter=progress_reporter, job_ctx=job_ctx) + + config = get_config(job_ctx.config_path) + + logger.info(f"Starting file I/O task with job context: {job_ctx}") + logger.info(f"Config: {config.model_dump_json(indent=2)}") + logger.info(f"NeMo Platform service URL: {sdk.base_url}") + + runner.run_upload(config.upload) + runner.run_download(config.download) + + progress_reporter.update_progress( + status=PlatformJobStatus.COMPLETED, + status_details={"phase": TaskPhase.COMPLETED, "message": "File I/O task completed successfully"}, + ) + + return 0 + except PathTraversalError as e: + logger.error(f"Security error - path traversal detected: {e}") + if progress_reporter: + progress_reporter.update_progress( + status=PlatformJobStatus.ERROR, + error_details={"message": str(e), "type": type(e).__name__}, + ) + return 1 + except (FileDownloadError, FileUploadError) as e: + logger.exception(f"File operation failed: {e}") + if progress_reporter: + progress_reporter.update_progress( + status=PlatformJobStatus.ERROR, + error_details={"message": str(e), "type": type(e).__name__}, + ) + return 1 + except Exception as e: + logger.exception(f"File I/O task failed: {e}") + if progress_reporter: + progress_reporter.update_progress( + status=PlatformJobStatus.ERROR, + error_details={"message": str(e), "type": type(e).__name__}, + ) + return 1 + finally: + if sdk_owned and sdk is not None: + sdk.close() + + +def build_output_metadata(spec) -> dict: + """Build the metadata dict stamped onto the output fileset. + + Captures the bits a downstream consumer (model-entity creation, + deployment) needs about this artefact without re-deriving them + from the training spec. + """ + return { + "model": spec.model.name, + "finetuning_type": spec.training.finetuning_type, + "save_method": spec.output.save_method, + "output_type": spec.output.type, + } diff --git a/services/unsloth/src/nmp/unsloth/tasks/file_io/utils.py b/services/unsloth/src/nmp/unsloth/tasks/file_io/utils.py new file mode 100644 index 0000000000..9bd25d4b36 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/tasks/file_io/utils.py @@ -0,0 +1,125 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import json +import logging +from collections.abc import Iterator +from contextlib import contextmanager +from pathlib import Path + +import httpx + +# https://docs.nvidia.com/nemo/microservices/latest/pysdk/index.html#handling-errors +from nemo_platform import ( + APIConnectionError, + APIStatusError, + APITimeoutError, + AuthenticationError, + PermissionDeniedError, +) +from nmp.unsloth.app.jobs.file_io.schemas import ( + FileDownloadError, + FileIOTaskConfig, + FileUploadError, + PathTraversalError, + ProgressReportError, +) + +logger = logging.getLogger(__name__) + + +@contextmanager +def filesystem_sdk_error_handler( + error_class: type[FileDownloadError | FileUploadError | ProgressReportError], + operation: str, + passthrough: tuple[type[BaseException], ...] = (), +) -> Iterator[None]: + """Context manager for consistent FilesetFileSystem error handling. + + Catches FilesetFileSystem-specific exceptions and re-raises them as the + specified error class with a consistent message format. + """ + try: + yield + except passthrough: + raise + except FileNotFoundError as e: + raise error_class(f"Failed to {operation} due to file not found error. Error: {e}") from e + except PermissionError as e: + raise error_class(f"Failed to {operation} due to permission denied error. Error: {e}") from e + except httpx.TimeoutException as e: + raise error_class(f"Failed to {operation} due to request timeout. Error: {e}") from e + except httpx.ConnectError as e: + raise error_class(f"Failed to {operation} due to connection error. Error: {e}") from e + except Exception as e: + raise error_class(f"Failed to {operation} due to unexpected error {type(e).__name__}: {e}") from e + + +@contextmanager +def sdk_error_handler( + error_class: type[FileDownloadError | FileUploadError | ProgressReportError], + operation: str, + passthrough: tuple[type[BaseException], ...] = (), +) -> Iterator[None]: + """Context manager for consistent SDK error handling. + + Catches SDK-specific exceptions and re-raises them as the specified error + class with a consistent message format. + """ + try: + yield + except passthrough: + raise + except APITimeoutError as e: + raise error_class( + f"Failed to {operation} due to request timeout error. Cause: {e.__cause__}. Error: {e}", + ) from e + except APIConnectionError as e: + raise error_class(f"Failed to {operation} due to connection error. Cause: {e.__cause__}. Error: {e}") from e + # AuthenticationError / PermissionDeniedError are subclasses of APIStatusError, + # so they must be caught before APIStatusError. + except AuthenticationError as e: + raise error_class(f"Failed to {operation} due to authentication error. Error: {e}") from e + except PermissionDeniedError as e: + raise error_class(f"Failed to {operation} due to permission denied error. Error: {e}") from e + except APIStatusError as e: + raise error_class(f"Failed to {operation} due to API error. Status code: {e.status_code}. Error: {e}") from e + except Exception as e: + raise error_class(f"Failed to {operation} due to unexpected error {type(e).__name__}: {e}") from e + + +def get_config(config_path: Path) -> FileIOTaskConfig: + """Load and validate the file_io step config from disk.""" + with open(config_path) as f: + return FileIOTaskConfig.model_validate(json.load(f)) + + +def validate_storage_path(storage_path: Path) -> Path: + """Validate that a storage path exists and is a directory.""" + if not storage_path.exists() or not storage_path.is_dir(): + raise FileUploadError( + f"Storage path does not exist: {storage_path}. Ensure the storage path exists and is a directory.", + ) + return storage_path + + +def validate_safe_path(base_path: Path, user_path: str) -> Path: + """Validate that a user-provided path stays within the base directory. + + Resolves both paths to canonical absolute form and verifies the result + is under the base path. Prevents path traversal via ``..`` etc. + + Raises: + PathTraversalError: If the resolved path would escape base_path. + """ + resolved_base = base_path.resolve() + resolved_path = (base_path / user_path).resolve() + + if not resolved_path.is_relative_to(resolved_base): + raise PathTraversalError( + f"Path '{user_path}' resolves outside of the base directory. " + "This may indicate a path traversal attack. " + "Ensure that paths such as ../.. are not used in the download destination path.", + ) + + return resolved_path diff --git a/services/unsloth/src/nmp/unsloth/tasks/model_entity/__init__.py b/services/unsloth/src/nmp/unsloth/tasks/model_entity/__init__.py new file mode 100644 index 0000000000..eae78497cc --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/tasks/model_entity/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Model entity task for creating model entities after unsloth customization.""" + +from nmp.unsloth.tasks.model_entity.run import run + +__all__ = ["run"] diff --git a/services/unsloth/src/nmp/unsloth/tasks/model_entity/__main__.py b/services/unsloth/src/nmp/unsloth/tasks/model_entity/__main__.py new file mode 100644 index 0000000000..eb393313c9 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/tasks/model_entity/__main__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Entry point for model_entity task. + +Usage: + python -m nmp.unsloth.tasks.model_entity +""" + +import sys + +from .run import run + +if __name__ == "__main__": + sys.exit(run()) diff --git a/services/unsloth/src/nmp/unsloth/tasks/model_entity/run.py b/services/unsloth/src/nmp/unsloth/tasks/model_entity/run.py new file mode 100644 index 0000000000..603549287a --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/tasks/model_entity/run.py @@ -0,0 +1,437 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Model entity task entry point. + +Handles creating model entities in the Models service after customization completes. + +The task reads configuration and creates a Model Entity that references the +uploaded model artifacts in the Files service. When ``deployment_config`` is set +on the task config, the task also launches an inference deployment. + +Usage: + export NEMO_JOB_STEP_CONFIG_FILE_PATH= + python -m nmp.unsloth.tasks.model_entity +""" + +import json +import logging +import re +import time +from pathlib import Path + +from nemo_platform import ( + APIConnectionError, + APITimeoutError, + ConflictError, + InternalServerError, + NeMoPlatform, + NotFoundError, +) +from nemo_platform.types.inference import ( + ModelDeploymentConfig, + ModelDeploymentConfigFilterParam, + ModelDeploymentFilterParam, + NIMDeploymentParam, +) +from nemo_platform.types.models import LoraParam, ModelEntity +from nemo_platform.types.shared_params.tool_call_config import ToolCallConfig as ToolCallConfigParam +from nmp.common.sdk_factory import get_task_sdk +from nmp.unsloth.app.constants import SERVICE_NAME +from nmp.unsloth.app.jobs.context import NMPJobContext +from nmp.unsloth.app.jobs.model_entity.schemas import ( + DeploymentParameters, + ModelEntityCreationError, + ModelEntityTaskConfig, +) +from nmp.unsloth.entities.values import FinetuningType +from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential + +logger = logging.getLogger(__name__) + +# Retry configuration. +MAX_RETRIES = 3 +INITIAL_BACKOFF_SECONDS = 1.0 +MAX_BACKOFF_SECONDS = 30.0 + +ACTIVE_DEPLOYMENT_STATUSES = frozenset({"CREATED", "PENDING", "READY"}) + +SPEC_POLL_INTERVAL_SECONDS = 10 +SPEC_POLL_TIMEOUT_SECONDS = 600 + + +def get_config(config_path: Path) -> ModelEntityTaskConfig: + """Load and validate the model_entity step config from disk.""" + with open(config_path) as f: + return ModelEntityTaskConfig.model_validate(json.load(f)) + + +def sanitize_name(prefix: str, name: str) -> str: + """Build a deployment-safe name from a free-form model name. + + Must match the API's ``{'pattern': '^[a-z](?!.*--)[a-z0-9\\-@.+_]{1,62}(? ModelEntity: + """Poll until the model_spec task has populated the model's spec. + + The spec must be populated before creating a deployment because the + inference service relies on ``spec.family`` and ``spec.base_num_parameters`` + to select the correct NIM profile. + + Raises: + ModelEntityCreationError: If the spec is not populated within the timeout. + """ + logger.info(f"Waiting for model_spec to populate spec on {workspace}/{name}") + start = time.monotonic() + + while time.monotonic() - start < SPEC_POLL_TIMEOUT_SECONDS: + try: + target = self.sdk.models.retrieve(name=name, workspace=workspace) + if target.spec: + logger.info(f"Spec populated on {workspace}/{name}") + return target + except (APIConnectionError, APITimeoutError, InternalServerError) as e: + logger.warning(f"Transient error polling spec for {workspace}/{name}: {e}") + time.sleep(SPEC_POLL_INTERVAL_SECONDS) + + raise ModelEntityCreationError( + f"Timed out waiting for model spec on {workspace}/{name} " + f"after {SPEC_POLL_TIMEOUT_SECONDS}s. The platform could not auto-detect the " + f"model's specifications. Verify the model checkpoint is valid and in a supported format." + ) + + def get_model_entity(self, model_entity: str, fileset_workspace: str) -> ModelEntity: + """Resolve ``"workspace/name"`` (or bare ``"name"``) to a ``ModelEntity``.""" + parts = model_entity.split("/") + if len(parts) == 1: + me_workspace, me_name = fileset_workspace, parts[0] + else: + me_workspace, me_name = parts[0], parts[1] + + try: + me: ModelEntity = self.sdk.models.retrieve(name=me_name, workspace=me_workspace) + except NotFoundError as e: + raise ModelEntityCreationError(f"Model entity {me_workspace}/{me_name} not found") from e + + return me + + @retry( + stop=stop_after_attempt(MAX_RETRIES), + wait=wait_exponential(multiplier=2, min=INITIAL_BACKOFF_SECONDS, max=MAX_BACKOFF_SECONDS), + retry=retry_if_exception_type((InternalServerError, APITimeoutError, APIConnectionError)), + reraise=True, + ) + def create_model_entity(self, config: ModelEntityTaskConfig) -> tuple[dict, ModelEntity]: + """Create a model entity in the Models service. + + Returns: + Tuple of (result dict, deploy target). For LoRA the deploy target is the + *base* model entity; for SFT it is the newly created output model entity. + + Raises: + ModelEntityCreationError: If creation fails. + """ + workspace = self.job_ctx.workspace + logger.info(f"Creating model entity: {workspace}/{config.name}") + + fileset_workspace = config.fileset.workspace or workspace + fileset_ref = f"{fileset_workspace}/{config.fileset.name}" + + logger.info(f"Validating fileset exists: {fileset_workspace}/{config.fileset.name}") + try: + self.sdk.files.filesets.retrieve(workspace=fileset_workspace, name=config.fileset.name) + logger.info(f"Fileset validation successful: {fileset_workspace}/{config.fileset.name}") + except Exception as e: + logger.error(f"Fileset validation failed: {fileset_workspace}/{config.fileset.name}") + raise ModelEntityCreationError( + f"Cannot create model entity: fileset '{fileset_workspace}/{config.fileset.name}' " + "does not exist or is not accessible" + ) from e + + base_me: ModelEntity = self.get_model_entity(config.model_entity, fileset_workspace) + + if config.peft is not None and config.peft.type == FinetuningType.LORA: + return self._create_or_update_adapter(config, base_me, fileset_ref) + return self._create_or_update_full_entity(config, base_me, fileset_ref, workspace) + + def _create_or_update_adapter( + self, + config: ModelEntityTaskConfig, + base_me: ModelEntity, + fileset_ref: str, + ) -> tuple[dict, ModelEntity]: + """Create or update a LoRA adapter on ``base_me``. Returns (result, base_me).""" + assert config.peft is not None # type narrowing — caller already checked + try: + output_me = self.sdk.models.adapters.create( + model_name=base_me.name, + workspace=base_me.workspace, + name=config.name, + description=config.description, + fileset=fileset_ref, + finetuning_type=config.peft.type.value, + lora_config=LoraParam( + alpha=config.peft.alpha, + rank=config.peft.rank, + ), + enabled=True, + ) + return output_me.model_dump(), base_me + except ConflictError: + logger.warning( + f"Adapter {base_me.workspace}/{config.name} already exists for model " + f"{base_me.workspace}/{base_me.name}, updating with new fileset" + ) + try: + output_me = self.sdk.models.adapters.update( + adapter=config.name, + model_name=base_me.name, + workspace=base_me.workspace, + fileset=fileset_ref, + description=config.description, + enabled=True, + ) + logger.info( + f"Successfully updated adapter: {base_me.workspace}/{config.name} " + f"for base model {base_me.workspace}/{base_me.name}" + ) + return output_me.model_dump(), base_me + except (InternalServerError, APITimeoutError, APIConnectionError): + raise + except Exception as update_error: + logger.exception( + f"Failed to update existing adapter, {base_me.workspace}/{config.name}: {update_error}" + ) + raise ModelEntityCreationError( + f"Adapter '{config.name}' already exists but update failed: {update_error}" + ) from update_error + except Exception as e: + logger.exception(f"Failed to create model adapter: {e}") + raise ModelEntityCreationError(f"Failed to create model adapter: {e}") from e + + def _create_or_update_full_entity( + self, + config: ModelEntityTaskConfig, + base_me: ModelEntity, + fileset_ref: str, + workspace: str, + ) -> tuple[dict, ModelEntity]: + """Create or update a full / merged model entity. Returns (result, output_me).""" + ft_type = config.peft.type.value if config.peft else FinetuningType.ALL_WEIGHTS.value + + request_body: dict = { + "name": config.name, + "description": config.description, + "fileset": fileset_ref, + "finetuning_type": ft_type, + "trust_remote_code": base_me.trust_remote_code, + } + if config.base_model: + request_body["base_model"] = config.base_model + + try: + output_me = self.sdk.models.create(workspace=workspace, **request_body) + logger.info(f"Successfully created model entity: {output_me.workspace}/{output_me.name}") + return output_me.model_dump(), output_me + except ConflictError: + logger.warning(f"Model entity already exists: {workspace}/{config.name}, updating existing model") + try: + update_body = {k: v for k, v in request_body.items() if k != "name"} + output_me = self.sdk.models.update( + name=config.name, + workspace=workspace, + **update_body, + ) + logger.info(f"Successfully updated model entity: {output_me.workspace}/{output_me.name}") + return output_me.model_dump(), output_me + except (InternalServerError, APITimeoutError, APIConnectionError): + raise + except Exception as update_error: + logger.exception(f"Failed to update existing model entity: {update_error}") + raise ModelEntityCreationError( + f"Model entity '{config.name}' already exists and update failed: {update_error}" + ) from update_error + except Exception as e: + logger.exception(f"Failed to create model entity: {e}") + raise ModelEntityCreationError(f"Failed to create model entity: {e}") from e + + def launch_model(self, config: ModelEntityTaskConfig, me: ModelEntity) -> None: + """Deploy a model entity after creation. + + For LoRA jobs, ``me`` should be the base model entity. + For SFT jobs, ``me`` should be the output model entity. + """ + dc = config.deployment_config + if dc is None: + return + + # LORA_MERGED produces a full-weight model, so it's deployed like SFT and + # is intentionally excluded from the LoRA-only checks below. + is_lora = config.peft is not None and config.peft.type == FinetuningType.LORA + if is_lora and self._has_active_deployment(me): + return + + if is_lora and isinstance(dc, DeploymentParameters) and not dc.lora_enabled: + logger.warning(f"Deployment requested but lora_enabled is false for a LoRA job: {dc}") + return + + if isinstance(dc, str): + logger.info(f"Resolving deployment config reference: {dc}") + deployment_config = self._resolve_config_ref(dc, me.workspace) + logger.info(f"Using deployment config: {deployment_config.workspace}/{deployment_config.name}") + else: + deployment_config = self._create_deployment_config(dc, me) + + self._create_deployment(deployment_config, me) + + def _has_active_deployment(self, me: ModelEntity) -> bool: + """Check if the model entity already has an active deployment.""" + deployment_configs = self.sdk.inference.deployment_configs.list( + workspace=me.workspace, + filter=ModelDeploymentConfigFilterParam(model_entity_id=f"{me.workspace}/{me.name}"), + ).data + + for c in deployment_configs: + deployments = self.sdk.inference.deployments.list( + filter=ModelDeploymentFilterParam(config=c.name, workspace=me.workspace) + ).data + for d in deployments: + if d.status in ACTIVE_DEPLOYMENT_STATUSES: + logger.info(f"Active deployment (status={d.status}) exists for config {c.name}, skipping") + return True + + return False + + def _resolve_config_ref(self, config_ref: str, me_workspace: str) -> ModelDeploymentConfig: + """Resolve a ``name`` or ``workspace/name`` reference to a ``ModelDeploymentConfig``.""" + parts = config_ref.split("/") + if len(parts) == 2: + workspace, name = parts[0], parts[1] + elif len(parts) == 1: + workspace, name = me_workspace, parts[0] + else: + raise ModelEntityCreationError( + f"Invalid deployment config reference '{config_ref}': expected 'name' or 'workspace/name'" + ) + + try: + return self.sdk.inference.deployment_configs.retrieve(workspace=workspace, name=name) + except Exception as e: + raise ModelEntityCreationError( + f"Failed to resolve deployment config '{config_ref}' in workspace '{workspace}': {e}" + ) from e + + def _create_deployment_config(self, deploy_params: DeploymentParameters, me: ModelEntity) -> ModelDeploymentConfig: + """Create (or update) a ``ModelDeploymentConfig`` from inline parameters.""" + nim_deployment = NIMDeploymentParam( + image_name=deploy_params.image_name, + image_tag=deploy_params.image_tag, + gpu=deploy_params.gpu, + model_name=me.name, + model_namespace=me.workspace, + additional_envs=deploy_params.additional_envs, + lora_enabled=deploy_params.lora_enabled, + ) + + if deploy_params.tool_call_config: + nim_deployment["tool_call_config"] = ToolCallConfigParam( + **deploy_params.tool_call_config.model_dump(exclude_none=True) + ) + + deployment_cfg_name = sanitize_name("sft-cfg", me.name) + try: + return self.sdk.inference.deployment_configs.create( + workspace=me.workspace, + name=deployment_cfg_name, + nim_deployment=nim_deployment, + ) + except ConflictError: + logger.info(f"Deployment config {me.workspace}/{deployment_cfg_name} already exists, updating") + return self.sdk.inference.deployment_configs.update( + workspace=me.workspace, + name=deployment_cfg_name, + nim_deployment=nim_deployment, + ) + + def _create_deployment(self, deployment_config: ModelDeploymentConfig, me: ModelEntity) -> None: + """Create a deployment from the given ``ModelDeploymentConfig``.""" + logger.info(f"Deployment config: {deployment_config}") + + if not me.spec: + _ = self._wait_for_spec(me.workspace, me.name) + + deployment_name = sanitize_name("sft-deploy", me.name) + try: + deployment = self.sdk.inference.deployments.create( + workspace=deployment_config.workspace, + name=deployment_name, + config=deployment_config.name, + ) + logger.info(f"Deployment created: {deployment}") + except ConflictError: + logger.info(f"Deployment {deployment_config.workspace}/{deployment_name} already exists") + deployment = self.sdk.inference.deployments.retrieve( + workspace=deployment_config.workspace, + name=deployment_name, + ) + + deployment_status = self.sdk.inference.deployments.retrieve( + workspace=deployment.workspace, + name=deployment.name, + ) + logger.info(f"Deployment status: {deployment_status}") + + +def run(sdk: NeMoPlatform | None = None, job_ctx: NMPJobContext | None = None) -> int: + """Execute the model entity creation task. + + Args: + sdk: Optional SDK instance for dependency injection (for testing). + If None, creates one via get_task_sdk(). + job_ctx: Optional job context for dependency injection (for testing). + If None, creates one via NMPJobContext.from_env(). + + Returns: + Exit code (0 for success, non-zero for failure). + """ + job_ctx = job_ctx or NMPJobContext.from_env() + + sdk_owned = sdk is None + try: + sdk = sdk or get_task_sdk(SERVICE_NAME).with_options(workspace=job_ctx.workspace) + runner = ModelEntityRunner(sdk=sdk, job_ctx=job_ctx) + + config = get_config(job_ctx.config_path) + + logger.info(f"Starting model entity task with job context: {job_ctx}") + logger.info(f"Config: {config.model_dump_json(indent=2)}") + logger.info(f"NeMo Platform service URL: {sdk.base_url}") + + result, deploy_target = runner.create_model_entity(config) + logger.info(f"Model entity creation complete: {result}") + + runner.launch_model(config, deploy_target) + return 0 + + except ModelEntityCreationError as e: + logger.exception(f"Model entity creation failed: {e}") + return 1 + except Exception as e: + logger.exception(f"Model entity task failed: {e}") + return 1 + finally: + if sdk_owned and sdk is not None: + sdk.close() diff --git a/services/unsloth/src/nmp/unsloth/tasks/progress_reporter.py b/services/unsloth/src/nmp/unsloth/tasks/progress_reporter.py new file mode 100644 index 0000000000..462cb59d9d --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/tasks/progress_reporter.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Re-export file_io progress types for backward-compatible imports.""" + +from nmp.unsloth.tasks.file_io.progress_reporter import ( + JobsServiceProgressReporter, + NoOpProgressReporter, + ProgressReporter, +) + +__all__ = ["JobsServiceProgressReporter", "NoOpProgressReporter", "ProgressReporter"] diff --git a/services/unsloth/src/nmp/unsloth/tasks/training/__main__.py b/services/unsloth/src/nmp/unsloth/tasks/training/__main__.py new file mode 100644 index 0000000000..d2c5886bf1 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/tasks/training/__main__.py @@ -0,0 +1,131 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Training container entrypoint. + +Reads a :class:`~nmp.unsloth.app.jobs.training.schemas.TrainingStepConfig` +from the platform Jobs envelope (``NEMO_JOB_STEP_CONFIG_FILE_PATH``) +and runs :func:`~nmp.unsloth.tasks.training.backends.unsloth_sft.train_sft` +against the paths the file_io download step populated. + +The container ENTRYPOINT bakes:: + + ENTRYPOINT ["/opt/venv/bin/python"] + CMD ["-m", "nmp.unsloth.tasks.training"] + +Heavy ML imports (``unsloth``, ``torch``, ``transformers``) are +deferred to ``train_sft`` so the parent process (and pytest collection +on a CPU box without unsloth) can import this module. +""" + +from __future__ import annotations + +import json +import logging +import os +import sys +from pathlib import Path + +logger = logging.getLogger(__name__) + + +def _read_step_config() -> dict | None: + """Load the JSON step config the platform Jobs runner injects.""" + env_var = "NEMO_JOB_STEP_CONFIG_FILE_PATH" + config_path = os.environ.get(env_var) + if not config_path: + logger.error( + f"{env_var} is not set. The training container expects the platform Jobs " + "runner to mount the step config file path." + ) + return None + path = Path(config_path) + if not path.is_file(): + logger.error(f"Step config file does not exist: {path}") + return None + return json.loads(path.read_text()) + + +def main() -> int: + """Run the unsloth training step inside a submit container.""" + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) + + raw = _read_step_config() + if raw is None: + sys.stderr.write( + "nmp.unsloth.tasks.training requires NEMO_JOB_STEP_CONFIG_FILE_PATH. " + "Submit via `nemo customization unsloth submit ` so the " + "platform Jobs runner populates this for you.\n", + ) + return 2 + + # Local imports so the parent process (e.g. CLI discovery, pytest + # collection) does not pay the ML import cost. + from nemo_platform_plugin.job_context import JobContext, StoragePaths + from nmp.common.jobs.constants import ( + DEFAULT_JOB_STORAGE_PATH, + PERSISTENT_JOB_STORAGE_PATH_ENVVAR, + ) + from nmp.unsloth.app.jobs.training.schemas import TrainingStepConfig + from nmp.unsloth.tasks.training.backends.unsloth_sft import train_sft + + config = TrainingStepConfig.model_validate(raw) + spec = config.spec + + persistent_root = Path(os.environ.get(PERSISTENT_JOB_STORAGE_PATH_ENVVAR, DEFAULT_JOB_STORAGE_PATH)) + storage = StoragePaths( + ephemeral=persistent_root / "ephemeral", + persistent=persistent_root, + ) + storage.ephemeral.mkdir(parents=True, exist_ok=True) + storage.persistent.mkdir(parents=True, exist_ok=True) + + # Container runs don't currently publish ``ctx.results``; ``train_sft`` + # doesn't touch it today, so passing ``None`` is safe. + ctx = JobContext( + workspace=os.environ.get("NEMO_JOB_WORKSPACE", "default"), + storage=storage, + results=None, # type: ignore[arg-type] + job_id=os.environ.get("NEMO_JOB_ID"), + ) + + if spec.hardware.gpus is not None: + os.environ.setdefault("CUDA_VISIBLE_DEVICES", spec.hardware.gpus) + logger.info(f"Container: CUDA_VISIBLE_DEVICES={os.environ.get('CUDA_VISIBLE_DEVICES')}") + + # Unsloth compiles patched modules into a cache dir that defaults to + # ``unsloth_compiled_cache`` relative to the CWD. The container's WORKDIR + # (/app) is root-owned and we run as a non-root user, so that write fails + # (it falls back to a temp dir, but logs an error and loses cache reuse). + # Point the compile cache and HF cache at the job's writable ephemeral + # storage. ``unsloth_zoo`` reads ``UNSLOTH_COMPILE_LOCATION`` at import + # time, so this must run before ``train_sft`` triggers ``import unsloth``. + compile_cache = storage.ephemeral / "unsloth_compiled_cache" + compile_cache.mkdir(parents=True, exist_ok=True) + os.environ.setdefault("UNSLOTH_COMPILE_LOCATION", str(compile_cache)) + hf_home = storage.ephemeral / "hf" + hf_home.mkdir(parents=True, exist_ok=True) + os.environ.setdefault("HF_HOME", str(hf_home)) + logger.info(f"Container: UNSLOTH_COMPILE_LOCATION={os.environ['UNSLOTH_COMPILE_LOCATION']} HF_HOME={os.environ['HF_HOME']}") + + try: + result = train_sft( + spec, + ctx, + model_path=config.model_path, + dataset_path=config.dataset_path, + output_path=config.output_path, + ) + except Exception: + logger.exception("Unsloth training step failed") + return 1 + + logger.info(f"Training step completed: {json.dumps(result, default=str)}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py b/services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py new file mode 100644 index 0000000000..41154f9524 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py @@ -0,0 +1,337 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Unsloth SFT training driver. + +Single entry point — :func:`train_sft` — invoked from two call-sites: + +- In-process from the plugin's ``UnslothJob.run()``. +- Inside a container by ``nmp.unsloth.tasks.training.__main__`` (when + container submit is wired). + +All heavyweight imports (``unsloth``, ``torch``, ``transformers``, +``trl``, ``peft``, ``datasets``) live inside :func:`train_sft` so the +parent process can import this module for dispatch / type lookups +without dragging in ML dependencies. + +``import unsloth`` MUST happen before ``transformers`` is imported — +Unsloth monkey-patches transformer modules at import time. Out-of-order +imports silently degrade performance. +""" + +from __future__ import annotations + +import logging +import os +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from nemo_platform_plugin.job_context import JobContext + from nmp.unsloth.schemas import UnslothJobOutput + +logger = logging.getLogger(__name__) + + +def train_sft( + spec: "UnslothJobOutput", + ctx: "JobContext", + *, + model_path: str | None = None, + dataset_path: str | None = None, + validation_path: str | None = None, + output_path: str | None = None, +) -> dict[str, Any]: + """Run SFT with Unsloth's FastLanguageModel + LoRA. + + Args: + spec: Canonical job spec. + ctx: Live job context (workspace + storage paths). + model_path: Resolved local path to the model weights. When set, + overrides ``spec.model.name`` for the call to + ``FastLanguageModel.from_pretrained``. Pass ``None`` to use + ``spec.model.name`` directly (legacy behavior, kept for + tests that exercise raw HF ids). + dataset_path: Resolved local path to the training dataset. When + set, overrides ``spec.dataset.path``. ``None`` keeps the + spec value verbatim. + validation_path: Resolved local path to the validation dataset + (optional). + output_path: Resolved local path the saved checkpoint must + land at. Set by the container entrypoint to the upload + step's expected location. When ``None`` falls back to + ``ctx.storage.persistent / spec.output.name`` (matches the + historical local-run layout — kept for tests). + + Returns: + A result dict with final training loss, step count, output + name/type, the resolved local checkpoint path, and the + ``CUDA_VISIBLE_DEVICES`` value the training process observed. + + Raises: + RuntimeError: if the configured ``training.training_type`` is not + ``"sft"`` (only SFT is implemented today). + """ + # ── Heavy imports — local to this function ───────────────────────── + # NB: `unsloth` MUST be imported BEFORE transformers/peft/trl. Do not + # reorder. + import unsloth # noqa: F401 (import-side-effects required) + from datasets import Dataset, load_dataset + from transformers import TrainingArguments + from trl import SFTTrainer + from unsloth import FastLanguageModel + + if spec.training.training_type != "sft": + raise RuntimeError( + f"Unsloth backend only supports training_type='sft', got {spec.training.training_type!r}", + ) + + # Resolve output path. Container submit passes ``output_path`` (the + # upload step's expected location); falling back to + # ``ctx.storage.persistent / spec.output.name`` matches the historic + # local-run layout and keeps the legacy unit tests green. + from pathlib import Path + + output_dir = Path(output_path) if output_path else ctx.storage.persistent / spec.output.name + output_dir.mkdir(parents=True, exist_ok=True) + + lora_rank = spec.training.lora.rank if spec.training.lora else None + lora_alpha = spec.training.lora.alpha if spec.training.lora else None + logger.info( + f"Unsloth SFT: model={spec.model.name} max_seq_length={spec.model.max_seq_length} " + f"steps={spec.schedule.max_steps} epochs={spec.schedule.epochs} " + f"lora=(r={lora_rank}, alpha={lora_alpha}) " + f"cuda_visible={os.environ.get('CUDA_VISIBLE_DEVICES', '')}" + ) + + # ── Model loading ────────────────────────────────────────────────── + resolved_model = model_path or spec.model.name + model_kwargs: dict[str, Any] = { + "model_name": resolved_model, + "max_seq_length": spec.model.max_seq_length, + "load_in_4bit": spec.model.load_in_4bit, + "load_in_8bit": spec.model.load_in_8bit, + "trust_remote_code": spec.model.trust_remote_code, + } + # Unsloth's `dtype` kwarg accepts `None` (auto) or a torch dtype. Map + # the JSON-friendly literal to a torch dtype lazily. + if spec.model.dtype != "auto": + import torch + + dtype_map = { + "bfloat16": torch.bfloat16, + "float16": torch.float16, + "float32": torch.float32, + } + model_kwargs["dtype"] = dtype_map[spec.model.dtype] + + model, tokenizer = FastLanguageModel.from_pretrained(**model_kwargs) + + # ── Adapter ──────────────────────────────────────────────────────── + if spec.training.finetuning_type == "lora": + assert spec.training.lora is not None # validated by UnslothJobInput + # use_gradient_checkpointing accepts True / False / "unsloth"; map + # the JSON literal back. + gc_value: bool | str + if spec.training.use_gradient_checkpointing == "unsloth": + gc_value = "unsloth" + elif spec.training.use_gradient_checkpointing == "true": + gc_value = True + else: + gc_value = False + model = FastLanguageModel.get_peft_model( + model, + r=spec.training.lora.rank, + lora_alpha=spec.training.lora.alpha, + lora_dropout=spec.training.lora.dropout, + target_modules=list(spec.training.lora.target_modules), + bias=spec.training.lora.bias, + use_rslora=spec.training.lora.use_rslora, + random_state=spec.training.lora.random_state, + use_gradient_checkpointing=gc_value, + max_seq_length=spec.model.max_seq_length, + ) + # Full FT: leave `model` as-is. `from_pretrained` already returned the + # un-wrapped HF model since `load_in_4bit`/`load_in_8bit` were both + # rejected by the spec validator for `finetuning_type='full'`. + + # ── Dataset ──────────────────────────────────────────────────────── + resolved_train_path = dataset_path or spec.dataset.path + train_ds = _load_training_dataset( + path=resolved_train_path, + text_field=spec.dataset.text_field, + apply_chat_template=spec.dataset.apply_chat_template, + tokenizer=tokenizer, + load_dataset=load_dataset, + Dataset=Dataset, + ) + eval_ds = None + resolved_validation_path = validation_path or spec.dataset.validation_path + if resolved_validation_path: + eval_ds = _load_training_dataset( + path=resolved_validation_path, + text_field=spec.dataset.text_field, + apply_chat_template=spec.dataset.apply_chat_template, + tokenizer=tokenizer, + load_dataset=load_dataset, + Dataset=Dataset, + ) + + # ── TrainingArguments ───────────────────────────────────────────── + bf16 = spec.hardware.precision == "bf16" + fp16 = spec.hardware.precision == "fp16" + + args_kwargs: dict[str, Any] = { + "output_dir": str(output_dir), + "per_device_train_batch_size": spec.batch.per_device_train_batch_size, + "gradient_accumulation_steps": spec.batch.gradient_accumulation_steps, + "learning_rate": spec.optimizer.learning_rate, + "weight_decay": spec.optimizer.weight_decay, + "optim": spec.optimizer.optim, + "lr_scheduler_type": spec.schedule.lr_scheduler_type, + "warmup_steps": spec.schedule.warmup_steps, + "logging_steps": spec.schedule.logging_steps, + "seed": spec.schedule.seed, + "bf16": bf16, + "fp16": fp16, + "report_to": list( + spec.integrations.report_to if spec.integrations is not None else ["none"], + ), + } + if spec.schedule.warmup_ratio is not None: + args_kwargs["warmup_ratio"] = spec.schedule.warmup_ratio + if spec.schedule.epochs is not None: + args_kwargs["num_train_epochs"] = spec.schedule.epochs + if spec.schedule.max_steps is not None: + args_kwargs["max_steps"] = spec.schedule.max_steps + if spec.schedule.save_steps is not None: + args_kwargs["save_steps"] = spec.schedule.save_steps + args_kwargs["save_strategy"] = "steps" + if spec.schedule.eval_steps is not None: + args_kwargs["eval_steps"] = spec.schedule.eval_steps + args_kwargs["eval_strategy"] = "steps" + + # Wandb run-name: surfaces in W&B when WANDB_API_KEY is set in the + # environment. We don't manage the secret — the user does. + if spec.integrations is not None and spec.integrations.wandb is not None and spec.integrations.wandb.enabled: + if spec.integrations.wandb.run_name: + args_kwargs["run_name"] = spec.integrations.wandb.run_name + if spec.integrations.wandb.project: + os.environ.setdefault("WANDB_PROJECT", spec.integrations.wandb.project) + + args = TrainingArguments(**args_kwargs) + + trainer = SFTTrainer( + model=model, + tokenizer=tokenizer, + train_dataset=train_ds, + eval_dataset=eval_ds, + dataset_text_field=spec.dataset.text_field, + max_seq_length=spec.model.max_seq_length, + packing=spec.dataset.packing, + args=args, + ) + train_result = trainer.train() + + # ── Save ────────────────────────────────────────────────────────── + saved_path = _save_model(model, tokenizer, output_dir, spec) + + return { + "loss": float(train_result.training_loss), + "model": spec.model.name, + "model_path_used": resolved_model, + "backend": "unsloth", + "output_name": spec.output.name, + "output_type": spec.output.type, + "output_save_method": spec.output.save_method, + "output_path": str(saved_path), + "cuda_visible_devices": os.environ.get("CUDA_VISIBLE_DEVICES"), + } + + +def _resolve_local_data_files(path: str) -> str | list[str]: + """Resolve a local dataset path to the JSON/JSONL file(s) to load. + + A file path is returned unchanged. A directory (the file_io download + step's output dir) is expanded to the sorted ``.jsonl`` files inside + it, falling back to ``.json``. Both ``.json``/``.jsonl`` and nested + layouts are handled via a recursive glob. + + Raises: + FileNotFoundError: if ``path`` is a directory with no JSON/JSONL + files under it. + """ + from pathlib import Path + + p = Path(path).expanduser() + if not p.is_dir(): + return str(p) + + for pattern in ("*.jsonl", "*.json"): + files = sorted(str(f) for f in p.rglob(pattern)) + if files: + return files + + raise FileNotFoundError( + f"No .jsonl/.json files found under dataset directory {path!r}. " + "Ensure the dataset fileset contains a JSON or JSONL file.", + ) + + +def _load_training_dataset( + *, + path: str, + text_field: str, + apply_chat_template: bool, + tokenizer: Any, + load_dataset: Any, + Dataset: Any, +) -> Any: + """Load a JSONL or HF dataset; optionally apply the chat template. + + Heuristic for HF id vs local path: presence of a ``.jsonl`` / + ``.json`` extension or a leading path-ish token (``/``, ``./``, + ``~``) implies a local path. Anything else is routed through + ``load_dataset`` as an HF dataset id. + + Container submit hands us the dataset *directory* the file_io + download step populated (``DEFAULT_DATASET_PATH``), not a single + file. ``Dataset.from_json`` does not expand a directory, so a local + directory is resolved to the ``.jsonl`` / ``.json`` file(s) inside it + before loading. A local file path is passed through unchanged (the + in-process plugin run hands us a concrete file). + """ + is_local = path.endswith((".jsonl", ".json")) or path.startswith(("/", "./", "~")) + raw = Dataset.from_json(_resolve_local_data_files(path)) if is_local else load_dataset(path) + + if not apply_chat_template: + return raw + + # Chat-template mode: rows must have a "messages" field; we render + # each into the requested ``text_field`` so SFTTrainer can find it. + def _render(example: dict[str, Any]) -> dict[str, Any]: + rendered = tokenizer.apply_chat_template(example["messages"], tokenize=False) + return {**example, text_field: rendered} + + return raw.map(_render) + + +def _save_model(model: Any, tokenizer: Any, output_dir: Any, spec: "UnslothJobOutput") -> Any: + """Dispatch on save_method; returns the path actually written to. + + Unsloth's recipes use three methods: + - ``"lora"`` → ``model.save_pretrained(output_dir)`` (adapter only) + - ``"merged_16bit"`` → ``model.save_pretrained_merged(..., save_method="merged_16bit")`` + - ``"merged_4bit"`` → ``model.save_pretrained_merged(..., save_method="merged_4bit")`` + """ + if spec.output.save_method == "lora": + model.save_pretrained(str(output_dir)) + tokenizer.save_pretrained(str(output_dir)) + return output_dir + + # merged_16bit or merged_4bit + model.save_pretrained_merged( + str(output_dir), + tokenizer, + save_method=spec.output.save_method, + ) + return output_dir diff --git a/services/unsloth/tests/test_compile.py b/services/unsloth/tests/test_compile.py new file mode 100644 index 0000000000..461c1767de --- /dev/null +++ b/services/unsloth/tests/test_compile.py @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the compile-side entry. + +The plugin's local ``run`` does not invoke this code path — it lives +here so a future container submit drops in. We only check the +delegation contract: ``compile.platform_job_config_compiler`` forwards +to :mod:`nmp.unsloth.app.jobs.compiler` with the args the platform +schedule passes through. +""" + +from __future__ import annotations + +from unittest.mock import AsyncMock, patch + +import pytest +from nmp.unsloth.compile import platform_job_config_compiler +from nmp.unsloth.schemas import ( + DatasetSpec, + LoRAParams, + ModelLoadSpec, + OutputResponse, + TrainingSpec, + UnslothJobOutput, +) + + +def _canonical_spec() -> UnslothJobOutput: + return UnslothJobOutput( + model=ModelLoadSpec(name="default/base"), + dataset=DatasetSpec(path="default/training"), + training=TrainingSpec(lora=LoRAParams()), + schedule={"max_steps": 1}, # type: ignore[arg-type] + output=OutputResponse( + name="r", + type="adapter", + save_method="lora", + fileset="r", + ), + ) + + +@pytest.mark.asyncio +async def test_compile_delegates_to_app_jobs_compiler() -> None: + spec = _canonical_spec() + sdk = object() + + sentinel = object() + target = "nmp.unsloth.compile._compile_canonical" + with patch(target, new=AsyncMock(return_value=sentinel)) as mock: + result = await platform_job_config_compiler( + workspace="default", + spec=spec, + sdk=sdk, # type: ignore[arg-type] + job_name="job-x", + profile="gpu-large", + ) + + assert result is sentinel + mock.assert_awaited_once_with( + "default", + spec, + sdk, + job_name="job-x", + profile="gpu-large", + ) diff --git a/services/unsloth/tests/test_dataset_loading.py b/services/unsloth/tests/test_dataset_loading.py new file mode 100644 index 0000000000..1b5b2eb1fb --- /dev/null +++ b/services/unsloth/tests/test_dataset_loading.py @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for local dataset path resolution in the unsloth SFT driver. + +These exercise ``_resolve_local_data_files`` only, which uses stdlib +``pathlib`` and pulls in no heavy ML deps — so the module imports fine on +a CPU box without ``unsloth``/``torch`` installed. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from nmp.unsloth.tasks.training.backends.unsloth_sft import _resolve_local_data_files + + +def test_single_file_passthrough(tmp_path: Path) -> None: + f = tmp_path / "train.jsonl" + f.write_text('{"text": "hi"}\n') + assert _resolve_local_data_files(str(f)) == str(f) + + +def test_directory_expands_to_jsonl(tmp_path: Path) -> None: + """Container submit hands the loader the download dir, not a file.""" + (tmp_path / "train.jsonl").write_text('{"text": "hi"}\n') + assert _resolve_local_data_files(str(tmp_path)) == [str(tmp_path / "train.jsonl")] + + +def test_directory_prefers_jsonl_over_json(tmp_path: Path) -> None: + (tmp_path / "a.jsonl").write_text("{}\n") + (tmp_path / "b.json").write_text("{}\n") + assert _resolve_local_data_files(str(tmp_path)) == [str(tmp_path / "a.jsonl")] + + +def test_directory_falls_back_to_json(tmp_path: Path) -> None: + (tmp_path / "data.json").write_text("{}\n") + assert _resolve_local_data_files(str(tmp_path)) == [str(tmp_path / "data.json")] + + +def test_directory_returns_sorted_files(tmp_path: Path) -> None: + for name in ("c.jsonl", "a.jsonl", "b.jsonl"): + (tmp_path / name).write_text("{}\n") + assert _resolve_local_data_files(str(tmp_path)) == [ + str(tmp_path / "a.jsonl"), + str(tmp_path / "b.jsonl"), + str(tmp_path / "c.jsonl"), + ] + + +def test_empty_directory_raises(tmp_path: Path) -> None: + with pytest.raises(FileNotFoundError, match="No .jsonl/.json files"): + _resolve_local_data_files(str(tmp_path)) diff --git a/services/unsloth/tests/test_file_io.py b/services/unsloth/tests/test_file_io.py new file mode 100644 index 0000000000..b1a7531c6c --- /dev/null +++ b/services/unsloth/tests/test_file_io.py @@ -0,0 +1,313 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the Unsloth file_io runner. + +Uses a hybrid testing strategy: + +- ``MagicMock`` for the SDK because the runner now uses chained calls like + ``self.sdk.with_options(timeout=...).files.upload(callback=...)``. Mock + fluents handle that cleanly. +- A ``NoOpProgressReporter`` to suppress Jobs-service reporting during tests. +- Light record-keeping wrappers for fileset / file objects to keep + assertions readable. + +The Files service contract is exercised in the SDK's own integration tests; +these focus on "what shape of arguments did this call site emit?". +""" + +from __future__ import annotations + +import types +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + + +def _make_runner(sdk, workspace: str = "default", storage_path: Path | None = None): + from nmp.unsloth.app.jobs.context import NMPJobContext + from nmp.unsloth.tasks.file_io.progress_reporter import NoOpProgressReporter + from nmp.unsloth.tasks.file_io.run import FileIORunner + + job_ctx = NMPJobContext( + workspace=workspace, + job_id="job-1", + attempt_id="attempt-0", + step="model-upload", + task="task-1", + jobs_url=None, + files_url=None, + storage_path=storage_path or Path("/tmp"), + config_path=Path("/tmp/cfg.json"), + ) + return FileIORunner(sdk=sdk, progress_reporter=NoOpProgressReporter(), job_ctx=job_ctx) + + +def _make_sdk(*, conflict_on_create: bool = False) -> MagicMock: + """Build a MagicMock SDK with sensible defaults for fluent chaining. + + ``with_options`` returns the same SDK so chained timeouts don't break + attribute access in tests. + """ + sdk = MagicMock() + sdk.with_options.return_value = sdk + + if conflict_on_create: + # Trigger ConflictError on filesets.create by raising the class the + # runner is bound against (see comment in _raise_runner_conflict). + def _raise_conflict(**_kwargs): + _raise_runner_conflict() + + sdk.files.filesets.create.side_effect = _raise_conflict + + return sdk + + +def _raise_runner_conflict() -> None: + """Raise the exact ``ConflictError`` class bound in tasks.file_io.run. + + The real ``nemo_platform.ConflictError`` is an ``APIStatusError`` that + needs ``response`` + ``body`` kwargs to instantiate. We grab the class + via ``sys.modules`` (the package ``__init__.py`` re-exports ``run`` as + a function, which shadows the submodule for plain attribute access) + and construct via ``__new__`` to dodge the constructor signature. + """ + import sys + + run_mod = sys.modules["nmp.unsloth.tasks.file_io.run"] + raise run_mod.ConflictError.__new__(run_mod.ConflictError, "already exists") + + +def _make_dir(tmp_path: Path) -> Path: + src = tmp_path / "checkpoint" + src.mkdir() + (src / "adapter_model.safetensors").write_bytes(b"\x00" * 16) + (src / "tokenizer.json").write_text("{}") + return src + + +# --------------------------------------------------------------------------- +# FileIORunner.create_fileset +# --------------------------------------------------------------------------- + + +class TestCreateFileset: + def test_creates_fileset_with_service_source_and_metadata(self) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + + sdk = _make_sdk() + runner = _make_runner(sdk) + metadata = {"model": "Qwen/Qwen3-0.6B", "save_method": "lora"} + dest = FileSetRef(workspace="default", name="qwen-test") + + runner.create_fileset(dest, metadata=metadata) + + sdk.files.filesets.create.assert_called_once() + call = sdk.files.filesets.create.call_args + assert call.kwargs["workspace"] == "default" + assert call.kwargs["name"] == "qwen-test" + assert call.kwargs["custom_fields"] == {"service_source": "unsloth"} + assert call.kwargs["metadata"] == metadata + + def test_conflict_patches_metadata_on_existing(self) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + + sdk = _make_sdk(conflict_on_create=True) + runner = _make_runner(sdk) + dest = FileSetRef(workspace="default", name="exists") + + runner.create_fileset(dest, metadata={"model": "x"}) + + sdk.files.filesets.update.assert_called_once() + update_call = sdk.files.filesets.update.call_args + assert update_call.kwargs["workspace"] == "default" + assert update_call.kwargs["name"] == "exists" + assert update_call.kwargs["metadata"] == {"model": "x"} + + def test_conflict_no_metadata_skips_update(self) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + + sdk = _make_sdk(conflict_on_create=True) + runner = _make_runner(sdk) + dest = FileSetRef(workspace="default", name="exists") + + runner.create_fileset(dest, metadata=None) + + sdk.files.filesets.update.assert_not_called() + + def test_update_failure_is_warning_not_fatal( + self, + caplog: pytest.LogCaptureFixture, + ) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + + sdk = _make_sdk(conflict_on_create=True) + sdk.files.filesets.update.side_effect = RuntimeError("backend down") + runner = _make_runner(sdk) + dest = FileSetRef(workspace="default", name="exists") + + with caplog.at_level("WARNING"): + runner.create_fileset(dest, metadata={"model": "x"}) + + assert any("Could not patch metadata" in r.getMessage() for r in caplog.records) + + +# --------------------------------------------------------------------------- +# FileIORunner.upload_fileset (uses FilesetFileSystem with callbacks) +# --------------------------------------------------------------------------- + + +class TestUploadFileset: + def test_directory_uploads_with_trailing_slash(self, tmp_path: Path) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + + sdk = _make_sdk() + runner = _make_runner(sdk) + src = _make_dir(tmp_path) + dest = FileSetRef(workspace="default", name="qwen-test") + + runner.upload_fileset(dest, src.resolve()) + + sdk.files.upload.assert_called_once() + call = sdk.files.upload.call_args + # Trailing slash → directory contents go to fileset root. + assert call.kwargs["local_path"] == f"{src.resolve()}/" + assert call.kwargs["remote_path"] == "" + assert call.kwargs["fileset"] == "qwen-test" + assert call.kwargs["workspace"] == "default" + + def test_single_file_uploads_to_basename(self, tmp_path: Path) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + + sdk = _make_sdk() + runner = _make_runner(sdk) + src = tmp_path / "result.json" + src.write_text("{}") + dest = FileSetRef(workspace="default", name="single-file") + + runner.upload_fileset(dest, src.resolve()) + + call = sdk.files.upload.call_args + assert call.kwargs["local_path"] == str(src.resolve()) + assert call.kwargs["remote_path"] == src.name + + def test_upload_failure_propagates_as_file_upload_error(self, tmp_path: Path) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef, FileUploadError + + sdk = _make_sdk() + sdk.files.upload.side_effect = RuntimeError("upload broke") + runner = _make_runner(sdk) + src = _make_dir(tmp_path) + dest = FileSetRef(workspace="default", name="x") + + with pytest.raises(FileUploadError, match="upload broke"): + runner.upload_fileset(dest, src.resolve()) + + +# --------------------------------------------------------------------------- +# FileIORunner.download_fileset +# --------------------------------------------------------------------------- + + +class TestDownloadFileset: + def test_lists_then_downloads(self, tmp_path: Path) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + + sdk = _make_sdk() + # files.list returns an object with .data (a list of FilesetFile-ish objects). + sdk.files.list.return_value = types.SimpleNamespace( + data=[ + types.SimpleNamespace(path="model.safetensors", size=100), + types.SimpleNamespace(path="config.json", size=20), + ] + ) + runner = _make_runner(sdk) + dest = tmp_path / "downloads" + fileset = FileSetRef(workspace="default", name="qwen") + + runner.download_fileset(fileset, dest) + + sdk.files.list.assert_called_once() + sdk.files.download.assert_called_once() + call = sdk.files.download.call_args + assert call.kwargs["fileset"] == "qwen" + assert call.kwargs["workspace"] == "default" + assert call.kwargs["local_path"] == str(dest.resolve()) + assert dest.exists() + + def test_empty_fileset_returns_zero_stats_without_downloading(self, tmp_path: Path) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + + sdk = _make_sdk() + sdk.files.list.return_value = types.SimpleNamespace(data=[]) + runner = _make_runner(sdk) + dest = tmp_path / "downloads" + fileset = FileSetRef(workspace="default", name="empty") + + stats = runner.download_fileset(fileset, dest) + + assert stats.files_downloaded == 0 + assert stats.total_bytes == 0 + sdk.files.download.assert_not_called() + + +# --------------------------------------------------------------------------- +# build_output_metadata +# --------------------------------------------------------------------------- + + +class TestBuildOutputMetadata: + def test_extracts_canonical_fields(self) -> None: + from nmp.unsloth.schemas import ( + DatasetSpec, + ModelLoadSpec, + OutputResponse, + TrainingSpec, + UnslothJobOutput, + ) + from nmp.unsloth.tasks.file_io.run import build_output_metadata + + spec = UnslothJobOutput( + model=ModelLoadSpec( + name="Qwen/Qwen3-0.6B", + load_in_4bit=False, + load_in_8bit=False, + ), + dataset=DatasetSpec(path="/data/sample.jsonl"), + training=TrainingSpec(finetuning_type="full", lora=None), + output=OutputResponse( + name="qwen-out", + type="model", + save_method="lora", + fileset="qwen-out", + ), + ) + + meta = build_output_metadata(spec) + assert meta == { + "model": "Qwen/Qwen3-0.6B", + "finetuning_type": "full", + "save_method": "lora", + "output_type": "model", + } + + +# --------------------------------------------------------------------------- +# validate_safe_path (path traversal protection) +# --------------------------------------------------------------------------- + + +class TestValidateSafePath: + def test_safe_path_resolves(self, tmp_path: Path) -> None: + from nmp.unsloth.tasks.file_io.utils import validate_safe_path + + result = validate_safe_path(tmp_path, "subdir/file.txt") + assert result == (tmp_path / "subdir/file.txt").resolve() + + def test_traversal_raises(self, tmp_path: Path) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import PathTraversalError + from nmp.unsloth.tasks.file_io.utils import validate_safe_path + + with pytest.raises(PathTraversalError): + validate_safe_path(tmp_path, "../../etc/passwd") diff --git a/services/unsloth/tests/test_main.py b/services/unsloth/tests/test_main.py new file mode 100644 index 0000000000..4a0dd20474 --- /dev/null +++ b/services/unsloth/tests/test_main.py @@ -0,0 +1,121 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the training container entrypoint. + +The container ``CMD`` is ``python -m nmp.unsloth.tasks.training``. It +expects the platform Jobs runner to mount a step-config JSON file via +``NEMO_JOB_STEP_CONFIG_FILE_PATH`` and then invokes +``train_sft`` against the paths the file_io step downloaded to. + +These tests cover the failure path (no env var → exit 2 with a hint +back to the local BYO-venv flow). The happy path requires the +``[unsloth]`` extra in the test env, so we don't exercise it here. +""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path + +import pytest +from nmp.unsloth.app.jobs.training.schemas import TrainingStepConfig +from nmp.unsloth.schemas import ( + DatasetSpec, + LoRAParams, + ModelLoadSpec, + OutputResponse, + TrainingSpec, + UnslothJobOutput, +) +from nmp.unsloth.tasks.training.__main__ import main + + +def _step_config() -> TrainingStepConfig: + spec = UnslothJobOutput( + model=ModelLoadSpec(name="default/base"), + dataset=DatasetSpec(path="default/training"), + training=TrainingSpec(lora=LoRAParams()), + schedule={"max_steps": 1}, # type: ignore[arg-type] + output=OutputResponse(name="r", type="adapter", save_method="lora", fileset="r"), + ) + return TrainingStepConfig( + spec=spec, + model_path="/var/run/scratch/job/model", + dataset_path="/var/run/scratch/job/dataset", + output_path="/var/run/scratch/job/output_model", + ) + + +class TestEntrypointCachePaths: + """The entrypoint must redirect unsloth's compile cache + HF cache off the + root-owned WORKDIR onto the job's writable ephemeral storage before + ``train_sft`` triggers ``import unsloth``.""" + + def test_sets_writable_cache_env_under_storage( + self, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + from nmp.common.jobs.constants import PERSISTENT_JOB_STORAGE_PATH_ENVVAR + from nmp.unsloth.tasks.training.backends import unsloth_sft + + config_file = tmp_path / "step.json" + config_file.write_text(json.dumps(_step_config().model_dump(mode="json"))) + + monkeypatch.setenv("NEMO_JOB_STEP_CONFIG_FILE_PATH", str(config_file)) + monkeypatch.setenv(PERSISTENT_JOB_STORAGE_PATH_ENVVAR, str(tmp_path / "job")) + monkeypatch.delenv("UNSLOTH_COMPILE_LOCATION", raising=False) + monkeypatch.delenv("HF_HOME", raising=False) + + captured: dict[str, str | None] = {} + + def _stub_train_sft(*_args: object, **_kwargs: object) -> dict[str, object]: + captured["UNSLOTH_COMPILE_LOCATION"] = os.environ.get("UNSLOTH_COMPILE_LOCATION") + captured["HF_HOME"] = os.environ.get("HF_HOME") + return {} + + # main() does `from ...unsloth_sft import train_sft` at call time, so + # patching the module attribute is enough. + monkeypatch.setattr(unsloth_sft, "train_sft", _stub_train_sft) + + rc = main() + + assert rc == 0 + ephemeral = tmp_path / "job" / "ephemeral" + assert captured["UNSLOTH_COMPILE_LOCATION"] == str(ephemeral / "unsloth_compiled_cache") + assert captured["HF_HOME"] == str(ephemeral / "hf") + # The dirs must actually exist (unsloth_zoo only makedirs lazily). + assert (ephemeral / "unsloth_compiled_cache").is_dir() + assert (ephemeral / "hf").is_dir() + + +class TestEntrypointWithoutStepConfig: + def test_returns_2_when_step_config_env_missing( + self, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.delenv("NEMO_JOB_STEP_CONFIG_FILE_PATH", raising=False) + rc = main() + assert rc == 2 + err = capsys.readouterr().err + # Friendly hint redirects the user to the local CLI path. + assert "nemo customization unsloth submit" in err + + def test_module_invocation_exits_2(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Smoke: ``python -m nmp.unsloth.tasks.training`` exits 2 without a step config.""" + env = {k: v for k, v in __import__("os").environ.items()} + env.pop("NEMO_JOB_STEP_CONFIG_FILE_PATH", None) + result = subprocess.run( + [sys.executable, "-m", "nmp.unsloth.tasks.training"], + capture_output=True, + text=True, + check=False, + env=env, + ) + assert result.returncode == 2 + assert "nemo customization unsloth submit" in result.stderr diff --git a/services/unsloth/tests/test_model_entity.py b/services/unsloth/tests/test_model_entity.py new file mode 100644 index 0000000000..e522fc1f5f --- /dev/null +++ b/services/unsloth/tests/test_model_entity.py @@ -0,0 +1,493 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the unsloth model_entity runner. + +Covers: +- Adapter (LoRA) creation +- Full / merged model entity creation +- Update-on-conflict semantics (matches automodel behavior) +- Deployment launch with string-ref and inline DeploymentParameters +- Skipping deployment when there's already an active one for a LoRA base +- sanitize_name utility +""" + +from __future__ import annotations + +import types +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + + +def _make_job_ctx(workspace: str = "default"): + from nmp.unsloth.app.jobs.context import NMPJobContext + + return NMPJobContext( + workspace=workspace, + job_id="job-1", + attempt_id="attempt-0", + step="model-entity-creation", + task="task-1", + jobs_url=None, + files_url=None, + storage_path=Path("/tmp"), + config_path=Path("/tmp/cfg.json"), + ) + + +def _make_runner(sdk): + from nmp.unsloth.tasks.model_entity.run import ModelEntityRunner + + return ModelEntityRunner(sdk=sdk, job_ctx=_make_job_ctx()) + + +def _make_sdk() -> MagicMock: + sdk = MagicMock() + sdk.with_options.return_value = sdk + return sdk + + +def _raise_runner_conflict() -> None: + """Raise the ``ConflictError`` class the runner is bound against. + + See test_file_io.py for the rationale; same trick applies here because + ``tasks/model_entity/__init__.py`` re-exports ``run`` as a function and + shadows the submodule for plain attribute access. + """ + import sys + + run_mod = sys.modules["nmp.unsloth.tasks.model_entity.run"] + raise run_mod.ConflictError.__new__(run_mod.ConflictError, "already exists") + + +def _model_entity(*, workspace: str = "default", name: str = "base", spec: object | None = None) -> MagicMock: + me = MagicMock() + me.workspace = workspace + me.name = name + me.trust_remote_code = False + me.spec = spec + return me + + +# --------------------------------------------------------------------------- +# sanitize_name +# --------------------------------------------------------------------------- + + +class TestSanitizeName: + def test_lowercases_and_replaces_invalid_chars(self) -> None: + from nmp.unsloth.tasks.model_entity.run import sanitize_name + + assert sanitize_name("sft-cfg", "Qwen/Qwen3-0.6B") == "sft-cfg-qwen-qwen3-0.6b" + + def test_collapses_consecutive_hyphens(self) -> None: + from nmp.unsloth.tasks.model_entity.run import sanitize_name + + # "/" is not in the allowed set, so each "/" becomes "-", then + # the consecutive-hyphen collapse fires. + assert sanitize_name("p", "a//b") == "p-a-b" + + def test_caps_length_below_60_and_strips_trailing_hyphen(self) -> None: + from nmp.unsloth.tasks.model_entity.run import sanitize_name + + # 59-char limit accounts for the "-v1" the backend appends. + long_name = "a" * 80 + result = sanitize_name("sft-deploy", long_name) + assert len(result) <= 59 + assert not result.endswith("-") + + +# --------------------------------------------------------------------------- +# ModelEntityRunner.create_model_entity — full / merged path +# --------------------------------------------------------------------------- + + +class TestCreateFullEntity: + def test_creates_model_entity_for_full_sft(self) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + from nmp.unsloth.app.jobs.model_entity.schemas import ModelEntityTaskConfig + + sdk = _make_sdk() + sdk.models.retrieve.return_value = _model_entity(name="base-model") + new_me = _model_entity(name="trained-model") + sdk.models.create.return_value = new_me + + runner = _make_runner(sdk) + config = ModelEntityTaskConfig( + name="trained-model", + workspace="default", + fileset=FileSetRef(workspace="default", name="trained-model"), + model_entity="default/base-model", + peft=None, + ) + + result, deploy_target = runner.create_model_entity(config) + + sdk.files.filesets.retrieve.assert_called_once_with(workspace="default", name="trained-model") + sdk.models.create.assert_called_once() + assert deploy_target is new_me + # ``result`` is the output of ``new_me.model_dump()`` — we just assert + # we got *something* back; the actual shape is controlled by the SDK. + assert result is not None + + def test_conflict_falls_back_to_update(self) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + from nmp.unsloth.app.jobs.model_entity.schemas import ModelEntityTaskConfig + + sdk = _make_sdk() + sdk.models.retrieve.return_value = _model_entity(name="base-model") + sdk.models.create.side_effect = lambda **_: _raise_runner_conflict() + sdk.models.update.return_value = _model_entity(name="trained-model") + + runner = _make_runner(sdk) + config = ModelEntityTaskConfig( + name="trained-model", + workspace="default", + fileset=FileSetRef(workspace="default", name="trained-model"), + model_entity="default/base-model", + peft=None, + ) + + _result, _deploy = runner.create_model_entity(config) + + sdk.models.update.assert_called_once() + update_call = sdk.models.update.call_args + assert update_call.kwargs["name"] == "trained-model" + assert update_call.kwargs["workspace"] == "default" + + def test_missing_fileset_raises_creation_error(self) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + from nmp.unsloth.app.jobs.model_entity.schemas import ModelEntityCreationError, ModelEntityTaskConfig + + sdk = _make_sdk() + sdk.files.filesets.retrieve.side_effect = RuntimeError("fileset missing") + runner = _make_runner(sdk) + config = ModelEntityTaskConfig( + name="x", + workspace="default", + fileset=FileSetRef(workspace="default", name="missing"), + model_entity="default/base-model", + ) + + with pytest.raises(ModelEntityCreationError, match="does not exist or is not accessible"): + runner.create_model_entity(config) + + +# --------------------------------------------------------------------------- +# ModelEntityRunner.create_model_entity — LoRA adapter path +# --------------------------------------------------------------------------- + + +class TestCreateAdapter: + def test_creates_adapter_for_lora(self) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + from nmp.unsloth.app.jobs.model_entity.schemas import ModelEntityTaskConfig, PEFTConfig + from nmp.unsloth.entities.values import FinetuningType + + sdk = _make_sdk() + base_me = _model_entity(name="base-model") + sdk.models.retrieve.return_value = base_me + sdk.models.adapters.create.return_value = _model_entity(name="adapter-x") + + runner = _make_runner(sdk) + config = ModelEntityTaskConfig( + name="adapter-x", + workspace="default", + fileset=FileSetRef(workspace="default", name="adapter-x"), + model_entity="default/base-model", + peft=PEFTConfig(type=FinetuningType.LORA, rank=8, alpha=16), + ) + + _result, deploy_target = runner.create_model_entity(config) + + sdk.models.adapters.create.assert_called_once() + # For LoRA, the deploy target is the BASE model, not the adapter. + assert deploy_target is base_me + + def test_adapter_conflict_falls_back_to_update(self) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + from nmp.unsloth.app.jobs.model_entity.schemas import ModelEntityTaskConfig, PEFTConfig + from nmp.unsloth.entities.values import FinetuningType + + sdk = _make_sdk() + sdk.models.retrieve.return_value = _model_entity(name="base-model") + sdk.models.adapters.create.side_effect = lambda **_: _raise_runner_conflict() + sdk.models.adapters.update.return_value = _model_entity(name="adapter-x") + + runner = _make_runner(sdk) + config = ModelEntityTaskConfig( + name="adapter-x", + workspace="default", + fileset=FileSetRef(workspace="default", name="adapter-x"), + model_entity="default/base-model", + peft=PEFTConfig(type=FinetuningType.LORA, rank=8, alpha=16), + ) + + runner.create_model_entity(config) + + sdk.models.adapters.update.assert_called_once() + + +# --------------------------------------------------------------------------- +# ModelEntityRunner.launch_model +# --------------------------------------------------------------------------- + + +class TestLaunchModel: + def test_no_deployment_config_returns_early(self) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + from nmp.unsloth.app.jobs.model_entity.schemas import ModelEntityTaskConfig + + sdk = _make_sdk() + runner = _make_runner(sdk) + me = _model_entity(name="x") + config = ModelEntityTaskConfig( + name="x", + workspace="default", + fileset=FileSetRef(workspace="default", name="x"), + model_entity="default/base", + deployment_config=None, + ) + + runner.launch_model(config, me) + + sdk.inference.deployments.create.assert_not_called() + sdk.inference.deployment_configs.create.assert_not_called() + + def test_inline_params_creates_config_then_deployment(self) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + from nmp.unsloth.app.jobs.model_entity.schemas import DeploymentParameters, ModelEntityTaskConfig + + sdk = _make_sdk() + sdk.inference.deployment_configs.create.return_value = types.SimpleNamespace( + workspace="default", + name="sft-cfg-x", + ) + sdk.inference.deployments.create.return_value = types.SimpleNamespace( + workspace="default", + name="sft-deploy-x", + ) + sdk.inference.deployments.retrieve.return_value = types.SimpleNamespace( + workspace="default", + name="sft-deploy-x", + status="PENDING", + ) + + runner = _make_runner(sdk) + me = _model_entity(name="x", spec=types.SimpleNamespace(family="llama", base_num_parameters=1_000_000_000)) + config = ModelEntityTaskConfig( + name="x", + workspace="default", + fileset=FileSetRef(workspace="default", name="x"), + model_entity="default/base", + deployment_config=DeploymentParameters(gpu=1, image_name="img", image_tag="1.0"), + ) + + runner.launch_model(config, me) + + sdk.inference.deployment_configs.create.assert_called_once() + sdk.inference.deployments.create.assert_called_once() + + def test_string_ref_resolves_existing_config(self) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + from nmp.unsloth.app.jobs.model_entity.schemas import ModelEntityTaskConfig + + sdk = _make_sdk() + sdk.inference.deployment_configs.retrieve.return_value = types.SimpleNamespace( + workspace="default", + name="existing-cfg", + ) + sdk.inference.deployments.create.return_value = types.SimpleNamespace( + workspace="default", + name="sft-deploy-x", + ) + sdk.inference.deployments.retrieve.return_value = types.SimpleNamespace( + workspace="default", + name="sft-deploy-x", + status="PENDING", + ) + + runner = _make_runner(sdk) + me = _model_entity(name="x", spec=types.SimpleNamespace(family="llama", base_num_parameters=1)) + config = ModelEntityTaskConfig( + name="x", + workspace="default", + fileset=FileSetRef(workspace="default", name="x"), + model_entity="default/base", + deployment_config="existing-cfg", + ) + + runner.launch_model(config, me) + + sdk.inference.deployment_configs.retrieve.assert_called_once_with( + workspace="default", + name="existing-cfg", + ) + sdk.inference.deployment_configs.create.assert_not_called() + sdk.inference.deployments.create.assert_called_once() + + def test_lora_with_active_deployment_skips(self) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + from nmp.unsloth.app.jobs.model_entity.schemas import DeploymentParameters, ModelEntityTaskConfig, PEFTConfig + from nmp.unsloth.entities.values import FinetuningType + + sdk = _make_sdk() + # Active deployment exists → launch_model should return without creating anything. + existing_config = types.SimpleNamespace(workspace="default", name="cfg-1") + active_deployment = types.SimpleNamespace(status="READY") + sdk.inference.deployment_configs.list.return_value = types.SimpleNamespace(data=[existing_config]) + sdk.inference.deployments.list.return_value = types.SimpleNamespace(data=[active_deployment]) + + runner = _make_runner(sdk) + me = _model_entity(name="base") + config = ModelEntityTaskConfig( + name="adapter", + workspace="default", + fileset=FileSetRef(workspace="default", name="adapter"), + model_entity="default/base", + peft=PEFTConfig(type=FinetuningType.LORA, rank=8, alpha=16), + deployment_config=DeploymentParameters(), + ) + + runner.launch_model(config, me) + + sdk.inference.deployment_configs.create.assert_not_called() + sdk.inference.deployments.create.assert_not_called() + + def test_lora_with_lora_enabled_false_warns_and_skips( + self, + caplog: pytest.LogCaptureFixture, + ) -> None: + from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + from nmp.unsloth.app.jobs.model_entity.schemas import DeploymentParameters, ModelEntityTaskConfig, PEFTConfig + from nmp.unsloth.entities.values import FinetuningType + + sdk = _make_sdk() + sdk.inference.deployment_configs.list.return_value = types.SimpleNamespace(data=[]) + + runner = _make_runner(sdk) + me = _model_entity(name="base") + config = ModelEntityTaskConfig( + name="adapter", + workspace="default", + fileset=FileSetRef(workspace="default", name="adapter"), + model_entity="default/base", + peft=PEFTConfig(type=FinetuningType.LORA, rank=8, alpha=16), + deployment_config=DeploymentParameters(lora_enabled=False), + ) + + with caplog.at_level("WARNING"): + runner.launch_model(config, me) + + assert any("lora_enabled is false" in r.getMessage() for r in caplog.records) + sdk.inference.deployments.create.assert_not_called() + + +# --------------------------------------------------------------------------- +# Compiler → deployment_config plumbing +# --------------------------------------------------------------------------- + + +class TestCompilerDeploymentConfigPlumbing: + @pytest.mark.asyncio + async def test_inline_params_pass_through_to_model_entity_step(self) -> None: + from unittest.mock import AsyncMock + + from nmp.unsloth.app.jobs.compiler import platform_job_config_compiler + from nmp.unsloth.schemas import ( + DatasetSpec, + DeploymentParams, + LoRAParams, + ModelLoadSpec, + OutputResponse, + ScheduleSpec, + TrainingSpec, + UnslothJobOutput, + ) + + spec = UnslothJobOutput( + model=ModelLoadSpec(name="default/base"), + dataset=DatasetSpec(path="default/training"), + training=TrainingSpec(lora=LoRAParams()), + schedule=ScheduleSpec(max_steps=1), + output=OutputResponse(name="r", type="adapter", save_method="lora", fileset="r"), + deployment_config=DeploymentParams(gpu=2, image_name="img", lora_enabled=True), + ) + + # Patch fetch_model_entity to avoid hitting the platform. + from nmp.unsloth.app.jobs import compiler as compiler_mod + + original_fetch = compiler_mod.fetch_model_entity + compiler_mod.fetch_model_entity = AsyncMock( # type: ignore[assignment] + return_value=types.SimpleNamespace( + workspace="default", + name="base", + fileset="default/base-fileset", + trust_remote_code=False, + ) + ) + try: + job_spec = await platform_job_config_compiler( + workspace="default", + job_spec=spec, + sdk=MagicMock(), + ) + finally: + compiler_mod.fetch_model_entity = original_fetch # type: ignore[assignment] + + # PlatformJobSpec is a TypedDict, so we index it instead of using attributes. + me_step = next(s for s in job_spec["steps"] if s["name"] == "model-entity-creation") + dc = me_step["config"]["deployment_config"] + # Inline params come through as a serialized dict, not the user-facing class. + assert dc["gpu"] == 2 + assert dc["image_name"] == "img" + assert dc["lora_enabled"] is True + + @pytest.mark.asyncio + async def test_string_ref_passes_through_unchanged(self) -> None: + from unittest.mock import AsyncMock + + from nmp.unsloth.app.jobs.compiler import platform_job_config_compiler + from nmp.unsloth.schemas import ( + DatasetSpec, + LoRAParams, + ModelLoadSpec, + OutputResponse, + ScheduleSpec, + TrainingSpec, + UnslothJobOutput, + ) + + spec = UnslothJobOutput( + model=ModelLoadSpec(name="default/base"), + dataset=DatasetSpec(path="default/training"), + training=TrainingSpec(lora=LoRAParams()), + schedule=ScheduleSpec(max_steps=1), + output=OutputResponse(name="r", type="adapter", save_method="lora", fileset="r"), + deployment_config="my-config", + ) + + from nmp.unsloth.app.jobs import compiler as compiler_mod + + original_fetch = compiler_mod.fetch_model_entity + compiler_mod.fetch_model_entity = AsyncMock( # type: ignore[assignment] + return_value=types.SimpleNamespace( + workspace="default", + name="base", + fileset="default/base-fileset", + trust_remote_code=False, + ) + ) + try: + job_spec = await platform_job_config_compiler( + workspace="default", + job_spec=spec, + sdk=MagicMock(), + ) + finally: + compiler_mod.fetch_model_entity = original_fetch # type: ignore[assignment] + + me_step = next(s for s in job_spec["steps"] if s["name"] == "model-entity-creation") + assert me_step["config"]["deployment_config"] == "my-config" diff --git a/services/unsloth/tests/test_schemas.py b/services/unsloth/tests/test_schemas.py new file mode 100644 index 0000000000..554a4b4dc0 --- /dev/null +++ b/services/unsloth/tests/test_schemas.py @@ -0,0 +1,105 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Smoke tests for the canonical Unsloth schemas. + +Submitter-side validation (mutexes, required fields, defaults) lives +with ``UnslothJobInput`` in the plugin. These tests pin only the +canonical-shape contract that ``train_sft`` and ``compile`` consume. +""" + +from __future__ import annotations + +import pytest +from nmp.unsloth.schemas import ( + DatasetSpec, + LoRAParams, + ModelLoadSpec, + OutputResponse, + ScheduleSpec, + TrainingSpec, + UnslothJobOutput, +) +from pydantic import ValidationError + + +def _canonical_dict() -> dict[str, object]: + return { + "model": {"name": "unsloth/Qwen2.5-0.5B-Instruct"}, + "dataset": {"path": "/d.jsonl"}, + "training": {"lora": {"rank": 8, "alpha": 16}}, + "schedule": {"max_steps": 60}, + "output": { + "name": "run-1", + "type": "adapter", + "save_method": "lora", + "fileset": "run-1", + }, + } + + +class TestUnslothJobOutput: + def test_minimal_canonical_validates(self) -> None: + out = UnslothJobOutput.model_validate(_canonical_dict()) + assert out.output.name == "run-1" + assert out.output.type == "adapter" + assert out.output.save_method == "lora" + + def test_output_required(self) -> None: + payload = _canonical_dict() + del payload["output"] + with pytest.raises(ValidationError): + UnslothJobOutput.model_validate(payload) + + def test_lora_default_target_modules_count(self) -> None: + # Unsloth's recommended 7-module set lives in this canonical shape. + lora = LoRAParams() + assert len(lora.target_modules) == 7 + + def test_extra_forbidden_top_level(self) -> None: + payload = _canonical_dict() + payload["mystery_field"] = "boom" + with pytest.raises(ValidationError): + UnslothJobOutput.model_validate(payload) + + +class TestSubShapesIndependently: + def test_dataset_path_required(self) -> None: + with pytest.raises(ValidationError): + DatasetSpec.model_validate({}) + + def test_model_name_required(self) -> None: + with pytest.raises(ValidationError): + ModelLoadSpec.model_validate({}) + + def test_schedule_defaults_pass_through(self) -> None: + # Canonical schedule allows neither epochs nor max_steps because + # the input-side validator already enforced that. Plugin-side + # UnslothJobInput owns the mutex. + sched = ScheduleSpec() + assert sched.epochs is None + assert sched.max_steps is None + + def test_training_defaults(self) -> None: + t = TrainingSpec() + assert t.training_type == "sft" + assert t.finetuning_type == "lora" + # Canonical shape doesn't auto-fill `lora` (that's the plugin's + # input validator's job). + assert t.lora is None + + def test_output_response_extras_forbidden(self) -> None: + with pytest.raises(ValidationError): + OutputResponse.model_validate( + { + "name": "x", + "type": "adapter", + "save_method": "lora", + "fileset": "x", + "junk": 1, + } + ) + + def test_output_response_requires_fileset(self) -> None: + with pytest.raises(ValidationError, match="fileset"): + OutputResponse.model_validate({"name": "x", "type": "adapter", "save_method": "lora"}) diff --git a/third_party/osv-licenses.json b/third_party/osv-licenses.json index 3af1cc51f7..a67f72fb2f 100644 --- a/third_party/osv-licenses.json +++ b/third_party/osv-licenses.json @@ -5594,7 +5594,7 @@ { "package": { "name": "transformers", - "version": "5.10.2", + "version": "5.5.0", "ecosystem": "PyPI" }, "licenses": [ diff --git a/third_party/requirements-main.txt b/third_party/requirements-main.txt index 64f3265a50..049452c2ac 100644 --- a/third_party/requirements-main.txt +++ b/third_party/requirements-main.txt @@ -30,6 +30,7 @@ # nemo-data-designer-plugin # nemo-guardrails-plugin # nemo-safe-synthesizer-plugin + # nemo-unsloth-plugin # nemoplatform -e ./packages/nemo_platform_ext ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') # via nemoplatform @@ -48,6 +49,7 @@ # nemo-platform-sdk # nemo-safe-synthesizer-plugin # nemo-switchyard + # nemo-unsloth-plugin # nmp-common # nmp-inference-gateway # nmp-platform @@ -79,6 +81,7 @@ # nmp-platform-seed # nmp-secrets # nmp-studio + # nmp-unsloth -e ./packages/nmp_platform ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') # via nemoplatform -e ./packages/nmp_platform_runner ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') @@ -99,6 +102,7 @@ -e ./plugins/nemo-switchyard ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') -e ./plugins/nemo-switchyard/vendor/switchyard ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') # via nemo-switchyard +-e ./plugins/nemo-unsloth ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') -e ./sdk/python/nemo-platform ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') # via # models @@ -110,6 +114,7 @@ # nmp-common # nmp-core-mcp # nmp-entities + # nmp-unsloth -e ./services/automodel ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') # via nemo-automodel-plugin -e ./services/core/auth ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') @@ -160,6 +165,8 @@ # via nemoplatform -e ./services/studio ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') # via nemoplatform +-e ./services/unsloth ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') + # via nemo-unsloth-plugin absl-py==2.4.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d \ --hash=sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4 @@ -954,6 +961,7 @@ httpx==0.28.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p # nmp-auth # nmp-automodel # nmp-guardrails + # nmp-unsloth # nvidia-nat-core # oci-openai # openai @@ -2174,6 +2182,7 @@ pydantic==2.12.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # nemo-platform-sdk # nemo-safe-synthesizer # nemo-safe-synthesizer-plugin + # nemo-unsloth-plugin # nemoguardrails # nemoplatform # nmp-auth @@ -2187,6 +2196,7 @@ pydantic==2.12.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # nmp-intake # nmp-jobs # nmp-models + # nmp-unsloth # nvidia-nat-atif # nvidia-nat-config-optimizer # nvidia-nat-core @@ -2242,6 +2252,7 @@ pydantic-settings==2.8.1 ; (platform_machine == 'arm64' and sys_platform == 'dar # nemo-platform-plugin # nemo-safe-synthesizer # nemo-safe-synthesizer-plugin + # nemo-unsloth-plugin # nmp-auth # nmp-automodel # nmp-common @@ -2254,6 +2265,7 @@ pydantic-settings==2.8.1 ; (platform_machine == 'arm64' and sys_platform == 'dar # nmp-intake # nmp-jobs # nmp-models + # nmp-unsloth pygments==2.20.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 @@ -2799,6 +2811,7 @@ tenacity==9.1.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # langchain-core # nmp-automodel # nmp-models + # nmp-unsloth tiktoken==0.12.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa \ --hash=sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e \ @@ -2865,9 +2878,9 @@ tqdm==4.67.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (pl # ragas # sqlfluff # transformers -transformers==5.10.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:8a669db546f82c7c3618cb46ceb0f0afd89292bc70f319c058f8332ec63e268d \ - --hash=sha256:f9a44b9c8ca9ab1156b467f574d832ea066284299c2fd0ed84641ccb592751fc +transformers==5.5.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:821a9ff0961abbb29eb1eb686d78df1c85929fdf213a3fe49dc6bd94f9efa944 \ + --hash=sha256:c8db656cf51c600cd8c75f06b20ef85c72e8b8ff9abc880c5d3e8bc70e0ddcbd # via nemo-customizer-plugin typer==0.24.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e \ @@ -2886,6 +2899,7 @@ typer==0.24.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p # nemo-platform-plugin # nemo-platform-sdk # nemo-safe-synthesizer-plugin + # nemo-unsloth-plugin # nemoguardrails # ragas # transformers diff --git a/uv.lock b/uv.lock index 259d87ffb3..6875fd0acf 100644 --- a/uv.lock +++ b/uv.lock @@ -42,6 +42,7 @@ members = [ "nemo-platform-sdk-tools", "nemo-safe-synthesizer-plugin", "nemo-switchyard", + "nemo-unsloth-plugin", "nemoplatform", "nmp-auth", "nmp-automodel", @@ -65,6 +66,7 @@ members = [ "nmp-secrets", "nmp-studio", "nmp-testing", + "nmp-unsloth", ] constraints = [ { name = "aiohttp", specifier = ">=3.13.4" }, @@ -142,6 +144,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" }, ] +[[package]] +name = "accelerate" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", 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 = "numpy", 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 = "packaging", 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 = "psutil", 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 = "safetensors", 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 = "torch", 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')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/14/787e5498cd062640f0f3d92ef4ae4063174f76f9afd29d13fc52a319daae/accelerate-1.13.0.tar.gz", hash = "sha256:d631b4e0f5b3de4aff2d7e9e6857d164810dfc3237d54d017f075122d057b236", size = 402835, upload-time = "2026-03-04T19:34:12.359Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl", hash = "sha256:cf1a3efb96c18f7b152eb0fa7490f3710b19c3f395699358f08decca2b8b62e0", size = 383744, upload-time = "2026-03-04T19:34:10.313Z" }, +] + [[package]] name = "aioboto3" version = "15.5.0" @@ -586,6 +606,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] +[[package]] +name = "bitsandbytes" +version = "0.49.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", 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 = "packaging", 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 = "torch", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/7d/f1fe0992334b18cd8494f89aeec1dcc674635584fcd9f115784fea3a1d05/bitsandbytes-0.49.2-py3-none-macosx_14_0_arm64.whl", hash = "sha256:87be5975edeac5396d699ecbc39dfc47cf2c026daaf2d5852a94368611a6823f", size = 131940, upload-time = "2026-02-16T21:26:04.572Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/acff7af06c818664aa87ff73e17a52c7788ad746b72aea09d3cb8e424348/bitsandbytes-0.49.2-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:2fc0830c5f7169be36e60e11f2be067c8f812dfcb829801a8703735842450750", size = 31442815, upload-time = "2026-02-16T21:26:06.783Z" }, + { url = "https://files.pythonhosted.org/packages/19/57/3443d6f183436fbdaf5000aac332c4d5ddb056665d459244a5608e98ae92/bitsandbytes-0.49.2-py3-none-manylinux_2_24_x86_64.whl", hash = "sha256:54b771f06e1a3c73af5c7f16ccf0fc23a846052813d4b008d10cb6e017dd1c8c", size = 60651714, upload-time = "2026-02-16T21:26:11.579Z" }, +] + [[package]] name = "black" version = "26.3.1" @@ -975,6 +1010,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, ] +[[package]] +name = "cuda-bindings" +version = "12.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/e7/b47792cc2d01c7e1d37c32402182524774dadd2d26339bd224e0e913832e/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c912a3d9e6b6651853eed8eed96d6800d69c08e94052c292fec3f282c5a817c9", size = 12210593, upload-time = "2025-10-21T14:51:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl", hash = "sha256:0228c023f95d1480f143ef5c8922d27a2ab052087a942e81dc289c9eb8f91689", size = 51671, upload-time = "2026-05-27T01:21:25.413Z" }, +] + +[[package]] +name = "cut-cross-entropy" +version = "25.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "torch", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "triton", version = "3.7.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/97/45ff09cfcda7b200389204daa0125168e6544fba257adbbcdf728501d4f9/cut_cross_entropy-25.1.1.tar.gz", hash = "sha256:5fe5924509248b1aea5c890f8887c6a7759f7c8b1ebc0490e42c247c4f7c1e34", size = 22972, upload-time = "2025-01-07T12:21:53.896Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/5f/62fdb048f84d19e2123b6bbd722fe09c8c79b4964c50094d1e979db808e2/cut_cross_entropy-25.1.1-py3-none-any.whl", hash = "sha256:e46f26d348f6a67927d17e65c5a212e795be13dcad5b10a77a200d6b8102d9d1", size = 22672, upload-time = "2025-01-07T12:21:51.678Z" }, +] + [[package]] name = "cyclopts" version = "4.10.1" @@ -1224,6 +1295,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/2c/61eeb887055a37150db824b6bf830e821a736580769ac2fea4eadb0d613f/diff_cover-10.2.0-py3-none-any.whl", hash = "sha256:59c328595e0b8948617cc5269af9e484c86462e2844bfcafa3fb37f8fca0af87", size = 56748, upload-time = "2026-01-09T01:59:06.028Z" }, ] +[[package]] +name = "diffusers" +version = "0.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", 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 = "huggingface-hub", 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 = "importlib-metadata", 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 = "numpy", 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 = "pillow", 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 = "regex", 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 = "requests", 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 = "safetensors", 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')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/ed/255d3dfd4a2271dffc8f1895f9d2720b3bf1beaecf02148bb5604439e594/diffusers-0.38.0.tar.gz", hash = "sha256:1e094ec5c16f18c42fb89d37f07a94cf9aab3ebbe527ab059c609597b8857626", size = 4328401, upload-time = "2026-05-01T05:42:15.276Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/c0/3237566ea6e3a542f3c0669a253d62fe75f27b84b3d7bd4fb3b5ee89d73c/diffusers-0.38.0-py3-none-any.whl", hash = "sha256:18e53f9e539096320470f62c6360a6fd5727ff28cffda566265316e13fcdb612", size = 5245919, upload-time = "2026-05-01T05:42:12.779Z" }, +] + [[package]] name = "dill" version = "0.3.8" @@ -1920,6 +2011,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d3/8a/44032265776062a89171285ede55a0bdaadc8ac00f27f0512a71a9e3e1c8/hatchling-1.29.0-py3-none-any.whl", hash = "sha256:50af9343281f34785fab12da82e445ed987a6efb34fd8c2fc0f6e6630dbcc1b0", size = 76356, upload-time = "2026-02-23T19:42:05.197Z" }, ] +[[package]] +name = "hf-transfer" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/eb/8fc64f40388c29ce8ce3b2b180a089d4d6b25b1d0d232d016704cb852104/hf_transfer-0.1.9.tar.gz", hash = "sha256:035572865dab29d17e783fbf1e84cf1cb24f3fcf8f1b17db1cfc7fdf139f02bf", size = 25201, upload-time = "2025-01-07T10:05:12.947Z" } +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" }, +] + [[package]] name = "hf-xet" version = "1.4.3" @@ -3063,6 +3178,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/5c/1b5691575420135e90578543b2bf219497caa33cfd0af64cb38f30288450/litellm-1.83.14-py3-none-any.whl", hash = "sha256:92b11ba2a32cf80707ddf388d18526696c7999a21b418c5e3b6eda1243d2cfdb", size = 16457054, upload-time = "2026-04-26T03:16:05.72Z" }, ] +[[package]] +name = "llguidance" +version = "1.7.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/91/6bc8bb503dc259e46d253b5424385a54fe06c38a4c7a12befe69a3c2455a/llguidance-1.7.6.tar.gz", hash = "sha256:db7febbe412ed2015501904646750071d7e00e6df7f85c4b956ad4f206fd2df7", size = 1156574, upload-time = "2026-06-03T20:13:25.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/fe/bb185f11bad82f2637e3cd8cbf6b200cbb6ed56ac395de47ea05a60d4649/llguidance-1.7.6-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:9c54c899db8cb4b4fba128a7d844730066576c70d806c95ada92b2bd2d6ab498", size = 3138127, upload-time = "2026-06-03T20:13:11.649Z" }, +] + [[package]] name = "loguru" version = "0.7.3" @@ -3264,6 +3388,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "miniaudio" +version = "1.71" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/d5/e5439dc08561f73656bfeb3340fc64ab63163e101426593d8fb9a025ff1e/miniaudio-1.71.tar.gz", hash = "sha256:ff51e2887bb673e2e757752b586b3dc924d59aa5fbcae9bbc45f4a111bd3262b", size = 1116480, upload-time = "2026-04-29T21:20:38.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/c1/4b13ac3c36a2574e0d70f322246d80259606cd24523279f542abc9ac6063/miniaudio-1.71-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5009b4e29cd43de3631d2d5ab09cc074192c085b4c8dd8a121b856ce1af6bab7", size = 351405, upload-time = "2026-04-29T21:20:10.533Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ac/30a324f758bed1b193e017ec25183cfb10a79e549656331f5d068a2d343a/miniaudio-1.71-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fc1a4f084cc1b4b25c567d22f54d1e46bfa505c17ed777c8b198e5c53d0f785", size = 351485, upload-time = "2026-04-29T21:20:17.761Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d1/071a560000c8ce903dc919968ecce40fbe7a73213ac399051b887184f8a3/miniaudio-1.71-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d9dc15eff711bcfc62a9d05e0c78e4bc34821a455595e049629f2fea7491a523", size = 351488, upload-time = "2026-04-29T21:20:25.183Z" }, +] + [[package]] name = "mistune" version = "3.2.1" @@ -3303,6 +3441,98 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4b/52/17460157271e70b0d8444d27f8ad730ef7d95fb82fac59dc19f11519b921/mlflow_skinny-3.10.1-py3-none-any.whl", hash = "sha256:df1dd507d8ddadf53bfab2423c76cdcafc235cd1a46921a06d1a6b4dd04b023c", size = 2987098, upload-time = "2026-03-05T10:48:59.566Z" }, ] +[[package]] +name = "mlx" +version = "0.31.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mlx-metal", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/89/1e77ec3ff380e8fb9e7258047374d31452a0f9828a0e370f127b07dd8288/mlx-0.31.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4a3f181b367d404e44a6bd68ef5eb573930809ac60cacd51d0c851c629b1b651", size = 586911, upload-time = "2026-04-22T03:14:29.675Z" }, + { url = "https://files.pythonhosted.org/packages/6a/41/c1907f05f8a3fc54025fb78ad68d3c4a4b931664d03c0a24f7f431cc4087/mlx-0.31.2-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:70297cbef7479429f69c966bfed10da20a6f0c2aa997eec2b4f6ba1a07caf2ef", size = 586915, upload-time = "2026-04-22T03:14:31.403Z" }, + { url = "https://files.pythonhosted.org/packages/97/b0/61ac2c14773c786fecbda28067b0207a0c654cb4d10c548808c51284d700/mlx-0.31.2-cp311-cp311-macosx_26_0_arm64.whl", hash = "sha256:c0ff158b7ac93a4b5659adbc70053498b30a5964fc45f78596398e056a96c36a", size = 587030, upload-time = "2026-04-22T03:14:32.961Z" }, + { url = "https://files.pythonhosted.org/packages/c3/47/5f33906cb03d6a378a697cd2d2641a26b37dea17ee3d9124d7e39e8eca01/mlx-0.31.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:e5067aaf2be1f3d7bba5be52348775804f111173c1ed04639618fd713b1a530f", size = 584863, upload-time = "2026-04-22T03:14:38.211Z" }, + { url = "https://files.pythonhosted.org/packages/08/e7/a851a451b1327af9fb4df3991b9ae87d066b6f6630e854af55c288b0995a/mlx-0.31.2-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:edb9797db7d852477ca1c99708058654ee860d4148fe5765f0d55528e2b1aa22", size = 584860, upload-time = "2026-04-22T03:14:39.746Z" }, + { url = "https://files.pythonhosted.org/packages/3b/15/0d1dc0597644e5e7b011ca954ba0c47e13cd880a3b909b0c3f1b4d8bf8f1/mlx-0.31.2-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:51ca102db641b01e7cb083ce8ecb580e281530a141a7ca12544bb370641630ae", size = 584887, upload-time = "2026-04-22T03:14:41.585Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3f/888f8664d4f8e23a1363a5f50024be5216e199ab7ad0ba20988c7ed6d729/mlx-0.31.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:1b3fb0dda955b0d552ce57bdd6f42b3309ab21b067e40587d6848443d307e91f", size = 584796, upload-time = "2026-04-22T03:14:47.215Z" }, + { url = "https://files.pythonhosted.org/packages/dd/14/e9cd18b51f9e1dbcb060eec0fafc2d2428c8e1eacd9b0a02d7c5ce75b661/mlx-0.31.2-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:34b0171cd9eb5c43fdd82091f6135d6ccc5a065363a4a3e68fac64fb4e53d37c", size = 584790, upload-time = "2026-04-22T03:14:48.519Z" }, + { url = "https://files.pythonhosted.org/packages/ca/20/c6c5fb998c7834d094b2bfb9f003b5246cb270f0266da055c55546c34999/mlx-0.31.2-cp313-cp313-macosx_26_0_arm64.whl", hash = "sha256:c05981684279a8935d58b0dde3ea5b02d210c3bad3319aa0e9934ec2df165752", size = 584795, upload-time = "2026-04-22T03:14:49.904Z" }, +] + +[[package]] +name = "mlx-audio" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "miniaudio", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "mlx", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "mlx-lm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "numpy", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "scipy", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "sounddevice", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "tqdm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "transformers", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/db/a9f95e3794eca373d681220c8b9f8f84451a0d14959f85cc341ca592394c/mlx_audio-0.4.3.tar.gz", hash = "sha256:8e87badf56a0f73bf91e3797b1195c01440a181cf0b64a2a08dc1bda4b037f54", size = 1144947, upload-time = "2026-04-28T20:18:12.09Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/25/0a89073ed7b7cdf34299042bd03d867c12c0c8b43f597be61bea7f146793/mlx_audio-0.4.3-py3-none-any.whl", hash = "sha256:6b87bf42d79d9ceb6b9310a77656b9b76429c2d6ddd89f634b2786c58a2e4721", size = 1373582, upload-time = "2026-04-28T20:18:10.512Z" }, +] + +[[package]] +name = "mlx-lm" +version = "0.31.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "mlx", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "numpy", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "protobuf", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "pyyaml", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "sentencepiece", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "transformers", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/94/9a38d6b0c6fcca995b9136c94eb7da1e9c5165652edf228b96b29960fa7a/mlx_lm-0.31.3.tar.gz", hash = "sha256:61eb0e3ba09444f77f874aff295401d7ccd20b39495cbbce0c782a15474ce733", size = 304318, upload-time = "2026-04-22T07:37:27.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/02/9a67b8e4f87e3e2e5cd7b1ad79304b93c09a0db6af34bee75e6551c06c60/mlx_lm-0.31.3-py3-none-any.whl", hash = "sha256:758cfddf1180053b7613db76fad3d246a331a2a905808e1164a275621fc983b8", size = 408890, upload-time = "2026-04-22T07:37:25.965Z" }, +] + +[[package]] +name = "mlx-metal" +version = "0.31.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/69/fe3b783ebe999f3118234e1e940feb622518bfb1dea6ac5d13b1d36a8449/mlx_metal-0.31.2-py3-none-macosx_14_0_arm64.whl", hash = "sha256:b25385bcee18fc194092255b8b53b9a3d8489eb650e59160f1b57aadd07aa2dc", size = 40055588, upload-time = "2026-04-22T03:14:14.43Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5d/4c690d5b93c30ba002656c37363159d978705bf8eb801b8481840fb942c2/mlx_metal-0.31.2-py3-none-macosx_15_0_arm64.whl", hash = "sha256:e9d4e5fce6ca10a87a0e388597f99519ad594d09e674708b5312bd8bd4f5997d", size = 40053220, upload-time = "2026-04-22T03:14:18.048Z" }, + { url = "https://files.pythonhosted.org/packages/99/82/11fd62a8d7a3e96e5c43220b17de0151e3f10101f8bb3b865f5bd9cdd074/mlx_metal-0.31.2-py3-none-macosx_26_0_arm64.whl", hash = "sha256:84ffb60ee503f03eb684f5fb168d5cff31e2a16b7f27c1731eaf7662bd6e9b46", size = 55792151, upload-time = "2026-04-22T03:14:22.059Z" }, +] + +[[package]] +name = "mlx-vlm" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "datasets", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "fastapi", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "llguidance", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "miniaudio", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "mlx", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "mlx-audio", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "mlx-lm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "numpy", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "opencv-python", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "pillow", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "requests", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "tqdm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "transformers", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "uvicorn", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/a3/70dce014f6a72efd2cecc07b6a68fc11c0694fbe54ea553b2e00499c7b36/mlx_vlm-0.5.0.tar.gz", hash = "sha256:24563cd1b3a399fd941b2359100628306e2754db1b48780516d1283138258793", size = 1033154, upload-time = "2026-05-06T21:09:33.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/66/fb955ccc442aa556e5e9d8836fb9041a7aadff5a88fa80c285e53dc19bf5/mlx_vlm-0.5.0-py3-none-any.whl", hash = "sha256:3351d6ccf609cbf57a4c8cd8308e9a1ce469883d8679d9968c6c6f77af016419", size = 1218132, upload-time = "2026-05-06T21:09:32.071Z" }, +] + [[package]] name = "mmh3" version = "5.2.1" @@ -3368,6 +3598,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] +[[package]] +name = "msgspec" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/60/f79b9b013a16fa3a58350c9295ddc6789f2e335f36ea61ed10a21b215364/msgspec-0.21.1.tar.gz", hash = "sha256:2313508e394b0d208f8f56892ca9b2799e2561329de9763b19619595a6c0f72c", size = 319193, upload-time = "2026-04-12T21:44:50.394Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/504886af1aaf854112663b842d5eea9a15d9588f9bf7d0d2df736424b84d/msgspec-0.21.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4692b7c1609155708c4418f88e92f63c13fdf08aa095c84bae82bad75b53389b", size = 186597, upload-time = "2026-04-12T21:43:57.242Z" }, + { url = "https://files.pythonhosted.org/packages/fa/54/d24ddeaa65b5278c9e67f48ce3c17a9831e8f3722f3c8322ee120aca22ef/msgspec-0.21.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3124010b3815451494c85ff345e693cb9fe5889cfcbbef39ed8622e0e72319c", size = 215158, upload-time = "2026-04-12T21:43:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/9f/75/bb79c8b89a93ae23cd33c0d802373f16feaf9633f05d8af77091350dda0a/msgspec-0.21.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6badc03b9725352219cca017bfe71c61f2fbd0fb5982b410ac17c97c213deb30", size = 219856, upload-time = "2026-04-12T21:44:00.015Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/c5ca26b46f0ebbd3a6683695ef89396712cb9e4199fd1f0bc1dd968216b1/msgspec-0.21.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5d2d4116ebe3035a78d9ec76e99a9d64e5fa6d44fe61a9c5de7fd1acf54bcc69", size = 220314, upload-time = "2026-04-12T21:44:01.548Z" }, + { url = "https://files.pythonhosted.org/packages/c8/31/645a351c4285dce40ed6755c3dcc0aa648e26dacb20a98018fe2cce5e87b/msgspec-0.21.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0d1009f6715f5bff3b54d4ff5c7428ad96197e0534e1645b8e9b955890c84664", size = 223215, upload-time = "2026-04-12T21:44:02.884Z" }, + { url = "https://files.pythonhosted.org/packages/6d/81/074612945c0666078f7366f40000013de9f6ba687491d450df699bceebc9/msgspec-0.21.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5102c7e9b3acff82178449b85006d96310e690291bb1ea0142f1b24bcb8aabcb", size = 188473, upload-time = "2026-04-12T21:44:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/8a/37/655101799590bcc5fddb2bd3fe0e6194e816c2d1da7c361725f5eb89a910/msgspec-0.21.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:846758412e9518252b2ac9bffd6f0e54d9ff614f5f9488df7749f81ff5c80920", size = 218871, upload-time = "2026-04-12T21:44:09.917Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d1/d4cd9fe89c7d400d7a18f86ccc94daa3f0927f53558846fcb60791dce5d6/msgspec-0.21.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21995e74b5c598c2e004110ad66ec7f1b8c20bf2bcf3b2de8fd9a3094422d3ff", size = 225025, upload-time = "2026-04-12T21:44:11.191Z" }, + { url = "https://files.pythonhosted.org/packages/24/bf/e20549e602b9edccadeeff98760345a416f9cce846a657e8b18e3396b212/msgspec-0.21.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6129f0cca52992e898fd5344187f7c8127b63d810b2fd73e36fca73b4c6475ee", size = 222672, upload-time = "2026-04-12T21:44:12.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/68/04d7a8f0f786545cf9b8c280c57aa6befb5977af6e884b8b54191cbe44b3/msgspec-0.21.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ef3ec2296248d1f8b9231acb051b6d471dfde8f21819e86c9adaaa9f42918521", size = 227303, upload-time = "2026-04-12T21:44:13.709Z" }, + { url = "https://files.pythonhosted.org/packages/bb/40/4476c1bd341418a046c4955aff632ec769315d1e3cb94e6acf86d461f9ed/msgspec-0.21.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:344c7cd0eaed1fb81d7959f99100ef71ec9b536881a376f11b9a6c4803365697", size = 188524, upload-time = "2026-04-12T21:44:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d9/9e9d7d7e5061b47540d03d640fab9b3965ba7ae49c1b2154861c8f007518/msgspec-0.21.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48943e278b3854c2f89f955ddc6f9f430d3f0784b16e47d10604ee0463cd21f5", size = 218880, upload-time = "2026-04-12T21:44:20.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/2bb344f34abb4b57e60c7c9c761994e0417b9718ec1460bf00c296f2a7ea/msgspec-0.21.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9aa659ebb0101b1cbc31461212b87e341d961f0ab0772aaf068a99e001ec4aa", size = 225050, upload-time = "2026-04-12T21:44:21.577Z" }, + { url = "https://files.pythonhosted.org/packages/1a/84/7c1e412f76092277bf760cef12b7979d03314d259ab5b5cafde5d0c1722d/msgspec-0.21.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7b27d1a8ead2b6f5b0c4f2d07b8be1ccfcc041c8a0e704781edebe3ae13c484", size = 222713, upload-time = "2026-04-12T21:44:22.83Z" }, + { url = "https://files.pythonhosted.org/packages/4e/27/0bba04b2b4ef05f3d068429410bc71d2cea925f1596a8f41152cccd5edb8/msgspec-0.21.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38fe93e86b61328fe544cb7fd871fad5a27c8734bfda90f65e5dbe288ae50f61", size = 227259, upload-time = "2026-04-12T21:44:24.11Z" }, +] + [[package]] name = "multidict" version = "6.7.1" @@ -3469,6 +3722,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d3/ac/686789b9145413f1a61878c407210e41bfdb097976864e0913078b24098c/myst_parser-5.0.0-py3-none-any.whl", hash = "sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211", size = 84533, upload-time = "2026-01-15T09:08:16.788Z" }, ] +[[package]] +name = "narwhals" +version = "2.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/62/3c/c4ef2164a71c1a63d7f1ae411c4082c5fa872405106db60a4b7114989ad7/narwhals-2.22.1.tar.gz", hash = "sha256:d62920805a0a43b7ff8b54b0c0d3142d796f8a9301836ada37e573d6a33cbcd9", size = 647493, upload-time = "2026-06-05T12:34:34.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/ca/36339329c4604adbcc99c899b7eb1ce1a555c499b6a6860757dc9bfed36d/narwhals-2.22.1-py3-none-any.whl", hash = "sha256:60567d774edf77db53906f89d9fbd164e66e56d66d388e1e6990f17ac33cfb53", size = 454815, upload-time = "2026-06-05T12:34:32.289Z" }, +] + [[package]] name = "nbclient" version = "0.10.4" @@ -5703,6 +5965,49 @@ requires-dist = [ { name = "switchyard", editable = "plugins/nemo-switchyard/vendor/switchyard" }, ] +[[package]] +name = "nemo-unsloth-plugin" +version = "0.1.0" +source = { editable = "plugins/nemo-unsloth" } +dependencies = [ + { name = "nemo-platform", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nemo-platform-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nmp-unsloth", 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 = "pydantic-settings", 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 = "typer", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[package.dev-dependencies] +dev = [ + { 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 = "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-customizer-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pytest", 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 = "pytest-asyncio", 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 = "ruff", 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 = "nemo-platform", editable = "packages/nemo_platform" }, + { name = "nemo-platform-plugin", editable = "packages/nemo_platform_plugin" }, + { name = "nmp-unsloth", editable = "services/unsloth" }, + { name = "pydantic", specifier = ">=2.10.6" }, + { name = "pydantic-settings", specifier = ">=2.6.1" }, + { name = "typer", specifier = ">=0.12.5" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "fastapi", specifier = ">=0.115.0" }, + { name = "httpx", specifier = ">=0.27.0" }, + { name = "nemo-customizer-plugin", editable = "plugins/nemo-customizer" }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-asyncio", specifier = ">=0.25.3" }, + { name = "ruff", specifier = ">=0.11.8" }, +] + [[package]] name = "nemoguardrails" version = "0.21.0" @@ -5803,6 +6108,7 @@ core-services = [ { name = "nemo-platform-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-safe-synthesizer-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-switchyard", 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-unsloth-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nmp-auth", 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 = "nmp-common", 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 = "nmp-entities", 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')" }, @@ -5812,6 +6118,7 @@ core-services = [ { name = "nmp-models", 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 = "nmp-platform", 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 = "nmp-secrets", 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 = "nmp-unsloth", 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')" }, ] cpu-tasks = [ { name = "nemo-anonymizer-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')" }, @@ -5898,6 +6205,8 @@ enabled-plugins = [ { name = "nemo-guardrails-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-safe-synthesizer-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-switchyard", 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-unsloth-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nmp-unsloth", 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')" }, ] functional-services = [ { name = "nemo-agents-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')" }, @@ -5912,6 +6221,7 @@ functional-services = [ { name = "nemo-platform-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-safe-synthesizer-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-switchyard", 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-unsloth-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nmp-auth", 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 = "nmp-common", 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 = "nmp-customizer", 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')" }, @@ -5928,6 +6238,7 @@ functional-services = [ { name = "nmp-platform-seed", 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 = "nmp-secrets", 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 = "nmp-studio", 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 = "nmp-unsloth", 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')" }, ] gpu-tasks = [ { name = "nemo-platform", 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')" }, @@ -6006,6 +6317,7 @@ core-services = [ { name = "nemo-platform-plugin", editable = "packages/nemo_platform_plugin" }, { name = "nemo-safe-synthesizer-plugin", editable = "plugins/nemo-safe-synthesizer" }, { name = "nemo-switchyard", editable = "plugins/nemo-switchyard" }, + { name = "nemo-unsloth-plugin", editable = "plugins/nemo-unsloth" }, { name = "nmp-auth", editable = "services/core/auth" }, { name = "nmp-common", editable = "packages/nmp_common" }, { name = "nmp-entities", editable = "services/core/entities" }, @@ -6015,6 +6327,7 @@ core-services = [ { name = "nmp-models", editable = "services/core/models" }, { name = "nmp-platform", editable = "packages/nmp_platform" }, { name = "nmp-secrets", editable = "services/core/secrets" }, + { name = "nmp-unsloth", editable = "services/unsloth" }, ] cpu-tasks = [ { name = "nemo-anonymizer-plugin", editable = "plugins/nemo-anonymizer" }, @@ -6103,6 +6416,8 @@ enabled-plugins = [ { name = "nemo-guardrails-plugin", editable = "plugins/nemo-guardrails" }, { name = "nemo-safe-synthesizer-plugin", editable = "plugins/nemo-safe-synthesizer" }, { name = "nemo-switchyard", editable = "plugins/nemo-switchyard" }, + { name = "nemo-unsloth-plugin", editable = "plugins/nemo-unsloth" }, + { name = "nmp-unsloth", editable = "services/unsloth" }, ] functional-services = [ { name = "nemo-agents-plugin", editable = "plugins/nemo-agents" }, @@ -6118,6 +6433,7 @@ functional-services = [ { name = "nemo-platform-plugin", editable = "packages/nemo_platform_plugin" }, { name = "nemo-safe-synthesizer-plugin", editable = "plugins/nemo-safe-synthesizer" }, { name = "nemo-switchyard", editable = "plugins/nemo-switchyard" }, + { name = "nemo-unsloth-plugin", editable = "plugins/nemo-unsloth" }, { name = "nmp-auth", editable = "services/core/auth" }, { name = "nmp-common", editable = "packages/nmp_common" }, { name = "nmp-customizer", editable = "services/customizer" }, @@ -6134,6 +6450,7 @@ functional-services = [ { name = "nmp-platform-seed", editable = "services/platform-seed" }, { name = "nmp-secrets", editable = "services/core/secrets" }, { name = "nmp-studio", editable = "services/studio" }, + { name = "nmp-unsloth", editable = "services/unsloth" }, ] gpu-tasks = [ { name = "nemo-platform", editable = "packages/nemo_platform" }, @@ -7275,6 +7592,48 @@ requires-dist = [ { name = "werkzeug", specifier = ">=2.0.0" }, ] +[[package]] +name = "nmp-unsloth" +version = "0.1.0" +source = { editable = "services/unsloth" } +dependencies = [ + { 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-platform-sdk", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nmp-common", 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 = "pydantic-settings", 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 = "tenacity", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[package.optional-dependencies] +unsloth = [ + { name = "unsloth", extra = ["huggingface"], marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest", 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 = "pytest-asyncio", 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 = "httpx", specifier = ">=0.27.0" }, + { name = "nemo-platform-sdk", editable = "sdk/python/nemo-platform" }, + { name = "nmp-common", editable = "packages/nmp_common" }, + { name = "pydantic", specifier = ">=2.10.6" }, + { name = "pydantic-settings", specifier = ">=2.6.1" }, + { name = "tenacity", specifier = ">=8.5.0" }, + { name = "unsloth", extras = ["huggingface"], marker = "extra == 'unsloth'" }, +] +provides-extras = ["unsloth"] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-asyncio", specifier = ">=0.25.3" }, +] + [[package]] name = "nodeenv" version = "1.10.0" @@ -7366,6 +7725,108 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, ] +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, +] + [[package]] name = "nvidia-ml-py" version = "13.595.45" @@ -7500,6 +7961,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/01/9ec9a91d9317158e200d424968d5ea49524fa6e0af85a6ec77f2a9f730d2/nvidia_nat_opentelemetry-1.7.0-py3-none-any.whl", hash = "sha256:e468fb4bd2a9e2fa4fb0d67daa1dea22b900c70301249d2785aa96f305808f7f", size = 68848, upload-time = "2026-05-21T19:19:34.156Z" }, ] +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, +] + [[package]] name = "oauthlib" version = "3.3.1" @@ -7598,6 +8091,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, ] +[[package]] +name = "opencv-python" +version = "4.13.0.92" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/6f/5a28fef4c4a382be06afe3938c64cc168223016fa520c5abaf37e8862aa5/opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:caf60c071ec391ba51ed00a4a920f996d0b64e3e46068aac1f646b5de0326a19", size = 46247052, upload-time = "2026-02-05T07:01:25.046Z" }, +] + [[package]] name = "openevals" version = "0.2.0" @@ -8069,6 +8573,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] +[[package]] +name = "peft" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accelerate", 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 = "huggingface-hub", 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 = "numpy", 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 = "packaging", 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 = "psutil", 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 = "safetensors", 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 = "torch", 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 = "tqdm", 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 = "transformers", 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')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/cf/037f1e3d5186496c05513a6754639e2dab3038a05f384284d49a9bd06a2d/peft-0.19.1.tar.gz", hash = "sha256:0d97542fe96dcdaa20d3b81c06f26f988618f416a73544ab23c3618ccb674a40", size = 763738, upload-time = "2026-04-16T15:46:45.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/b6/f54d676ed93cc2dd2234c3b172ea9c8c3d7d29361e66b1b23dec57a67465/peft-0.19.1-py3-none-any.whl", hash = "sha256:2113f72a81621b5913ef28f9022204c742df111890c5f49d812716a4a301e356", size = 680692, upload-time = "2026-04-16T15:46:42.886Z" }, +] + [[package]] name = "pexpect" version = "4.9.0" @@ -9342,6 +9867,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/03/53ecffb459f5c58b8712e9fde57e8e2a011274eb7ec1f8de3db4641039a2/safetensors-0.8.0rc1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cf949f6a37287572de1c47294b36131bf49528436724eec2f96015f75a3d0bc8", size = 722435, upload-time = "2026-06-01T09:54:52.767Z" }, ] +[[package]] +name = "scikit-learn" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib", 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 = "narwhals", 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 = "numpy", 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 = "scipy", 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 = "threadpoolctl", 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')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/e2/ff880f62677a17d035817d543cb0fc8727d01eccbee81c5f7fc733a9d856/scikit_learn-1.9.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f401448645a3e7bc115aa3c094097865155b34bff1cba8101857d9104e99074c", size = 8256782, upload-time = "2026-06-02T11:53:08.904Z" }, + { url = "https://files.pythonhosted.org/packages/25/64/eb40435e1a508ab1b4e284ce43ae80f6a162e5be5e38ed5a6fab467a9ea4/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd3a8ef0c758555a3b23c03adaa858af32f7736785ded50ad5991f59c4ed03fa", size = 8992419, upload-time = "2026-06-02T11:53:11.551Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/4810a28e473185429e45a57eebcc91fc991b33d889cc0676063e671db03d/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7e254636164090da847715a27f8e5478feb98c40a9e0ee90cbd277de9e5ceb8", size = 9281411, upload-time = "2026-06-02T11:53:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d5/2b5148f2279196775e1db2aeb85d14b70ac80e7e32b3b28e7ebeafb0901d/scikit_learn-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5be45aa4a42a68a533913a6ed736cf309de2226411c79ef8d609a5456f1939b1", size = 8261512, upload-time = "2026-06-02T11:53:27.183Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ee/5adbc77656b71f9456a2f5a7a9fdb4bcf9207a6b962889f1c2f9323afa4e/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e50ed4da51974e86e940690e9a3d82e729b62b5a49f7c9bac534d515d39d86f", size = 8837603, upload-time = "2026-06-02T11:53:30.328Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/63fdda36c56437eeb44aaf9493c8bcd62ce230ab1598924fc626ffbfa943/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8", size = 9132097, upload-time = "2026-06-02T11:53:33.456Z" }, + { url = "https://files.pythonhosted.org/packages/3e/04/5acd7ae280c5f93b6ac5ef6cdec14eef4c8d1cd91d85b3292989c94d96b1/scikit_learn-1.9.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5b934c45c252844a91d69fda3a34cff5e7307e1db10d77cb10a3980312c74713", size = 8228299, upload-time = "2026-06-02T11:53:44.817Z" }, + { url = "https://files.pythonhosted.org/packages/0c/39/ffe829a5b8ecb40a518724a997794657fdc354ada5e8fe8e64d998c0bac9/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:38c3dcb9a1ffb85505ec53d54c7b4aea0cff70050425a7760c2af661ac85df05", size = 8789690, upload-time = "2026-06-02T11:53:47.461Z" }, + { url = "https://files.pythonhosted.org/packages/1f/88/8dab5de10c638c083772a6be83a3d8106ced492f74a928c8693638e5bb50/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da76d09304a4706db7cc1e3ebaa3b6b98a67365cc11d2996c4f1e58ba47df714", size = 9087723, upload-time = "2026-06-02T11:53:50.702Z" }, +] + [[package]] name = "scikit-network" version = "0.33.5" @@ -9420,6 +9969,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, ] +[[package]] +name = "sentence-transformers" +version = "5.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", 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 = "numpy", 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 = "scikit-learn", 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 = "scipy", 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 = "torch", 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 = "tqdm", 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 = "transformers", 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 = "typing-extensions", 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')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/d4/7ef93157485e978c016f49da05363c1e4e7237beb5343b64b5631101f0f1/sentence_transformers-5.5.1.tar.gz", hash = "sha256:02b7740dfc60bdbbcb6061625f5d97a5c1a4e2d3baac5f9391b912bb5eae2290", size = 445161, upload-time = "2026-05-20T07:37:44.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/03/ee99a6b030e7a2e056547729f8a4709dd93e13d9c6f07590f74c395c4017/sentence_transformers-5.5.1-py3-none-any.whl", hash = "sha256:4fe11d433badc5282d32f7fc08bc714216b7a5aca426f9df77a45a554756deb7", size = 588887, upload-time = "2026-05-20T07:37:43.004Z" }, +] + +[[package]] +name = "sentencepiece" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/15/2e7a025fc62d764b151ae6d0f2a92f8081755ebe8d4a64099accc6f77ba6/sentencepiece-0.2.1.tar.gz", hash = "sha256:8138cec27c2f2282f4a34d9a016e3374cd40e5c6e9cb335063db66a0a3b71fad", size = 3228515, upload-time = "2025-08-12T07:00:51.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/15/46afbab00733d81788b64be430ca1b93011bb9388527958e26cc31832de5/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6356d0986b8b8dc351b943150fcd81a1c6e6e4d439772e8584c64230e58ca987", size = 1942560, upload-time = "2025-08-12T06:59:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/bb/88/2b41e07bd24f33dcf2f18ec3b74247aa4af3526bad8907b8727ea3caba03/sentencepiece-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02593eca45440ef39247cee8c47322a34bdcc1d8ae83ad28ba5a899a2cf8d79a", size = 1253319, upload-time = "2025-08-12T06:59:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a0/54/38a1af0c6210a3c6f95aa46d23d6640636d020fba7135cd0d9a84ada05a7/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a0d15781a171d188b661ae4bde1d998c303f6bd8621498c50c671bd45a4798e", size = 1316162, upload-time = "2025-08-12T06:59:30.914Z" }, + { url = "https://files.pythonhosted.org/packages/ef/66/fb191403ade791ad2c3c1e72fe8413e63781b08cfa3aa4c9dfc536d6e795/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f5a3e0d9f445ed9d66c0fec47d4b23d12cfc858b407a03c194c1b26c2ac2a63", size = 1387785, upload-time = "2025-08-12T06:59:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/4a/be/32ce495aa1d0e0c323dcb1ba87096037358edee539cac5baf8755a6bd396/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57cae326c8727de58c85977b175af132a7138d84c764635d7e71bbee7e774133", size = 1943152, upload-time = "2025-08-12T06:59:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/19/84/42eb3ce4796777a1b5d3699dfd4dca85113e68b637f194a6c8d786f16a04/sentencepiece-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9381351182ff9888cc80e41c632e7e274b106f450de33d67a9e8f6043da6f76", size = 1253645, upload-time = "2025-08-12T06:59:42.903Z" }, + { url = "https://files.pythonhosted.org/packages/89/fa/d3d5ebcba3cb9e6d3775a096251860c41a6bc53a1b9461151df83fe93255/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f955df238021bf11f0fc37cdb54fd5e5b5f7fd30ecc3d93fb48b6815437167", size = 1316273, upload-time = "2025-08-12T06:59:44.476Z" }, + { url = "https://files.pythonhosted.org/packages/04/88/14f2f4a2b922d8b39be45bf63d79e6cd3a9b2f248b2fcb98a69b12af12f5/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdfecef430d985f1c2bcbfff3defd1d95dae876fbd0173376012d2d7d24044b", size = 1387881, upload-time = "2025-08-12T06:59:46.09Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4a/85fbe1706d4d04a7e826b53f327c4b80f849cf1c7b7c5e31a20a97d8f28b/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dcd8161eee7b41aae57ded06272905dbd680a0a04b91edd0f64790c796b2f706", size = 1943150, upload-time = "2025-08-12T06:59:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/8d/de/5a007fb53b1ab0aafc69d11a5a3dd72a289d5a3e78dcf2c3a3d9b14ffe93/sentencepiece-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:097f3394e99456e9e4efba1737c3749d7e23563dd1588ce71a3d007f25475fff", size = 1253641, upload-time = "2025-08-12T06:59:56.562Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d2/f552be5928105588f4f4d66ee37dd4c61460d8097e62d0e2e0eec41bc61d/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b670879c370d350557edabadbad1f6561a9e6968126e6debca4029e5547820", size = 1316271, upload-time = "2025-08-12T06:59:58.109Z" }, + { url = "https://files.pythonhosted.org/packages/96/df/0cfe748ace5485be740fed9476dee7877f109da32ed0d280312c94ec259f/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7f0fd2f2693309e6628aeeb2e2faf6edd221134dfccac3308ca0de01f8dab47", size = 1387882, upload-time = "2025-08-12T07:00:00.701Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b6/08fe2ce819e02ccb0296f4843e3f195764ce9829cbda61b7513f29b95718/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8dd4b477a7b069648d19363aad0cab9bad2f4e83b2d179be668efa672500dc94", size = 1946052, upload-time = "2025-08-12T07:00:08.136Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/1fb26e8a21613f6200e1ab88824d5d203714162cf2883248b517deb500b7/sentencepiece-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad8493bea8432dae8d6830365352350f3b4144415a1d09c4c8cb8d30cf3b6c3c", size = 1254857, upload-time = "2025-08-12T07:00:11.021Z" }, + { url = "https://files.pythonhosted.org/packages/bc/85/c72fd1f3c7a6010544d6ae07f8ddb38b5e2a7e33bd4318f87266c0bbafbf/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b81a24733726e3678d2db63619acc5a8dccd074f7aa7a54ecd5ca33ca6d2d596", size = 1315722, upload-time = "2025-08-12T07:00:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e8/661e5bd82a8aa641fd6c1020bd0e890ef73230a2b7215ddf9c8cd8e941c2/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a81799d0a68d618e89063fb423c3001a034c893069135ffe51fee439ae474d6", size = 1387452, upload-time = "2025-08-12T07:00:15.088Z" }, +] + [[package]] name = "sentry-sdk" version = "2.57.0" @@ -9517,6 +10109,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, ] +[[package]] +name = "sounddevice" +version = "0.5.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/f9/2592608737553638fca98e21e54bfec40bf577bb98a61b2770c912aab25e/sounddevice-0.5.5.tar.gz", hash = "sha256:22487b65198cb5bf2208755105b524f78ad173e5ab6b445bdab1c989f6698df3", size = 143191, upload-time = "2026-01-23T18:36:43.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/0a/478e441fd049002cf308520c0d62dd8333e7c6cc8d997f0dda07b9fbcc46/sounddevice-0.5.5-py3-none-any.whl", hash = "sha256:30ff99f6c107f49d25ad16a45cacd8d91c25a1bcdd3e81a206b921a3a6405b1f", size = 32807, upload-time = "2026-01-23T18:36:35.649Z" }, + { url = "https://files.pythonhosted.org/packages/56/f9/c037c35f6d0b6bc3bc7bfb314f1d6f1f9a341328ef47cd63fc4f850a7b27/sounddevice-0.5.5-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:05eb9fd6c54c38d67741441c19164c0dae8ce80453af2d8c4ad2e7823d15b722", size = 108557, upload-time = "2026-01-23T18:36:37.41Z" }, +] + [[package]] name = "soupsieve" version = "2.8.3" @@ -9858,6 +10463,15 @@ clickhouse = [ { name = "clickhouse-driver", 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]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + [[package]] name = "tiktoken" version = "0.12.0" @@ -9979,6 +10593,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, ] +[[package]] +name = "torch" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "filelock", 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 = "fsspec", 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 = "jinja2", 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 = "networkx", 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 = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools", 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 = "sympy", 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 = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/8b/4b61d6e13f7108f36910df9ab4b58fd389cc2520d54d81b88660804aad99/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:418997cb02d0a0f1497cf6a09f63166f9f5df9f3e16c8a716ab76a72127c714f", size = 79423467, upload-time = "2026-02-10T21:44:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/7b562f1808d3f65414cd80a4f7d4bb00979d9355616c034c171249e1a303/torch-2.10.0-3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac5bdcbb074384c66fa160c15b1ead77839e3fe7ed117d667249afce0acabfac", size = 915518691, upload-time = "2026-03-11T14:15:43.147Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/abada41517ce0011775f0f4eacc79659bc9bc6c361e6bfe6f7052a6b9363/torch-2.10.0-3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:98c01b8bb5e3240426dcde1446eed6f40c778091c8544767ef1168fc663a05a6", size = 915622781, upload-time = "2026-03-11T14:17:11.354Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:80b1b5bfe38eb0e9f5ff09f206dcac0a87aadd084230d4a36eea5ec5232c115b", size = 915627275, upload-time = "2026-03-11T14:16:11.325Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/72bf18847f58f877a6a8acf60614b14935e2f156d942483af1ffc081aea0/torch-2.10.0-3-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:46b3574d93a2a8134b3f5475cfb98e2eb46771794c57015f6ad1fb795ec25e49", size = 915523474, upload-time = "2026-03-11T14:17:44.422Z" }, + { url = "https://files.pythonhosted.org/packages/78/89/f5554b13ebd71e05c0b002f95148033e730d3f7067f67423026cc9c69410/torch-2.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3282d9febd1e4e476630a099692b44fdc214ee9bf8ee5377732d9d9dfe5712e4", size = 145992610, upload-time = "2026-01-21T16:25:26.327Z" }, + { url = "https://files.pythonhosted.org/packages/ae/30/a3a2120621bf9c17779b169fc17e3dc29b230c29d0f8222f499f5e159aa8/torch-2.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2f9edd8dbc99f62bc4dfb78af7bf89499bca3d753423ac1b4e06592e467b763", size = 915607863, upload-time = "2026-01-21T16:25:06.696Z" }, + { url = "https://files.pythonhosted.org/packages/61/d8/15b9d9d3a6b0c01b883787bd056acbe5cc321090d4b216d3ea89a8fcfdf3/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:b7bd80f3477b830dd166c707c5b0b82a898e7b16f59a7d9d42778dd058272e8b", size = 79423461, upload-time = "2026-01-21T16:24:50.266Z" }, + { url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" }, + { url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5c/dee910b87c4d5c0fcb41b50839ae04df87c1cfc663cf1b5fca7ea565eeaa/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294", size = 79498198, upload-time = "2026-01-21T16:24:34.704Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6f/f2e91e34e3fcba2e3fc8d8f74e7d6c22e74e480bbd1db7bc8900fdf3e95c/torch-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b", size = 146004247, upload-time = "2026-01-21T16:24:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/98/fb/5160261aeb5e1ee12ee95fe599d0541f7c976c3701d607d8fc29e623229f/torch-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738", size = 915716445, upload-time = "2026-01-21T16:22:45.353Z" }, + { url = "https://files.pythonhosted.org/packages/1a/0b/39929b148f4824bc3ad6f9f72a29d4ad865bcf7ebfc2fa67584773e083d2/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382", size = 79851305, upload-time = "2026-01-21T16:24:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d8/14/21fbce63bc452381ba5f74a2c0a959fdf5ad5803ccc0c654e752e0dbe91a/torch-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8", size = 146005472, upload-time = "2026-01-21T16:22:29.022Z" }, + { url = "https://files.pythonhosted.org/packages/54/fd/b207d1c525cb570ef47f3e9f836b154685011fce11a2f444ba8a4084d042/torch-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f", size = 915612644, upload-time = "2026-01-21T16:21:47.019Z" }, + { url = "https://files.pythonhosted.org/packages/0e/13/e76b4d9c160e89fff48bf16b449ea324bda84745d2ab30294c37c2434c0d/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f", size = 79498248, upload-time = "2026-01-21T16:23:09.315Z" }, +] + +[[package]] +name = "torchao" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/fe/a4036a8e80fa800c92dbcbf75f541cd4c106248b6b579db6dab1800f616a/torchao-0.17.0-cp310-abi3-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87a418ce0ec064a821ceab83c921b501acef0ce9a6ccd1be358fcd16c3ae8c58", size = 3206172, upload-time = "2026-03-30T22:25:52.974Z" }, + { url = "https://files.pythonhosted.org/packages/c9/37/ef37ca885265e5f79a168616767dd416a3cea1cc3b28bb6b503ce4a5b652/torchao-0.17.0-py3-none-any.whl", hash = "sha256:02eba449036715b9ae784fbaa1a6f97994bb7b0421ce92d1d5d1c08e5bd6d349", size = 1200680, upload-time = "2026-03-30T22:25:54.457Z" }, +] + +[[package]] +name = "torchvision" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", 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 = "pillow", 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 = "torch", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/be/c704bceaf11c4f6b19d64337a34a877fcdfe3bd68160a8c9ae9bea4a35a3/torchvision-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db74a551946b75d19f9996c419a799ffdf6a223ecf17c656f90da011f1d75b20", size = 1874923, upload-time = "2026-01-21T16:27:46.574Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/f143cd71232430de1f547ceab840f68c55e127d72558b1061a71d0b193cd/torchvision-0.25.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f49964f96644dbac2506dffe1a0a7ec0f2bf8cf7a588c3319fed26e6329ffdf3", size = 2344808, upload-time = "2026-01-21T16:27:43.191Z" }, + { url = "https://files.pythonhosted.org/packages/43/ae/ad5d6165797de234c9658752acb4fce65b78a6a18d82efdf8367c940d8da/torchvision-0.25.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:153c0d2cbc34b7cf2da19d73450f24ba36d2b75ec9211b9962b5022fb9e4ecee", size = 8070752, upload-time = "2026-01-21T16:27:33.748Z" }, + { url = "https://files.pythonhosted.org/packages/56/3a/6ea0d73f49a9bef38a1b3a92e8dd455cea58470985d25635beab93841748/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2abe430c90b1d5e552680037d68da4eb80a5852ebb1c811b2b89d299b10573b", size = 1874920, upload-time = "2026-01-21T16:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/51/f8/c0e1ef27c66e15406fece94930e7d6feee4cb6374bbc02d945a630d6426e/torchvision-0.25.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b75deafa2dfea3e2c2a525559b04783515e3463f6e830cb71de0fb7ea36fe233", size = 2344556, upload-time = "2026-01-21T16:27:40.125Z" }, + { url = "https://files.pythonhosted.org/packages/68/2f/f24b039169db474e8688f649377de082a965fbf85daf4e46c44412f1d15a/torchvision-0.25.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f25aa9e380865b11ea6e9d99d84df86b9cc959f1a007cd966fc6f1ab2ed0e248", size = 8072351, upload-time = "2026-01-21T16:27:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5b/1562a04a6a5a4cf8cf40016a0cdeda91ede75d6962cff7f809a85ae966a5/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:24e11199e4d84ba9c5ee7825ebdf1cd37ce8deec225117f10243cae984ced3ec", size = 1874918, upload-time = "2026-01-21T16:27:39.02Z" }, + { url = "https://files.pythonhosted.org/packages/36/b1/3d6c42f62c272ce34fcce609bb8939bdf873dab5f1b798fd4e880255f129/torchvision-0.25.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f271136d2d2c0b7a24c5671795c6e4fd8da4e0ea98aeb1041f62bc04c4370ef", size = 2309106, upload-time = "2026-01-21T16:27:30.624Z" }, + { url = "https://files.pythonhosted.org/packages/c7/60/59bb9c8b67cce356daeed4cb96a717caa4f69c9822f72e223a0eae7a9bd9/torchvision-0.25.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:855c0dc6d37f462482da7531c6788518baedca1e0847f3df42a911713acdfe52", size = 8071522, upload-time = "2026-01-21T16:27:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/52/99/dca81ed21ebaeff2b67cc9f815a20fdaa418b69f5f9ea4c6ed71721470db/torchvision-0.25.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a8f8061284395ce31bcd460f2169013382ccf411148ceb2ee38e718e9860f5a7", size = 1896209, upload-time = "2026-01-21T16:27:32.159Z" }, + { url = "https://files.pythonhosted.org/packages/28/cc/2103149761fdb4eaed58a53e8437b2d716d48f05174fab1d9fcf1e2a2244/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:146d02c9876858420adf41f3189fe90e3d6a409cbfa65454c09f25fb33bf7266", size = 2310735, upload-time = "2026-01-21T16:27:22.327Z" }, + { url = "https://files.pythonhosted.org/packages/76/ad/f4c985ad52ddd3b22711c588501be1b330adaeaf6850317f66751711b78c/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c4d395cb2c4a2712f6eb93a34476cdf7aae74bb6ea2ea1917f858e96344b00aa", size = 8089557, upload-time = "2026-01-21T16:27:27.666Z" }, +] + [[package]] name = "tornado" version = "6.5.5" @@ -10012,7 +10711,7 @@ wheels = [ [[package]] name = "transformers" -version = "5.10.2" +version = "5.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub", 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')" }, @@ -10025,9 +10724,58 @@ dependencies = [ { name = "tqdm", 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 = "typer", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8d/38/d5f978bd5091019e89aef29b9a831f5cd70f2598963a3ead8b9570cab592/transformers-5.10.2.tar.gz", hash = "sha256:f9a44b9c8ca9ab1156b467f574d832ea066284299c2fd0ed84641ccb592751fc", size = 8799687, upload-time = "2026-06-04T18:43:49.119Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/9d/fb46e729b461985f41a5740167688b924a4019141e5c164bea77548d3d9e/transformers-5.5.0.tar.gz", hash = "sha256:c8db656cf51c600cd8c75f06b20ef85c72e8b8ff9abc880c5d3e8bc70e0ddcbd", size = 8237745, upload-time = "2026-04-02T16:13:08.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/6f/e1564b0cc182afa05e219a8e09a8e770ffaab879b6b824b56c819bd221da/transformers-5.10.2-py3-none-any.whl", hash = "sha256:8a669db546f82c7c3618cb46ceb0f0afd89292bc70f319c058f8332ec63e268d", size = 11003830, upload-time = "2026-06-04T18:43:45.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/28/35f7411ff80a3640c1f4fc907dcbb6a65061ebb82f66950e38bfc9f7f740/transformers-5.5.0-py3-none-any.whl", hash = "sha256:821a9ff0961abbb29eb1eb686d78df1c85929fdf213a3fe49dc6bd94f9efa944", size = 10245591, upload-time = "2026-04-02T16:13:03.462Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, +] + +[[package]] +name = "triton" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/c1/5d842314bb6c78442cc60437928781701c6050b8d479bc2a1aed691d37ca/triton-3.7.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9e71fc392675fac364e0ecf4ef3f76f85b7f5433a16f4c3c5fe5f05a52c85fe", size = 188480277, upload-time = "2026-05-07T19:05:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/f7/13/ec05adfcd87311d532ba61e3af143e8be59fcd26675884c4682841406a20/triton-3.7.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4bf49b00a7a377a68a6da603a876e797614e6455a80e9021669c476a953ad9a", size = 188505104, upload-time = "2026-05-07T19:05:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/01/e1/a59a583de59b8f62c495d67c80ee3ea97d09e91ac80c4c6e76456ed8d8ac/triton-3.7.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abdf6beaa89b1bcfb9a43cd990536ce66091a997841a4814b260b7bee4c88c3c", size = 188503209, upload-time = "2026-05-07T19:05:17.935Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8f/0bea7a6a0c989315c9135a1d7fb37e41905cfb3a17cbc1f10044ebd4cc3a/triton-3.7.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc1d61c172d257db80ddf42595131fb196ad2e9bdd751e90fe2ef13531734e8b", size = 188612899, upload-time = "2026-05-07T19:05:24.955Z" }, +] + +[[package]] +name = "trl" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accelerate", 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 = "datasets", 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 = "transformers", 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')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/2e/30ece0055eee5763126e2d52f6e04aec294bcae34b46d9ca16c53c4b5852/trl-0.24.0.tar.gz", hash = "sha256:eee495223725d3da0596be2607581969db89ba0f7c00b075802addc31e61eac9", size = 368447, upload-time = "2025-10-16T00:10:37.65Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/5f/c647fedde9d59ae35ee189cc49e419da5ac1d9ad9933cb69401a7eac4705/trl-0.24.0-py3-none-any.whl", hash = "sha256:a9145b7d4a4a33778de117bda48530f0cf5b2ac25acc07db80ad04836f490dfc", size = 423143, upload-time = "2025-10-16T00:10:35.809Z" }, ] [[package]] @@ -10214,6 +10962,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "tyro" +version = "1.0.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docstring-parser", 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 = "typeguard", 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 = "typing-extensions", 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')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/d6/7126f9e7de139632134d59b5d1972e93c610ee2cb13829e8f4f48f6613cb/tyro-1.0.13.tar.gz", hash = "sha256:731a90c9836b77fffe7c3fa0477ef2d3b6fa91252ddc0bb4d32dadd4fcc143d4", size = 489479, upload-time = "2026-04-14T18:21:52.888Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/4f/c43a0a8f0c66fd40a1d6cc47332a5a1d1043e9b331f7070ea701b91a7598/tyro-1.0.13-py3-none-any.whl", hash = "sha256:a0bdb8462c551dd84fc00a76916ce4d37e879c84eefaf34e2165312407cc6c09", size = 185221, upload-time = "2026-04-14T18:21:54.328Z" }, +] + [[package]] name = "tzdata" version = "2025.3" @@ -10241,6 +11003,110 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ff/7f/4320d9ce3be404e6310b915c3629fe27bf1e2f438a1a7a3cb0396e32e9a9/uncalled_for-0.2.0-py3-none-any.whl", hash = "sha256:2c0bd338faff5f930918f79e7eb9ff48290df2cb05fcc0b40a7f334e55d4d85f", size = 11351, upload-time = "2026-02-27T17:40:56.804Z" }, ] +[[package]] +name = "unsloth" +version = "2026.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accelerate", 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 = "bitsandbytes", 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 = "datasets", 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 = "diffusers", 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 = "hf-transfer", 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 = "huggingface-hub", 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 = "nest-asyncio", 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 = "numpy", 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 = "packaging", 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 = "peft", 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 = "psutil", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pydantic", extra = ["email"], marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pyyaml", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sentencepiece", 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 = "torch", 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 = "torchvision", 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 = "tqdm", 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 = "transformers", 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 = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and 'linux' in sys_platform" }, + { name = "triton", version = "3.7.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'arm64' and sys_platform == 'darwin' and 'linux' in sys_platform) or (platform_machine == 'aarch64' and sys_platform == 'linux' and 'linux' in sys_platform)" }, + { name = "trl", 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 = "typer", 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 = "tyro", 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 = "unsloth-zoo", 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 = "wheel", 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 = "xformers", marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and 'linux' in sys_platform" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/a9/08c3ad96aaf2d29c3d011f0824b7f1617a4ea0afae815bd0295bc8ecf0ec/unsloth-2026.6.1.tar.gz", hash = "sha256:f4ce75af8349500d9927f28ac5872d9a627e718b65604646c41c1717107bb81d", size = 76174449, upload-time = "2026-06-03T14:33:35.197Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/14/00e0318306ed536ce915f8b895fd6206a3a2029e8749b6d85b7fccbf0201/unsloth-2026.6.1-py3-none-any.whl", hash = "sha256:5084fff4571d527f4a61ded79e628243c13234e5da6d76705fcc54cd9e78c654", size = 71613685, upload-time = "2026-06-03T14:33:30.848Z" }, +] + +[package.optional-dependencies] +huggingface = [ + { name = "accelerate", 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 = "datasets", 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 = "diffusers", 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 = "hf-transfer", 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 = "huggingface-hub", 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 = "nest-asyncio", 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 = "numpy", 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 = "packaging", 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 = "peft", 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 = "psutil", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pydantic", extra = ["email"], marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pyyaml", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sentence-transformers", 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 = "sentencepiece", 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 = "torchvision", 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 = "tqdm", 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 = "transformers", 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 = "trl", 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 = "typer", 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 = "tyro", 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 = "unsloth-zoo", 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 = "wheel", 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]] +name = "unsloth-zoo" +version = "2026.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accelerate", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "cut-cross-entropy", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "datasets", 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 = "filelock", 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 = "hf-transfer", 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 = "huggingface-hub", 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 = "mlx", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "mlx-lm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "mlx-vlm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, + { name = "msgspec", 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 = "numpy", 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 = "packaging", 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 = "peft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pillow", 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 = "psutil", 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 = "regex", 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 = "sentencepiece", 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 = "torch", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "torchao", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "tqdm", 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 = "transformers", 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 = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and 'linux' in sys_platform" }, + { name = "triton", version = "3.7.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'arm64' and sys_platform == 'darwin' and 'linux' in sys_platform) or (platform_machine == 'aarch64' and sys_platform == 'linux' and 'linux' in sys_platform)" }, + { name = "trl", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "typing-extensions", 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 = "tyro", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "wheel", 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')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/83/c22583e8d9f1c88c0cb33542c48c0a364ba50e64891a5df7788f09b42742/unsloth_zoo-2026.6.1.tar.gz", hash = "sha256:6feaeb6cac35cd56db2c35b0d36eb9594efd12ffaae8c2287c2415677af66711", size = 846182, upload-time = "2026-06-03T13:34:40.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/11/4306117c203a74ab5dcb33eba33d6b6ba327e4712c2a9ed5935461d6c862/unsloth_zoo-2026.6.1-py3-none-any.whl", hash = "sha256:0ac07a2ab9aba8fb360dd477074cfd810009c7b52f2c78b69cf92fdc7a69aa8c", size = 924415, upload-time = "2026-06-03T13:34:38.212Z" }, +] + [[package]] name = "uri-template" version = "1.3.0" @@ -10487,6 +11353,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/b2/0bba9bbb4596d2d2f285a16c2ab04118f6b957d8441566e1abb892e6a6b2/werkzeug-3.1.7-py3-none-any.whl", hash = "sha256:4b314d81163a3e1a169b6a0be2a000a0e204e8873c5de6586f453c55688d422f", size = 226295, upload-time = "2026-03-24T01:08:06.133Z" }, ] +[[package]] +name = "wheel" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging", 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')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/62/75f18a0f03b4219c456652c7780e4d749b929eb605c098ce3a5b6b6bc081/wheel-0.47.0.tar.gz", hash = "sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3", size = 63854, upload-time = "2026-04-22T15:51:27.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl", hash = "sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced", size = 32218, upload-time = "2026-04-22T15:51:26.296Z" }, +] + [[package]] name = "wikipedia" version = "1.4.0" @@ -10533,6 +11411,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/03/030b47fd46b60fc87af548e57ff59c2ca84b2a1dadbe721bb0ce33896b2e/xdg_base_dirs-6.0.2-py3-none-any.whl", hash = "sha256:3c01d1b758ed4ace150ac960ac0bd13ce4542b9e2cdf01312dcda5012cfebabe", size = 4747, upload-time = "2024-10-19T14:35:05.931Z" }, ] +[[package]] +name = "xformers" +version = "0.0.35" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "torch", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/5a/6e27734bd793adc44d0b8d294e67cfacf4ec590572c1aef51d683fc7a791/xformers-0.0.35.tar.gz", hash = "sha256:f7fc183a58e4bf0e2ae339a18fb1b1d4a37854c0f2545b4f360fef001646ab76", size = 4258182, upload-time = "2026-02-20T20:33:05.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/85/6d71f9b16f2ac647877e66ed4af723b3fbd477806ab8b8a89d39a362b85f/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl", hash = "sha256:ccc73c7db9890224ab05f5fb60e2034f9e6c8672a10be0cf00e95cbbae3eda7c", size = 3264751, upload-time = "2026-02-20T20:33:02.444Z" }, +] + [[package]] name = "xxhash" version = "3.6.0" From 94139baf2eab2b0c903fcd1d669d0f06a09acb71 Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Wed, 3 Jun 2026 13:04:02 -0600 Subject: [PATCH 04/26] Cleanup and PR comment addressing - Merge unsloth docker bake into top level docker bake - Potential fix for pull request finding 'CodeQL / Empty except' - fix remaining tests - Update e2e conftest - Fix code rabbit changes - code rabbit is incorrect, ctx is definitely required - Cleanup the image and registries in docker-bake.hcl - Add both platforms to the workspace steps - Identify sub-plugins auth in the customizer router - reduce build time on base - Regenerate stainless - fix python types - remove dead code - Fix skill to not reference run for unsloth - fix bug with validation path on unsloth - Fix unsloth build caching - Fix _pickle.PicklingError: Can't pickle : it's not the same object as trl.trainer.sft_config.SFTConfig - Add progress callback to unsloth - Force the agent to avoid 2>&1 - General scripts shouldn't be tied to automodel Signed-off-by: Sam Oluwalana --- .github/trufflehog-exclude.txt | 3 + .github/workflows/ci.yaml | 1 + .github/workflows/security.yaml | 1 + conftest.py | 5 +- docker-bake.automodel.hcl => docker-bake.hcl | 101 +- docs/set-up/config-reference.md | 2 - e2e/conftest.py | 103 +- openapi/ga/individual/platform.openapi.yaml | 709 +--- openapi/ga/openapi.yaml | 709 +--- openapi/openapi.yaml | 709 +--- packages/nemo_platform/pyproject.toml | 11 +- .../cli/core/help_formatter.py | 2 +- .../src/nemo_platform_plugin/authz.py | 18 + .../nemo_platform_plugin/authz_discovery.py | 2 + .../src/nemo_platform_plugin/commands.py | 36 +- .../src/nemo_platform_plugin/config.py | 24 +- .../customization_contributor.py | 14 +- .../src/nemo_platform_plugin/discovery.py | 20 +- .../src/nemo_platform_plugin/entities.py | 2 +- .../jobs/result_manager.py | 6 +- .../nemo_platform_plugin/tests/test_authz.py | 80 +- .../tests/test_cli_progress.py | 2 +- .../tests/test_dispatcher.py | 2 +- packages/nmp_platform/config/local.env | 6 +- packages/nmp_platform/config/local.yaml | 2 +- .../src/nmp/platform_runner/config.py | 5 +- .../src/nmp/platform_runner/config/local.yaml | 16 +- .../nmp_platform_runner/tests/test_config.py | 13 + .../src/calculator_agent/register.py | 8 +- .../nemo_agents_plugin/jobs/analyze_batch.py | 2 +- .../nemo_agents_plugin/jobs/evaluate_agent.py | 2 +- .../jobs/optimize_skills.py | 2 +- .../telemetry/files_service_exporter.py | 14 +- .../tests/unit/test_runner_controller.py | 2 +- plugins/nemo-agents/tests/unit/test_utils.py | 50 +- .../nemo-anonymizer/tests/unit/test_input.py | 6 +- .../tests/unit/test_preview_function.py | 2 +- .../tests/unit/test_routing.py | 2 +- plugins/nemo-auditor/openapi/openapi.yaml | 77 +- plugins/nemo-automodel/SCOPE.md | 967 ----- .../src/nemo_automodel_plugin/cli/inputs.py | 6 +- .../src/nemo_automodel_plugin/contributor.py | 2 +- .../src/nemo_automodel_plugin/schema.py | 72 +- .../src/nemo_automodel_plugin/transform.py | 2 +- .../src/nemo_customizer/router.py | 37 +- .../skills/nemo-customizer/SKILL.md | 164 +- .../references/hyperparameters.md | 42 +- .../references/troubleshooting.md | 160 +- ...model_job.sh => poll_customization_job.sh} | 8 +- .../skills/nemo-customizer/tests.json | 8 +- plugins/nemo-customizer/tests/test_router.py | 60 + plugins/nemo-customizer/tests/test_skills.py | 2 + .../cli/renderers.py | 4 +- .../tests/unit/test_middleware.py | 2 +- plugins/nemo-unsloth/README.md | 2 +- .../src/nemo_unsloth_plugin/__init__.py | 2 +- .../src/nemo_unsloth_plugin/cli/inputs.py | 6 +- .../src/nemo_unsloth_plugin/jobs/jobs.py | 10 +- .../src/nemo_unsloth_plugin/transform.py | 2 +- plugins/nemo-unsloth/tests/test_cli.py | 1 - .../nemo-unsloth/tests/test_contributor.py | 17 +- plugins/nemo-unsloth/tests/test_jobs.py | 4 +- plugins/nemo-unsloth/tests/test_schema.py | 6 +- pyproject.toml | 144 +- pytest.ini | 3 +- .../nemo-platform/.nmpcontext/openapi.yaml | 709 +--- .../nemo-platform/.nmpcontext/stainless.yaml | 45 +- .../nemo_platform/cli/core/help_formatter.py | 2 +- .../nemo_platform/resources/evaluation/api.md | 2 +- .../src/nemo_platform/resources/files/api.md | 2 +- .../nemo_platform/resources/files/filesets.py | 10 +- .../nemo_platform/resources/guardrail/api.md | 15 +- .../src/nemo_platform/resources/jobs/api.md | 5 - .../src/nemo_platform/resources/jobs/jobs.py | 1 - .../types/evaluation/extended_benchmark.py | 2 +- .../nemo_platform/types/evaluation/fileset.py | 4 +- .../types/evaluation/fileset_param.py | 4 +- .../src/nemo_platform/types/files/__init__.py | 2 - .../types/files/fileset_create_params.py | 4 +- .../types/files/fileset_metadata_param.py | 46 - .../types/files/fileset_update_params.py | 4 +- .../types/shared_params/__init__.py | 1 + .../fileset_metadata.py} | 8 +- .../evaluation/test_benchmark_job_results.py | 8 +- .../evaluation/test_benchmark_jobs.py | 20 +- .../evaluation/test_benchmarks.py | 20 +- .../evaluation/test_metric_job_results.py | 8 +- .../evaluation/test_metric_jobs.py | 20 +- .../api_resources/evaluation/test_metrics.py | 860 +---- .../nemo_platform_ext/cli/test_app.py | 3 +- sdk/stainless.yaml | 45 +- services/automodel/README.md | 2 + .../automodel/docker/Dockerfile.mamba-wheel | 2 +- .../docker/Dockerfile.nmp-automodel-base | 26 +- .../docker/Dockerfile.nmp-automodel-tasks | 3 + .../docker/Dockerfile.nmp-automodel-training | 3 + services/automodel/docker/README.md | 10 +- .../docker}/cherry-picks/e6d2930a.diff | 0 services/automodel/docker/docker-bake.hcl | 4 +- services/automodel/docker/locks/README.md | 2 +- .../docs/automodel_errors.md | 58 +- .../automodel/src/nmp/automodel/adapter.py | 7 +- .../src/nmp/automodel/app/__init__.py | 2 +- .../src/nmp/automodel/app/jobs/compiler.py | 20 +- .../automodel/app/jobs/training/compiler.py | 20 +- .../automodel/src/nmp/automodel/compile.py | 2 +- .../tasks/docker/docker-compose.yaml | 2 +- .../src/nmp/automodel/tasks/file_io/run.py | 4 +- .../tasks/training/backends/finetune.py | 6 +- .../automodel/tests/contract}/README.md | 14 +- .../tests/contract}/generate_configs.py | 12 +- .../embed-1b/embed_1b_full_sft.json | 0 .../input_configs/embed-1b/embed_1b_lora.json | 0 .../gpt-oss/gpt_oss_full_sft.json | 0 .../gpt-oss/gpt_oss_full_sft_chat.json | 0 .../input_configs/gpt-oss/gpt_oss_lora.json | 0 .../gpt-oss/gpt_oss_lora_packing.json | 0 .../llama_3_1_8b_full_sft_tp.json | 0 .../llama-3.2-1b/llama_3_2_1b_full_sft.json | 0 .../llama_3_2_1b_full_sft_chat.json | 0 .../llama-3.2-1b/llama_3_2_1b_lora.json | 0 .../llama_3_2_1b_lora_packing.json | 0 .../nemotron-nano/nemotron_nano_full_sft.json | 0 .../nemotron_nano_full_sft_chat.json | 0 .../nemotron-nano/nemotron_nano_lora.json | 0 .../nemotron_nano_lora_packing.json | 0 .../output_configs/embed_1b_full_sft.yaml | 0 .../output_configs/embed_1b_lora.yaml | 0 .../output_configs/gpt_oss_full_sft.yaml | 0 .../output_configs/gpt_oss_full_sft_chat.yaml | 0 .../output_configs/gpt_oss_lora.yaml | 0 .../output_configs/gpt_oss_lora_packing.yaml | 0 .../llama_3_1_8b_full_sft_tp.yaml | 0 .../output_configs/llama_3_2_1b_full_sft.yaml | 0 .../llama_3_2_1b_full_sft_chat.yaml | 0 .../output_configs/llama_3_2_1b_lora.yaml | 0 .../llama_3_2_1b_lora_packing.yaml | 0 .../nemotron_nano_full_sft.yaml | 0 .../nemotron_nano_full_sft_chat.yaml | 0 .../output_configs/nemotron_nano_lora.yaml | 0 .../nemotron_nano_lora_packing.yaml | 0 .../sample-datasets/chat/training.jsonl | 0 .../sample-datasets/chat/validation.jsonl | 0 .../sample-datasets/embedding/training.jsonl | 0 .../embedding/validation.jsonl | 0 .../prompt_completion/training.jsonl | 0 .../prompt_completion/validation.jsonl | 0 .../tasks/training/backends}/test_backend.py | 8 +- .../training/backends}/test_callbacks.py | 15 +- .../tasks/training/backends}/test_config.py | 59 +- .../tests/tasks/training/test_errors.py | 273 ++ services/automodel/tests/test_compiler.py | 2 +- .../automodel/tests/test_contract_configs.py | 2 +- .../entities/api/v2/entities/endpoints.py | 4 + .../entities/api/v2/workspaces/endpoints.py | 18 +- services/customizer/pyproject.toml | 3 - .../customizer/app/jobs/training/compiler.py | 18 +- .../customizer/src/nmp/customizer/config.py | 6 - .../customizer/src/nmp/customizer/main.py | 11 - .../training/backends/automodel/__init__.py | 20 - .../training/backends/automodel/backend.py | 194 - .../backends/automodel/checkpoints.py | 522 --- .../training/backends/automodel/config.py | 848 ----- .../training/backends/automodel/finetune.py | 297 -- .../backends/automodel/requirements.txt | 1 - .../training/backends/nemo_rl/dpo_config.py | 2 +- .../nmp/customizer/tasks/training/runner.py | 6 +- services/customizer/tests/constants.py | 2 - .../tests/tasks/training/test_errors.py | 297 +- .../tests/tasks/training/test_integrations.py | 2 +- .../tests/tasks/training/test_runner.py | 10 +- services/customizer/tests/test_compiler.py | 162 +- services/evaluator/pyproject.toml | 2 +- .../integration/spans/test_atif_ingest.py | 53 +- .../docker/Dockerfile.nmp-unsloth-training | 45 +- services/unsloth/docker/README.md | 46 +- services/unsloth/docker/docker-bake.hcl | 58 +- .../unsloth/src/nmp/unsloth/app/constants.py | 2 + .../src/nmp/unsloth/app/jobs/compiler.py | 66 +- .../nmp/unsloth/app/jobs/training/compiler.py | 12 +- .../nmp/unsloth/app/jobs/training/schemas.py | 8 + services/unsloth/src/nmp/unsloth/compile.py | 16 +- services/unsloth/src/nmp/unsloth/schemas.py | 2 +- .../src/nmp/unsloth/tasks/file_io/run.py | 4 +- .../nmp/unsloth/tasks/training/__main__.py | 7 +- .../tasks/training/backends}/callbacks.py | 59 +- .../training/backends/hf_trainer_callback.py | 88 + .../tasks/training/backends/unsloth_sft.py | 79 +- .../nmp/unsloth/tasks/training/progress.py | 106 + services/unsloth/tests/test_callbacks.py | 74 + services/unsloth/tests/test_compile.py | 4 +- .../tests/test_compiler_validation_path.py | 99 + .../unsloth/tests/test_dataset_loading.py | 14 + .../unsloth/tests/test_hf_trainer_callback.py | 81 + services/unsloth/tests/test_main.py | 2 +- services/unsloth/tests/test_model_entity.py | 10 +- services/unsloth/tests/test_progress.py | 36 + .../customizer-lora-job-cli/README.md | 27 +- .../environment/Dockerfile | 3 +- .../environment/docker-compose.yaml | 10 +- .../environment/entrypoint.sh | 8 +- .../environment/image-env.sh | 25 + .../environment/setup-env.sh | 41 +- .../customizer-lora-job-cli/instruction.md | 89 +- .../tests/test_outputs.py | 45 +- tests/smoke_gpu/conftest.py | 3 - tests/smoke_gpu/test_customizer_automodel.py | 29 +- .../smoke_gpu/test_customizer_entry_points.py | 1 - tests/smoke_gpu/test_nemo_automodel.py | 43 - third_party/licenses.jsonl | 7 +- third_party/osv-licenses.json | 2280 ++--------- third_party/requirements-main.txt | 2331 +++++++----- .../license/overrides.yaml | 4 +- uv.lock | 3386 ++++++++++------- 214 files changed, 7026 insertions(+), 11614 deletions(-) create mode 100644 .github/trufflehog-exclude.txt rename docker-bake.automodel.hcl => docker-bake.hcl (58%) delete mode 100644 plugins/nemo-automodel/SCOPE.md rename plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/scripts/{poll_automodel_job.sh => poll_customization_job.sh} (77%) delete mode 100644 sdk/python/nemo-platform/src/nemo_platform/types/files/fileset_metadata_param.py rename sdk/python/nemo-platform/src/nemo_platform/types/{files/fileset_metadata_param_param.py => shared_params/fileset_metadata.py} (84%) rename services/{customizer/src => automodel/docker}/cherry-picks/e6d2930a.diff (100%) rename services/{customizer => automodel}/docs/automodel_errors.md (82%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/README.md (91%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/generate_configs.py (97%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/input_configs/embed-1b/embed_1b_full_sft.json (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/input_configs/embed-1b/embed_1b_lora.json (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/input_configs/gpt-oss/gpt_oss_full_sft.json (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/input_configs/gpt-oss/gpt_oss_full_sft_chat.json (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/input_configs/gpt-oss/gpt_oss_lora.json (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/input_configs/gpt-oss/gpt_oss_lora_packing.json (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/input_configs/llama-3.1-8b/llama_3_1_8b_full_sft_tp.json (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/input_configs/llama-3.2-1b/llama_3_2_1b_full_sft.json (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/input_configs/llama-3.2-1b/llama_3_2_1b_full_sft_chat.json (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/input_configs/llama-3.2-1b/llama_3_2_1b_lora.json (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/input_configs/llama-3.2-1b/llama_3_2_1b_lora_packing.json (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/input_configs/nemotron-nano/nemotron_nano_full_sft.json (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/input_configs/nemotron-nano/nemotron_nano_full_sft_chat.json (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/input_configs/nemotron-nano/nemotron_nano_lora.json (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/input_configs/nemotron-nano/nemotron_nano_lora_packing.json (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/output_configs/embed_1b_full_sft.yaml (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/output_configs/embed_1b_lora.yaml (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/output_configs/gpt_oss_full_sft.yaml (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/output_configs/gpt_oss_full_sft_chat.yaml (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/output_configs/gpt_oss_lora.yaml (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/output_configs/gpt_oss_lora_packing.yaml (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/output_configs/llama_3_1_8b_full_sft_tp.yaml (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/output_configs/llama_3_2_1b_full_sft.yaml (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/output_configs/llama_3_2_1b_full_sft_chat.yaml (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/output_configs/llama_3_2_1b_lora.yaml (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/output_configs/llama_3_2_1b_lora_packing.yaml (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/output_configs/nemotron_nano_full_sft.yaml (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/output_configs/nemotron_nano_full_sft_chat.yaml (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/output_configs/nemotron_nano_lora.yaml (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/output_configs/nemotron_nano_lora_packing.yaml (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/sample-datasets/chat/training.jsonl (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/sample-datasets/chat/validation.jsonl (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/sample-datasets/embedding/training.jsonl (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/sample-datasets/embedding/validation.jsonl (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/sample-datasets/prompt_completion/training.jsonl (100%) rename {tests/customizer-automodel-contract => services/automodel/tests/contract}/sample-datasets/prompt_completion/validation.jsonl (100%) rename services/{customizer/tests/tasks/training/backends/automodel => automodel/tests/tasks/training/backends}/test_backend.py (87%) rename services/{customizer/tests/tasks/training/backends/automodel => automodel/tests/tasks/training/backends}/test_callbacks.py (90%) rename services/{customizer/tests/tasks/training/backends/automodel => automodel/tests/tasks/training/backends}/test_config.py (81%) create mode 100644 services/automodel/tests/tasks/training/test_errors.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/automodel/__init__.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/automodel/backend.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/automodel/checkpoints.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/automodel/config.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/automodel/finetune.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/automodel/requirements.txt rename services/{customizer/src/nmp/customizer/tasks/training/backends/automodel => unsloth/src/nmp/unsloth/tasks/training/backends}/callbacks.py (64%) create mode 100644 services/unsloth/src/nmp/unsloth/tasks/training/backends/hf_trainer_callback.py create mode 100644 services/unsloth/src/nmp/unsloth/tasks/training/progress.py create mode 100644 services/unsloth/tests/test_callbacks.py create mode 100644 services/unsloth/tests/test_compiler_validation_path.py create mode 100644 services/unsloth/tests/test_hf_trainer_callback.py create mode 100644 services/unsloth/tests/test_progress.py create mode 100644 tests/agentic-use/customizer-lora-job-cli/environment/image-env.sh delete mode 100644 tests/smoke_gpu/test_nemo_automodel.py diff --git a/.github/trufflehog-exclude.txt b/.github/trufflehog-exclude.txt new file mode 100644 index 0000000000..cedf1da913 --- /dev/null +++ b/.github/trufflehog-exclude.txt @@ -0,0 +1,3 @@ +# Newline-separated regexes for paths TruffleHog should skip. +# uv.lock contains many sha256 hex digests that false-positive as SentryToken. +uv\.lock diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9d0534c805..eea7f1b1de 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -281,6 +281,7 @@ jobs: env: PYTHON_VERSION: ${{ matrix.python-version }} NMP_DATA_DIR: ${{ runner.temp }}/nemo-data + NMP_AUTH_ENABLED: "false" _TYPER_FORCE_DISABLE_TERMINAL: "1" run: | set -euo pipefail diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index 8041ba1eb0..2f7b4eeae9 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -38,6 +38,7 @@ jobs: with: path: ./ version: 3.95.3 + extra_args: --exclude-paths=.github/trufflehog-exclude.txt - name: Scan Results Status if: ${{ github.event_name != 'merge_group' && steps.trufflehog.outcome == 'failure' }} diff --git a/conftest.py b/conftest.py index 87351128fc..193651143e 100644 --- a/conftest.py +++ b/conftest.py @@ -213,8 +213,9 @@ def pytest_collection_modifyitems(config, items): "e2e", "smoke_gpu_tasks", "smoke_customizer_tasks", - "smoke_customizer_automodel", "smoke_customizer_rl", + "smoke_nmp_automodel_tasks", + "smoke_nmp_automodel_training", "integration", "regression", "canary", @@ -304,4 +305,4 @@ def _patched_reschedule(self, node): _original_reschedule(self, node) -LoadScopeScheduling._reschedule = _patched_reschedule # type: ignore[invalid-assignment] +LoadScopeScheduling._reschedule = _patched_reschedule diff --git a/docker-bake.automodel.hcl b/docker-bake.hcl similarity index 58% rename from docker-bake.automodel.hcl rename to docker-bake.hcl index d51f2157dc..8c7a166c5e 100644 --- a/docker-bake.automodel.hcl +++ b/docker-bake.hcl @@ -1,18 +1,38 @@ -# nmp-automodel image bake - run from Platform repo root (context = "."). +# NeMo Platform GPU image bake — run from Platform repo root (context = "."). # -# Inspect targets (no build; finishes in ~0s): -# docker buildx bake --print -f docker-bake.automodel.hcl nmp-automodel-gpu-wheels +# Groups: +# nmp-automodel-gpu-wheels causal-conv1d-wheel, mamba-ssm-wheel +# nmp-automodel base, tasks, training, smoke-test targets +# nmp-unsloth nmp-unsloth-training # -# Build wheels (override registry/tag via env, not --set): +# Automodel — inspect wheels (no build): +# docker buildx bake --print -f docker-bake.hcl nmp-automodel-gpu-wheels +# +# Automodel — build and push wheels: # export WHEELS_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev # export WHEELS_TAG=$(git rev-parse --short HEAD) -# docker buildx bake -f docker-bake.automodel.hcl nmp-automodel-gpu-wheels --push +# docker buildx bake -f docker-bake.hcl nmp-automodel-gpu-wheels --push +# +# Automodel — build runtime images: +# docker buildx bake -f docker-bake.hcl nmp-automodel-base-builder # -# Build automodel images: -# docker buildx bake -f docker-bake.automodel.hcl nmp-automodel-base-builder +# Unsloth — local build (--load): +# docker buildx bake -f docker-bake.hcl nmp-unsloth-training --load \ +# --set "*.platform=linux/amd64" # -# Published tags: nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-{base,tasks,training}: -# NVCR allows only one repo segment after the registry prefix (no nmp/automodel-base nesting). +# Unsloth — push to registry: +# export IMAGE_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev +# export BAKE_TAG=$(git rev-parse --short HEAD) +# docker buildx bake -f docker-bake.hcl nmp-unsloth-training --push \ +# --set "*.platform=linux/amd64" +# +# Published tags: +# ${IMAGE_REGISTRY}/nmp-automodel-{base,tasks,training}:${BAKE_TAG} +# ${IMAGE_REGISTRY}/nmp-unsloth-training:${BAKE_TAG} + +# --------------------------------------------------------------------------- +# Shared / automodel variables +# --------------------------------------------------------------------------- variable "IMAGE_REGISTRY" { default = "nvcr.io/0921617854601259/nemo-platform-dev" @@ -56,9 +76,21 @@ variable "CAUSAL_CONV1D_VERSION" { # For local builds: --set "*.platform=linux/amd64" variable "BUILD_PLATFORMS" { - default = ["linux/arm64"] + default = ["linux/amd64", "linux/arm64"] +} + +# --------------------------------------------------------------------------- +# Unsloth variables +# --------------------------------------------------------------------------- + +variable "BUILD_PLATFORM" { + default = "linux/amd64" } +# --------------------------------------------------------------------------- +# Automodel helpers +# --------------------------------------------------------------------------- + function "wheel_tags" { params = [name] result = ["${WHEELS_REGISTRY}/${name}:${WHEELS_TAG}"] @@ -74,6 +106,10 @@ function "get_mamba_ssm_wheel_image" { result = "${WHEELS_REGISTRY}/mamba-ssm-wheel:${WHEELS_TAG}" } +# --------------------------------------------------------------------------- +# Groups +# --------------------------------------------------------------------------- + group "nmp-automodel-gpu-wheels" { targets = [ "causal-conv1d-wheel", @@ -91,7 +127,14 @@ group "nmp-automodel" { ] } -# Pre-built mamba-ssm / causal-conv1d wheels (cp311, cp312, cu13.1.1). Pushed to WHEELS_REGISTRY. +group "nmp-unsloth" { + targets = ["nmp-unsloth-training"] +} + +# --------------------------------------------------------------------------- +# Automodel — GPU wheels +# --------------------------------------------------------------------------- + target "causal-conv1d-wheel" { target = "causal-conv1d-wheel" context = "." @@ -117,14 +160,15 @@ target "mamba-ssm-wheel" { platforms = BUILD_PLATFORMS } -target "platform-workspace" { +target "automodel-platform-workspace" { target = "platform-workspace" context = "." dockerfile = "services/automodel/docker/Dockerfile.platform-workspace" + platforms = BUILD_PLATFORMS } target "nmp-automodel-base-builder" { - target = "nmp-automodel-base-builder" + target = "nmp-automodel-base" context = "." dockerfile = "services/automodel/docker/Dockerfile.nmp-automodel-base" no-cache-filter = ["automodel-clone"] @@ -141,7 +185,7 @@ target "nmp-automodel-tasks-docker" { context = "." dockerfile = "services/automodel/docker/Dockerfile.nmp-automodel-tasks" contexts = { - platform-workspace = "target:platform-workspace" + platform-workspace = "target:automodel-platform-workspace" nmp-automodel-base = "target:nmp-automodel-base-builder" } tags = ["${IMAGE_REGISTRY}/nmp-automodel-tasks:${BAKE_TAG}"] @@ -157,7 +201,7 @@ target "nmp-automodel-training-docker" { context = "." dockerfile = "services/automodel/docker/Dockerfile.nmp-automodel-training" contexts = { - platform-workspace = "target:platform-workspace" + platform-workspace = "target:automodel-platform-workspace" nmp-automodel-base = "target:nmp-automodel-base-builder" } tags = ["${IMAGE_REGISTRY}/nmp-automodel-training:${BAKE_TAG}"] @@ -173,7 +217,7 @@ target "nmp-automodel-tasks-smoke-test" { context = "." dockerfile = "services/automodel/docker/Dockerfile.nmp-automodel-tasks" contexts = { - platform-workspace = "target:platform-workspace" + platform-workspace = "target:automodel-platform-workspace" nmp-automodel-base = "target:nmp-automodel-base-builder" } args = { @@ -190,7 +234,7 @@ target "nmp-automodel-training-smoke-test" { context = "." dockerfile = "services/automodel/docker/Dockerfile.nmp-automodel-training" contexts = { - platform-workspace = "target:platform-workspace" + platform-workspace = "target:automodel-platform-workspace" nmp-automodel-base = "target:nmp-automodel-base-builder" } args = { @@ -201,3 +245,26 @@ target "nmp-automodel-training-smoke-test" { output = ["type=cacheonly"] platforms = BUILD_PLATFORMS } + +# --------------------------------------------------------------------------- +# Unsloth +# --------------------------------------------------------------------------- + +target "unsloth-platform-workspace" { + context = "." + dockerfile = "services/unsloth/docker/Dockerfile.platform-workspace" + target = "platform-workspace" + output = ["type=cacheonly"] + platforms = BUILD_PLATFORMS +} + +target "nmp-unsloth-training" { + context = "." + dockerfile = "services/unsloth/docker/Dockerfile.nmp-unsloth-training" + target = "runtime" + contexts = { + platform-workspace = "target:unsloth-platform-workspace" + } + tags = ["${IMAGE_REGISTRY}/nmp-unsloth-training:${BAKE_TAG}"] + platforms = BUILD_PLATFORMS +} diff --git a/docs/set-up/config-reference.md b/docs/set-up/config-reference.md index bee7db3d9f..a1196d2833 100644 --- a/docs/set-up/config-reference.md +++ b/docs/set-up/config-reference.md @@ -766,8 +766,6 @@ customizer: port: 8000 # Enable debug mode | default: False debug: false - # Override container image for Automodel training. If not set, uses platform defaults. - training_automodel_image: # Override container image for DPO training. If not set, uses platform defaults. training_rl_image: # default: '1' diff --git a/e2e/conftest.py b/e2e/conftest.py index f9de40bfc2..1e4654b376 100644 --- a/e2e/conftest.py +++ b/e2e/conftest.py @@ -54,9 +54,21 @@ def pytest_configure(config: pytest.Config) -> None: _HEALTH_TIMEOUT = 60 _HEALTH_POLL_INTERVAL = 1.0 +_AUTH_READY_TIMEOUT = 60 +_E2E_ADMIN_EMAIL = "admin@example.com" _SERVICES_LOG = Path(os.environ.get("E2E_SERVICES_LOG", os.path.join(tempfile.gettempdir(), "services.log"))) +def _e2e_auth_enabled() -> bool: + """Return whether the e2e harness should run with authorization enabled. + + Default is disabled so ``make test-e2e`` does not depend on platform-admin + seeding, PDP refresh, or role propagation timing. Opt in with + ``E2E_AUTH_ENABLED=true`` (see ``make test-e2e-docker-auth``). + """ + return os.environ.get("E2E_AUTH_ENABLED", "false").lower() in ("1", "true", "yes") + + def _find_free_port() -> int: """Bind to port 0 and let the OS assign a free port.""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: @@ -78,15 +90,80 @@ def _wait_for_healthy(url: str, timeout: float = _HEALTH_TIMEOUT) -> bool: return False +def _admin_headers() -> dict[str, str]: + return { + "X-NMP-Principal-Id": _E2E_ADMIN_EMAIL, + "X-NMP-Principal-Email": _E2E_ADMIN_EMAIL, + } + + +def _wait_for_auth_ready(url: str, timeout: float = _AUTH_READY_TIMEOUT) -> bool: + """Poll until platform admin can create entities in a fresh workspace. + + Workspace create/list alone is insufficient: entity CRUD requires + PlatformAdmin (or entities.create, which workspace Admin lacks). The first + entity e2e test was flaky when only workspace visibility was probed. + """ + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + probe_name = f"auth-probe-{uuid.uuid4().hex[:8]}" + entity_name = f"auth-probe-entity-{uuid.uuid4().hex[:8]}" + try: + create_resp = httpx.post( + f"{url}/apis/entities/v2/workspaces", + json={"name": probe_name}, + headers=_admin_headers(), + timeout=5.0, + ) + if create_resp.status_code != 201: + time.sleep(_HEALTH_POLL_INTERVAL) + continue + + entity_resp = httpx.post( + f"{url}/apis/entities/v2/workspaces/{probe_name}/entities/e2e-auth-probe", + json={"name": entity_name, "data": {"ready": True}}, + headers=_admin_headers(), + timeout=5.0, + ) + if entity_resp.status_code != 201: + httpx.delete( + f"{url}/apis/entities/v2/workspaces/{probe_name}", + headers=_admin_headers(), + timeout=5.0, + ) + time.sleep(_HEALTH_POLL_INTERVAL) + continue + + httpx.delete( + f"{url}/apis/entities/v2/workspaces/{probe_name}/entities/e2e-auth-probe/{entity_name}", + headers=_admin_headers(), + timeout=5.0, + ) + httpx.delete( + f"{url}/apis/entities/v2/workspaces/{probe_name}", + headers=_admin_headers(), + timeout=5.0, + ) + return True + except httpx.RequestError as exc: + logger.debug("Auth readiness probe failed; will retry: %s", exc) + time.sleep(_HEALTH_POLL_INTERVAL) + return False + + @contextlib.contextmanager -def background_process(args: list[str], stdout: IO[Any] | None = None) -> Iterator[subprocess.Popen]: +def background_process( + args: list[str], + stdout: IO[Any] | None = None, + env: dict[str, str] | None = None, +) -> Iterator[subprocess.Popen]: """Run a subprocess, yield the ``Popen``, and terminate on exit. Unlike ``Popen``'s built-in context manager (which only waits for the process), this sends SIGTERM/SIGKILL so long-running servers are cleaned up. """ - proc = subprocess.Popen(args, stdout=stdout, stderr=subprocess.STDOUT) + proc = subprocess.Popen(args, stdout=stdout, stderr=subprocess.STDOUT, env=env) try: yield proc finally: @@ -120,16 +197,27 @@ def _services() -> Iterator[str]: url = f"http://127.0.0.1:{port}" nemo_bin = str(Path(sys.executable).parent / "nemo") - args = [nemo_bin, "services", "run", "--port", str(port)] + args = [nemo_bin, "services", "run", "--service-group", "all", "--port", str(port)] + env = os.environ.copy() + env["NMP_SEED_ON_STARTUP"] = "true" + if not _e2e_auth_enabled(): + # Bundled local.yaml enables auth; disable it for the default e2e harness. + env["NMP_AUTH_ENABLED"] = "false" + elif "NMP_AUTH_ENABLED" not in env: + env["NMP_AUTH_ENABLED"] = "true" logger.info("Starting nemo services on port %d", port) log_path = _SERVICES_LOG - with open(log_path, "w") as log_file, background_process(args, stdout=log_file) as proc: + with open(log_path, "w") as log_file, background_process(args, stdout=log_file, env=env) as proc: if not _wait_for_healthy(url): pytest.fail( f"nemo services run did not become healthy within {_HEALTH_TIMEOUT}s.\nlog:\n{log_path.read_text()}" ) + if _e2e_auth_enabled() and not _wait_for_auth_ready(url): + pytest.fail( + f"Platform auth seed did not become ready within {_AUTH_READY_TIMEOUT}s.\nlog:\n{log_path.read_text()}" + ) logger.info("Platform services ready on port %d (pid %d)", port, proc.pid) yield url @@ -139,7 +227,12 @@ def _services() -> Iterator[str]: @pytest.fixture(scope="session") def sdk(_services: str) -> NeMoPlatform: """Provide an SDK client connected to the running platform.""" - return NeMoPlatform(base_url=_services, max_retries=2) + headers = _admin_headers() if _e2e_auth_enabled() else {} + return NeMoPlatform( + base_url=_services, + max_retries=2, + default_headers=headers, + ) @pytest.fixture(scope="function") diff --git a/openapi/ga/individual/platform.openapi.yaml b/openapi/ga/individual/platform.openapi.yaml index 8135fb7c79..675abf90e1 100644 --- a/openapi/ga/individual/platform.openapi.yaml +++ b/openapi/ga/individual/platform.openapi.yaml @@ -11881,6 +11881,8 @@ components: for the general entity reference pattern used across the platform.' + examples: + - workspace/benchmark-name BenchmarkRequest: properties: name: @@ -11978,7 +11980,7 @@ components: - ragas/amnesty_qa title: BuiltInDataset description: Well-known dataset (BEIR or RAGAS) referenced by its identifier. - CPUExecutionProviderInput: + CPUExecutionProvider: properties: provider: type: string @@ -11998,34 +12000,7 @@ components: type: object required: - container - title: CPUExecutionProviderInput - description: 'CPU-based execution provider. - - - Provides configuration for running jobs on CPU resources with - - resource requests and limits.' - CPUExecutionProviderOutput: - properties: - provider: - type: string - const: cpu - title: Provider - default: cpu - profile: - type: string - title: Profile - default: default - container: - $ref: '#/components/schemas/ContainerSpec' - resources: - allOf: - - $ref: '#/components/schemas/ComputeResources' - description: Resource requests and limits for CPU execution. - type: object - required: - - container - title: CPUExecutionProviderOutput + title: CPUExecutionProvider description: 'CPU-based execution provider. @@ -13630,7 +13605,7 @@ components: default: generic metadata: allOf: - - $ref: '#/components/schemas/FilesetMetadataInput' + - $ref: '#/components/schemas/FilesetMetadata' description: 'Purpose-specific metadata. Use the purpose as the key (e.g., {dataset: {...}}).' custom_fields: @@ -13963,7 +13938,7 @@ components: type: object title: Spec platform_spec: - $ref: '#/components/schemas/PlatformJobSpecInput' + $ref: '#/components/schemas/PlatformJobSpec' source: type: string title: Source @@ -14165,34 +14140,7 @@ components: type: object title: DialogRails description: Configuration of topical rails. - DistributedGPUExecutionProviderInput: - properties: - provider: - type: string - const: gpu_distributed - title: Provider - default: gpu_distributed - profile: - type: string - title: Profile - default: default - container: - $ref: '#/components/schemas/ContainerSpec' - resources: - allOf: - - $ref: '#/components/schemas/ComputeResources' - description: Resource requests and limits for distributed GPU execution. - type: object - required: - - container - title: DistributedGPUExecutionProviderInput - description: 'GPU-based execution provider. - - - Provides configuration for running jobs on GPU resources with - - resource requests and limits.' - DistributedGPUExecutionProviderOutput: + DistributedGPUExecutionProvider: properties: provider: type: string @@ -14212,7 +14160,7 @@ components: type: object required: - container - title: DistributedGPUExecutionProviderOutput + title: DistributedGPUExecutionProvider description: 'GPU-based execution provider. @@ -15619,7 +15567,7 @@ components: dataset: anyOf: - $ref: '#/components/schemas/DatasetRows' - - $ref: '#/components/schemas/FilesetOutput' + - $ref: '#/components/schemas/Fileset' - $ref: '#/components/schemas/FilesetRef' title: Dataset description: Dataset containing the test cases for this benchmark. @@ -16365,6 +16313,34 @@ components: enum: - fileset title: FileStorageType + Fileset: + properties: + path: + title: Path + description: The relative path to file/directory in the storage. + type: string + minLength: 1 + storage: + anyOf: + - $ref: '#/components/schemas/NGCStorageConfig' + - $ref: '#/components/schemas/HuggingfaceStorageConfig' + title: Storage + description: The storage configuration for the fileset. + metadata: + allOf: + - $ref: '#/components/schemas/FilesetMetadata' + description: Purpose-specific metadata for the fileset. + custom_fields: + additionalProperties: true + type: object + title: Custom Fields + description: Custom fields for the fileset. + additionalProperties: false + type: object + required: + - storage + title: Fileset + description: Fileset definition for use without persisting to the Files API. FilesetFileOutput: properties: file_ref: @@ -16420,53 +16396,14 @@ components: (on or before) datetime filters. title: FilesetFilter type: object - FilesetInput: - properties: - path: - title: Path - description: The relative path to file/directory in the storage. - type: string - minLength: 1 - storage: - anyOf: - - $ref: '#/components/schemas/NGCStorageConfig' - - $ref: '#/components/schemas/HuggingfaceStorageConfig' - title: Storage - description: The storage configuration for the fileset. - metadata: - allOf: - - $ref: '#/components/schemas/FilesetMetadataInput' - description: Purpose-specific metadata for the fileset. - custom_fields: - additionalProperties: true - type: object - title: Custom Fields - description: Custom fields for the fileset. - additionalProperties: false - type: object - required: - - storage - title: FilesetInput - description: Fileset definition for use without persisting to the Files API. - FilesetMetadataInput: - properties: - dataset: - $ref: '#/components/schemas/DatasetMetadataContent' - model: - $ref: '#/components/schemas/ModelMetadataContent' - type: object - title: FilesetMetadataInput - description: "Tagged metadata container - the key indicates the type.\n\nExample:\n\ - \ metadata = FilesetMetadata(\n dataset=DatasetMetadataContent(\n\ - \ schema={\"columns\": [\"id\", \"name\"]},\n )\n )" - FilesetMetadataOutput: + FilesetMetadata: properties: dataset: $ref: '#/components/schemas/DatasetMetadataContent' model: $ref: '#/components/schemas/ModelMetadataContent' type: object - title: FilesetMetadataOutput + title: FilesetMetadata description: "Tagged metadata container - the key indicates the type.\n\nExample:\n\ \ metadata = FilesetMetadata(\n dataset=DatasetMetadataContent(\n\ \ schema={\"columns\": [\"id\", \"name\"]},\n )\n )" @@ -16494,7 +16431,7 @@ components: - $ref: '#/components/schemas/S3StorageConfig' title: Storage metadata: - $ref: '#/components/schemas/FilesetMetadataOutput' + $ref: '#/components/schemas/FilesetMetadata' custom_fields: additionalProperties: true type: object @@ -16718,34 +16655,7 @@ components: type: object title: GLiNERDetectionOptions description: Configuration options for GLiNER. - GPUExecutionProviderInput: - properties: - provider: - type: string - const: gpu - title: Provider - default: gpu - profile: - type: string - title: Profile - default: default - container: - $ref: '#/components/schemas/ContainerSpec' - resources: - allOf: - - $ref: '#/components/schemas/ComputeResources' - description: Resource requests and limits for GPU execution. - type: object - required: - - container - title: GPUExecutionProviderInput - description: 'GPU-based execution provider. - - - Provides configuration for running jobs on GPU resources with - - resource requests and limits.' - GPUExecutionProviderOutput: + GPUExecutionProvider: properties: provider: type: string @@ -16765,7 +16675,7 @@ components: type: object required: - container - title: GPUExecutionProviderOutput + title: GPUExecutionProvider description: 'GPU-based execution provider. @@ -17196,7 +17106,7 @@ components: type: string data: allOf: - - $ref: '#/components/schemas/RailsConfigOutput' + - $ref: '#/components/schemas/RailsConfig' type: object description: Guardrail configuration data additionalProperties: true @@ -17375,7 +17285,7 @@ components: - type: string title: Reference description: A reference to RailsConfig. - - $ref: '#/components/schemas/RailsConfigInput' + - $ref: '#/components/schemas/RailsConfig' title: Config description: The id of the configuration or its dict representation to be used. @@ -19462,7 +19372,7 @@ components: anyOf: - $ref: '#/components/schemas/DatasetRows' - $ref: '#/components/schemas/FilesetRef' - - $ref: '#/components/schemas/FilesetInput' + - $ref: '#/components/schemas/Fileset' title: Dataset description: The dataset to evaluate which may represent generated outputs from a model. @@ -19552,7 +19462,7 @@ components: anyOf: - $ref: '#/components/schemas/DatasetRows' - $ref: '#/components/schemas/FilesetRef' - - $ref: '#/components/schemas/FilesetInput' + - $ref: '#/components/schemas/Fileset' title: Dataset description: The dataset to use for agent prompts and evaluation. params: @@ -19676,7 +19586,7 @@ components: anyOf: - $ref: '#/components/schemas/DatasetRows' - $ref: '#/components/schemas/FilesetRef' - - $ref: '#/components/schemas/FilesetInput' + - $ref: '#/components/schemas/Fileset' title: Dataset description: The dataset to use for model prompts and evaluation. params: @@ -19751,6 +19661,8 @@ components: for the general entity reference pattern used across the platform.' + examples: + - workspace/metric-name MetricRetrieverJob: properties: metric: @@ -19772,14 +19684,14 @@ components: to dataset column paths for this job. retriever_pipeline: allOf: - - $ref: '#/components/schemas/RetrieverPipelineInput' + - $ref: '#/components/schemas/RetrieverPipeline' description: The pipeline configuration for retriever-based evaluation. dataset: anyOf: - $ref: '#/components/schemas/BuiltInDataset' - $ref: '#/components/schemas/DatasetRows' - $ref: '#/components/schemas/FilesetRef' - - $ref: '#/components/schemas/FilesetInput' + - $ref: '#/components/schemas/Fileset' title: Dataset description: The dataset to use for evaluation. params: @@ -20848,6 +20760,8 @@ components: the general entity reference pattern used across the platform.' + examples: + - workspace/model_name ModelSpec: properties: context_size: @@ -22355,23 +22269,14 @@ components: type: object title: PatronusEvaluateApiParams description: Config to parameterize the Patronus Evaluate API call - PatronusEvaluateConfigInput: - properties: - evaluate_config: - allOf: - - $ref: '#/components/schemas/PatronusEvaluateApiParams' - description: Configuration passed to the Patronus Evaluate API - type: object - title: PatronusEvaluateConfigInput - description: Config for the Patronus Evaluate API call - PatronusEvaluateConfigOutput: + PatronusEvaluateConfig: properties: evaluate_config: allOf: - $ref: '#/components/schemas/PatronusEvaluateApiParams' description: Configuration passed to the Patronus Evaluate API type: object - title: PatronusEvaluateConfigOutput + title: PatronusEvaluateConfig description: Config for the Patronus Evaluate API call PatronusEvaluationSuccessStrategy: type: string @@ -22388,31 +22293,18 @@ components: ALL_PASS requires all evaluators to pass for success. ANY_PASS requires only one evaluator to pass for success.' - PatronusRailConfigInput: - properties: - input: - allOf: - - $ref: '#/components/schemas/PatronusEvaluateConfigInput' - description: Patronus Evaluate API configuration for an Input Guardrail - output: - allOf: - - $ref: '#/components/schemas/PatronusEvaluateConfigInput' - description: Patronus Evaluate API configuration for an Output Guardrail - type: object - title: PatronusRailConfigInput - description: Configuration data for the Patronus Evaluate API - PatronusRailConfigOutput: + PatronusRailConfig: properties: input: allOf: - - $ref: '#/components/schemas/PatronusEvaluateConfigOutput' + - $ref: '#/components/schemas/PatronusEvaluateConfig' description: Patronus Evaluate API configuration for an Input Guardrail output: allOf: - - $ref: '#/components/schemas/PatronusEvaluateConfigOutput' + - $ref: '#/components/schemas/PatronusEvaluateConfig' description: Patronus Evaluate API configuration for an Output Guardrail type: object - title: PatronusRailConfigOutput + title: PatronusRailConfig description: Configuration data for the Patronus Evaluate API Percentiles: properties: @@ -22614,7 +22506,7 @@ components: title: Spec description: Job Spec platform_spec: - $ref: '#/components/schemas/PlatformJobSpecOutput' + $ref: '#/components/schemas/PlatformJobSpec' fileset: type: string title: Fileset @@ -22753,31 +22645,18 @@ components: - updated_at - -updated_at title: PlatformJobSortField - PlatformJobSpecInput: - properties: - steps: - items: - $ref: '#/components/schemas/PlatformJobStepSpecInput' - type: array - title: Steps - description: List of steps to be executed in the job - type: object - required: - - steps - title: PlatformJobSpecInput - description: Specification for a platform job, containing steps and secrets. - PlatformJobSpecOutput: + PlatformJobSpec: properties: steps: items: - $ref: '#/components/schemas/PlatformJobStepSpecOutput' + $ref: '#/components/schemas/PlatformJobStepSpec' type: array title: Steps description: List of steps to be executed in the job type: object required: - steps - title: PlatformJobSpecOutput + title: PlatformJobSpec description: Specification for a platform job, containing steps and secrets. PlatformJobStatus: type: string @@ -22952,57 +22831,7 @@ components: Parent-scoped: unique within (workspace, entity_type, parent=attempt_id).' - PlatformJobStepSpecInput: - properties: - name: - type: string - pattern: ^[a-z](?!.*--)[a-z0-9\-@.+_]{1,62}(?=31.0.0", "openai>=1.61.0", "ragas==0.3.5", - "langchain-community>=0.3.27,<0.4", + "langchain-community>=0.3.31,<0.4", "pymilvus==2.6.9", "langchain-nvidia-ai-endpoints>=1.0.0,<2.0.0", "nemo-evaluator-sdk", @@ -433,13 +433,6 @@ nmp-common = [ "pyjwt[crypto]>=2.12.0", ] -# Generated from [tool.bundle-package]; do not edit by hand. -platform-seed-service = [ - "nmp-common", - "nmp-auth", - "nmp-guardrails", -] - # Generated from [tool.bundle-package]; do not edit by hand. plugins = [ "nemo-platform[nemo-agents-example-calculator]", @@ -468,7 +461,6 @@ services = [ "nmp-common", "pyleak>=0.1.0", "rich>=14.1.0", - "nemo-platform[platform-seed-service]", "nemo-platform[core-service]", "nemo-platform[studio-service]", "nemo-platform[intake-service]", @@ -650,7 +642,6 @@ nmp-inference-gateway = { source = "../../services/core/inference-gateway/src/nm nmp-guardrails = { source = "../../services/guardrails/src/nmp/guardrails", module = "nmp/guardrails", deps_group = "guardrails-service" } nmp-customizer = { source = "../../services/customizer/src/nmp/customizer", module = "nmp/customizer", deps_group = "customizer-service" } nmp-evaluator = { source = "../../services/evaluator/src/nmp/evaluator", module = "nmp/evaluator", deps_group = "evaluator-service" } -nmp-platform-seed = { source = "../../services/platform-seed/src/nmp/platform_seed", module = "nmp/platform_seed", deps_group = "platform-seed-service" } nmp-hello-world = { source = "../../services/hello-world/src/nmp/hello_world", module = "nmp/hello_world", deps_group = "hello-world-service" } nmp-intake = { source = "../../services/intake/src/nmp/intake", module = "nmp/intake", deps_group = "intake-service" } diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/core/help_formatter.py b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/core/help_formatter.py index 68b40149e0..21f3d833ce 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/core/help_formatter.py +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/core/help_formatter.py @@ -345,7 +345,7 @@ def get_help_record(self, ctx: click.Context) -> tuple[str, str] | None: return None # Get the argument name (metavar) and strip any surrounding brackets - metavar = self.make_metavar() + metavar = self.make_metavar(ctx) # Remove square brackets that Click adds for optional arguments metavar = metavar.strip("[]") diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/authz.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/authz.py index 7cb264d746..029c2f1778 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/authz.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/authz.py @@ -13,6 +13,9 @@ from nemo_platform_plugin.authz import AuthzContribution, authz_for_workspace_job_collection + # Backend contributors implement get_authz_contribution on the contributor class. + # CustomizationRouterService (nemo.services) aggregates them at policy discovery time. + class AutomodelContributor: ... def get_authz_contribution(self) -> AuthzContribution: @@ -139,3 +142,18 @@ def authz_for_workspace_job_collection( } return AuthzContribution(permissions=perms, endpoints=endpoints) + + +def combine_authz_contributions(*contribs: AuthzContribution) -> AuthzContribution: + """Merge multiple :class:`AuthzContribution` objects into one (e.g. hub + backends).""" + merged = AuthzContribution() + for contrib in contribs: + merged.permissions.update(contrib.permissions) + for path, methods in contrib.endpoints.items(): + merged.endpoints.setdefault(path, {}).update(methods) + for role, perms in contrib.role_permissions.items(): + existing = merged.role_permissions.setdefault(role, []) + for perm in perms: + if perm not in existing: + existing.append(perm) + return merged diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/authz_discovery.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/authz_discovery.py index 920ea043d4..461ab6069b 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/authz_discovery.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/authz_discovery.py @@ -129,6 +129,8 @@ def discover_authz_contributions() -> list[AuthzContribution]: 1. ``nemo.authz`` entry points (callable or class) 2. ``nemo.services`` classes implementing :meth:`get_authz_contribution` + (e.g. :class:`~nemo_customizer.router.CustomizationRouterService` aggregates + ``nemo.customization.contributors`` backend policy) """ from nemo_platform_plugin.discovery import discover_entry_points, discover_services diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/commands.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/commands.py index 548c572a50..c80b964a6a 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/commands.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/commands.py @@ -441,7 +441,7 @@ def _do_run() -> Any: renderer.on_complete(ctx=rctx) help_text = f"Run {job_cls.name} locally, in-process." - _run.__signature__ = _build_job_run_signature(leaves) # type: ignore[attr-defined] + _run.__signature__ = _build_job_run_signature(leaves) group.command(name="run", help=help_text)(_run) @@ -618,7 +618,7 @@ def _do_submit() -> Any: renderer.on_complete(ctx=rctx) help_text = f"Submit {job_cls.name} to a cluster." - _submit.__signature__ = _build_job_submit_signature(leaves) # type: ignore[attr-defined] + _submit.__signature__ = _build_job_submit_signature(leaves) group.command(name="submit", help=help_text)(_submit) @@ -960,7 +960,7 @@ def _run(typer_ctx: typer.Context, **kwargs: object) -> None: help_text = f"Run {fn_cls.name} locally, in-process." epilog = build_epilog(schema=fn_cls.spec_schema, leaves=leaves, kind="Function") - _run.__signature__ = _build_function_run_signature(leaves) # type: ignore[attr-defined] + _run.__signature__ = _build_function_run_signature(leaves) group.command(name="run", help=help_text, epilog=epilog)(_run) @@ -1094,22 +1094,6 @@ def _resolve_submit_auth_headers(typer_ctx: typer.Context) -> dict[str, str]: return {} -def _resolve_submit_auth_headers(typer_ctx: typer.Context) -> dict[str, str]: - """Bearer (and other) default headers from the active CLI context.""" - state = typer_ctx.obj - if state is None or not hasattr(state, "get_sdk_context"): - return {} - try: - ctx = state.get_sdk_context() - client_config = ctx.user.get_client_config() - headers = client_config.get("default_headers") - if isinstance(headers, dict): - return {str(k): str(v) for k, v in headers.items()} - except Exception: - return {} - return {} - - # ---- submit ------------------------------------------------------ # @@ -1129,12 +1113,12 @@ def _add_function_submit_command( def _submit(typer_ctx: typer.Context, **kwargs: object) -> None: original_kwargs = dict(kwargs) - spec_str: str = kwargs.pop("spec", "{}") # type: ignore[assignment] - spec_file: Path | None = kwargs.pop("spec_file", None) # type: ignore[assignment] - cluster: str | None = kwargs.pop("cluster", None) # type: ignore[assignment] - base_url: str | None = kwargs.pop("base_url", None) # type: ignore[assignment] - workspace: str = kwargs.pop("workspace", "default") # type: ignore[assignment] - request_id: str | None = kwargs.pop("request_id", None) # type: ignore[assignment] + spec_str: str = kwargs.pop("spec", "{}") + spec_file: Path | None = kwargs.pop("spec_file", None) + cluster: str | None = kwargs.pop("cluster", None) + base_url: str | None = kwargs.pop("base_url", None) + workspace: str = kwargs.pop("workspace", "default") + request_id: str | None = kwargs.pop("request_id", None) base = _load_spec(spec_str, spec_file) overlay = build_overlay(leaves, kwargs, unset_sentinel=UNSET) @@ -1180,7 +1164,7 @@ def _submit(typer_ctx: typer.Context, **kwargs: object) -> None: help_text = f"Submit {fn_cls.name} over HTTP." epilog = build_epilog(schema=fn_cls.spec_schema, leaves=leaves, kind="Function") - _submit.__signature__ = _build_function_submit_signature(leaves) # type: ignore[attr-defined] + _submit.__signature__ = _build_function_submit_signature(leaves) group.command(name="submit", help=help_text, epilog=epilog)(_submit) diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/config.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/config.py index f64b610056..d2ab4a1e85 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/config.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/config.py @@ -255,7 +255,7 @@ def _get_cached_config(service_config: Type[_T_config]) -> _T_config: @classmethod def get_service_config(cls, service_config: Type[_T_config]) -> _T_config: if service_config in cls._overrides: - return cls._overrides[service_config] # type: ignore[return-value] + return cls._overrides[service_config] return cls._get_cached_config(service_config) @staticmethod @@ -405,9 +405,8 @@ def from_string(cls, value: str) -> "Runtime": return cls.NONE -class NemoPlatformConfig(create_service_config_class("platform")): - """ - Platform-wide configuration settings. It inherits from ServiceConfig and provides Platform-centric settings, which may +class NemoPlatformConfig(ServiceConfig): + """Platform-wide configuration settings. It inherits from ServiceConfig and provides Platform-centric settings, which may be used by other microservices to interact with other Platform services. Environment variables NMP__URL (e.g. NMP_FILES_URL) are read and merged into @@ -415,6 +414,21 @@ class NemoPlatformConfig(create_service_config_class("platform")): service_discovery. """ + model_config = SettingsConfigDict( + env_prefix=get_service_config_prefix("platform"), + env_nested_delimiter="_", + extra="allow", + populate_by_name=True, + ) + + @staticmethod + def global_settings_key() -> str: + return "platform" + + @classmethod + def get(cls) -> NemoPlatformConfig: + return Configuration.get_service_config(cls) + services: str = internal_field( default="", description="Comma-separated list of services to run in this process. If not set, all services will be run. This field is only meant to be set by the deployer.", @@ -595,7 +609,7 @@ def get_nemo_platform_config() -> NemoPlatformConfig: # Default implementation — subclasses (nmp-common) override this to return their extended PlatformConfig. -Configuration.get_platform_config = classmethod(lambda cls: cls.get_service_config(NemoPlatformConfig)) # type: ignore[attr-defined] +Configuration.get_platform_config = classmethod(lambda cls: cls.get_service_config(NemoPlatformConfig)) # Aliases — nmp-common and services use PlatformConfig / get_platform_config PlatformConfig = NemoPlatformConfig diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/customization_contributor.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/customization_contributor.py index 71092596ab..401468fa6a 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/customization_contributor.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/customization_contributor.py @@ -5,13 +5,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar, Protocol, runtime_checkable +from typing import ClassVar, Protocol, runtime_checkable import typer - -if TYPE_CHECKING: - from nemo_platform_plugin.authz import AuthzContribution - from nemo_platform_plugin.service import RouterSpec +from nemo_platform_plugin.authz import AuthzContribution +from nemo_platform_plugin.service import RouterSpec @runtime_checkable @@ -30,7 +28,9 @@ def get_cli(self) -> typer.Typer | None: def get_authz_contribution(self) -> AuthzContribution | None: """Optional authorization policy (endpoints + permissions) for this contributor. - Implement to return :class:`~nemo_platform_plugin.authz.AuthzContribution`, or - register a ``nemo.authz`` entry point instead. + Return :class:`~nemo_platform_plugin.authz.AuthzContribution`. Policy is + aggregated by :class:`~nemo_customizer.router.CustomizationRouterService` + (``nemo.services``) at discovery time — do not register a separate + ``nemo.authz`` entry point for customization backends. """ ... diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py index 83d8a965aa..d9fb4e810f 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py @@ -45,19 +45,17 @@ import os from functools import cache from importlib.metadata import EntryPoint, entry_points -from typing import TYPE_CHECKING, Any, cast +from typing import Any, cast +from nemo_platform_plugin.cli import NemoCLI +from nemo_platform_plugin.controller import NemoController +from nemo_platform_plugin.customization_contributor import CustomizationContributor +from nemo_platform_plugin.function import NemoFunction +from nemo_platform_plugin.inference_middleware import NemoInferenceMiddleware from nemo_platform_plugin.interface import PluginManifest - -if TYPE_CHECKING: - from nemo_platform_plugin.cli import NemoCLI - from nemo_platform_plugin.controller import NemoController - from nemo_platform_plugin.customization_contributor import CustomizationContributor - from nemo_platform_plugin.function import NemoFunction - from nemo_platform_plugin.inference_middleware import NemoInferenceMiddleware - from nemo_platform_plugin.job import NemoJob - from nemo_platform_plugin.seed import NemoSeedJob - from nemo_platform_plugin.service import NemoService +from nemo_platform_plugin.job import NemoJob +from nemo_platform_plugin.seed import NemoSeedJob +from nemo_platform_plugin.service import NemoService logger = logging.getLogger(__name__) diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/entities.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/entities.py index 56dde5bd12..fdbbb27cf2 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/entities.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/entities.py @@ -105,7 +105,7 @@ class EntityBase(BaseModel): "_db_version", } - __entity_type__: ClassVar[str] = EntityTypeDefault() # type: ignore[assignment] + __entity_type__: ClassVar[str] = EntityTypeDefault() model_config = {"populate_by_name": True} diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/jobs/result_manager.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/jobs/result_manager.py index b65a31de3f..95bcfccb5f 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/jobs/result_manager.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/jobs/result_manager.py @@ -241,9 +241,9 @@ def result_manager_factory( job_name=job_name, workspace=workspace, attempt_id=attempt_id, - file_manager_cls=file_manager_cls, # type: ignore - files_sdk=files_sdk, # type: ignore - jobs_sdk=jobs_sdk, # type: ignore + file_manager_cls=file_manager_cls, + files_sdk=files_sdk, + jobs_sdk=jobs_sdk, ) diff --git a/packages/nemo_platform_plugin/tests/test_authz.py b/packages/nemo_platform_plugin/tests/test_authz.py index 703350a68a..7829522ba8 100644 --- a/packages/nemo_platform_plugin/tests/test_authz.py +++ b/packages/nemo_platform_plugin/tests/test_authz.py @@ -7,7 +7,12 @@ import httpx import pytest -from nemo_platform_plugin.authz import AuthzContribution, authz_for_workspace_job_collection +from nemo_platform_plugin.authz import ( + AuthzContribution, + AuthzEndpointMethod, + authz_for_workspace_job_collection, + combine_authz_contributions, +) from nemo_platform_plugin.authz_discovery import ( AUTHZ_GROUP, _collect_from_plugin_surface, @@ -72,6 +77,79 @@ def get_routers(self): assert "/apis/example-svc/v2/workspaces/{workspace}/jobs" in contribs[0].endpoints +def test_combine_authz_contributions_merges_endpoints_and_permissions() -> None: + a = authz_for_workspace_job_collection( + api_area="customization", + collection_suffix="/automodel/jobs", + permission_prefix="customization.automodel.jobs", + ) + b = authz_for_workspace_job_collection( + api_area="customization", + collection_suffix="/unsloth/jobs", + permission_prefix="customization.unsloth.jobs", + ) + merged = combine_authz_contributions(a, b) + assert "customization.automodel.jobs.create" in merged.permissions + assert "customization.unsloth.jobs.create" in merged.permissions + assert "/apis/customization/v2/workspaces/{workspace}/automodel/jobs" in merged.endpoints + assert "/apis/customization/v2/workspaces/{workspace}/unsloth/jobs" in merged.endpoints + + +def test_customization_router_authz_discovered_via_nemo_services(monkeypatch: pytest.MonkeyPatch) -> None: + """Customization hub aggregates backend authz through nemo.services discovery.""" + + class _FakeContributor: + def get_authz_contribution(self) -> AuthzContribution: + return _example_automodel_authz() + + class _CustomizationHub(NemoService): + name = "customization" + dependencies = [] + + @classmethod + def get_authz_contribution(cls) -> AuthzContribution: + from nemo_platform_plugin.discovery import discover_customization_contributors + + hub = AuthzContribution( + endpoints={ + "/apis/customization/healthz": { + "get": AuthzEndpointMethod(permissions=[], scopes=[]), + }, + }, + ) + backend_parts = [ + contributor.get_authz_contribution() for contributor in discover_customization_contributors().values() + ] + return combine_authz_contributions(hub, *backend_parts) + + def get_routers(self): + return [] + + monkeypatch.setattr( + "nemo_platform_plugin.discovery.discover_entry_points", + lambda group: {}, + ) + monkeypatch.setattr( + "nemo_platform_plugin.discovery.discover_services", + lambda: {"customization": _CustomizationHub}, + ) + monkeypatch.setattr( + "nemo_platform_plugin.discovery.discover_customization_contributors", + lambda: {"automodel": _FakeContributor()}, + ) + discover_authz_contributions.cache_clear() + try: + contributions = discover_authz_contributions() + finally: + discover_authz_contributions.cache_clear() + + assert len(contributions) == 1 + paths = set(contributions[0].endpoints.keys()) + assert "/apis/customization/healthz" in paths + assert "/apis/customization/v2/workspaces/{workspace}/automodel/jobs" in paths + assert "/apis/customization/v2/workspaces/{workspace}/automodel/healthz" in paths + + def test_nemo_authz_entry_point_discovered(monkeypatch: pytest.MonkeyPatch) -> None: """Plugins can register authz via a nemo.authz entry point callable.""" ep = MagicMock() diff --git a/packages/nemo_platform_plugin/tests/test_cli_progress.py b/packages/nemo_platform_plugin/tests/test_cli_progress.py index 25bb3aa67a..23834abd01 100644 --- a/packages/nemo_platform_plugin/tests/test_cli_progress.py +++ b/packages/nemo_platform_plugin/tests/test_cli_progress.py @@ -93,5 +93,5 @@ def __init__(self, elapsed: float | None) -> None: self.finished = False self.finished_time = None - rendered = _MinutesSecondsElapsedColumn().render(_StubTask(elapsed)) # type: ignore[arg-type] + rendered = _MinutesSecondsElapsedColumn().render(_StubTask(elapsed)) assert rendered.plain == expected diff --git a/packages/nemo_platform_plugin/tests/test_dispatcher.py b/packages/nemo_platform_plugin/tests/test_dispatcher.py index c8d0c68d47..38a4e2e75f 100644 --- a/packages/nemo_platform_plugin/tests/test_dispatcher.py +++ b/packages/nemo_platform_plugin/tests/test_dispatcher.py @@ -156,7 +156,7 @@ class _Job(NemoJob): name = "non-dict" def run(self, config: dict) -> dict: - return "hello" # type: ignore[return-value] + return "hello" rc = run_task(_Job, sdk=_DEFAULT_SDK) diff --git a/packages/nmp_platform/config/local.env b/packages/nmp_platform/config/local.env index 9e62549d1a..5008d6e0a8 100644 --- a/packages/nmp_platform/config/local.env +++ b/packages/nmp_platform/config/local.env @@ -7,14 +7,14 @@ # uv run nemo services run --host 127.0.0.1 --port 8080 # # CLI against a remote platform (no local nemo services run): -# export NMP_BASE_URL=http://10.0.0.51:8080 +# export NMP_BASE_URL=http://127.0.0.1:8080 # nemo auth login --unsigned-token --email you@example.com # # Reset local DB + files: stop the platform, then rm -rf ~/.local/share/nemo # Platform API for `nemo` CLI (overrides ~/.config/nmp/config.yaml when this file is sourced) -NMP_BASE_URL=http://10.0.0.51:8080 -NEMO_BASE_URL=http://10.0.0.51:8080 +NMP_BASE_URL=http://127.0.0.1:8080 +NEMO_BASE_URL=http://127.0.0.1:8080 # Config file NMP_CONFIG_FILE_PATH=packages/nmp_platform/config/local.yaml diff --git a/packages/nmp_platform/config/local.yaml b/packages/nmp_platform/config/local.yaml index 87940bde69..38558d82cb 100644 --- a/packages/nmp_platform/config/local.yaml +++ b/packages/nmp_platform/config/local.yaml @@ -26,7 +26,7 @@ auth: policy_decision_point_base_url: "http://localhost:8080" # Low timeouts for fast test feedback (same as integration tests) policy_data_refresh_interval: 2 - bundle_cache_seconds: 0 # 0 = refresh on every authz call for instant permission changes + bundle_cache_seconds: 15 # 0 = refresh on every authz call for instant permission changes admin_email: "admin@example.com" # --- Azure AD OIDC (NeMo Platform dev deployment) --- diff --git a/packages/nmp_platform_runner/src/nmp/platform_runner/config.py b/packages/nmp_platform_runner/src/nmp/platform_runner/config.py index 911b534e40..f7a99acd6c 100644 --- a/packages/nmp_platform_runner/src/nmp/platform_runner/config.py +++ b/packages/nmp_platform_runner/src/nmp/platform_runner/config.py @@ -162,7 +162,10 @@ def apply_run_environment( effective_port = env.setdefault("NMP_SERVICE_PORT", str(config.port)) normalized = effective_host.strip("[]") url_host = f"[{normalized}]" if ":" in normalized else normalized - env.setdefault("NMP_BASE_URL", f"http://{url_host}:{effective_port}") + base_url = env.setdefault("NMP_BASE_URL", f"http://{url_host}:{effective_port}") + # Embedded PDP is served from the same platform process; keep the auth client + # origin aligned with NMP_BASE_URL when services run on a non-default port. + env.setdefault("NMP_AUTH_POLICY_DECISION_POINT_BASE_URL", base_url) _set_or_clear_env(env, NMP_SERVICES_ENV_VAR, config.services) _set_or_clear_env(env, NMP_CONTROLLERS_ENV_VAR, config.controllers) _set_or_clear_env(env, NMP_SIDECARS_ENV_VAR, config.sidecars) diff --git a/packages/nmp_platform_runner/src/nmp/platform_runner/config/local.yaml b/packages/nmp_platform_runner/src/nmp/platform_runner/config/local.yaml index b85998d442..3b670b0761 100644 --- a/packages/nmp_platform_runner/src/nmp/platform_runner/config/local.yaml +++ b/packages/nmp_platform_runner/src/nmp/platform_runner/config/local.yaml @@ -2,7 +2,7 @@ # source packages/nmp_platform/config/local.env (SQLite + paths) platform: runtime: "docker" - base_url: "http://127.0.0.1:8080" + base_url: "http://0.0.0.0:8080" service: {} @@ -12,7 +12,7 @@ auth: policy_decision_point_provider: embedded policy_decision_point_base_url: "http://127.0.0.1:8080" policy_data_refresh_interval: 2 - bundle_cache_seconds: 0 + bundle_cache_seconds: 15 admin_email: "admin@example.com" entities: {} @@ -28,6 +28,18 @@ jobs: ttl_seconds_before_active: 60 ttl_seconds_active: 3600 ttl_seconds_after_finished: 300 + # Uncomment for using customizer + # - provider: cpu + # profile: gpu + # backend: docker + # config: + # launcher_tool_path: ./services/core/jobs/jobs-launcher/jobs-launcher + # - provider: gpu + # profile: gpu + # backend: docker + # config: + # launcher_tool_path: ./services/core/jobs/jobs-launcher/jobs-launcher + executor_defaults: docker: cleanup_completed_jobs_immediately: false diff --git a/packages/nmp_platform_runner/tests/test_config.py b/packages/nmp_platform_runner/tests/test_config.py index bd1655e6fa..33e6498ca9 100644 --- a/packages/nmp_platform_runner/tests/test_config.py +++ b/packages/nmp_platform_runner/tests/test_config.py @@ -130,6 +130,11 @@ def test_sets_base_url_when_not_present(self): apply_run_environment(_make_config(host="0.0.0.0", port=8080), env=env) assert env["NMP_BASE_URL"] == "http://127.0.0.1:8080" + def test_sets_embedded_pdp_base_url_from_base_url(self): + env: dict[str, str] = {} + apply_run_environment(_make_config(host="0.0.0.0", port=9090), env=env) + assert env["NMP_AUTH_POLICY_DECISION_POINT_BASE_URL"] == "http://127.0.0.1:9090" + def test_sets_service_host_when_not_present(self): env: dict[str, str] = {} apply_run_environment(_make_config(host="0.0.0.0", port=8080), env=env) @@ -179,6 +184,14 @@ def test_preserves_existing_base_url(self): apply_run_environment(_make_config(host="0.0.0.0", port=8080), env=env) assert env["NMP_BASE_URL"] == "http://nemo-platform-api:8080" + def test_preserves_existing_embedded_pdp_base_url(self): + env: dict[str, str] = { + "NMP_BASE_URL": "http://nemo-platform-api:8080", + "NMP_AUTH_POLICY_DECISION_POINT_BASE_URL": "http://nemo-auth:8080", + } + apply_run_environment(_make_config(host="0.0.0.0", port=8080), env=env) + assert env["NMP_AUTH_POLICY_DECISION_POINT_BASE_URL"] == "http://nemo-auth:8080" + def test_preserves_existing_service_host(self): env: dict[str, str] = {"NMP_SERVICE_HOST": "nemo-platform-api"} apply_run_environment(_make_config(host="0.0.0.0", port=8080), env=env) diff --git a/plugins/nemo-agents/examples/calculator-agent/src/calculator_agent/register.py b/plugins/nemo-agents/examples/calculator-agent/src/calculator_agent/register.py index 1435c56c76..d7cfdf0d56 100644 --- a/plugins/nemo-agents/examples/calculator-agent/src/calculator_agent/register.py +++ b/plugins/nemo-agents/examples/calculator-agent/src/calculator_agent/register.py @@ -8,10 +8,10 @@ from collections.abc import AsyncGenerator -from nat.builder.builder import Builder # type: ignore -from nat.builder.function import FunctionGroup # type: ignore -from nat.cli.register_workflow import register_function_group # type: ignore -from nat.data_models.function import FunctionGroupBaseConfig # type: ignore +from nat.builder.builder import Builder +from nat.builder.function import FunctionGroup +from nat.cli.register_workflow import register_function_group +from nat.data_models.function import FunctionGroupBaseConfig from pydantic import Field diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/jobs/analyze_batch.py b/plugins/nemo-agents/src/nemo_agents_plugin/jobs/analyze_batch.py index 016d4d0b1a..ba114ca20a 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/jobs/analyze_batch.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/jobs/analyze_batch.py @@ -155,7 +155,7 @@ def run(self, config: dict, *, ctx: JobContext | None = None) -> dict: ga = asyncio.run(generate_gap_analysis(batch=batch, parser=parser, baselines=baselines)) if cfg.format == "json": - return _serialize(ga) # type: ignore[return-value] + return _serialize(ga) # markdown from nemo_agents_plugin.improvement._analysis_reporting import generate_gap_report diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/jobs/evaluate_agent.py b/plugins/nemo-agents/src/nemo_agents_plugin/jobs/evaluate_agent.py index 3c9397cf59..5d76334886 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/jobs/evaluate_agent.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/jobs/evaluate_agent.py @@ -150,7 +150,7 @@ class EvaluateAgentJob(NemoJob): spec_schema: ClassVar[type[BaseModel]] = EvaluateAgentSpec @classmethod - async def compile( # type: ignore[override] + async def compile( cls, *, workspace: str, diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/jobs/optimize_skills.py b/plugins/nemo-agents/src/nemo_agents_plugin/jobs/optimize_skills.py index 219901e2e1..d7e5183fb0 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/jobs/optimize_skills.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/jobs/optimize_skills.py @@ -220,4 +220,4 @@ def run(self, config: dict, *, ctx: JobContext | None = None) -> dict: trace_parser=cfg.trace_parser, ) ) - return _serialize(state) # type: ignore[return-value] + return _serialize(state) diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/telemetry/files_service_exporter.py b/plugins/nemo-agents/src/nemo_agents_plugin/telemetry/files_service_exporter.py index 10d77c1bbe..a922679ed2 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/telemetry/files_service_exporter.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/telemetry/files_service_exporter.py @@ -31,13 +31,13 @@ import time import uuid -from nat.builder.builder import Builder # type: ignore[unresolved-import] -from nat.cli.register_workflow import register_telemetry_exporter # type: ignore[unresolved-import] -from nat.data_models.intermediate_step import IntermediateStep # type: ignore[unresolved-import] -from nat.data_models.telemetry_exporter import TelemetryExporterBaseConfig # type: ignore[unresolved-import] -from nat.observability.exporter.base_exporter import IsolatedAttribute # type: ignore[unresolved-import] -from nat.observability.exporter.raw_exporter import RawExporter # type: ignore[unresolved-import] -from nat.observability.processor.intermediate_step_serializer import ( # type: ignore[unresolved-import] +from nat.builder.builder import Builder +from nat.cli.register_workflow import register_telemetry_exporter +from nat.data_models.intermediate_step import IntermediateStep +from nat.data_models.telemetry_exporter import TelemetryExporterBaseConfig +from nat.observability.exporter.base_exporter import IsolatedAttribute +from nat.observability.exporter.raw_exporter import RawExporter +from nat.observability.processor.intermediate_step_serializer import ( IntermediateStepSerializer, ) from nemo_agents_plugin.utils import get_base_url diff --git a/plugins/nemo-agents/tests/unit/test_runner_controller.py b/plugins/nemo-agents/tests/unit/test_runner_controller.py index 4b222036fc..5699b78ac0 100644 --- a/plugins/nemo-agents/tests/unit/test_runner_controller.py +++ b/plugins/nemo-agents/tests/unit/test_runner_controller.py @@ -45,7 +45,7 @@ def _make_controller() -> tuple[AgentDeploymentController, Any]: ctrl._backend = backend ctrl._entities = MagicMock() ctrl._controller_config = ControllerConfig(health_check_timeout_seconds=120) - ctrl._save = AsyncMock() # type: ignore[method-assign] + ctrl._save = AsyncMock() return ctrl, cast(Any, backend) diff --git a/plugins/nemo-agents/tests/unit/test_utils.py b/plugins/nemo-agents/tests/unit/test_utils.py index 3005976835..111bd6b14e 100644 --- a/plugins/nemo-agents/tests/unit/test_utils.py +++ b/plugins/nemo-agents/tests/unit/test_utils.py @@ -567,7 +567,7 @@ def fake_upload( with job._resolve_output( FilesetRef("eval-results"), workspace="default", - sdk=sentinel_sdk, # type: ignore[arg-type] + sdk=sentinel_sdk, ctx=ctx, ) as base: (base / "summary.json").write_text("{}") @@ -599,7 +599,7 @@ def fake_upload( with job._resolve_output( FilesetRef("prod/eval-results"), workspace="default", - sdk=object(), # type: ignore[arg-type] + sdk=object(), ctx=ctx, ) as _: pass @@ -629,7 +629,7 @@ def fake_upload( with job._resolve_output( FilesetRef("eval-results"), workspace="default", - sdk=object(), # type: ignore[arg-type] + sdk=object(), ctx=ctx, ) as base: (base / "partial.json").write_text("{}") @@ -658,7 +658,7 @@ def fake_upload( with job._resolve_output( FilesetRef("eval-results"), workspace="default", - sdk=object(), # type: ignore[arg-type] + sdk=object(), ctx=ctx, ) as base: captured_path = base @@ -698,7 +698,7 @@ def __init__(self) -> None: Path("/tmp/eval-out"), fileset="eval-results", workspace="prod", - sdk=sdk, # type: ignore[arg-type] + sdk=sdk, ) assert sdk.files.calls == [ @@ -1024,7 +1024,7 @@ def __init__(self) -> None: agent_config, endpoint = OptimizeAgentJob._resolve_agent( ref, workspace="default", - sdk=_StubSDK(), # type: ignore[arg-type] + sdk=_StubSDK(), ) assert captured == {"name": "react-agent", "workspace": "default"} @@ -1047,7 +1047,7 @@ def __init__(self) -> None: self.agents = _StubAgents() ref = AgentRef("prod/react-agent") - OptimizeAgentJob._resolve_agent(ref, workspace="default", sdk=_StubSDK()) # type: ignore[arg-type] + OptimizeAgentJob._resolve_agent(ref, workspace="default", sdk=_StubSDK()) assert captured == {"name": "react-agent", "workspace": "prod"} @@ -1065,7 +1065,7 @@ def __init__(self) -> None: ref = AgentRef("react-agent") with pytest.raises(RuntimeError, match="empty or invalid stored config"): - OptimizeAgentJob._resolve_agent(ref, workspace="default", sdk=_StubSDK()) # type: ignore[arg-type] + OptimizeAgentJob._resolve_agent(ref, workspace="default", sdk=_StubSDK()) class TestRebaseOptimizeOutputs: @@ -1333,7 +1333,7 @@ class _StubResponse: headers: dict[str, str] = {} request = None - return NotFoundError(message=message, response=_StubResponse(), body={"detail": message}) # type: ignore[arg-type] + return NotFoundError(message=message, response=_StubResponse(), body={"detail": message}) class _RecordingVirtualModels: @@ -1386,7 +1386,7 @@ def test_happy_path_dedupes_repeated_model_names(self) -> None: vms = _RecordingVirtualModels() sdk = _StubSDKWithVirtualModels(vms) - validate_llm_models(config, workspace="default", sdk=sdk) # type: ignore[arg-type] + validate_llm_models(config, workspace="default", sdk=sdk) assert vms.calls == [{"name": "shared-model", "workspace": "default"}] @@ -1400,7 +1400,7 @@ def test_distinct_model_names_each_get_one_call(self) -> None: vms = _RecordingVirtualModels() sdk = _StubSDKWithVirtualModels(vms) - validate_llm_models(config, workspace="ws", sdk=sdk) # type: ignore[arg-type] + validate_llm_models(config, workspace="ws", sdk=sdk) names = sorted(call["name"] for call in vms.calls) assert names == ["model-a", "model-b"] @@ -1416,7 +1416,7 @@ def test_missing_model_raises_value_error_with_actionable_message(self) -> None: sdk = _StubSDKWithVirtualModels(vms) with pytest.raises(ValueError) as exc_info: - validate_llm_models(config, workspace="default", sdk=sdk) # type: ignore[arg-type] + validate_llm_models(config, workspace="default", sdk=sdk) message = str(exc_info.value) # Names the missing model + the YAML key + the workspace, and points @@ -1438,7 +1438,7 @@ def test_multiple_missing_models_listed_in_single_error(self) -> None: sdk = _StubSDKWithVirtualModels(vms) with pytest.raises(ValueError) as exc_info: - validate_llm_models(config, workspace="default", sdk=sdk) # type: ignore[arg-type] + validate_llm_models(config, workspace="default", sdk=sdk) message = str(exc_info.value) assert "'missing-1'" in message @@ -1458,7 +1458,7 @@ def test_non_igw_llm_types_are_skipped(self) -> None: vms = _RecordingVirtualModels() sdk = _StubSDKWithVirtualModels(vms) - validate_llm_models(config, workspace="ws", sdk=sdk) # type: ignore[arg-type] + validate_llm_models(config, workspace="ws", sdk=sdk) assert [call["name"] for call in vms.calls] == ["real-model"] @@ -1477,7 +1477,7 @@ def test_unexpanded_env_var_placeholder_is_skipped(self) -> None: vms = _RecordingVirtualModels() sdk = _StubSDKWithVirtualModels(vms) - validate_llm_models(config, workspace="ws", sdk=sdk) # type: ignore[arg-type] + validate_llm_models(config, workspace="ws", sdk=sdk) assert [call["name"] for call in vms.calls] == ["real-model"] @@ -1491,7 +1491,7 @@ def test_partial_placeholder_is_skipped(self) -> None: vms = _RecordingVirtualModels() sdk = _StubSDKWithVirtualModels(vms) - validate_llm_models(config, workspace="ws", sdk=sdk) # type: ignore[arg-type] + validate_llm_models(config, workspace="ws", sdk=sdk) assert vms.calls == [] @@ -1506,7 +1506,7 @@ def test_missing_model_name_is_skipped(self) -> None: vms = _RecordingVirtualModels() sdk = _StubSDKWithVirtualModels(vms) - validate_llm_models(config, workspace="ws", sdk=sdk) # type: ignore[arg-type] + validate_llm_models(config, workspace="ws", sdk=sdk) assert [call["name"] for call in vms.calls] == ["real-model"] @@ -1515,7 +1515,7 @@ def test_empty_llms_block_is_noop(self) -> None: vms = _RecordingVirtualModels() sdk = _StubSDKWithVirtualModels(vms) - validate_llm_models(config, workspace="ws", sdk=sdk) # type: ignore[arg-type] + validate_llm_models(config, workspace="ws", sdk=sdk) assert vms.calls == [] @@ -1525,7 +1525,7 @@ def test_missing_llms_key_is_noop(self) -> None: vms = _RecordingVirtualModels() sdk = _StubSDKWithVirtualModels(vms) - validate_llm_models(config, workspace="ws", sdk=sdk) # type: ignore[arg-type] + validate_llm_models(config, workspace="ws", sdk=sdk) assert vms.calls == [] @@ -1542,7 +1542,7 @@ def test_soft_fails_on_non_not_found_exception(self, caplog: pytest.LogCaptureFi with caplog.at_level("WARNING"): # Must not raise — the underlying eval/optimize call will surface # the real error if the model truly isn't reachable. - validate_llm_models(config, workspace="ws", sdk=sdk) # type: ignore[arg-type] + validate_llm_models(config, workspace="ws", sdk=sdk) assert any("Could not validate LLM" in record.message for record in caplog.records) @@ -1557,7 +1557,7 @@ def test_non_dict_llm_entry_is_skipped(self) -> None: vms = _RecordingVirtualModels() sdk = _StubSDKWithVirtualModels(vms) - validate_llm_models(config, workspace="ws", sdk=sdk) # type: ignore[arg-type] + validate_llm_models(config, workspace="ws", sdk=sdk) assert [call["name"] for call in vms.calls] == ["real-model"] @@ -1584,7 +1584,7 @@ def test_happy_path_loads_yaml_and_validates(self, tmp_path: Path) -> None: vms = _RecordingVirtualModels() sdk = _StubSDKWithVirtualModels(vms) - preflight_validate_llm_models(config_path, workspace="ws", sdk=sdk) # type: ignore[arg-type] + preflight_validate_llm_models(config_path, workspace="ws", sdk=sdk) assert vms.calls == [{"name": "real-model", "workspace": "ws"}] @@ -1605,7 +1605,7 @@ def test_expands_env_vars_before_validation(self, tmp_path: Path, monkeypatch: p vms = _RecordingVirtualModels() sdk = _StubSDKWithVirtualModels(vms) - preflight_validate_llm_models(config_path, workspace="ws", sdk=sdk) # type: ignore[arg-type] + preflight_validate_llm_models(config_path, workspace="ws", sdk=sdk) # The expanded name reached the SDK; the literal placeholder did not. assert vms.calls == [{"name": "expanded-model", "workspace": "ws"}] @@ -1634,7 +1634,7 @@ def test_merges_agent_config_before_validation(self, tmp_path: Path) -> None: preflight_validate_llm_models( config_path, workspace="ws", - sdk=sdk, # type: ignore[arg-type] + sdk=sdk, agent_config=agent_config, ) @@ -1651,7 +1651,7 @@ def test_missing_model_propagates_validate_llm_models_error(self, tmp_path: Path sdk = _StubSDKWithVirtualModels(vms) with pytest.raises(ValueError) as exc_info: - preflight_validate_llm_models(config_path, workspace="default", sdk=sdk) # type: ignore[arg-type] + preflight_validate_llm_models(config_path, workspace="default", sdk=sdk) # Sanity-check the message shape; full message coverage lives in # TestValidateLLMModels. diff --git a/plugins/nemo-anonymizer/tests/unit/test_input.py b/plugins/nemo-anonymizer/tests/unit/test_input.py index 041d3cee62..28b77b461f 100644 --- a/plugins/nemo-anonymizer/tests/unit/test_input.py +++ b/plugins/nemo-anonymizer/tests/unit/test_input.py @@ -193,7 +193,7 @@ def raise_bad_input(*args: object, **kwargs: object) -> object: with pytest.raises(AnonymizerInvalidConfigError, match="bad input"): prepare_anonymizer_input( AnonymizerInputSpec(source="fs#input.csv"), - sdk=object(), # type: ignore[arg-type] + sdk=object(), workspace="team-a", allow_local_paths=False, ) @@ -222,7 +222,7 @@ def fake_prepare_anonymizer_input( captured["sdk"] = sdk captured["workspace"] = workspace captured["allow_local_paths"] = allow_local_paths - return input_module.PreparedAnonymizerInput(input=sentinel) # type: ignore[arg-type] + return input_module.PreparedAnonymizerInput(input=sentinel) sdk = FakeSyncPlatform() monkeypatch.setattr(input_module, "NeMoPlatform", FakeSyncPlatform) @@ -230,7 +230,7 @@ def fake_prepare_anonymizer_input( prepared = await prepare_anonymizer_input_async( AnonymizerInputSpec(source="fs#input.csv"), - sdk=sdk, # type: ignore[arg-type] + sdk=sdk, workspace="team-a", allow_local_paths=False, ) diff --git a/plugins/nemo-anonymizer/tests/unit/test_preview_function.py b/plugins/nemo-anonymizer/tests/unit/test_preview_function.py index e5641939f2..551814df73 100644 --- a/plugins/nemo-anonymizer/tests/unit/test_preview_function.py +++ b/plugins/nemo-anonymizer/tests/unit/test_preview_function.py @@ -56,7 +56,7 @@ def preview(self, **kwargs: object) -> FakeResult: worker_module._make_preview( frames.append, _preview_spec(tmp_path), - data=object(), # type: ignore[arg-type] + data=object(), model_configs_yaml="", dd_providers=None, num_records=1, diff --git a/plugins/nemo-anonymizer/tests/unit/test_routing.py b/plugins/nemo-anonymizer/tests/unit/test_routing.py index dbf3c0748d..5ce4932eb0 100644 --- a/plugins/nemo-anonymizer/tests/unit/test_routing.py +++ b/plugins/nemo-anonymizer/tests/unit/test_routing.py @@ -50,7 +50,7 @@ def get(self, url: str, **kwargs: object) -> Response: default_headers={}, _client=Client(), ) - resource = AnonymizerResource(platform) # type: ignore[arg-type] + resource = AnonymizerResource(platform) request = AnonymizerRequest( config=AnonymizerConfig(replace=Redact()), data=AnonymizerInputSpec(source="inputs#records.csv"), diff --git a/plugins/nemo-auditor/openapi/openapi.yaml b/plugins/nemo-auditor/openapi/openapi.yaml index 1423232e2a..0fa3ef6f54 100644 --- a/plugins/nemo-auditor/openapi/openapi.yaml +++ b/plugins/nemo-auditor/openapi/openapi.yaml @@ -446,7 +446,7 @@ components: run: $ref: '#/components/schemas/AuditRunData' plugins: - $ref: '#/components/schemas/AuditPluginsDataOutput' + $ref: '#/components/schemas/AuditPluginsData' reporting: $ref: '#/components/schemas/AuditReportData' id: @@ -501,7 +501,7 @@ components: type: object title: AuditModuleConfig description: Per-module plugin configuration mapping. - AuditPluginsDataInput: + AuditPluginsData: properties: model_type: title: Model Type @@ -567,74 +567,7 @@ components: type: object title: Probes type: object - title: AuditPluginsDataInput - AuditPluginsDataOutput: - properties: - model_type: - title: Model Type - type: string - model_name: - title: Model Name - type: string - probe_spec: - type: string - title: Probe Spec - default: all - detector_spec: - type: string - title: Detector Spec - default: auto - extended_detectors: - type: boolean - title: Extended Detectors - default: false - buff_spec: - title: Buff Spec - type: string - buffs_include_original_prompt: - type: boolean - title: Buffs Include Original Prompt - default: false - buff_max: - title: Buff Max - type: string - detectors: - additionalProperties: - anyOf: - - $ref: '#/components/schemas/AuditModuleConfig' - - $ref: '#/components/schemas/AuditClassConfig' - type: object - title: Detectors - generators: - additionalProperties: - anyOf: - - $ref: '#/components/schemas/AuditModuleConfig' - - $ref: '#/components/schemas/AuditClassConfig' - type: object - title: Generators - buffs: - additionalProperties: - anyOf: - - $ref: '#/components/schemas/AuditModuleConfig' - - $ref: '#/components/schemas/AuditClassConfig' - type: object - title: Buffs - harnesses: - additionalProperties: - anyOf: - - $ref: '#/components/schemas/AuditModuleConfig' - - $ref: '#/components/schemas/AuditClassConfig' - type: object - title: Harnesses - probes: - additionalProperties: - anyOf: - - $ref: '#/components/schemas/AuditModuleConfig' - - $ref: '#/components/schemas/AuditClassConfig' - type: object - title: Probes - type: object - title: AuditPluginsDataOutput + title: AuditPluginsData AuditReportData: properties: report_prefix: @@ -838,7 +771,7 @@ components: run: $ref: '#/components/schemas/AuditRunData' plugins: - $ref: '#/components/schemas/AuditPluginsDataInput' + $ref: '#/components/schemas/AuditPluginsData' reporting: $ref: '#/components/schemas/AuditReportData' type: object @@ -938,7 +871,7 @@ components: run: $ref: '#/components/schemas/AuditRunData' plugins: - $ref: '#/components/schemas/AuditPluginsDataInput' + $ref: '#/components/schemas/AuditPluginsData' reporting: $ref: '#/components/schemas/AuditReportData' type: object diff --git a/plugins/nemo-automodel/SCOPE.md b/plugins/nemo-automodel/SCOPE.md deleted file mode 100644 index 1d5f8c85ab..0000000000 --- a/plugins/nemo-automodel/SCOPE.md +++ /dev/null @@ -1,967 +0,0 @@ -# NeMo Automodel Plugin — Work Scope - -**Start here:** [Implementation order](#implementation-order) (sequence, checklists, success criteria). - -This document scopes the work to replace the legacy Customizer Automodel path with a first-party **NeMo Automodel plugin** (customization **contributor**), the **`nemo-customizer-plugin`** router at `/apis/customization`, and the **`nmp-automodel`** task/compiler package (no standalone HTTP server). Legacy `Platform/services/customizer/` is reference only. New work: `plugins/nemo-customizer/`, `plugins/nemo-automodel/`, `services/automodel/`. - -Training is powered by the upstream **`nemo_automodel`** library (repo: `Automodel/` at workspace root, NGC image `nvcr.io/nvidia/nemo-automodel:25.11.00`). - ---- - -## Implementation order - -Canonical sequencing for this scope. **Work breakdown** (below) and design sections add detail; checklists live here only. - -### Sequence overview - -| Step | Focus | Package / area | Blocks | -|------|--------|----------------|--------| -| **0** | Design lock + platform Jobs flag | cross-cutting | — | -| **1** | Customization router | `plugins/nemo-customizer` | Automodel HTTP (step 4) | -| **2** | Task/compiler library | `services/automodel` (`nmp-automodel`) | Images (step 3), contributor compile (step 4) | -| **3** | Container images | `nmp-automodel` Dockerfiles | E2E GPU runs | -| **4** | Automodel plugin + Docker gate | `plugins/nemo-automodel` | CLI submit (step 5), integration (step 6) | -| **5** | CLI submit path | `nemo-automodel` + router CLI | — | -| **6** | Tests & contracts | `Platform/tests/...` | — | -| **7** | SDK, OpenAPI, docs, deploy | platform + plugins | — | - -**Parallel OK:** Step 0 Jobs flag with step 1–2. Step 2 compiler port with step 1 router (after contributor protocol is sketched). - -```mermaid -flowchart LR - S0[Step 0 Design lock] - S1[Step 1 nemo-customizer] - S2[Step 2 nmp-automodel] - S3[Step 3 Images] - S4[Step 4 nemo-automodel plugin] - S5[Step 5 CLI] - S6[Step 6 Tests] - S7[Step 7 Docs deploy] - S0 --> S1 - S0 --> S2 - S1 --> S4 - S2 --> S3 - S2 --> S4 - S3 --> S6 - S4 --> S5 - S4 --> S6 - S5 --> S7 - S6 --> S7 -``` - ---- - -### Step 0 — Design lock & platform prerequisites - -Lock names, routes, schemas, and cross-cutting Jobs config before feature PRs. First implementation PR can be plugin + `nmp-automodel` without Studio migration. - -**Design lock checklist** - -- [x] **Name & routes:** Router `NemoService.name = customization`; Automodel contributor prefix `v2/workspaces/{workspace}/automodel` → `/apis/customization/v2/workspaces/{workspace}/automodel/...`; CLI `nemo customization automodel` — [URL routing](#url-routing-decided), [Customization router](#customization-router-in-scope--v1). -- [x] **Workspace contract:** Path `{workspace}` authoritative; spec uses workspace-relative names + optional `ws/name` qualifiers; dataset URI rules documented; **no** `workspace` key in JSON body — [Workspace scoping](#workspace-scoping-required). -- [x] **Simplified JSON schema:** Publish `AutomodelJobInput` (v1) for POST/CLI; `AutomodelJobOutput` for stored/GET; `extra="forbid"` — [Simplified JSON spec](#simplified-json-spec-draft--automodeljobinput-only). -- [x] **Schema validators (legacy parity):** Reject `output_model` with message to use `output` (legacy `CustomizationJobInput`); `model_config` / field validators for distillation-only fields when `training_type: sft`. -- [x] **Dataset shape:** `{ training, validation? }` fileset URIs; `to_spec()` runs `check_dataset_access` per ref (port `platform_client`) — legacy API used a single `dataset` string; mapping documented in migration guide. -- [x] **Integrations:** `wandb` / `mlflow` accept `api_key_secret` (`SecretRef`) plus enabled/project fields — not only `null` placeholders. -- [x] **v1 exclusions (locked):** `deployment_config` (post-train NIM deploy), embedding-model SFT, DPO/GRPO — see [Decisions](#decisions-resolved). -- [x] **Input vs canonical spec (Option A):** Two schemas + `to_spec()` — port `transform_input_to_output` — [Input vs canonical spec](#input-vs-canonical-spec--decided-option-a). -- [x] **Deprecation / Studio:** Legacy customizer not in default `AVAILABLE_SERVICES`; UI feature-flagged off — [Deprecation](#deprecation--platform-spin-up-and-studio-verified). - -**Workspace registration (do before first integration test):** - -- [x] Add `plugins/nemo-customizer`, `plugins/nemo-automodel`, `services/automodel` to root `Platform/pyproject.toml` workspace members. -- [x] Add `nemo-customizer-plugin` and `nemo-automodel-plugin` to `[dependency-groups] enabled-plugins` (pattern: `nemo-evaluator-plugin`). - -**Platform Jobs — `jobs.enable_subprocess_executor`** (cross-cutting, not Automodel-only; rationale in [Platform jobs: `runtime` vs step executors](#platform-jobs-runtime-vs-step-executors)): - -- [x] Add field to `JobsServiceConfig` (`Platform/services/core/jobs/src/nmp/core/jobs/config.py`). -- [x] Gate `SubprocessJobExecutionProfile` in `get_default_executor_profiles_for_runtime()` (K8s default `false`, docker local default `true`). -- [ ] Document in `Platform/services/core/jobs/README.md`. -- [ ] Expose in `packages/nmp_platform/config/local.yaml` and `nmp_platform_runner` local config. -- [ ] `GET /v2/execution-profiles` reflects the flag. - ---- - -### Step 1 — `nemo-customizer` (blocks Automodel HTTP) - -**Problem:** `discover_services()` maps each `nemo.services` key to one `/apis//` mount — only one owner for `customization`. Training backends (Automodel, RL, Megatron, Unsloth) must share one URL tree without a monolithic `nmp-customizer` or per-backend top-level services. - -**Solution:** New package `plugins/nemo-customizer/` (`nemo_customizer`) ships the sole `nemo.services` → `customization` registration. Backends register as **contributors** via `nemo.customization.contributors`. Full design: [Customization router](#customization-router-in-scope--v1). - -**Router behavior (implement in this step):** - -1. `discover_customization_contributors()` — fault-isolated; allowlist `NEMO_PLUGIN_CUSTOMIZATION_CONTRIBUTORS_ALLOWLIST` (or `NEMO_PLUGIN_ALLOWLIST`). -2. **Zero contributors** → fail startup with clear error. -3. `CustomizationRouterService.get_routers()` — merge `RouterSpec` lists; **`dependencies`** = union of contributor + platform deps. -4. `CustomizationCLI.get_cli()` — `typer.Typer(name="customization")` + mount contributor subgroups (`automodel`, …). -5. OpenAPI / SDK — single service name `customization` when router + ≥1 contributor enabled. -6. **Route collision guard** — distinct segment per contributor under `.../workspaces/{workspace}/`; legacy `.../jobs` unmounted in v1. - -**`nemo-customizer-plugin` pyproject.toml:** - -```toml -[project.entry-points."nemo.services"] -customization = "nemo_customizer.router:CustomizationRouterService" - -[project.entry-points."nemo.cli"] -customization = "nemo_customizer.cli:CustomizationCLI" -``` - -**Deliverables** - -- [x] `CustomizationContributor` protocol in `nemo_platform_plugin/customization_contributor.py`; `discover_customization_contributors()` in `nemo_platform_plugin/discovery.py` (fault-isolated via `discover_entry_points`; allowlist `NEMO_PLUGIN_CUSTOMIZATION_CONTRIBUTORS_ALLOWLIST` or `NEMO_PLUGIN_ALLOWLIST`). `nemo_customizer/discovery.py` re-exports for backward compatibility. -- [x] **Zero contributors:** fail router startup with a clear error (do not mount an empty `/apis/customization` tree silently). -- [x] `CustomizationRouterService` + `CustomizationCLI` (merge contributors); **`dependencies`** = union of all contributor `dependencies` plus platform deps (`entities`, `jobs`, `auth`, …). -- [x] Entry points: `nemo.services` + `nemo.cli` → key `customization`. -- [x] Unit tests: two fake contributors → merged routes; prefix collision detection; zero contributors → startup error. -- [x] `OPENAPI_SERVICES` / registry: include `customization` when router plugin enabled **and** ≥1 contributor discovered. -- [x] `docs/CUSTOMIZATION.md` — contributor author guide (RL / Megatron / Unsloth). -- [x] Workspace members + `enabled-plugins` — [Step 0 workspace registration](#step-0--design-lock--platform-prerequisites). - -**Out of scope:** Legacy `POST .../workspaces/{ws}/jobs` multi-backend path; Studio cutover. - ---- - -### Step 2 — `nmp-automodel` package core - -`Platform/services/automodel/` — Python package **`nmp-automodel`**: compilers, task entrypoints, Dockerfiles. **No HTTP server** (unlike legacy `customizer-server`). Reference port: `Platform/services/customizer/` (trim multi-backend paths only). - -**4-step `PlatformJobSpec` pipeline** (Automodel-only): - -1. `file_io` (CPU) — download model + datasets (`nmp/automodel-tasks` image). -2. Training (GPU) — `finetune.py` + `nemo_automodel` recipes (SFT + KD); `nmp/automodel-training` image. -3. `file_io` upload. -4. `model_entity` — register model in Models service (behavior unchanged from legacy). - -| Area | Source | Action | -|------|--------|--------| -| Automodel config compiler | `tasks/training/backends/automodel/config.py` | Move; drop non-automodel imports; SFT + `_configure_kd()` | -| Training runner/backend | `backend.py`, `finetune.py`, `callbacks.py`, `checkpoints.py` | Move; keep `JobsServiceProgressReporter` + `TrainingProgressCallback` (rank-0) | -| Training step compiler | `app/jobs/training/compiler.py` | **Strip** to automodel-only; fixed `nmp/automodel-training` image ref | -| Job compiler | `app/jobs/compiler.py` | **Strip** DPO/RL/`nemo_rl`/`megatron_bridge`; keep distillation (KD); 4-step only | -| File I/O tasks | `tasks/file_io/` | Ported: `run.py`, `callbacks.py`, `utils.py`, `progress_reporter.py` | -| Model entity task | `tasks/model_entity/` | Move unchanged behavior | -| Schemas | `api/v2/jobs/schemas.py` | `AutomodelJobInput` + `AutomodelJobOutput` (+ sub-models) for plugin `to_spec` / compiler | - -**Deliverables** - -- [x] `nmp-automodel` installable; task entry points via console scripts + `nemo-platform run task --task nmp.automodel.tasks.*`. -- [x] Unit tests: adapter + compiler (`services/automodel/tests/`); contract `generate_configs.py` imports `nmp.automodel`. -- [x] Prove `PlatformJobSpec` generation for SFT (4-step pipeline, `nmp.automodel.tasks.*` commands, `nmp/automodel-training` / `nmp/automodel-tasks` images). -- [x] `validate_for_training()` on legacy `CustomizationJobOutput` (compiler); plugin `AutomodelJobOutput` has parallel validator in `nemo_automodel_plugin.schema`. -- [x] `platform_client.py` — `fetch_model_entity`, `check_dataset_access`. -- [x] `_resolve_v4_compatible()` in training compiler. -- [x] Task modules `nmp.automodel.tasks.{file_io,training,model_entity}`; compiled steps use `nmp.automodel.tasks.*`. -- [x] `AutomodelConfig.default_training_execution_profile` (`NMP_AUTOMODEL_*`); adapter + compile wrapper apply request `profile`. - -**Internal Jobs callback path** (not a new public route — same contract as legacy customizer): - -- [x] `NMPJobContext` env vars for job id, step, workspace, task name. -- [x] `JobsServiceProgressReporter` / `TrainingProgressCallback` → `sdk.jobs.tasks.create_or_update` (rank-0 only). -- [ ] Document as internal; exclude from public OpenAPI if auxiliary routes are added. - -*Optional later:* webhook-style callbacks — out of initial scope. - ---- - -### Step 3 — Container images - -Two runtime images (`nmp/automodel-tasks`, `nmp/automodel-training`) built from `nmp/automodel-base` (PyTorch + `nemo_automodel` deps), published under `nvcr.io/0921617854601259/nemo-platform-dev/nmp/...` — not the upstream NGC `nvcr.io/nvidia/nemo-automodel` training container name and not full `nmp-customizer` / RL / Megatron stack. Do **not** reuse or extend `customizer-automodel` during transition. - -| Image key | Dockerfile | Used by | Contents | -|-----------|------------|---------|----------| -| `nmp/automodel-training` | `Dockerfile.nmp-automodel-training` | GPU training step | `nmp/automodel-base` + `nmp-automodel` finetune backend (SFT + KD recipes) | -| `nmp/automodel-tasks` | `Dockerfile.nmp-automodel-tasks` | CPU steps (`file_io`, `model_entity`) | Slim glue; task entrypoints without customizer API server / RL / Megatron | - -**Deliverables** - -- [ ] Wire both keys in plugin `get_qualified_image()` / `NMP_AUTOMODEL_*` env overrides. -- [ ] CI: smoke import on **training** image (pattern: `Platform/tests/smoke_gpu/test_customizer_automodel.py`); lighter smoke on **tasks** image. -- [ ] Plugin README: size/dependency audit vs `customizer-automodel`. -- [ ] Helm/assets: image refs (Studio cutover to new URLs still out of scope). - ---- - -### Step 4 — `nemo-automodel` plugin (contributor + job) - -Plugin HTTP only — merged by router at `/apis/customization/.../automodel/...`. Requires **step 1** (`nemo-customizer-plugin`) in workspace. **`compile()`** depends on **step 2** (`nmp-automodel`). - -**Automodel plugin `pyproject.toml` (contributor — not `nemo.services`):** - -```toml -[project.entry-points."nemo.customization.contributors"] -automodel = "nemo_automodel_plugin.contributor:AutomodelContributor" - -[project.entry-points."nemo.jobs"] -"customization.automodel.jobs" = "nemo_automodel_plugin.jobs.jobs:AutomodelJob" -``` - -**Deliverables** - -- [x] **pyproject.toml:** `nemo-platform-plugin`, `nmp-automodel` (no `nemo-customizer-plugin` wheel dep — router installed via `enabled-plugins` only). Entry points: - - `nemo.customization.contributors` → `AutomodelContributor` (`automodel`) - - `nemo.jobs` → `customization.automodel.jobs` → `AutomodelJob` - - optional `nemo.docs` (no `nemo.services` / no top-level `nemo.cli`) -- [x] **`AutomodelContributor.get_routers()`** — optional `.../automodel/healthz`; mount jobs via `add_job_routes` (see wiring below); prefix `v2/workspaces/{workspace}/automodel`; `job_collection_path = "/automodel/jobs"`. -- [x] **`add_job_routes` wiring (required):** - - `service_name="customization"` — Jobs `source`, list filters, and OpenAPI service segment (default `_derive_service_name()` → `nemo-automodel-plugin` is **wrong**). - - `generate_job_name=generate_automodel_id` — `automodel-{uuid.hex[:12]}` when body omits `name` (same pattern as legacy `generate_customization_id`). - - `route_options=[JobRouteOption.CORE]` — create/list/get/delete/status/cancel/results; **no** PAUSE_RESUME in v1 (legacy parity). - - `default_profile` from plugin config when spec omits `training.execution_profile`. - - Request-body `profile` on `BaseJobRequest` — **deferred** (platform `add_job_routes` still drops it); v1 uses **`training.execution_profile`** in JSON only. -- [x] **`AutomodelJob`:** `description` set; `input_spec_schema` / `spec_schema` / `to_spec()` (Option A); `compile()` on `AutomodelJobOutput` only; `dependencies`: `entities`, `auth`, `jobs`, `secrets`, `files`, `models`. -- [ ] **Job envelope:** `description`, `project`, `ownership`, `custom_fields` — inherited from `job_route_factory` (no Automodel-specific fields); document in README. -- [x] **`get_cli()`** — `automodel` Typer subgroup via `add_job_commands` (`jobs` → `run` / `submit` / `explain` to `.../automodel/jobs`). Data Designer–style `cli/inputs.py` simplified JSON is [Step 5](#step-5--cli-submit-path), not required for Step 4. -- [x] SDK: `nemo-customizer-plugin` owns `nemo.sdk` → `customization`; composes `client.customization.automodel` from `nemo-automodel-plugin`. `nemo.docs` if user docs ship with plugin. -- [x] Workspace members + `enabled-plugins` — [Step 0 workspace registration](#step-0--design-lock--platform-prerequisites). - -**Docker enforcement & GPU validation** (`nemo_platform_plugin.jobs.docker` today: `validate_gpu_available_for_docker` only when `runtime == DOCKER` and reserved GPU list is empty — **extend for all Automodel jobs**): - -- [x] At **compile** (plugin `compile()` or shared helper): require `NemoPlatformConfig.runtime == DOCKER`. -- [x] Require `validate_docker_available()` (daemon reachable). -- [x] Require GPU pool configured (reuse or extend `validate_gpu_available_for_docker`). -- [x] `PlatformJobCompilationError` → 422, e.g. *“Automodel training requires `platform.runtime: docker` with GPU-backed container execution (Docker daemon reachable and GPUs configured).”* -- [x] Do **not** silently downgrade `platform.runtime` to `NONE` for this plugin. -- [ ] Do **not** conflate with `jobs.enable_subprocess_executor` — Automodel never schedules `subprocess` training steps. - ---- - -### Step 5 — CLI submit path - -First-class CLI for simplified JSON jobs (pattern: Data Designer `[CONFIG_SOURCE]` → canonical spec in `plugins/nemo-data-designer/.../cli/inputs.py`). Commands hang under `nemo customization automodel` (router CLI + contributor subgroup). - -**Submit URL** (via `nemo_platform_plugin.commands` job submit helper): - -`/apis/customization/v2/workspaces/{workspace}/automodel/jobs` - -Custom wrappers must **forward** `--workspace` / `-w` to the framework callback (default `"default"` for local dev only). - -**Deliverables** - -- [x] `nemo customization automodel jobs submit --workspace ` — `cli/inputs.py` validates `AutomodelJobInput` and POSTs to `.../automodel/jobs` (`tests/test_cli.py`). -- [x] `jobs explain` — exposes `input_spec_schema` + `spec_schema` via framework `explain` (`tests/test_cli.py`). -- [x] CLI tests: `-w` / `--workspace` in submit URL (`submit_path_for` + mocked `submit_remote`). - ---- - -### Step 6 — Tests & contract continuity - -Relocate contract tests from legacy `customizer-automodel` path; validate router + contributor + compiler together. - -**Deliverables** - -- [x] Contract script import path fixed (`generate_configs.py` → `backends.config`); `services/automodel/tests/test_contract_configs.py` parses SFT/packing inputs + optional `--check` (embedding gated/skipped for v1). -- [x] Unit/API: Automodel + customization router routes under `/v2/workspaces/{workspace}/automodel/...` (`plugins/nemo-automodel/tests/test_api.py`). -- [x] Integration: compile-only via `services/automodel/tests/test_compiler.py` (contract fixture when present); CLI submit mocked in `test_cli.py`. -- [ ] Agentic smoke: adapt `Platform/tests/agentic-use/customizer-lora-job-cli` → `nemo customization automodel` CLI. -- [ ] E2E: job completes → **Model** entity exists → fileset populated → LoRA metadata when `finetuning_type=lora`. -- [x] Workspace isolation: routes scoped by `{workspace}` path segment (`test_api.py`); full cross-workspace API test deferred to Jobs service integration. - ---- - -### Step 7 — SDK, OpenAPI, docs & rollout - -**API & SDK polish** - -- [ ] OpenAPI tags: “Automodel Training Jobs”. -- [ ] List/get/delete/results routes via `add_job_routes` defaults under `/v2/workspaces/{workspace}/automodel/jobs`. -- [x] SDK hub: `client.customization.automodel.jobs.create(workspace=..., spec=...)` — paths under `/v2/workspaces/{workspace}/automodel/jobs`; **no** silent global namespace default (document `workspace="default"` for local dev). -- [ ] Error mapping: `PlatformJobCompilationError` / `validate_for_training` → 422; `check_dataset_access` / model entity auth failures → 403 or 422 with clear copy. -- [ ] Migration guide field table: legacy flat `training` + single `dataset` string → `AutomodelJobInput` sections; `output_model` → `output`. - -**Docs & deploy** - -- [ ] Automodel plugin README: install, enabled-plugins, CLI examples, sample `job.json`. -- [ ] Config reference: `NMP_AUTOMODEL_*` (training/tasks image overrides, resource defaults); link `NMP_JOBS_ENABLE_SUBPROCESS_EXECUTOR` / [Step 0](#step-0--design-lock--platform-prerequisites). -- [ ] Migration guide: `CustomizationJob` / `CustomizationJobInput` → `AutomodelJobInput` field mapping. -- [ ] Helm/assets: deploy `nmp/automodel-training` + `nmp/automodel-tasks` (replace `customizer-automodel` on product cutover — Studio migration still out of scope). - ---- - -### Success criteria (exit checks) - -- [ ] `nemo customization automodel jobs submit job.json -w acme-corp` → `/apis/customization/v2/workspaces/acme-corp/automodel/jobs`; fails fast without Docker/GPU. -- [ ] `POST` accepts `AutomodelJobInput`; GET returns enriched `AutomodelJobOutput` in `acme-corp`. -- [ ] Completed job: **Model** entity + fileset + adapter metadata in same workspace. -- [ ] Training progress on Jobs task `status_details.metrics`. -- [ ] Training image CI smoke passes. -- [ ] No legacy `platform_job_config_compiler` / multi-backend customizer dependency. -- [ ] Router test: second fake contributor merges without router code changes. - ---- - -## Goals (from requirements) - -| Requirement | Intent | -|-------------|--------| -| **First-class CLI** | Submit/run jobs from a **simplified JSON** job config (not the full CustomizationJob API surface). Pattern: Data Designer’s `[CONFIG_SOURCE]` → canonical spec (`plugins/nemo-data-designer/.../cli/inputs.py`). | -| **Fail if Docker disabled for jobs** | Automodel training is GPU + container-only. Reject compile/submit when `platform.runtime` is not `docker` or Docker daemon/GPUs are unavailable (stricter than today’s “warn and set runtime NONE”). Independent of `jobs.enable_subprocess_executor`. | -| **First-class API** | Workspace-scoped REST under `/apis/customization/v2/workspaces/{workspace}/automodel/...` — `{workspace}` is a **required path segment** on every job route (create, list, get, delete, results). Served via the **customization router** (single `/apis/customization` mount); Automodel is the first contributor. | -| **Customization router** | **`nemo-customizer-plugin`** owns `/apis/customization` and merges HTTP/CLI/SDK from contributors (Automodel v1; RL / Megatron / Unsloth later) — no monolithic `nmp-customizer`, no per-backend top-level `/apis/*` services. | -| **Automodel-only job path** | No NeMo RL, Megatron-Bridge, DPO, GRPO, or multi-backend dispatch. Single compiler → single training step image. | -| **Internal callback API** | Keep task-level progress updates to the Jobs service (`sdk.jobs.tasks.create_or_update`) from training subprocesses — not a public user API. | -| **Simplified training image** | New image derived from `nemo-automodel` NGC base with only platform task glue + `nemo_automodel`, not full `nmp-customizer` / RL / Megatron stack. | -| **Entity lifecycle** | Jobs still: download artifacts → train → upload checkpoint → **create/update Model entity** (and LoRA adapter metadata where applicable). | -| **Jobs API parity** | `service_name="customization"` on `add_job_routes`; auto `automodel-{id}` names; `training.execution_profile` in spec; CORE routes only. | - ---- - -## Platform jobs: `runtime` vs step executors - -Two layers are easy to conflate; this plugin only cares about the second for **training steps**, but operators need both clear in config and docs. - -| Layer | Config | Cardinality | Meaning | -|-------|--------|-------------|---------| -| **Platform deployment** | `platform.runtime` | **One value** per process (`docker` \| `kubernetes` \| `none`) | How the platform orchestrates container workloads (Docker daemon vs K8s vs neither). **Not** “how every job step runs.” | -| **Job step execution** | `platform_spec.steps[].executor` | **Per step** | Backend for that step: `cpu`/`gpu` + container → Docker or K8s; `subprocess` → host process (local dev / lightweight tasks). | - -Today, when `platform.runtime: docker`, the Jobs service **implicitly** also registers `subprocess/default` (host execution) alongside `cpu/default` and `gpu/default` (Docker). That coupling is what makes `runtime: docker` sound like “everything runs in Docker.” - -### Proposed: `jobs.enable_subprocess_executor` - -Make host subprocess execution an **explicit** platform choice instead of a side effect of `runtime: docker`. - -| Field | Type | Default | Behavior | -|-------|------|---------|----------| -| `jobs.enable_subprocess_executor` | `bool` | `true` when `platform.runtime == docker` (local dev); **`false` on Kubernetes** unless explicitly set `true` | When `true`, register `subprocess/default` and allow steps with `provider: subprocess`. When `false`, omit subprocess from default profiles; CPU/GPU container steps use Docker (or K8s) only. Dev clusters may opt in explicitly; production K8s should leave host execution disabled. | - -**Implementation:** [Step 0 — Platform Jobs flag](#step-0--design-lock--platform-prerequisites) (cross-cutting, not Automodel-only). - -**Automodel plugin implications:** - -- Training steps are **always** `cpu`/`gpu` + container → Docker; Automodel does **not** depend on `enable_subprocess_executor`. -- Compile gate ([Step 4](#step-4--nemo-automodel-plugin-contributor--job)): **`platform.runtime == docker`** + daemon + GPUs — not “subprocess enabled.” -- Optional `jobs run` ([Step 4](#step-4--nemo-automodel-plugin-contributor--job) 2b): subprocess only if the flag is enabled. -- Prefer error copy: *“Automodel training requires `platform.runtime: docker` with GPU-backed container execution”* — avoid *“Docker job runtime”* without qualification. - -**Example local config (explicit):** - -```yaml -platform: - runtime: docker - -jobs: - enable_subprocess_executor: true # host steps for dev; training still uses cpu/gpu + container - executor_defaults: - docker: - launcher_tool_path: ./services/core/jobs/jobs-launcher/jobs-launcher - subprocess: - working_directory: /tmp/nmp-subprocess-jobs -``` - -Production / GPU-only deployments can set `enable_subprocess_executor: false` to avoid registering host execution while keeping `runtime: docker` for Automodel and other container jobs. - ---- - -## Current state (reference) - -### Legacy Customizer (`Platform/services/customizer/`) - -- **API**: `CustomizationJobInput` / `CustomizationJobOutput` via `job_route_factory` (`api/v2/jobs/endpoints.py`). -- **Compiler**: `platform_job_config_compiler` builds a **4-step** `PlatformJobSpec`: - 1. `nmp.customizer.tasks.file_io` (CPU) — download model + datasets - 2. Training (GPU) — backend selected in training compiler (`automodel` \| `nemo_rl` \| `megatron_bridge`) - 3. `file_io` upload - 4. `nmp.customizer.tasks.model_entity` — register model in Models service -- **Automodel backend**: `tasks/training/backends/automodel/` — `compile_automodel_config()`, `AutomodelBackend`, `finetune.py` (wraps `nemo_automodel` recipes + `TrainingProgressCallback`). -- **Image**: `customizer-automodel` (see `nmp/docker/Dockerfile.nmp-customizer`); contract tests in `Platform/tests/customizer-automodel-contract/`. -- **Progress “callbacks”**: `JobsServiceProgressReporter` + `TrainingProgressCallback` call Jobs internal task API (rank-0 only). - -### Platform plugin patterns (`Platform/plugins/`) - -- Entry points: training plugins use `nemo.customization.contributors`; **`nemo-customizer-plugin`** uses `nemo.services` + `nemo.cli` key `customization`; jobs via `nemo.jobs` (`customization..`). -- Jobs: `NemoJob` + `add_job_routes()` (`nemo_platform_plugin.jobs.routes`). -- Reference plugins: `nemo-evaluator` (service + job scaffold), `nemo-data-designer` (CLI config file → spec), `nemo-agents` (service + multiple routers). - -### Simplified config shape (already validated) - -Contract input JSONs under `Platform/tests/customizer-automodel-contract/input_configs/` are a good starting point for the **CLI/API simplified spec** (e.g. `llama_3_2_1b_lora.json`): `model`, `dataset`, `training`, `schedule`, `batch`, `optimizer`, `parallelism`, `output_model`, optional `seed`. - ---- - -## Target architecture - -```mermaid -flowchart TB - subgraph surfaces [Plugin surfaces] - CLI["nemo customization automodel jobs submit -w WS job.json"] - API["POST .../v2/workspaces/WS/automodel/jobs"] - SDK["client...jobs.create(workspace=WS)"] - end - - subgraph router [nemo-customizer] - CUST["CustomizationRouterService"] - MERGE["merge contributors"] - end - - subgraph plugin [plugins/nemo-automodel] - CONTrib["AutomodelContributor"] - JOB["AutomodelJob\n(NemoJob.compile)"] - CLI_MOD["automodel CLI subgroup"] - end - - subgraph pkg [services/automodel — library only, no HTTP server] - CORE["compile_spec / validate"] - TASK_TRAIN["tasks/training\n(automodel only)"] - TASK_IO["tasks/file_io"] - TASK_ME["tasks/model_entity"] - end - - subgraph deploy [platform.runtime docker] - JOBS["Jobs service"] - DOCKER["cpu/gpu steps → Docker"] - MODELS["Models service"] - FILES["Files service"] - end - - CLI --> CUST - API --> CUST - SDK --> CUST - CUST --> MERGE --> CONTrib - CONTrib --> JOB - CONTrib --> CLI_MOD - CONTrib --> CORE - JOB -->|compile PlatformJobSpec| JOBS - JOBS --> DOCKER - DOCKER --> TASK_IO - DOCKER --> TASK_TRAIN - DOCKER --> TASK_ME - TASK_TRAIN -->|internal tasks API| JOBS - TASK_ME --> MODELS - TASK_IO --> FILES -``` - -### Package layout (proposed) - -``` -Platform/ - plugins/nemo-customizer/ # router + contributor protocol (v1) - pyproject.toml - src/nemo_customizer/ - router.py # CustomizationRouterService (nemo.services → customization) - cli.py # CustomizationCLI - contributor.py # re-export CustomizationContributor from nemo_platform_plugin - discovery.py # re-export discover_customization_contributors - docs/CUSTOMIZATION.md # contributor author guide - - plugins/nemo-automodel/ - SCOPE.md # this file - pyproject.toml - src/nemo_automodel_plugin/ - contributor.py # AutomodelContributor (routers + CLI subgroup) - cli.py + cli/inputs.py # JSON config → spec - config.py # NemoConfig (image names, defaults) - schema.py # AutomodelJobInput, AutomodelJobOutput, sub-models - jobs/ - jobs.py # AutomodelJob (compile + optional local run) - sdk/ # optional hub resources - docs/ - - services/automodel/ # Python package nmp-automodel — tasks/compiler only (no HTTP server) - pyproject.toml - src/nmp/automodel/ - config.py - platform_client.py # model entity fetch (from customizer) - app/jobs/ - compiler.py # Automodel-only PlatformJobSpec (4 steps, slim) - training/ - compiler.py # single GPU step - schemas.py - file_io/ # port or thin wrapper from customizer - model_entity/ # port from customizer - tasks/ - training/backends/automodel/ # port: config, backend, finetune, callbacks - file_io/ - model_entity/ - docker/ - Dockerfile.nmp-automodel-training # GPU: nmp-automodel-base + finetune - Dockerfile.nmp-automodel-tasks # CPU: file_io / model_entity glue (slimmer) - tests/ - -``` - -**Dependency rule**: - -| Package | Depends on | Provides | -|---------|------------|----------| -| **`nemo-customizer-plugin`** | `nemo-platform-plugin` | Router service/CLI; `CustomizationContributor` protocol and `discover_customization_contributors()` live in **`nemo_platform_plugin`** | -| **`nemo-automodel`** (plugin) | `nemo-platform-plugin`, `nmp-automodel` (+ `nemo-customizer-plugin` at runtime via `enabled-plugins`) | `AutomodelContributor`, schemas; Step 5 `cli/inputs.py` optional | -| **`nmp-automodel`** (service) | `nmp-common`, platform SDK types | Compilers, task entrypoints, Dockerfiles | - -Avoid pulling entire legacy `nmp-customizer`. **`nemo-platform-plugin`** holds the contributor protocol and discovery (IGW-aligned); **`nemo-customizer-plugin`** holds only the router service/CLI merge logic. - -### Customization router (in scope — v1) - -**Problem:** `discover_services()` maps `nemo.services` entry-point **keys** 1:1 to mounted apps (`/apis//...`). Only one plugin can own `customization`. A monolithic customizer is out; multiple training backends (Automodel, RL, Megatron, Unsloth) must share one URL tree without boxing future plugins into Automodel’s package. - -**Solution:** **`nemo-customizer-plugin`** ships **`CustomizationRouterService`** as the sole `nemo.services` registration for `customization`. Training plugins register as **contributors** via a new entry-point group; they do **not** register their own top-level `nemo.services` key. - -| Piece | Owner | Registration | -|-------|--------|----------------| -| `/apis/customization/...` mount | `nemo-customizer-plugin` | `nemo.services` → `customization` = `CustomizationRouterService` | -| Automodel routes | `nemo-automodel` plugin | `nemo.customization.contributors` → `automodel` = `AutomodelContributor` | -| Future RL / Megatron / Unsloth | Each backend’s plugin | Same group, distinct keys: `rl`, `megatron`, `unsloth`, … | -| Task/compiler library | `nmp-automodel` | No HTTP; imported by plugin + Jobs task images | - -**Contributor contract** (protocol in `nemo_customizer.contributor`): - -```python -class CustomizationContributor(Protocol): - """One training backend under /apis/customization.""" - - name: ClassVar[str] # must match entry-point key, e.g. "automodel" - - def get_routers(self) -> list[RouterSpec]: - """e.g. prefix v2/workspaces/{workspace}/automodel + job routes.""" - - def get_cli(self) -> typer.Typer | None: - """Subgroup mounted at `nemo customization `.""" -``` - -SDK: **`nemo-customizer-plugin`** registers `nemo.sdk` → `customization` and composes per-contributor SDK modules (e.g. `nemo_automodel_plugin.sdk.resources` → `client.customization.automodel`). -``` - -**Router behavior:** - -1. `discover_customization_contributors()` loads all `nemo.customization.contributors` entry points (fault-isolated, allowlist via `NEMO_PLUGIN_CUSTOMIZATION_CONTRIBUTORS_ALLOWLIST` or `NEMO_PLUGIN_ALLOWLIST`). -2. If **zero** contributors load, **fail startup** with a clear configuration error (router enabled but no backends). -3. `CustomizationRouterService.get_routers()` concatenates each contributor’s `RouterSpec` list (stable sort by `name`); `dependencies` = union of contributor + platform service deps (`merge_router_dependencies()` at router startup). -4. `CustomizationCLI.get_cli()` builds `typer.Typer(name="customization")` and mounts each contributor subgroup (`automodel`, …). -5. OpenAPI / SDK generation includes the merged tree under service name `customization` only. -6. **No route collision:** each contributor owns a distinct path segment after `.../workspaces/{workspace}/` (Automodel → `automodel`; legacy multi-backend `jobs` stays unmounted until a contributor revives it intentionally). - -**Automodel plugin wiring (v1):** - -| Surface | Entry point | Notes | -|---------|-------------|--------| -| HTTP | `nemo.customization.contributors.automodel` | **Not** `nemo.services` — router owns the mount | -| Jobs | `nemo.jobs` → `customization.automodel.jobs` | Unchanged | -| CLI | Via contributor `get_cli()` | `nemo customization automodel jobs ...` | -| SDK | `nemo-customizer-plugin` → `nemo.sdk:customization` composes contributor SDKs | `client.customization.automodel.jobs` | -| Tasks | `nmp-automodel` package | No server | - -**pyproject.toml (Automodel plugin):** - -```toml -[project.entry-points."nemo.customization.contributors"] -automodel = "nemo_automodel_plugin.contributor:AutomodelContributor" - -[project.entry-points."nemo.jobs"] -"customization.automodel.jobs" = "nemo_automodel_plugin.jobs.jobs:AutomodelJob" -``` - -**`nemo-customizer-plugin` pyproject.toml:** - -```toml -[project.entry-points."nemo.services"] -customization = "nemo_customizer.router:CustomizationRouterService" - -[project.entry-points."nemo.cli"] -customization = "nemo_customizer.cli:CustomizationCLI" - -[project.entry-points."nemo.sdk"] -customization = "nemo_customizer.sdk.resources:customization_sdk_resources" -``` - -Enable in platform workspace / `enabled-plugins` alongside `nemo-automodel`. - -**Implementation checklist:** [Step 1 — `nemo-customizer-plugin`](#step-1--nemo-customizer-blocks-automodel-http). - -### URL routing (decided) - -Platform mounts the router at `/apis/customization//...`. Automodel contributor prefix: - -| Piece | Value | -|-------|--------| -| Router `NemoService.name` | `customization` | -| Contributor key | `automodel` | -| Automodel `RouterSpec.prefix` | `v2/workspaces/{workspace}/automodel` | -| Example job create | `POST /apis/customization/v2/workspaces/{workspace}/automodel/jobs` | -| Legacy (deprecated) | `POST /apis/customization/v2/workspaces/{workspace}/jobs` — **not registered** in v1 | - -**No `/train/` segment:** Flat `/jobs` under `.../automodel/` (`NemoJob.job_collection_path = "/jobs"`). - -| Job wiring | Value | -|----------|--------| -| `NemoJob.job_collection_path` | `"/jobs"` | -| `NemoJob.name` | `"jobs"` (CLI/SDK subgroup suffix only) | -| `nemo.jobs` entry key | `customization.automodel.jobs` | -| `add_job_routes(..., service_name=)` | **`"customization"`** (required; sets Jobs `source` + filters) | -| `generate_job_name` | **`generate_automodel_id`** → `automodel-{hex}` | -| `route_options` | **`[JobRouteOption.CORE]`** (no pause/resume v1) | -| `training.execution_profile` | Spec field → GPU step profile; default from `NMP_AUTOMODEL_DEFAULT_TRAINING_EXECUTION_PROFILE` | -| Request `profile` body | **Deferred** — use spec field until `BaseJobRequest` plumbing lands | - -Do **not** register `nemo.services` = `automodel` (would split the product URL tree). - -**Contributor job mount (reference):** - -```python -from nmp.common.jobs.api_factory import JobRouteOption -from nemo_platform_plugin.jobs.routes import add_job_routes - -def get_routers(self) -> list[RouterSpec]: - return [ - RouterSpec( - prefix="v2/workspaces/{workspace}/automodel", - router=add_job_routes( - AutomodelJob, - service_name="customization", - generate_job_name=generate_automodel_id, - route_options=[JobRouteOption.CORE], - default_profile=plugin_config.default_training_execution_profile, - ), - ), - ] -``` - -**CLI:** `nemo customization automodel jobs submit job.json` — router CLI + Automodel contributor subgroup. - -### Workspace scoping (required) - -All Automodel resources are scoped to a **platform workspace** (tenant/project boundary). The workspace is carried on the URL path for HTTP, on CLI/SDK calls for clients, and in job/task runtime env — it is **not** a separate top-level field in the simplified job JSON body. - -#### API routes (full pattern) - -Mount prefix: `v2/workspaces/{workspace}/automodel` → base: - -`/apis/customization/v2/workspaces/{workspace}/automodel` - -| Operation | Method | Path (after base) | -|-----------|--------|-------------------| -| Create job | `POST` | `/jobs` | -| List jobs | `GET` | `/jobs` | -| Get job | `GET` | `/jobs/{job_name}` | -| Delete job | `DELETE` | `/jobs/{job_name}` | -| Job results | `GET` | `/jobs/{job_name}/results/...` | - -Example: - -```http -POST /apis/customization/v2/workspaces/acme-corp/automodel/jobs -Content-Type: application/json - -{ "model": "llama-3-8b-base", "dataset": { ... }, ... } -``` - -`acme-corp` is the scope for: authz checks, Jobs service record, Models/Filesets entities, and compiled fileset `workspace` fields. - -#### Workspace in the job spec (body vs path) - -| Source | Role | -|--------|------| -| **Path `{workspace}`** | Authoritative scope for the job and all entities created by it (output model, output fileset, Jobs record). | -| **Spec `model`** | Model entity **name** in the path workspace, or qualified `other-workspace/model-name` for cross-workspace reads (same as legacy `CustomizationJobInput.model`). | -| **Spec `dataset`** | `{ training: "name" }` or `{ training: "workspace/name" }` — bare names resolve in the path workspace (no `fileset://` prefix). | -| **Spec `output.name`** | New or updated `ModelEntity` **in the path workspace** only. | -| **Body `workspace` field** | **Do not add** — avoids conflicting with the path param. | - -`compile(workspace, spec, ...)` and `to_spec(..., workspace=...)` receive the path workspace from `add_job_routes` / `job_route_factory` (same contract as `nemo_platform_plugin.jobs.routes`). - -#### CLI - -Auto-generated `submit` / `run` include `--workspace` / `-w` (default `"default"`). Custom wrappers must **forward** it to the framework callback: - -```bash -nemo customization automodel jobs submit job.json --workspace acme-corp -nemo customization automodel jobs submit job.json -w acme-corp -# execution profile: set training.execution_profile in job.json (request --profile body deferred) -``` - -Submit URL (see `nemo_platform_plugin.commands` job submit helper): - -`/apis/customization/v2/workspaces/{workspace}/automodel/jobs` - -i.e. `/apis/{NemoService.name}/{RouterSpec.prefix}/...` with `name=customization` and prefix `v2/workspaces/{workspace}/automodel`. - -#### SDK - -Hub resources take `workspace` on every call (pattern: evaluator `client.evaluator...`): - -```python -job = client.customization.automodel.jobs.create( - workspace="acme-corp", - spec=AutomodelJobInput(...), -) -status = client.customization.automodel.jobs.retrieve( - workspace="acme-corp", - name=job.name, -) -``` - -SDK must not default silently to a global namespace; document `workspace="default"` for local dev only. - -#### Runtime (compiled job + tasks) - -| Stage | Workspace usage | -|-------|------------------| -| **Compile** | `fetch_model_entity(spec.model, workspace, sdk)`; output fileset refs use `workspace=None` in compile JSON and are resolved at runtime to the job workspace (legacy customizer pattern). | -| **Jobs service** | Job created in path workspace. | -| **Task containers** | `NEMO_JOB_WORKSPACE` (and `JobContext.workspace` / `get_workspace()`) set from job; `model_entity` task creates entities in that workspace. | -| **Progress callbacks** | `sdk.jobs.tasks.create_or_update(..., workspace=job_ctx.workspace, job=job_ctx.job_id, ...)`. | -| **List/filter** | API list endpoints return only jobs in the path workspace. | - -#### Tests - -→ [Step 5](#step-5--cli-submit-path) (CLI `-w`), [Step 6](#step-6--tests--contract-continuity) (API, integration, workspace isolation). - ---- - -## Work breakdown - -Phases map to [Implementation order](#implementation-order). **Checklists and step-level detail live in the steps above**; sections below add design reference (Option A wiring, Studio verification, JSON spec) without duplicating deliverables. - -| Phase | Implementation step(s) | Topic | -|-------|------------------------|--------| -| 0 | [Step 0](#step-0--design-lock--platform-prerequisites) | Design lock, Jobs flag, schemas | -| 1 | [Step 1](#step-1--nemo-customizer-blocks-automodel-http) | Customization router | -| 2 | [Step 2](#step-2--nmp-automodel-package-core) | `nmp-automodel` compiler/tasks | -| 3 | [Step 3](#step-3--container-images) | Docker images | -| 4 | [Step 4](#step-4--nemo-automodel-plugin-contributor--job) | Automodel plugin + Docker gate | -| 5 | [Step 5](#step-5--cli-submit-path) | CLI | -| 6 | [Step 6](#step-6--tests--contract-continuity) | Tests | -| 7 | [Step 7](#step-7--sdk-openapi-docs--rollout) | SDK / docs / deploy | -| — | [Step 2](#step-2--nmp-automodel-package-core) (callbacks) | Internal Jobs task API (not public) | - -### Phase 0 — Design lock - -→ [Step 0](#step-0--design-lock--platform-prerequisites). Router design: [Customization router](#customization-router-in-scope--v1). - -#### Input vs canonical spec — **decided: Option A** - -On job **create**, the platform always: - -1. Validates the POST body against **`AutomodelJobInput`** (`input_spec_schema`). -2. Runs **`AutomodelJob.to_spec()`** → **`AutomodelJobOutput`** stored on the Jobs record (`spec_schema`). -3. Runs **`compile()`** on the canonical output → `platform_spec` for execution. - -Enrichment (auto output name/fileset, adapter vs model type, dataset ACL, model entity fetch) happens in step 2 — the Jobs service persists that result, not a post-compile rewrite. Rejected alternatives: single-schema POST (manual output fields), enrich-only-in-`compile()` (broken persistence), renamed input fields (unnecessary vs legacy). - -**`AutomodelJob` wiring:** - -```python -class AutomodelJobInput(BaseModel): # POST body / CLI JSON - model: str # name or workspace/name - dataset: DatasetSpec # training + optional validation fileset URIs - training: TrainingSpec # includes training_type, execution_profile, ... - output: OutputRequest | None = None # optional name only - # @model_validator: reject "output_model" key with legacy error message - -class AutomodelJobOutput(BaseModel): # stored spec + GET response shape - output: OutputResponse # required: name, fileset, type (model | adapter) - # ... enriched fields from input ... - - def validate_for_training(self) -> None: - # Port MoE / parallelism rules from CustomizationJobOutput - -class AutomodelJob(NemoJob): - name = "jobs" - job_collection_path = "/jobs" - input_spec_schema = AutomodelJobInput - spec_schema = AutomodelJobOutput - dependencies = ["entities", "auth", "jobs", "secrets", "files", "models"] - - @classmethod - async def to_spec(cls, input_spec, *, workspace, entity_client, async_sdk, is_local): - # Port transform_input_to_output + check_dataset_access per fileset - - @classmethod - async def compile(cls, *, workspace, spec: AutomodelJobOutput, ...): - spec.validate_for_training() - # nmp.automodel.app.jobs.compiler → PlatformJobSpec (4 steps) -``` - -**Implementation notes:** - -- Port source: `Platform/services/customizer/src/nmp/customizer/utils.py` (`transform_input_to_output`). -- `to_spec()` generates `output.fileset`, infers `output.type`, runs `fetch_model_entity` + `check_dataset_access`. -- `compile()` receives **`AutomodelJobOutput` only**; calls `validate_for_training()` before building `PlatformJobSpec`. -- Mount via `add_job_routes(..., service_name="customization", generate_job_name=generate_automodel_id)` — [URL routing](#url-routing-decided). -- **CLI JSON** = `AutomodelJobInput`. **`jobs explain`** exposes both schemas. - -#### Deprecation — Platform spin-up and Studio (verified) - -**Platform `AVAILABLE_SERVICES`** (`packages/nmp_platform_runner/src/nmp/platform_runner/registry.py`) does **not** include `customization` / `customizer`: - -```18:33:packages/nmp_platform_runner/src/nmp/platform_runner/registry.py -AVAILABLE_SERVICES: dict[str, str] = { - "hello-world": "nmp.hello_world.main:service", - "studio": "nmp.studio.main:service", - ... - "inference-gateway": "nmp.core.inference_gateway.main:service", -} -``` - -`API_SERVICES` and `OPENAPI_SERVICES` likewise omit customization. Plugin services are merged at runtime via `discover_services()` (e.g. future `customization` from `nemo.services`), but the **legacy `nmp.customizer` microservice is not started** by default platform spin-up in this repo. - -**Note:** The older `nmp/` tree still lists `"customization": "nmp.customizer.main:service"` in its copy of the registry — do not treat that as Platform default behavior. - -**Studio today:** - -| Signal | Status | -|--------|--------| -| `VITE_FF_CUSTOMIZER_ENABLED` | Default **`false`** (`featureFlags.ts`) | -| Routes | Gated via `CUSTOMIZER_ENABLED` / `gateRoutes` — customization pages hidden when flag off | -| Live API | Vendored hooks target `/apis/customization/v2/.../jobs`; comment states service removed and UI must not call at runtime | -| Tests | MSW handlers in `mocks/handlers/customizer.ts`; `create-a-customization.spec.tsx` is **`describe.skip`** | - -```8:9:Platform/web/packages/sdk/vendored/customizer/api.ts -// Note: these hooks call /apis/customization/v2/... endpoints that won't exist while the customizer -// service is removed. The customizer UI is feature-flagged off, so they should never be invoked at runtime. -``` - -**First PR implication:** Safe to register **`CustomizationRouterService`** plus **`AutomodelContributor`** without legacy `nmp-customizer`. Studio/SDK migration **out of scope**. - -### Phase 1 — `nmp-automodel` package core - -→ [Step 2](#step-2--nmp-automodel-package-core). Port table and deliverables are defined there. - -### Phase 2 — Plugin surfaces - -→ [Step 4](#step-4--nemo-automodel-plugin-contributor--job) + [Step 5](#step-5--cli-submit-path). Requires [Step 1](#step-1--nemo-customizer-blocks-automodel-http). - -### Phase 3 — Docker enforcement & GPU validation - -→ [Step 4](#step-4--nemo-automodel-plugin-contributor--job) (compile-time checks). Today `validate_gpu_available_for_docker` only runs when `runtime == DOCKER` and reserved GPU list is empty — extend for all Automodel jobs. - -### Phase 4 — Container images - -→ [Step 3](#step-3--container-images). - -### Phase 5 — Internal Jobs callback path - -→ [Step 2](#step-2--nmp-automodel-package-core) (not a new public route). Optional later: webhooks. - -### Phase 6 — API & SDK polish - -→ [Step 7](#step-7--sdk-openapi-docs--rollout). - -### Phase 7 — Testing & contract continuity - -→ [Step 6](#step-6--tests--contract-continuity). - -### Phase 8 — Docs & rollout - -→ [Step 7](#step-7--sdk-openapi-docs--rollout). - ---- - -## Simplified JSON spec (draft) — `AutomodelJobInput` only - -POST body and CLI JSON file use **`AutomodelJobInput`** only. After create, GET returns **`AutomodelJobOutput`** with enriched `output` (fileset, type). Validated in the context of the path **`workspace`** (or CLI `-w`). Entity names below are relative to that workspace unless qualified as `other-ws/name`. - -```json -{ - "name": "optional-job-name", - "model": "llama-3-8b-base", - "dataset": { - "training": "my-sft-train", - "validation": "my-sft-val" - }, - "training": { - "training_type": "sft | distillation", - "finetuning_type": "lora | all_weights | lora_merged", - "lora": { "rank": 16, "alpha": 32, "merge": false, "target_modules": null }, - "max_seq_length": 2048, - "execution_profile": "gpu", - "teacher_model": "meta/llama-3.2-3b-instruct", - "distillation_ratio": 0.5, - "distillation_temperature": 1.0, - "teacher_precision": "bf16", - "offload_teacher": false - }, - "schedule": { "epochs": 1, "max_steps": 50, "val_check_interval": 25, "seed": 42 }, - "batch": { "global_batch_size": 8, "micro_batch_size": 1, "sequence_packing": false }, - "optimizer": { - "learning_rate": 5e-6, - "weight_decay": 0.01, - "warmup_steps": 0 - }, - "parallelism": { - "num_nodes": 1, - "num_gpus_per_node": 1, - "tensor_parallel_size": 1, - "pipeline_parallel_size": 1, - "context_parallel_size": 1 - }, - "output": { "name": "my-finetuned-model", "description": "optional" }, - "integrations": { - "wandb": { "enabled": true, "project": "my-project", "api_key_secret": "wandb-api-key" }, - "mlflow": null - } -} -``` - -**Validation rules:** - -- **`output_model` is rejected** at parse time (legacy: *"spec.output_model was removed. Use spec.output instead."*). -- `teacher_model`, `distillation_*`, and `offload_teacher` only when `training_type` is `distillation` (omit for `sft`). -- Optional `dataset.prompt_template` for non-chat prompt/completion data (chat datasets use tokenizer chat template — document in README). -- Compiler may accept additional optimizer/parallelism fields required by contract JSONs even if omitted from this minimal example (`adam_beta1`, `expert_parallel_size`, …). - -**Training types in v1:** - -| `training_type` | Automodel recipe | Notes | -|-----------------|------------------|-------| -| `sft` | `TrainFinetuneRecipeForNextTokenPrediction` | Default; LoRA / all_weights / lora_merged | -| `distillation` | `KnowledgeDistillationRecipeForNextTokenPrediction` | Requires `teacher_model`; maps to Automodel `teacher_model`, `kd_ratio`, `kd_loss_fn` ([`nemo_automodel/recipes/llm/kd.py`](https://github.com/NVIDIA-NeMo/Automodel/blob/main/nemo_automodel/recipes/llm/kd.py), example [`examples/llm_kd/llama3_2/llama3_2_1b_kd.yaml`](https://github.com/NVIDIA-NeMo/Automodel/blob/main/examples/llm_kd/llama3_2/llama3_2_1b_kd.yaml)) | - -**KD / distillation fields** (when `training_type: distillation`): mirror legacy Customizer API — `teacher_model` (entity ref in path workspace), `distillation_ratio` (→ `kd_ratio`, default `0.5`), `distillation_temperature` (→ `kd_loss_fn.temperature`, default `1.0`), `teacher_precision` (default `bf16`), optional `offload_teacher` (→ `offload_teacher_model`). Compiler port: `_configure_kd()` in legacy `automodel/config.py`. Validate tokenizer compatibility student/teacher before submit. - -**Explicitly out of scope (v1):** DPO, GRPO, `nemo_rl`, `megatron_bridge`, quantized LoRA, DoRA, **embedding-model SFT** (`embed_1b` / biencoder recipe), **`deployment_config`** (post-train NIM deploy), request-body **`profile`** on job create (use `training.execution_profile`). - -**Compiler responsibilities** (unchanged from legacy): - -1. Resolve `model` → `ModelEntity` in **path workspace** (or explicit `ws/name` ref). -2. Resolve dataset filesets in **path workspace** → local paths in download step. -3. `compile_automodel_config()` → YAML/JSON for `finetune.py`. -4. Generate output fileset + `ModelEntityTaskConfig` with `workspace` = path workspace (output model and fileset live in that workspace). - ---- - -## Risk & complexity notes - -| Topic | Note | -|-------|------| -| **Largest port** | `compile_automodel_config()` (~800 LOC) and `validate_for_training()` (MoE/parallelism). `deployment_config` and embedding SFT are **out of scope v1**. | -| **Shared code** | File I/O and model_entity tasks are backend-agnostic — candidate for `nmp-common` or small `nmp-training-tasks` lib later; v1 can duplicate to ship faster. | -| **Python version** | NGC automodel uses 3.12; platform pins 3.11 for API — task image runs 3.12 (existing customizer pattern). | -| **KD / distillation** | In v1 JSON as `training_type: distillation`; compiler maps to Automodel KD recipe (see simplified JSON section). | -| **Customizer service** | Remains in repo but unused; avoid dual registration in `NMP_SERVICES`. | -| **Studio cutover** | **Out of scope** — no feature flag or Studio migration in Automodel v1; `VITE_FF_CUSTOMIZER_ENABLED` stays off. | -| **Customization router** | v1 in scope: **`nemo-customizer-plugin`** (`CustomizationRouterService` + contributor protocol); Automodel first contributor; RL/Megatron/Unsloth add contributors later without new `/apis/*` services. | -| **`runtime` vs subprocess flag** | `platform.runtime: docker` enables Docker-backed job profiles; `jobs.enable_subprocess_executor` separately controls host subprocess. Automodel training requires the former, not the latter. | - ---- - -## Non-critical follow-ups (post-v1) - -Merged into [Implementation order](#implementation-order) and [Decisions](#decisions-resolved). Remaining items are not blocking the first PR: - -| Topic | Notes | -|-------|--------| -| **`nemo.customization.contributors` in `_ALL_SURFACE_GROUPS`** | **Done:** `nemo_platform_plugin.discovery` — manifests + `discover_customization_contributors()` (IGW-aligned). | -| **Request-body `profile` on job create** | Platform follow-up MR on `BaseJobRequest` + `add_job_routes`; until then CLI `--profile` may only map to `training.execution_profile` in JSON. | -| **`custom_fields` passthrough** | Factory already supports; document if customers rely on it. | -| **Full optimizer / MoE parallelism in public JSON** | Compiler + contracts may need fields beyond the minimal example; expand OpenAPI as contract port discovers gaps. | -| **Chat dataset contract tests** | Port `*_full_sft_chat.json` when `prompt_template` behavior is documented. | - ---- - -## Decisions (resolved) - -| # | Topic | Decision | -|---|--------|----------| -| 1 | **Service vs plugin-only** | **No standalone `nmp-automodel` HTTP server.** Automodel HTTP lives on **`AutomodelContributor`** merged by **`nemo-customizer-plugin`** (`CustomizationRouterService`) at `/apis/customization`. `nmp-automodel` is compiler + tasks only. | -| 2 | **KD / distillation** | **Include in v1** simplified JSON when `training_type: distillation`. Map to [Automodel KD recipe](https://github.com/NVIDIA-NeMo/Automodel/tree/main/nemo_automodel/recipes/llm/kd.py) (`teacher_model`, `kd_ratio`, `kd_loss_fn`, optional `offload_teacher_model`). Port legacy `_configure_kd()` / `DistillationConfig` from customizer automodel backend. | -| 3 | **Image naming** | **`nvcr.io/0921617854601259/nemo-platform-dev/nmp/automodel-training`** (GPU) and **`.../nmp/automodel-tasks`** (CPU). Do **not** reuse `customizer-automodel` or the upstream `nvcr.io/nvidia/nemo-automodel` image name. | -| 4 | **Workspace package name** | **`nmp-automodel`** (PyPI upstream library remains `nemo-automodel` / NGC image name unchanged). | -| 5 | **Studio cutover** | **Punted** — no Studio feature flag or migration to `.../automodel/...` in this scope. | -| 6 | **`customization` owner** | **In scope v1:** dedicated **`nemo-customizer-plugin`** owns `nemo.services` key `customization`; backends register via **`nemo.customization.contributors`** (Automodel first; RL / Megatron / Unsloth later). `nemo-automodel` must **not** register `nemo.services` directly. | -| 7 | **`enable_subprocess_executor` on K8s** | **Default `false` on Kubernetes**; explicit `true` only when dev clusters need host subprocess. Default `true` for `platform.runtime: docker` local dev. | -| 8 | **Jobs `source` / naming** | **`service_name="customization"`** on `add_job_routes` (never default `nemo-automodel-plugin`). Auto names: **`automodel-{hex}`** via `generate_automodel_id`. | -| 9 | **`execution_profile` v1** | In **`training.execution_profile`** on job spec; default from **`NMP_AUTOMODEL_DEFAULT_TRAINING_EXECUTION_PROFILE`**. Request-body `profile` on create — **deferred** (platform gap). | -| 10 | **Embedding SFT** | **Out of scope v1** (causal LM + KD only); `embed_1b` contracts gated in Step 6 until product expands. | -| 11 | **`deployment_config`** | **Out of scope v1** (post-train NIM deploy; Studio-adjacent). | -| 12 | **Router zero contributors** | **Fail startup** if customization plugin is enabled but no `nemo.customization.contributors` load. | - diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/inputs.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/inputs.py index 1dcd6e7ca5..cf401be67a 100644 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/inputs.py +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/inputs.py @@ -30,10 +30,10 @@ def apply_automodel_job_cli_overrides(group: typer.Typer) -> None: def _pluck_callback(group: typer.Typer, verb: str) -> Callable[..., None]: - callback = next(c for c in group.registered_commands if c.name == verb).callback - if callback is None: + command = next((c for c in group.registered_commands if c.name == verb), None) + if command is None or command.callback is None: raise RuntimeError(f"missing {verb!r} callback to override") - return callback + return command.callback def _drop_command(group: typer.Typer, name: str) -> None: diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/contributor.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/contributor.py index c8e70a9b39..b6b7b8ce92 100644 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/contributor.py +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/contributor.py @@ -10,9 +10,9 @@ import typer from fastapi import APIRouter from nemo_platform_plugin.authz import AuthzContribution, authz_for_workspace_job_collection +from nemo_platform_plugin.jobs.api_factory import JobRouteOption from nemo_platform_plugin.jobs.routes import add_job_routes from nemo_platform_plugin.service import RouterSpec -from nmp.common.jobs.api_factory import JobRouteOption from nemo_automodel_plugin.config import generate_automodel_id, get_config from nemo_automodel_plugin.jobs.jobs import AutomodelJob diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py index 30a9ef5870..0dc8d79973 100644 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py @@ -7,7 +7,11 @@ from typing import Any, Literal, Self -from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_validator + + +class ValidationError(ValueError): + """Raised when automodel job input validation fails.""" class LoRAParams(BaseModel): @@ -170,20 +174,10 @@ def validate_for_training(self) -> None: total_gpus = num_gpus_per_node * num_nodes model_parallel_size = tp * pp * cp if total_gpus % model_parallel_size != 0: - raise ValidationError.from_exception_data( - "parallelism", - [ - { - "type": "value_error", - "loc": ("parallelism",), - "msg": ( - f"Total GPUs ({total_gpus}) must be divisible by " - f"tensor_parallel_size ({tp}) * pipeline_parallel_size ({pp}) * " - f"context_parallel_size ({cp}) = {model_parallel_size}" - ), - "input": p.model_dump(), - } - ], + raise ValidationError( + f"Total GPUs ({total_gpus}) must be divisible by " + f"tensor_parallel_size ({tp}) * pipeline_parallel_size ({pp}) * " + f"context_parallel_size ({cp}) = {model_parallel_size}" ) derived_dp = total_gpus // model_parallel_size @@ -191,50 +185,20 @@ def validate_for_training(self) -> None: mb = self.batch.micro_batch_size divisor = mb * derived_dp if gb % divisor != 0: - raise ValidationError.from_exception_data( - "batch", - [ - { - "type": "value_error", - "loc": ("batch", "global_batch_size"), - "msg": ( - f"global_batch_size ({gb}) must be divisible by " - f"micro_batch_size ({mb}) * data_parallel_size ({derived_dp}) = {divisor}" - ), - "input": gb, - } - ], + raise ValidationError( + f"global_batch_size ({gb}) must be divisible by " + f"micro_batch_size ({mb}) * data_parallel_size ({derived_dp}) = {divisor}" ) if ep is not None: dp_cp = derived_dp * cp if dp_cp % ep != 0: - raise ValidationError.from_exception_data( - "parallelism", - [ - { - "type": "value_error", - "loc": ("parallelism", "expert_parallel_size"), - "msg": ( - f"(data_parallel_size * context_parallel_size) ({dp_cp}) " - f"must be divisible by expert_parallel_size ({ep})" - ), - "input": ep, - } - ], + raise ValidationError( + f"(data_parallel_size * context_parallel_size) ({dp_cp}) " + f"must be divisible by expert_parallel_size ({ep})" ) if ep > 1 and tp > 1 and total_gpus > 1: - raise ValidationError.from_exception_data( - "parallelism", - [ - { - "type": "value_error", - "loc": ("parallelism", "tensor_parallel_size"), - "msg": ( - f"Tensor parallelism (tensor_parallel_size={tp}) is not supported for MoE models " - f"when expert_parallel_size > 1 ({ep}); tensor_parallel_size must be 1." - ), - "input": tp, - } - ], + raise ValidationError( + f"Tensor parallelism (tensor_parallel_size={tp}) is not supported for MoE models " + f"when expert_parallel_size > 1 ({ep}); tensor_parallel_size must be 1." ) diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/transform.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/transform.py index 290fa60613..e947e19fda 100644 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/transform.py +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/transform.py @@ -80,7 +80,7 @@ async def transform_input_to_output( output = OutputResponse( name=out_name, - type=output_type, # type: ignore[arg-type] + type=output_type, fileset=fileset, description=input_spec.output.description if input_spec.output else None, ) diff --git a/plugins/nemo-customizer/src/nemo_customizer/router.py b/plugins/nemo-customizer/src/nemo_customizer/router.py index 1f372138b2..b885978e4b 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/router.py +++ b/plugins/nemo-customizer/src/nemo_customizer/router.py @@ -8,6 +8,7 @@ from typing import ClassVar from fastapi import APIRouter +from nemo_platform_plugin.authz import AuthzContribution, AuthzEndpointMethod, combine_authz_contributions from nemo_platform_plugin.discovery import ( CUSTOMIZATION_CONTRIBUTORS_GROUP, discover_customization_contributors, @@ -43,7 +44,7 @@ def _assert_no_route_collisions(contributors: dict[str, object]) -> None: # Map (method, full_path) -> contributor key seen: dict[tuple[str, str], str] = {} for key, contributor in contributors.items(): - for spec in contributor.get_routers(): # type: ignore[union-attr] + for spec in contributor.get_routers(): prefix = spec.prefix.rstrip("/") for route in spec.router.routes: methods = getattr(route, "methods", None) or {"*"} @@ -53,12 +54,37 @@ def _assert_no_route_collisions(contributors: dict[str, object]) -> None: op = (method, full_path) if op in seen: raise CustomizationRouterError( - f"Route collision: contributors {seen[op]!r} and {key!r} " - f"both handle {method} {full_path}", + f"Route collision: contributors {seen[op]!r} and {key!r} both handle {method} {full_path}", ) seen[op] = key +def _hub_authz_contribution() -> AuthzContribution: + """Authz for the customization router hub (authenticated health check only).""" + return AuthzContribution( + endpoints={ + "/apis/customization/healthz": { + "get": AuthzEndpointMethod(permissions=[], scopes=[]), + }, + }, + ) + + +def _authz_from_contributors(contributors: dict[str, object]) -> AuthzContribution | None: + """Collect and merge authz from installed customization backends.""" + backend_parts: list[AuthzContribution] = [] + for contributor in contributors.values(): + getter = getattr(contributor, "get_authz_contribution", None) + if not callable(getter): + continue + contrib = getter() + if contrib is not None: + backend_parts.append(contrib) + if not backend_parts: + return None + return combine_authz_contributions(_hub_authz_contribution(), *backend_parts) + + class CustomizationRouterService(NemoService): """Sole ``nemo.services`` owner for ``/apis/customization``.""" @@ -76,6 +102,11 @@ def __init__(self) -> None: _assert_no_route_collisions(self._contributors) type(self).dependencies = merge_router_dependencies(self._contributors) + @classmethod + def get_authz_contribution(cls) -> AuthzContribution | None: + """Merge backend contributor authz (automodel, unsloth, …) for policy discovery.""" + return _authz_from_contributors(discover_customization_contributors()) + def get_routers(self) -> list[RouterSpec]: router = APIRouter() diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/SKILL.md b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/SKILL.md index 0805de6885..82ee24129a 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/SKILL.md +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/SKILL.md @@ -1,11 +1,11 @@ --- name: nemo-customizer description: >- - Fine-tune models on NeMo Platform with `automodel` (`submit` → GPU containers on - the platform) or `unsloth` (`run --venv` → local in-process, BYO-venv): HF dataset - conversion, filesets, model entities, SFT/LoRA job JSON (hyperparameters, batch, - schedule, optimizer), and job polling. Use for train, fine-tune, customize, SFT, - LoRA, learning rate, epochs, or nemo customization. + Fine-tune models on NeMo Platform with `automodel` or `unsloth` (both `submit` → + Docker GPU jobs via the platform Jobs service): HF dataset conversion, filesets, + model entities, SFT/LoRA job JSON (hyperparameters, batch, schedule, optimizer), + and job polling. Use for train, fine-tune, customize, SFT, LoRA, learning rate, + epochs, or nemo customization. triggers: - nemo-customizer - nemo customizer @@ -23,6 +23,7 @@ triggers: - customizer - customization training - automodel submit + - unsloth submit not-for: - nemo-build-agent (agent scaffold/deploy, not weight training) - nemo-explore (agent design only) @@ -38,12 +39,14 @@ allowed-tools: [Bash, Read, Grep] # NeMo Customizer -End-to-end **SFT + LoRA** on NeMo Platform. Two backend plugins ship in this repo: +End-to-end **SFT + LoRA** on NeMo Platform. Two backend plugins ship in this repo — both are **`submit`-only** (local `run` is hard-disabled on each): | Backend | Verb | Where it runs | Pick when | |---------|------|---------------|-----------| -| **`automodel`** (default) | `submit` | Platform-managed GPU containers (jobs service) | Platform exposes a GPU execution profile, or the user wants a persisted job they can poll/share | -| **`unsloth`** | `run --venv ` | **Locally**, in a BYO-venv re-exec on the caller's GPU | User asks for Unsloth, the platform has **no GPU execution profile**, or the user wants a quick single-GPU local SFT/LoRA | +| **`automodel`** (default) | `submit` | Platform **Docker GPU executor** (Jobs service schedules containers on the platform host's daemon) | General SFT/LoRA; multi-GPU (data/tensor parallel); distillation; full-weight SFT | +| **`unsloth`** | `submit` | Same — Docker GPU job with 4 steps (download → train → upload → model-entity) | User asks for Unsloth, or wants Unsloth's 4-bit LoRA path / optimizer defaults on a single GPU | + +`nemo-customizer` is the router (`nemo customization …`); training backends are separate plugins (`nemo-automodel`, `nemo-unsloth`). `submit` posts to the platform API; the platform runs training in container steps — **not** in the CLI shell. Heavy ML deps live in container images only. Decision rule below in **Plugin pick**. Batch shell work; reuse resources with `--exist-ok`; skip CLI `--help` unless a command fails. @@ -51,65 +54,71 @@ Decision rule below in **Plugin pick**. Batch shell work; reuse resources with ` 1. After `nemo auth login`, run `uv run nemo jobs list-execution-profiles -f json` (see `references/troubleshooting.md` for parsing). 2. If the user explicitly asked for Unsloth → **`unsloth`**. -3. Else if any profile has `provider: gpu` or `gpu_distributed` → **`automodel`** (default). -4. Else if the caller machine has a usable local GPU → **`unsloth`** (after the `--venv` one-time setup below). -5. Else stop and tell the user remote GPU customization is unavailable. - -**Unsloth `--venv` requirement.** The base `nemo` install has no Unsloth/torch. Before the first `unsloth run`, set up a separate venv with the heavy ML extras (one-time): - -```bash -UNSLOTH_VENV=/workspace/.venv-unsloth -uv venv "$UNSLOTH_VENV" --python 3.11 -uv pip install --python "$UNSLOTH_VENV/bin/python" -e plugins/nemo-unsloth[unsloth] -``` +3. Else if the user explicitly asked for Automodel → **`automodel`**. +4. Else if any profile has `provider: gpu` or `gpu_distributed` → **`automodel`** (default). +5. Else stop and tell the user GPU customization is unavailable (both backends need a GPU execution profile and `platform.runtime: docker` on the connected platform). -Then every `unsloth run` passes `--venv "$UNSLOTH_VENV"`. The platform re-execs into that interpreter before importing `unsloth`. Without `--venv`, the run errors with an actionable setup hint. +Training never runs inside the `nemo` CLI process. After `submit`, the platform's **local Docker executor** launches GPU container steps on the daemon attached to that platform host (often the same machine as `http://127.0.0.1:8080`, but always query the platform — not the agent's shell GPU or a separate `docker info` on another box). ## Gotchas - Run all `uv run` commands from the **nemo-platform** git root (top-level `pyproject.toml`), not a plugin subfolder. -- Set `NEMO_BASE_URL` (or `NMP_BASE_URL`) only when the user gives a platform URL; default `http://127.0.0.1:8080`. -- **Verb is backend-specific.** Automodel is **`submit` only** (no local `run`). Unsloth is **`run` only** (no `submit`); a stray `nemo customization unsloth submit ...` exits non-zero with a friendly hint. Do not improvise verbs. +- Set `NEMO_BASE_URL` (or `NMP_BASE_URL`) only when the user gives a platform URL; default `http://127.0.0.1:8080` (same as `http://localhost:8080`). Track whether the user **overrode** the base URL — see **Platform unreachable** below. +- **Platform unreachable** — if any platform API call fails with a connection error (`Connection error`, timeout, refused): + - **User gave a custom URL** (e.g. `10.0.0.51:8080`) or you exported a non-default `NEMO_BASE_URL` / `NMP_BASE_URL`: stop and tell the user the platform is not reachable at that address. Do **not** offer to start local services. + - **Default URL only** (no user override): **ask** whether to start the platform locally. If they agree, from the **nemo-platform** git root run in the **background**: + + ```bash + uv run nemo services run \ + --host 0.0.0.0 \ + --port 8080 \ + --controllers jobs,entities,models \ + --service-group all + ``` + + Poll until healthy (`curl -sf http://127.0.0.1:8080/health/ready` or retry `nemo jobs list-execution-profiles -f json`), then continue the workflow. Do not start services without asking. +- **Both backends are `submit` only** — `nemo customization run …` hard-fails on automodel and unsloth with a pointer to `submit`. Do not improvise verbs or pass `--venv`. - **Never set `max_steps` together with `epochs`** (both backends). `max_steps` is a global cap and stops mid-epoch. Test fixtures include `max_steps` for smoke tests — do not copy into production jobs. Unsloth's schema enforces this as a hard mutex; automodel allows both but the result is surprising. -- **Job done (automodel) = top-level `status`** in `completed` | `error` | `cancelled`. Steps can all be `completed` while the job is still `active` (upload, entity registration). `status_details.phase` may stay `training` with `progress_pct: 100` for a long time — keep polling. `poll_automodel_job.sh` exits **1** on `error` or `cancelled`. -- **Job done (unsloth) = `nemo customization unsloth run` returns.** It is synchronous and in-process — the result dict (`loss`, `output_path`, `output_fileset`, `output_model_entity`) prints to stdout. There is no remote job record to poll. +- **Job done (both backends) = top-level `status`** in `completed` | `error` | `cancelled`. Steps can all be `completed` while the job is still `active` (upload, entity registration). `status_details.phase` may stay `training` with `progress_pct: 100` for a long time — keep polling. `poll_customization_job.sh` works for any job id (`automodel-…` or `unsloth-…`); it exits **1** on `error` or `cancelled`. - Model spec fills async: **submit without polling** `nemo models get` unless submit fails. - HF dataset id from the user → convert locally; do not ask for local paths first. - Dataset fileset name = HF dataset **name** only (`tau/commonsense_qa` → `commonsense_qa`), not the model name. - Prefer **CHAT** JSONL when the model has a chat template; details in `references/dataset-formats.md` (automodel auto-detects schema; unsloth needs `dataset.apply_chat_template: true` to consume `messages`). - User asks to tune **batch or parallelism** (automodel) → **Batch sizing** / **Multi-GPU** below. Other fields (LR, epochs, LoRA rank, distillation) → `references/hyperparameters.md`. For unsloth, see **Batch sizing — unsloth** and the `Unsloth job JSON` section in `references/hyperparameters.md`. Run `nemo customization explain` for the live schema. - Skill **defaults** (`micro_batch_size` 1, `global_batch_size` 4) are safe on unknown VRAM. When the user has **≥48 GB** on one GPU, use **Batch sizing** instead of defaults. Unsloth's analogues are `batch.per_device_train_batch_size` and `batch.gradient_accumulation_steps` (effective batch = product). -- **Unsloth is single-GPU**. `hardware.gpus` is **selection, not reservation** (sets `CUDA_VISIBLE_DEVICES` before `import torch`). No `parallelism`/TP/PP block exists. Multi-GPU sharding → use automodel. +- **Unsloth training is single-GPU per job** (inside the container). `hardware.gpus` sets `CUDA_VISIBLE_DEVICES` before `import torch` — **selection, not reservation**. No `parallelism`/TP/PP block in job JSON. Multi-GPU sharding → use automodel. Pass `--profile ` on `unsloth submit` when the default `gpu` profile is wrong (automodel sets `training.execution_profile` in JSON instead). - **Do not use local `docker info`** to pick automodel vs unsloth. After auth, run `uv run nemo jobs list-execution-profiles -f json` against the user's platform (see `references/troubleshooting.md`). Default output is a table — **`-f json` is required** for scripting; parse **stdout only** (do not pipe `2>&1` into `json.load`). -- For submit/image/plugin errors (both backends) and the unsloth `--venv` install probe, read `references/troubleshooting.md`. +- **Do not merge stderr into stdout when parsing JSON** — `submit`, `explain`, and `-f json` commands write **JSON on stdout**; harmless warnings like `Configuration file not found, using defaults` go to **stderr**. Piping with **`2>&1`** before `json.load` raises `JSONDecodeError` even when submit **succeeded** — a common cause of **duplicate jobs** when the agent re-submits after a parse error. Parse stdout only; redirect stderr if needed (`2>/dev/null`). See `references/troubleshooting.md` § **Parsing CLI JSON**. +- For submit/image/plugin errors (both backends), read `references/troubleshooting.md`. Unsloth needs the `nmp-unsloth-training` container image on the **platform host's** Docker daemon (see `services/unsloth/docker/README.md`). +- **Missing training image on a remote platform** — if the user gave a non-localhost `NEMO_BASE_URL` / `NMP_BASE_URL` (e.g. `10.0.0.51:8080`) and the job errors with `Failed to pull image`, `manifest unknown`, or missing `nmp-unsloth-training` / automodel training image: **do not** run `docker build`, `docker pull`, or `docker buildx bake` on the agent machine. Report with **Report to user** (use **Output adapter fileset (planned):** on error), then append on-target build steps from `references/troubleshooting.md` § **Missing training images**. ## Workflow Common steps then **branch by plugin pick**: -``` -- [ ] export NEMO_BASE_URL (if user provided endpoint) +```text +- [ ] export NEMO_BASE_URL (if user provided endpoint); note whether base URL is user-overridden - [ ] cd nemo-platform && uv run nemo auth login --unsigned-token --email - [ ] uv run nemo jobs list-execution-profiles -f json — apply Plugin pick rules above +- [ ] On connection error: default URL → ask to start platform (see Platform unreachable); custom URL → report unreachable and stop - [ ] Convert HF dataset → /tmp/train-data/*.jsonl (see references/hf-conversion.md) - [ ] Create dataset fileset (--exist-ok), upload train.jsonl (+ validation.jsonl), nemo files list to verify - [ ] Create HF weights fileset + model entity if missing (--exist-ok) -# automodel branch (remote, submit) +# automodel branch (submit → Docker GPU job) - [ ] Write /tmp/job.json (batch sizing for ≥48 GB GPU; else Defaults table) - [ ] uv run nemo customization automodel submit /tmp/job.json --workspace default -- [ ] Poll until top-level terminal (scripts/poll_automodel_job.sh or 60–120s manual polls) +- [ ] Poll until top-level terminal (`poll_customization_job.sh`; default 15s interval, or 30–60s manual polls) - [ ] Report using output template below -# unsloth branch (local, run --venv) -- [ ] Ensure $UNSLOTH_VENV is set up (see Plugin pick — Unsloth --venv requirement) +# unsloth branch (submit → Docker GPU job) - [ ] Write /tmp/job.json using the UnslothJobInput shape (see Fast path — unsloth) -- [ ] uv run nemo customization unsloth run /tmp/job.json --venv "$UNSLOTH_VENV" --workspace default -- [ ] Read result dict from stdout (loss / output_path / output_fileset / output_model_entity) +- [ ] uv run nemo customization unsloth submit /tmp/job.json --workspace default [--profile ] +- [ ] Poll until top-level terminal (`poll_customization_job.sh unsloth-`; default 15s interval) - [ ] Report using output template below ``` -## Fast path — automodel (remote) +## Fast path — automodel Substitute ``, ``, ``, ``, ``, ``. @@ -173,30 +182,20 @@ uv run nemo models create "$MODEL_ENTITY" --workspace default --exist-ok \ ```bash uv run nemo customization automodel submit /tmp/job.json --workspace default -bash plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/scripts/poll_automodel_job.sh automodel- 90 +bash plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/scripts/poll_customization_job.sh automodel- ``` -Or poll manually: `uv run nemo jobs get-status automodel-` every 60–120s. - -## Fast path — unsloth (local) - -Same substitutions as automodel. Steps 1 (dataset) and 2 (model entity) are identical — the differences are the job JSON shape and the run verb. +Read `` from the `"name"` field in submit stdout (JSON). **Do not use `2>&1`** before `json.load` — warnings on stderr break parsing; see Gotchas. Optional interval override: append seconds (e.g. `… 30`). Or poll manually: `uv run nemo jobs get-status automodel-` every 30–60s. -**0. One-time `--venv` setup** (skip if `$UNSLOTH_VENV` already passes the import probe): +## Fast path — unsloth -```bash -export UNSLOTH_VENV=/workspace/.venv-unsloth # any path you own -uv venv "$UNSLOTH_VENV" --python 3.11 -uv pip install --python "$UNSLOTH_VENV/bin/python" -e plugins/nemo-unsloth[unsloth] -# Probe: this must print nothing and exit 0. -"$UNSLOTH_VENV/bin/python" -c "import nemo_platform, nemo_unsloth_plugin, unsloth" -``` +Same substitutions as automodel. Steps 1 (dataset) and 2 (model entity) are identical — the differences are the job JSON shape (`UnslothJobInput`) and the `unsloth submit` command. **1. Dataset** — same as automodel Fast path step 1. **2. Model** — same as automodel Fast path step 2. -**3. Job JSON** — write `/tmp/job.json` using the **`UnslothJobInput`** shape (see `references/hyperparameters.md` → *Unsloth job JSON*). `model` is an **object** (not a string), `dataset.path` is a single fileset ref, `hardware.gpus` replaces the `parallelism` block (single GPU). `nemo customization unsloth explain` prints the live schema. +**3. Job JSON** — write `/tmp/job.json` using the **`UnslothJobInput`** shape (see `references/hyperparameters.md` → *Unsloth job JSON*). `model` is an **object** (not a string), `dataset.path` is a single fileset ref, `hardware.gpus` replaces the `parallelism` block (single GPU in the training container). `nemo customization unsloth explain` prints the live schema. ```json { @@ -227,13 +226,16 @@ uv pip install --python "$UNSLOTH_VENV/bin/python" -e plugins/nemo-unsloth[unslo If the model uses `messages` chat format (preferred when the tokenizer has a chat template), keep `dataset.apply_chat_template: true`. Otherwise emit a single `text` column from your converter and set `apply_chat_template: false`. -**4. Run (no polling — synchronous)** +**4. Submit and poll** ```bash -uv run nemo customization unsloth run /tmp/job.json --venv "$UNSLOTH_VENV" --workspace default +uv run nemo customization unsloth submit /tmp/job.json --workspace default +bash plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/scripts/poll_customization_job.sh unsloth- ``` -The result dict prints to stdout — capture `output_path`, `output_fileset`, and `output_model_entity` from it. If the command exits with the "Backend 'unsloth' is not importable" message, the `--venv` probe failed: re-run the one-time setup above. +Read `` from the `"name"` field in submit stdout (JSON). **Do not use `2>&1`** before `json.load` — warnings on stderr break parsing; see Gotchas. Optional interval override: append seconds (e.g. `… 30`). Or poll manually: `uv run nemo jobs get-status unsloth-` every 30–60s. If submit fails on an unknown profile, re-list execution profiles and pass `--profile ` on submit (default is `gpu`). + +If you try `nemo customization unsloth run …`, the CLI hard-fails with a pointer to `submit`. ## Defaults @@ -376,7 +378,7 @@ Field glossary, distillation/KD, and schema pointers: `references/hyperparameter Unsloth is single-GPU by design. The effective batch is the **product** of two fields, not a global/micro split: -``` +```text effective_batch = batch.per_device_train_batch_size × batch.gradient_accumulation_steps ``` @@ -409,45 +411,59 @@ There is no `parallelism` block, no TP / PP / DP, no GBS divisibility math. Mult | Symptom | Action | |---------|--------| | CUDA OOM | Halve `per_device_train_batch_size` (keep effective batch via `gradient_accumulation_steps`); then lower `model.max_seq_length`; then drop `lora.rank` to 8 | -| `torch.cuda.is_available()` is False | Host CUDA / venv mismatch — see `references/troubleshooting.md` | -| `Backend 'unsloth' is not importable` | `$UNSLOTH_VENV` setup never ran or `--venv` was omitted (see Plugin pick) | +| Missing `nmp-unsloth-training` image | Build/pull the Unsloth container image — see `references/troubleshooting.md` and `services/unsloth/docker/README.md` | +| `Unsloth training requires platform.runtime: docker` | Platform not using the Docker executor | Start platform with `platform.runtime: docker` and a GPU execution profile; training runs in containers on that host's Docker daemon | | Loss not moving | Raise `learning_rate` one step (e.g. `5e-5` → `1e-4`); confirm `apply_chat_template` matches the data shape; check the LoRA `target_modules` covers the right layers (defaults are Unsloth's 7-module set) | ## Worked example **Automodel:** `Qwen/Qwen3-1.7B` + `tau/commonsense_qa` → CHAT JSONL, fileset `commonsense_qa`, entity `qwen3-1.7b`, output `qwen3-1.7b-commonsense-qa-lora`, `epochs: 1` (no `max_steps`). On ≥48 GB GPU use LoRA ≤4B **default**: `micro` 32, GBS 128, `learning_rate` `1e-4` (high-util: 64 / 256). -**Unsloth:** same model + dataset + entity + fileset, but `nemo customization unsloth run /tmp/job.json --venv "$UNSLOTH_VENV" -w default`. Job JSON ≤4B row: `batch.per_device_train_batch_size` 8, `batch.gradient_accumulation_steps` 16 (effective 128), `learning_rate` `1e-4`, `hardware.gpus` `"0"`, `output.save_method` `"lora"`. Reference fixture: `plugins/nemo-unsloth/tests/fixtures/minimal_unsloth_sft.json` (ignore `max_steps` for real runs). +**Unsloth:** same model + dataset + entity + fileset, but `nemo customization unsloth submit /tmp/job.json -w default`. Job JSON ≤4B row: `batch.per_device_train_batch_size` 8, `batch.gradient_accumulation_steps` 16 (effective 128), `learning_rate` `1e-4`, `hardware.gpus` `"0"`, `output.save_method` `"lora"`. Poll `unsloth-` to completion. Reference fixture: `plugins/nemo-unsloth/tests/fixtures/minimal_unsloth_sft.json` (ignore `max_steps` for real runs). ## Report to user -**Automodel (submit):** +After polling reaches a **terminal** status (`completed`, `error`, or `cancelled`), report using this template for **both** backends. Fill fields from the job JSON and `nemo jobs get-status`. ```markdown ## Fine-tune result -- **Job:** automodel- +- **Job:** +- **Backend:** - **Model entity:** default/ -- **Output adapter fileset:** +- **Dataset fileset:** default/ +- **Output adapter fileset:** - **Status:** -- **Notes:** +- **Notes:** ``` -**Unsloth (run):** the `run` command prints a result dict to stdout — pick fields from it. +**Field guidance** -```markdown -## Fine-tune result +| Field | Source | +|-------|--------| +| **Job** | Job id from submit or poll (`automodel-…` / `unsloth-…`) | +| **Backend** | Plugin used for submit | +| **Model entity** | `model` in job JSON (automodel: string ref; unsloth: `model.name`) | +| **Dataset fileset** | automodel: `dataset.training`; unsloth: `dataset.path` | +| **Output adapter fileset** | `output.name` from job JSON. Label **Output adapter fileset (planned):** when status is `error` or `cancelled` and no output was registered | +| **Status** | Top-level `status` from `nemo jobs get-status` — not step-level status | +| **Notes** | See **Notes by status** below | -- **Backend:** unsloth (local) -- **Model entity:** default/ -- **Output model entity:** / -- **Output fileset:** / -- **Local checkpoint:** -- **Final loss:** -- **Status:** completed (or `error: ` if the command exited non-zero) -``` +**Notes by status** + +| Status | Notes | +|--------|-------| +| `completed` | Brief success summary (e.g. adapter registered on model entity). | +| `error` | Quote `error_details.message` or the failing step; note setup that succeeded before the failure (auth, dataset upload, submit). | +| `cancelled` | Cancellation reason if available. | + +**Error follow-ups** — when the failure has a known fix, append sections **below** the header block (do not replace the header). Examples: + +| Error type | Append | +|------------|--------| +| Missing training image + user-overridden `NEMO_BASE_URL` / `NMP_BASE_URL` | `references/troubleshooting.md` § **Missing training images** — on-target build steps, env vars, re-submit commands. **Do not** `docker build` locally for a remote platform. | -If the dict contains `upload_error` or `model_entity_error`, surface those — training succeeded but registration failed; the local `output_path` is still usable. +For other terminal errors, keep the same header template; put remediation detail in **Notes** or a short **Next steps** section as appropriate. ## Reference files @@ -458,7 +474,7 @@ If the dict contains `upload_error` or `model_entity_error`, surface those — t | Field glossary, distillation/KD, schema (both backends) | `references/hyperparameters.md` (not batch sizing) | | Batch sizing (≥48 GB), OOM / throughput | **Batch sizing — automodel** / **Batch sizing — unsloth** above | | Multi-GPU same node | **Multi-GPU (same node)** under automodel batch sizing (unsloth is single-GPU) | -| Backend choice, execution profiles, `--venv` setup, submit/run failure, images, CLI | `references/troubleshooting.md` | +| Backend choice, execution profiles, submit failure, container images, missing image on remote platform, CLI, connection errors | `references/troubleshooting.md` (§ **Parsing CLI JSON** for `2>&1` / `json.load`) | | Live JSON schema | `uv run nemo customization automodel explain` / `uv run nemo customization unsloth explain` | | Job JSON fixture (automodel) | `plugins/nemo-automodel/tests/fixtures/qwen3_0.6b_sft_lora.json` (ignore `max_steps` for real runs) | | Job JSON fixture (unsloth) | `plugins/nemo-unsloth/tests/fixtures/minimal_unsloth_sft.json` (ignore `max_steps` for real runs) | diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md index c024701af0..4caa6299e7 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md @@ -23,7 +23,7 @@ Job JSON for `nemo customization automodel submit` uses **`AutomodelJobInput`** uv run nemo customization automodel explain ``` -**Contract examples:** `tests/customizer-automodel-contract/input_configs/` (legacy shape; map `batch_size` → `global_batch_size` in submit JSON). +**Contract examples:** `services/automodel/tests/contract/input_configs/` (legacy shape; map `batch_size` → `global_batch_size` in submit JSON). ## Job JSON layout @@ -106,7 +106,7 @@ Full template: ## Field reference -### `training` +### Automodel `training` | Field | Default | Notes | |-------|---------|-------| @@ -125,7 +125,7 @@ Full template: LoRA block is auto-created when `finetuning_type` is `lora` or `lora_merged`. -### `schedule` +### Automodel `schedule` | Field | Default | Notes | |-------|---------|-------| @@ -136,7 +136,7 @@ LoRA block is auto-created when `finetuning_type` is `lora` or `lora_merged`. **Gotcha:** Do **not** set `max_steps` with `epochs` for normal training. `max_steps` stops early (e.g. `epochs: 1` + `max_steps: 100` ends at step 100). Use `max_steps` **alone** only for smoke tests. -### `batch` +### Automodel `batch` | Field | Default | Notes | |-------|---------|-------| @@ -150,7 +150,7 @@ LoRA block is auto-created when `finetuning_type` is `lora` or `lora_merged`. Example: 1 node, 2 GPUs, TP=1 → DP=2 → GBS must be a multiple of `2 × micro_batch_size`. See **`SKILL.md` § Multi-GPU** for data parallel vs tensor parallel. -### `optimizer` +### Automodel `optimizer` | Field | Default | Notes | |-------|---------|-------| @@ -173,7 +173,7 @@ Example: 1 node, 2 GPUs, TP=1 → DP=2 → GBS must be a multiple of `2 × micro **MoE:** If `expert_parallel_size > 1` and multiple GPUs, `tensor_parallel_size` must be **1**. -### `integrations` (optional) +### Automodel `integrations` (optional) ```json "integrations": { @@ -197,7 +197,7 @@ Apply user overrides to `/tmp/job.json` before submit. For **batch / GPU count / | Quick smoke test | `max_steps` only (e.g. 10–50), **omit or ignore epoch goal**; or `epochs: 1` on tiny slice | | Reproducibility | Set `schedule.seed` | -### Learning rate (LoRA SFT, starting points) +### Automodel learning rate (LoRA SFT, starting points) | Model scale | Suggested `learning_rate` | |-------------|---------------------------| @@ -207,7 +207,7 @@ Apply user overrides to `/tmp/job.json` before submit. For **batch / GPU count / Schema default is `5e-6` (conservative). Fixtures: `qwen3_0.6b_sft_lora.json` uses `5e-5`; `minimal_sft_lora.json` uses `5e-6`. -### LoRA rank / alpha +### Automodel LoRA rank / alpha **Deployment cap:** Default **NIM** and **vLLM** LoRA serving paths support rank **≤ 32**. Use `rank` 32 (not higher) when the fine-tuned adapter will be deployed for inference on those stacks unless the user confirms a higher rank is supported. @@ -314,7 +314,7 @@ Verify: `nemo models get --workspace default`. Reuse an existin # Unsloth job JSON -Job JSON for `nemo customization unsloth run` uses **`UnslothJobInput`** (`plugins/nemo-unsloth/src/nemo_unsloth_plugin/schema.py`). Only fields in that schema are accepted (`extra="forbid"`). The canonical post-transform shape lives in `services/unsloth/src/nmp/unsloth/schemas.py` (`UnslothJobOutput`) and is what the training driver consumes. +Job JSON for `nemo customization unsloth submit` uses **`UnslothJobInput`** (`plugins/nemo-unsloth/src/nemo_unsloth_plugin/schema.py`). Only fields in that schema are accepted (`extra="forbid"`). The canonical post-transform shape lives in `services/unsloth/src/nmp/unsloth/schemas.py` (`UnslothJobOutput`) and is what the training driver consumes in the GPU container. **Schema dump:** @@ -322,13 +322,13 @@ Job JSON for `nemo customization unsloth run` uses **`UnslothJobInput`** (`plugi uv run nemo customization unsloth explain ``` -Unsloth is **single-GPU, local, in-process** (BYO-venv). There is no `parallelism` block, no `tensor_parallel_size`, and no platform `execution_profile` — `hardware.gpus` selects which CUDA device the local interpreter uses. Multi-GPU sharding → use automodel. +Unsloth is **submit-only, single-GPU inside the training container**. There is no `parallelism` block and no `training.execution_profile` in job JSON — pass `--profile` on `nemo customization unsloth submit` instead (default `gpu`). `hardware.gpus` sets `CUDA_VISIBLE_DEVICES` in the container before `import torch`. Multi-GPU sharding → use automodel. ## Job JSON layout (unsloth) | Section | Purpose | |---------|---------| -| `name` | Optional job name (auto-generated if omitted, mostly cosmetic for local run) | +| `name` | Optional job name (auto-generated if omitted) | | `model` | **Object** — base model entity ref + how to load it (4-bit, dtype, max_seq_length) | | `dataset` | Single fileset ref (`path`) + optional `validation_path`; row shape selector (`text_field`, `apply_chat_template`, `packing`) | | `training` | Method (`sft`), adapter shape (`lora`/`full`), LoRA hyperparams, gradient checkpointing | @@ -435,7 +435,7 @@ See `references/dataset-formats.md` § Unsloth for row-shape rules. | `apply_chat_template` | `false` | Set `true` for rows with a `messages` array (preferred when the tokenizer has a chat template). | | `packing` | `false` | trl.SFTTrainer packing for throughput on short rows. | -### `training` +### Unsloth `training` | Field | Default | Notes | |-------|---------|-------| @@ -458,7 +458,7 @@ See `references/dataset-formats.md` § Unsloth for row-shape rules. `lora` is auto-filled with these defaults when `finetuning_type: "lora"` and the user omits the block. Must be `null` / omitted when `finetuning_type: "full"`. -### `schedule` +### Unsloth `schedule` | Field | Default | Notes | |-------|---------|-------| @@ -474,7 +474,7 @@ See `references/dataset-formats.md` § Unsloth for row-shape rules. **Hard mutex enforced by the schema:** `epochs` xor `max_steps`; `warmup_steps` xor `warmup_ratio`. Validation errors surface at submit time. -### `batch` +### Unsloth `batch` | Field | Default | Notes | |-------|---------|-------| @@ -483,7 +483,7 @@ See `references/dataset-formats.md` § Unsloth for row-shape rules. `effective_batch = per_device_train_batch_size × gradient_accumulation_steps`. No GBS divisibility math (single GPU). Starting points by model size are in `SKILL.md` § Batch sizing — unsloth. -### `optimizer` +### Unsloth `optimizer` | Field | Default | Notes | |-------|---------|-------| @@ -497,10 +497,10 @@ See `references/dataset-formats.md` § Unsloth for row-shape rules. | Field | Default | Notes | |-------|---------|-------| -| `gpus` | `null` | Comma-separated CUDA indices: `"0"` or `"0,1"` (Unsloth only uses one). Sets `CUDA_VISIBLE_DEVICES` **before** `import torch`. **Selection, not reservation.** Leave unset to inherit the caller's env. | +| `gpus` | `null` | Comma-separated CUDA indices inside the training container: `"0"` (typical). Sets `CUDA_VISIBLE_DEVICES` **before** `import torch`. **Selection, not reservation.** Unsloth uses one GPU per training process. | | `precision` | `"bf16"` | `"bf16"` (Ampere+) or `"fp16"`. | -### `integrations` +### Unsloth `integrations` ```json "integrations": { @@ -516,7 +516,7 @@ See `references/dataset-formats.md` § Unsloth for row-shape rules. | `wandb.run_name` | Becomes `TrainingArguments.run_name`. | | `report_to` | List of `"wandb"`, `"tensorboard"`, `"mlflow"`, `"none"`. Empty default = `["none"]`. | -The user sets `WANDB_API_KEY` themselves in `$UNSLOTH_VENV` (or shell) — the plugin does **not** manage that secret. No `api_key_secret` field today. +The platform pulls `WANDB_API_KEY` from Secrets when W&B is enabled — the plugin does **not** read a local shell env for training containers. No `api_key_secret` field in job JSON today. ### `output` @@ -532,7 +532,7 @@ After `to_spec`, the canonical `OutputResponse` also carries `type` (`"adapter"` VRAM / batch tuning is in **`SKILL.md` § Batch sizing — unsloth**. Below covers non-batch fields. -### Learning rate (LoRA SFT, starting points) +### Unsloth learning rate (LoRA SFT, starting points) Same scale as automodel (the underlying optimizer math is the same): @@ -544,7 +544,7 @@ Same scale as automodel (the underlying optimizer math is the same): Schema default is `2e-4` (Unsloth notebook default — works for small adapters with `adamw_8bit`). Skill defaults are conservative `5e-5`. -### LoRA rank / alpha +### Unsloth LoRA rank / alpha | Use case | `rank` | `alpha` | |----------|--------|---------| @@ -594,4 +594,4 @@ Not supported by unsloth today (`training_type` is `Literal["sft"]`). Use automo | JSON examples (automodel) | `plugins/nemo-automodel/tests/fixtures/*.json` | Copy-paste templates (ignore fixture `max_steps` in prod) | | JSON example (unsloth) | `plugins/nemo-unsloth/tests/fixtures/minimal_unsloth_sft.json` | Smoke-test template (ignore `max_steps` for real runs) | | Full spec doc (automodel) | `plugins/nemo-automodel/SCOPE.md` (simplified JSON section) | Design notes | -| Plugin README (unsloth) | `plugins/nemo-unsloth/README.md` | `--venv` setup, CUDA caveat, container-submit roadmap | +| Plugin README (unsloth) | `plugins/nemo-unsloth/README.md` | Submit-only CLI, 4-step container job, GPU selection | diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/troubleshooting.md b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/troubleshooting.md index e9021e6976..0587097a39 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/troubleshooting.md +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/troubleshooting.md @@ -1,6 +1,37 @@ # Troubleshooting -Read this file when submit fails, jobs fail on images, or the user asks for Unsloth. +Read this file when submit fails, jobs fail on images, the platform is unreachable, or the user asks for Unsloth. + +## Platform unreachable (connection error) + +Any `nemo …` call may fail with `Connection error`, timeout, or connection refused — typically on the first `nemo jobs list-execution-profiles` after auth. + +**Did the user override the base URL?** + +| Situation | Action | +|-----------|--------| +| User gave a platform host/URL (e.g. `10.0.0.51:8080`) or you set `NEMO_BASE_URL` / `NMP_BASE_URL` to something other than `http://127.0.0.1:8080` or `http://localhost:8080` | Report that the platform is not reachable at that address. Ask them to confirm the host is up and the URL is correct. **Do not** start local services. | +| Default URL only — no user override | **Ask** whether to start the platform locally. If they agree, from the **nemo-platform** git root run in the **background**, then poll until healthy and retry the failed command: | + +```bash +uv run nemo services run \ + --host 0.0.0.0 \ + --port 8080 \ + --controllers jobs,entities,models \ + --service-group all +``` + +Health check (repeat until success or ~2 min): + +```bash +curl -sf http://127.0.0.1:8080/health/ready +# or +uv run nemo jobs list-execution-profiles -f json +``` + +Do **not** auto-start services without asking. Customization needs **jobs**, **entities** (filesets), and **models** controllers — the command above is the minimal local set for this skill. + +If the user already has a listener on `:8080` but health fails, see **nemo-status** (stale lock / wedged platform) before starting a second instance. ## Backend choice (automodel vs unsloth) @@ -18,17 +49,18 @@ Each entry has `provider`, `profile` (name), and `backend` (e.g. `docker`, `kube | Condition | Plugin | |-----------|--------| -| User explicitly asks for Unsloth | `unsloth` (verify `--venv` setup — see *Unsloth `--venv` setup and probe* below) | -| User explicitly asks for Automodel | `automodel` (verify a GPU execution profile exists) | +| User explicitly asks for Unsloth | `unsloth` | +| User explicitly asks for Automodel | `automodel` | | Response includes **`provider`: `gpu` or `gpu_distributed`** | **`automodel`** (default) | -| No GPU profiles (only `subprocess` and/or CPU `provider`), caller has a usable local GPU | **`unsloth`** locally (one-time `--venv` setup required) | -| No GPU profiles **and** no local GPU | Report that GPU customization is unavailable; offer to set up a remote platform with a GPU profile | +| No GPU profiles (only `subprocess` and/or CPU `provider`) | Report that GPU customization is unavailable | + +Both backends are **`submit`-only**. After submit, the platform's **Docker executor** runs GPU container steps on the daemon attached to the connected platform host (`platform.runtime: docker`). Training does not run in the CLI shell — query execution profiles on the platform (`NEMO_BASE_URL`), not GPU availability in the agent's terminal. -Automodel training steps need a **GPU execution profile** on the platform. `subprocess` profiles run host commands and are not a substitute for automodel's GPU container step. Unsloth ignores execution profiles entirely — it runs in-process inside `$UNSLOTH_VENV` on the caller's machine and reads `hardware.gpus` (selection via `CUDA_VISIBLE_DEVICES`, not platform reservation). +### Pick execution profile -### Pick `training.execution_profile` +**Automodel** — set `training.execution_profile` in job JSON to the **`profile`** string of a GPU row from the list (e.g. `default`, `docker_gpu`). If omitted, the plugin default is usually `gpu` — submit errors mentioning an unknown profile mean you should re-list and set an exact name from the API. -When using automodel, set `training.execution_profile` in job JSON to the **`profile`** string of a GPU row from the list (e.g. `default`, `docker_gpu`). If omitted, the plugin default is usually `gpu` — submit errors mentioning an unknown profile mean you should re-list and set an exact name from the API. +**Unsloth** — pass `--profile ` on `nemo customization unsloth submit …` when the default `gpu` profile is wrong. There is no `execution_profile` field in `UnslothJobInput` today. Quick filter (stdout only — do not use `2>&1` or `json.load` breaks on stderr warnings): @@ -43,49 +75,98 @@ for p in json.load(sys.stdin): Do not run `nemo customization --help` unless submit returns unknown plugin. -## Verb is backend-specific +## Parsing CLI JSON + +`submit`, `explain`, and `-f json` commands write **JSON on stdout**. Harmless config warnings (e.g. `Configuration file not found, using defaults`) go to **stderr**, not stdout. + +**Do not** merge stderr into stdout with **`2>&1`** before `json.load` or jq — the warnings prefix the stream and cause `JSONDecodeError` even when submit **succeeded** and the job is already queued. That parse failure often leads to **duplicate jobs** when the agent re-runs submit. -- **Automodel** uses **`submit` only** (no local `run`). Dataset refs in job JSON: `default/`. -- **Unsloth** uses **`run` only** (no `submit`). The CLI exits non-zero with a friendly hint if you try `nemo customization unsloth submit ...`. Dataset refs in job JSON: `default/` (single `dataset.path`). +| Do | Don't | +|----|-------| +| Pipe **stdout only**: `… submit /tmp/job.json \| python3 -c "import sys,json; print(json.load(sys.stdin)['name'])"` | `… submit /tmp/job.json 2>&1 \| python3 -c "import sys,json; json.load(sys.stdin)"` | +| Suppress stderr if noisy: append `2>/dev/null` to the `nemo` command (not `2>&1`) | Merge stderr into the JSON pipe with `2>&1` | +| On `json.load` failure, check `nemo jobs list` before re-submitting | Assume submit failed and submit again | -## Unsloth `--venv` setup and probe +Same rule for `nemo jobs list-execution-profiles -f json`: parse stdout only; use `2>/dev/null` if needed, never `2>&1` into `json.load`. -Unsloth requires a **separate Python venv** containing the heavy ML extras (`unsloth`, `torch`, `transformers`, `trl`, `peft`, `accelerate`, `bitsandbytes`). The base `nemo` install does **not** pull these. The platform's `--venv` flag re-execs `nemo customization unsloth run` inside the supplied interpreter before importing anything torch-related. +## Verb is backend-specific (both submit-only) -One-time setup (from nemo-platform root): +- **Automodel** and **Unsloth** both use **`submit` only**. `nemo customization run …` hard-fails with a pointer to `submit`. +- Dataset refs in job JSON: `default/` (automodel: `dataset.training` / `dataset.validation`; unsloth: `dataset.path` / optional `dataset.validation_path`). + +## Missing training images + +Job errors like `Failed to pull image … nmp-unsloth-training:… Not Found`, `manifest unknown`, or a missing automodel training image mean the **connected platform's Docker daemon** (the one that runs GPU job steps) does not have the image. With the default `NEMO_BASE_URL` / `NMP_BASE_URL` (`127.0.0.1:8080` / `localhost:8080`), that daemon is usually on the same machine as the agent; with a user-overridden URL (e.g. `10.0.0.51:8080`), it is on the remote target host instead. + +**Did the user override the base URL?** (same rule as **Platform unreachable** — track this from the start of the workflow.) + +| Situation | Action | +|-----------|--------| +| **Remote platform** — user gave a host/URL (e.g. `10.0.0.51:8080`) or you set `NEMO_BASE_URL` / `NMP_BASE_URL` to something other than `http://127.0.0.1:8080` or `http://localhost:8080` | **Do not** run `docker build`, `docker pull`, or `docker buildx bake` on the agent machine — that only affects the agent's local daemon, not the remote platform. Tell the user they must build or load the image **on the target host** (the machine whose Docker daemon runs the GPU job steps). Report with **Report to user** in `SKILL.md`, then append **Report follow-up — missing image (remote platform)** below. Stop; do not retry submit until the user confirms the image is available on the target. | +| **Local platform** — default URL only (`127.0.0.1:8080` / `localhost:8080`) | Build or pull on **that same host** where `nemo services run` and Docker share a daemon. See build commands below and `services/unsloth/docker/README.md` (unsloth) or automodel docker docs. Set env vars **before** starting/restarting the platform. | + +Image env vars are read when the platform starts (not per job): ```bash -export UNSLOTH_VENV=/workspace/.venv-unsloth # any path the user owns -uv venv "$UNSLOTH_VENV" --python 3.11 -uv pip install --python "$UNSLOTH_VENV/bin/python" -e plugins/nemo-unsloth[unsloth] +export NMP_IMAGE_REGISTRY= +export NMP_IMAGE_TAG= ``` -Probe (must print nothing and exit 0): +**Automodel** — also set `NMP_AUTOMODEL_IMAGE_REGISTRY=$NMP_IMAGE_REGISTRY`. + +**Unsloth** — set `NMP_UNSLOTH_TRAINING_IMAGE` (and optionally `NMP_UNSLOTH_TASKS_IMAGE`) to the full built ref, then restart platform services so the env var takes effect. + +### Build on the target host (unsloth) + +Run on the **platform host** (SSH, console, or CI on that box — not from the agent when the platform is remote): ```bash -"$UNSLOTH_VENV/bin/python" -c "import nemo_platform, nemo_unsloth_plugin, unsloth" +cd /path/to/nemo-platform + +# Local build (platform and Docker on the same machine) +docker buildx bake \ + -f docker-bake.hcl \ + nmp-unsloth-training \ + --load \ + --set "*.platform=linux/amd64" + +export NMP_UNSLOTH_TRAINING_IMAGE="${IMAGE_REGISTRY:-nvcr.io/0921617854601259/nemo-platform-dev}/nmp-unsloth-training:${BAKE_TAG:-local}" +# Restart platform so the env var is picked up +nemo services restart ``` -| Error from `unsloth run` | Cause | Fix | -|--------------------------|-------|-----| -| `Backend 'unsloth' is not importable in the current interpreter` | `--venv` omitted, or the venv doesn't have the `[unsloth]` extra | Pass `--venv "$UNSLOTH_VENV"`; if it still fails, re-run the install above and the probe | -| `torch.cuda.is_available()` returns False inside training | Host CUDA / driver mismatch with the torch wheel installed into `$UNSLOTH_VENV` | Verify `nvidia-smi` works on the host, then reinstall torch in the venv against a compatible CUDA wheel (`uv pip install --python "$UNSLOTH_VENV/bin/python" torch --index-url https://download.pytorch.org/whl/cu121` or matching cu-tag) | -| `ImportError: cannot import name 'FastLanguageModel'` | Old `unsloth` version | `uv pip install --python "$UNSLOTH_VENV/bin/python" -U unsloth` | -| `OSError: Could not find a suitable CUDA install` | No GPU on the caller machine | Switch to automodel (remote GPU); unsloth is single-GPU on the local box | +Or push to a registry the target can pull from — see **Option B** in `services/unsloth/docker/README.md` — then set `NMP_UNSLOTH_TRAINING_IMAGE` to that full ref before restart. + +After the image is on the target, re-submit the same job JSON (use a fresh `output.name` if a prior partial run already registered an adapter). -The plugin does **not** auto-create venvs and does **not** manage CUDA versions — that's the BYO-venv contract. See `plugins/nemo-unsloth/README.md` for the canonical install snippet. +### Report follow-up — missing image (remote platform) -## Missing training images (automodel only) +When submit or poll returns a missing-image error and the base URL is **user-overridden**, start with the **Report to user** template in `SKILL.md` (status `error`, **Output adapter fileset (planned):**, Notes quoting the pull error and naming the target host). Then append these sections: -Set **before** starting the platform (not per job): +**What you need to do on the target host** — build or load the training image on the machine running the NeMo platform (where `docker info` works for the platform's daemon), set `NMP_UNSLOTH_TRAINING_IMAGE` or automodel image env vars, and restart platform services. Full steps: `services/unsloth/docker/README.md` (unsloth) or automodel docker docs. + +**Re-submit after the image is available:** ```bash -export NMP_IMAGE_REGISTRY= -export NMP_IMAGE_TAG= -export NMP_AUTOMODEL_IMAGE_REGISTRY=$NMP_IMAGE_REGISTRY +export NEMO_BASE_URL= +cd /path/to/nemo-platform +uv run nemo customization submit /tmp/job.json --workspace default [--profile ] ``` -Pull automodel images only when the job error mentions a missing image. Unsloth does **not** consume platform-managed training images — it runs in-process inside `$UNSLOTH_VENV`, so registry env vars are irrelevant to it. +Then poll until terminal status. Offer to re-submit once the user confirms the image is on the target — do not attempt a local Docker build from the agent for a remote platform. + +## Unsloth submit errors + +| Error / symptom | Cause | Fix | +|-----------------|-------|-----| +| `Unsloth does not support local run` | Used `run` instead of `submit` | `nemo customization unsloth submit -w ` | +| `Unsloth training requires platform.runtime: docker` | Platform not configured for Docker GPU jobs | Start platform with Docker runtime and a GPU execution profile | +| Unknown execution profile | Default `gpu` profile missing or wrong | Re-list profiles; pass `--profile ` on submit | +| Missing `nmp-unsloth-training` image / `Failed to pull image` / `manifest unknown` | Image not on the **platform host's** Docker daemon | **Remote platform** (`NEMO_BASE_URL` not localhost): tell user to build on the target — **do not** `docker build` locally. **Local platform**: build on same host; see **Missing training images** above and `services/unsloth/docker/README.md` | +| `torch.cuda.is_available()` False in training step logs | GPU not exposed to the container step | Confirm the execution profile is GPU-backed; check platform Docker GPU setup | +| Job stuck in `active` after training step completes | Upload / model-entity steps still running | Keep polling top-level status (same as automodel) | + +See `plugins/nemo-unsloth/README.md` for the 4-step job flow (download → train → upload → model-entity). ## CLI quick reference @@ -99,22 +180,23 @@ Shared: | Upload | `nemo files upload --workspace default --remote-path train.jsonl` | | List files | `nemo files list --workspace default` | | Create model | `nemo models create --workspace default --exist-ok --input-data ''` | +| Poll job | `nemo jobs get-status -` | -Automodel (remote): +Automodel: | Action | Command | |--------|---------| | Submit | `nemo customization automodel submit --workspace default` | -| Status | `nemo jobs get-status automodel-` | +| Status | `nemo jobs get-status automodel-` | | Live schema | `nemo customization automodel explain` | -Unsloth (local): +Unsloth: | Action | Command | |--------|---------| -| One-time venv setup | `uv venv "$UNSLOTH_VENV" --python 3.11 && uv pip install --python "$UNSLOTH_VENV/bin/python" -e plugins/nemo-unsloth[unsloth]` | -| Venv probe | `"$UNSLOTH_VENV/bin/python" -c "import nemo_platform, nemo_unsloth_plugin, unsloth"` | -| Run | `nemo customization unsloth run --venv "$UNSLOTH_VENV" --workspace default` | +| Submit | `nemo customization unsloth submit --workspace default [--profile P] [--cluster C]` | +| Status | `nemo jobs get-status unsloth-` | | Live schema | `nemo customization unsloth explain` | +| Run (disabled) | `nemo customization unsloth run …` → hard-fails; use `submit` | -There is **no** `nemo jobs get-status` for unsloth — `run` is synchronous and prints the result dict to stdout. There is no `submit` verb either (it is intentionally disabled). +Both backends return a job id from `submit` — poll until top-level status is terminal (`completed`, `error`, or `cancelled`). diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/scripts/poll_automodel_job.sh b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/scripts/poll_customization_job.sh similarity index 77% rename from plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/scripts/poll_automodel_job.sh rename to plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/scripts/poll_customization_job.sh index 7eae73315e..76f8b54398 100755 --- a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/scripts/poll_automodel_job.sh +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/scripts/poll_customization_job.sh @@ -1,13 +1,13 @@ #!/usr/bin/env bash -# Poll automodel job until top-level status is terminal. -# Usage: poll_automodel_job.sh automodel- [interval_seconds] +# Poll customization job until top-level status is terminal. +# Usage: poll_customization_job.sh - [interval_seconds] # Requires: NEMO_BASE_URL or NMP_BASE_URL, run from nemo-platform root with `uv run`. # Exit 0 on completed; exit 1 on error, cancelled, or get-status failure. set -euo pipefail -JOB="${1:?usage: poll_automodel_job.sh automodel- [interval_seconds]}" -INTERVAL="${2:-90}" +JOB="${1:?usage: poll_customization_job.sh - [interval_seconds]}" +INTERVAL="${2:-15}" while true; do JSON=$(uv run nemo jobs get-status "$JOB" 2>/dev/null) || { diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/tests.json b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/tests.json index a57c37314d..7663d036a4 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/tests.json +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/tests.json @@ -33,22 +33,22 @@ }, { "type": "explicit", - "prompt": "Use nemo customization unsloth run to do a LoRA SFT on my workstation GPU.", + "prompt": "Use nemo customization unsloth submit to do a LoRA SFT as a Docker GPU job on the platform.", "expected_skill": "nemo-customizer" }, { "type": "explicit", - "prompt": "Help me set up the unsloth --venv and run a local fine-tune via nemo customization.", + "prompt": "Help me submit an unsloth fine-tune job via nemo customization with 4-bit LoRA.", "expected_skill": "nemo-customizer" }, { "type": "implicit", - "prompt": "My platform has no GPU profile but my laptop has an RTX 5880 — fine-tune Qwen3-1.7B with LoRA locally.", + "prompt": "Fine-tune Qwen3-1.7B with Unsloth on NeMo Platform — I want the Unsloth training driver and bitsandbytes 4-bit loading.", "expected_skill": "nemo-customizer" }, { "type": "implicit", - "prompt": "I want a quick LoRA adapter on Qwen with bitsandbytes 4-bit loading, single GPU, in-process.", + "prompt": "I want a quick LoRA adapter on Qwen with Unsloth's optimizer defaults, single GPU container job.", "expected_skill": "nemo-customizer" }, { diff --git a/plugins/nemo-customizer/tests/test_router.py b/plugins/nemo-customizer/tests/test_router.py index 43cd3ca157..d6af3e876f 100644 --- a/plugins/nemo-customizer/tests/test_router.py +++ b/plugins/nemo-customizer/tests/test_router.py @@ -14,6 +14,7 @@ CustomizationRouterService, merge_router_dependencies, ) +from nemo_platform_plugin.authz import authz_for_workspace_job_collection from nemo_platform_plugin.service import RouterSpec @@ -142,3 +143,62 @@ def get_cli(self) -> typer.Typer: # Should not raise. service = CustomizationRouterService() assert sorted(service._contributors.keys()) == ["automodel", "unsloth"] + + +def test_get_authz_contribution_merges_backend_contributors(monkeypatch: pytest.MonkeyPatch) -> None: + class _AutomodelContributor: + name: ClassVar[str] = "automodel" + dependencies: ClassVar[list[str]] = [] + + def get_authz_contribution(self) -> object: + return authz_for_workspace_job_collection( + api_area="customization", + collection_suffix="/automodel/jobs", + permission_prefix="customization.automodel.jobs", + include_healthz=True, + healthz_suffix="/automodel/healthz", + ) + + def get_routers(self) -> list[RouterSpec]: + return [] + + def get_cli(self) -> typer.Typer: + return typer.Typer() + + class _UnslothContributor: + name: ClassVar[str] = "unsloth" + dependencies: ClassVar[list[str]] = [] + + def get_authz_contribution(self) -> object: + return authz_for_workspace_job_collection( + api_area="customization", + collection_suffix="/unsloth/jobs", + permission_prefix="customization.unsloth.jobs", + ) + + def get_routers(self) -> list[RouterSpec]: + return [] + + def get_cli(self) -> typer.Typer: + return typer.Typer() + + monkeypatch.setattr( + "nemo_customizer.router.discover_customization_contributors", + lambda: {"automodel": _AutomodelContributor(), "unsloth": _UnslothContributor()}, + ) + + contrib = CustomizationRouterService.get_authz_contribution() + assert contrib is not None + assert "/apis/customization/healthz" in contrib.endpoints + assert "/apis/customization/v2/workspaces/{workspace}/automodel/jobs" in contrib.endpoints + assert "/apis/customization/v2/workspaces/{workspace}/unsloth/jobs" in contrib.endpoints + assert "customization.automodel.jobs.create" in contrib.permissions + assert "customization.unsloth.jobs.create" in contrib.permissions + + +def test_get_authz_contribution_returns_none_without_backends(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + "nemo_customizer.router.discover_customization_contributors", + lambda: {}, + ) + assert CustomizationRouterService.get_authz_contribution() is None diff --git a/plugins/nemo-customizer/tests/test_skills.py b/plugins/nemo-customizer/tests/test_skills.py index 5f60256d6a..6c8f020094 100644 --- a/plugins/nemo-customizer/tests/test_skills.py +++ b/plugins/nemo-customizer/tests/test_skills.py @@ -19,3 +19,5 @@ def test_nemo_customizer_skill_present() -> None: text = skill.read_text() assert "name: nemo-customizer" in text assert "nemo customization automodel submit" in text + assert "nemo customization unsloth submit" in text + assert "run --venv" not in text diff --git a/plugins/nemo-data-designer/src/nemo_data_designer_plugin/cli/renderers.py b/plugins/nemo-data-designer/src/nemo_data_designer_plugin/cli/renderers.py index 1f76ce76cc..ff8c1f0f88 100644 --- a/plugins/nemo-data-designer/src/nemo_data_designer_plugin/cli/renderers.py +++ b/plugins/nemo-data-designer/src/nemo_data_designer_plugin/cli/renderers.py @@ -141,8 +141,8 @@ def _build_preview_results(self, df: pd.DataFrame) -> PreviewResults | None: return PreviewResults( config_builder=builder, dataset=df, - dataset_metadata=self._dataset_metadata, # type: ignore[arg-type] - analysis=self._analysis, # type: ignore[arg-type] + dataset_metadata=self._dataset_metadata, + analysis=self._analysis, processor_artifacts=self._processor_outputs or None, ) except Exception as exc: # pragma: no cover - defensive diff --git a/plugins/nemo-guardrails/tests/unit/test_middleware.py b/plugins/nemo-guardrails/tests/unit/test_middleware.py index 30a78c3a61..6ebda1fbbd 100644 --- a/plugins/nemo-guardrails/tests/unit/test_middleware.py +++ b/plugins/nemo-guardrails/tests/unit/test_middleware.py @@ -712,7 +712,7 @@ async def test_inline_source_blocked_rail_uses_inline_label(self, middleware: Gu ) assert isinstance(result, ImmediateResponse) - data: dict[str, Any] = result.data # type: ignore[assignment] + data: dict[str, Any] = result.data assert "guardrails_data" not in data assert result.response_body_annotations["guardrails_data"]["config_ids"] == [""] diff --git a/plugins/nemo-unsloth/README.md b/plugins/nemo-unsloth/README.md index b4cb5ae872..39521b2bd2 100644 --- a/plugins/nemo-unsloth/README.md +++ b/plugins/nemo-unsloth/README.md @@ -41,7 +41,7 @@ What happens after submit: ## CLI surface -``` +```bash nemo customization unsloth --help nemo customization unsloth submit JOB_JSON -w WORKSPACE [--profile P] [--cluster C] [-o k=v] nemo customization unsloth run ... # hard-fails: Unsloth is submit-only diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/__init__.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/__init__.py index 1f3bcd4291..d90bb76e4b 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/__init__.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/__init__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""NeMo Unsloth customization contributor — local fine-tuning via BYO-venv.""" +"""NeMo Unsloth customization contributor — local fine-tuning via submit.""" diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/inputs.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/inputs.py index 27ef692a81..8181a08e3e 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/inputs.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/inputs.py @@ -51,10 +51,10 @@ def apply_unsloth_job_cli_overrides(group: typer.Typer) -> None: def _pluck_callback(group: typer.Typer, verb: str) -> Callable[..., None]: - callback = next(c for c in group.registered_commands if c.name == verb).callback - if callback is None: + command = next((c for c in group.registered_commands if c.name == verb), None) + if command is None or command.callback is None: raise RuntimeError(f"missing {verb!r} callback to override") - return callback + return command.callback def _drop_command(group: typer.Typer, name: str) -> None: diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/jobs/jobs.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/jobs/jobs.py index bbe7769ed7..43b4081ff8 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/jobs/jobs.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/jobs/jobs.py @@ -24,8 +24,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar, cast +from typing import ClassVar, cast +from nemo_platform import AsyncNeMoPlatform from nemo_platform_plugin.config import NemoPlatformConfig, Runtime from nemo_platform_plugin.job import NemoJob from nemo_platform_plugin.jobs.api_factory import PlatformJobSpec @@ -39,9 +40,6 @@ from nemo_unsloth_plugin.schema import UnslothJobInput from nemo_unsloth_plugin.transform import transform_input_to_output -if TYPE_CHECKING: - from nemo_platform import AsyncNeMoPlatform - def _require_docker_runtime() -> None: """Refuse to compile when the platform isn't configured for Docker. @@ -98,7 +96,7 @@ async def to_spec( return await transform_input_to_output( job_input, workspace, - cast("AsyncNeMoPlatform", async_sdk), + cast(AsyncNeMoPlatform, async_sdk), ) @classmethod @@ -143,7 +141,7 @@ async def compile( platform_spec = await platform_job_config_compiler( workspace=workspace, spec=canonical, - sdk=cast("AsyncNeMoPlatform", async_sdk), + sdk=cast(AsyncNeMoPlatform, async_sdk), job_name=job_name, profile=execution_profile, ) diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py index 4dd4fedf50..8ec7819721 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py @@ -108,7 +108,7 @@ async def transform_input_to_output( output = OutputResponse( name=out_name, - type=_infer_output_type(output_request), # type: ignore[arg-type] + type=_infer_output_type(output_request), save_method=output_request.save_method, fileset=out_name, # default the fileset to the entity name (mirrors automodel) description=output_request.description, diff --git a/plugins/nemo-unsloth/tests/test_cli.py b/plugins/nemo-unsloth/tests/test_cli.py index 4d8d59677e..6706077df4 100644 --- a/plugins/nemo-unsloth/tests/test_cli.py +++ b/plugins/nemo-unsloth/tests/test_cli.py @@ -14,7 +14,6 @@ import json import re from pathlib import Path -from unittest.mock import patch import httpx import pytest diff --git a/plugins/nemo-unsloth/tests/test_contributor.py b/plugins/nemo-unsloth/tests/test_contributor.py index 7d9572242a..12df2de8b6 100644 --- a/plugins/nemo-unsloth/tests/test_contributor.py +++ b/plugins/nemo-unsloth/tests/test_contributor.py @@ -36,28 +36,27 @@ def contributor() -> object: class TestIdentity: def test_name(self, contributor: object) -> None: - assert contributor.name == "unsloth" # type: ignore[attr-defined] + assert contributor.name == "unsloth" def test_dependencies_match_submit_path(self, contributor: object) -> None: # Remote container submit needs the same set of platform services # automodel needs: workspace/auth, jobs API, secrets, files + models. for required in ("entities", "auth", "jobs", "files", "secrets", "models"): - assert required in contributor.dependencies, ( # type: ignore[attr-defined] - f"{required!r} missing from {contributor.dependencies!r}" # type: ignore[attr-defined] - ) + assert required in contributor.dependencies, f"{required!r} missing from {contributor.dependencies!r}" class TestAuthz: def test_authz_contribution_targets_unsloth_collection(self, contributor: object) -> None: - ac = contributor.get_authz_contribution() # type: ignore[attr-defined] + ac = contributor.get_authz_contribution() repr_ = repr(ac) assert "unsloth" in repr_ class TestRouters: def test_returns_two_router_specs(self, contributor: object) -> None: + specs = () try: - specs = contributor.get_routers() # type: ignore[attr-defined] + specs = contributor.get_routers() except ImportError as exc: pytest.skip(f"router deps unavailable in this env: {exc}") assert len(specs) == 2 @@ -72,7 +71,7 @@ def test_returns_two_router_specs(self, contributor: object) -> None: class TestCLI: def test_cli_root_help_lists_three_verbs(self, contributor: object) -> None: try: - cli = contributor.get_cli() # type: ignore[attr-defined] + cli = contributor.get_cli() except ImportError as exc: pytest.skip(f"CLI deps unavailable in this env: {exc}") runner = CliRunner() @@ -85,7 +84,7 @@ def test_cli_root_help_lists_three_verbs(self, contributor: object) -> None: def test_run_hard_fails(self, contributor: object) -> None: try: - cli = contributor.get_cli() # type: ignore[attr-defined] + cli = contributor.get_cli() except ImportError as exc: pytest.skip(f"CLI deps unavailable in this env: {exc}") runner = CliRunner() @@ -97,7 +96,7 @@ def test_run_hard_fails(self, contributor: object) -> None: def test_submit_help_shows_job_json_positional(self, contributor: object) -> None: try: - cli = contributor.get_cli() # type: ignore[attr-defined] + cli = contributor.get_cli() except ImportError as exc: pytest.skip(f"CLI deps unavailable in this env: {exc}") runner = CliRunner() diff --git a/plugins/nemo-unsloth/tests/test_jobs.py b/plugins/nemo-unsloth/tests/test_jobs.py index 49d117b5bc..60d52d9cd6 100644 --- a/plugins/nemo-unsloth/tests/test_jobs.py +++ b/plugins/nemo-unsloth/tests/test_jobs.py @@ -83,7 +83,9 @@ class TestCompile: def test_compile_delegates_to_service_compiler(self) -> None: """When the runtime check passes, ``compile`` returns whatever the service builds.""" canonical = _make_canonical() - fake_spec = SimpleNamespace(steps=["model-and-dataset-download", "training", "model-upload", "model-entity-creation"]) + fake_spec = SimpleNamespace( + steps=["model-and-dataset-download", "training", "model-upload", "model-entity-creation"] + ) with ( patch("nemo_unsloth_plugin.jobs.jobs._require_docker_runtime"), diff --git a/plugins/nemo-unsloth/tests/test_schema.py b/plugins/nemo-unsloth/tests/test_schema.py index 56a960a54f..06cf9c1043 100644 --- a/plugins/nemo-unsloth/tests/test_schema.py +++ b/plugins/nemo-unsloth/tests/test_schema.py @@ -148,14 +148,14 @@ def test_full_ft_rejects_4bit(self) -> None: def test_full_ft_rejects_lora_block(self) -> None: payload = _minimal_payload() - payload["model"]["load_in_4bit"] = False # type: ignore[index] + payload["model"]["load_in_4bit"] = False payload["training"] = {"finetuning_type": "full", "lora": {"rank": 8}} with pytest.raises(ValidationError, match="training.lora must be unset"): UnslothJobInput.model_validate(payload) def test_full_ft_clean(self) -> None: payload = _minimal_payload() - payload["model"]["load_in_4bit"] = False # type: ignore[index] + payload["model"]["load_in_4bit"] = False payload["training"] = {"finetuning_type": "full"} spec = UnslothJobInput.model_validate(payload) assert spec.training.lora is None @@ -177,7 +177,7 @@ def test_merged_save_with_lora_ok(self) -> None: def test_merged_save_with_full_rejected(self) -> None: payload = _minimal_payload() - payload["model"]["load_in_4bit"] = False # type: ignore[index] + payload["model"]["load_in_4bit"] = False payload["training"] = {"finetuning_type": "full"} payload["output"] = {"save_method": "merged_16bit"} with pytest.raises(ValidationError, match="only valid for training.finetuning_type='lora'"): diff --git a/pyproject.toml b/pyproject.toml index 71dfdc9f59..25b7239906 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,11 +33,11 @@ dependencies = [ "sacrebleu", "jinja2==3.1.6", "h11>=0.16.0", - "protobuf<7.0.dev0,>=6.33.5", # fix for GHSA OOS + "protobuf<7.0.dev0,>=6.33.5", # fix for GHSA OOS "pyyaml>=6.0.2", "pydantic>=2.10.6", "mypy-extensions==1.0.0", - "tornado>=6.5.5", # pinned for CVE GHSA-7cx3-6m66-7c5m (>=6.5.0) + GHSA-qjxf-f2mg-c6mc (<=6.5.4, fixed in 6.5.5) + "tornado>=6.5.5", # pinned for CVE GHSA-7cx3-6m66-7c5m (>=6.5.0) + GHSA-qjxf-f2mg-c6mc (<=6.5.4, fixed in 6.5.5) "tomlkit>=0.13.3", "nmp-customizer", "nmp-evaluator", @@ -86,12 +86,12 @@ dev = [ "ty==0.0.17", "hatchling>=1.26.3", "nmp-testing", - "mlflow-skinny", # Optional dep for fine-tuning (shipped in training images); skinny variant to test API surface without bloating uv.lock - "nemo-platform-plugin", # Optional runtime dep; included here for type checking - "nvidia-nat-atif>=1.7.0,<1.8", # ATIF schema models for agentic-use trajectory validation - "nemo-platform-sdk-tools", # Include SDK/license tool migration target for testing - "nmp-build-tools", # Build hook helpers imported by package-local hatch_build.py files - "nmp-dev-mcp", # Include MCP dev tools + "mlflow-skinny", # Optional dep for fine-tuning (shipped in training images); skinny variant to test API surface without bloating uv.lock + "nemo-platform-plugin", # Optional runtime dep; included here for type checking + "nvidia-nat-atif>=1.7.0,<1.8", # ATIF schema models for agentic-use trajectory validation + "nemo-platform-sdk-tools", # Include SDK/license tool migration target for testing + "nmp-build-tools", # Build hook helpers imported by package-local hatch_build.py files + "nmp-dev-mcp", # Include MCP dev tools # These are the dev dependencies from `sdk/python/nemo-platform`. # Including here until we find a better way to include them. "pyright==1.1.399", @@ -156,7 +156,12 @@ dev = [ # `nemo-platform` for task-oriented backward compatibility. # nemo-platform-plugin provides the runtime interfaces used by first-party plugins bundled into images. # Each container group explicitly lists only the services it needs. -nmp-base = ["nmp-common", "nemo-platform", "nmp-platform", "nemo-platform-plugin"] +nmp-base = [ + "nmp-common", + "nemo-platform", + "nmp-platform", + "nemo-platform-plugin", +] # Extra runtime needed specifically for `nemo services run` in service images. nmp-services-runtime = ["nemo-platform[services]"] @@ -258,35 +263,36 @@ environments = [ ] constraint-dependencies = [ - "GitPython>=3.1.49", - "Mako>=1.3.12", - "Pygments>=2.20.0", - "aiohttp>=3.13.4", # High/Medium/Low OOS – auditor-tasks + customizer - "authlib>=1.6.11", - "black>=26.3.1", - "cryptography>=46.0.7", - "diffusers>=0.38.0", - "filelock>=3.20.3", - "jupyter-server>=2.18.0", - "jupyterlab>=4.5.7", - "langchain-core>=1.3.3", - "langchain-openai>=1.1.14", - "langgraph>=1.0.10", - "langsmith>=0.7.31", - "litellm>=1.83.10", - "lxml>=6.1.0", - "mistune>=3.2.1", - "nbconvert>=7.17.1", - "nltk>=3.9.4", - "pillow>=12.2.0", - "postcss>=8.5.10", - "pyasn1>=0.6.3", - "python-multipart>=0.0.27", - "regex>=2025.10.22", - "safetensors>=0.8.0rc0", # explicit prerelease so uv allows the rc - "sqlfluff==4.1.0", - "urllib3>=2.7.0", - "uv>=0.9.14,<0.10.0", + "GitPython>=3.1.49", + "Mako>=1.3.12", + "Pygments>=2.20.0", + "aiohttp>=3.13.4", # High/Medium/Low OOS – auditor-tasks + customizer + "authlib>=1.6.11", + "black>=26.3.1", + "cryptography>=46.0.7", + "diffusers>=0.38.0", + "filelock>=3.20.3", + "jupyter-server>=2.18.0", + "jupyterlab>=4.5.7", + "langchain-core>=1.3.3", + "langchain-openai>=1.1.14", + "langgraph>=1.0.10", + "langsmith==0.8.2", # osv-scanner UNKNOWN for 0.8.9+; PyPI metadata is MIT + "json-repair==0.58.7", # osv-scanner UNKNOWN for 0.60.1+; PyPI metadata is MIT + "litellm>=1.83.10", + "lxml>=6.1.0", + "mistune>=3.2.1", + "nbconvert>=7.17.1", + "nltk>=3.9.4", + "pillow>=12.2.0", + "postcss>=8.5.10", + "pyasn1>=0.6.3", + "python-multipart>=0.0.27", + "regex>=2025.10.22", + "safetensors>=0.8.0rc0", # explicit prerelease so uv allows the rc + "sqlfluff==4.1.0", + "urllib3>=2.7.0", + "uv>=0.9.14,<0.10.0", ] override-dependencies = [ @@ -295,36 +301,36 @@ override-dependencies = [ "jsonpath-ng>=1.6.1", "grpcio>=1.71.0", "huggingface-hub>=1.0.1,<2.0.0", - "pydantic[email]>=2.9.2", # openai-harmony depends on a later pydantic, but that causes the stainless gen to hit an infinite loop - "ray; sys_platform == 'never'", # ray has an unfixed critical CVE, as for why we're removing it this way see https://github.com/astral-sh/uv/issues/9174 + "pydantic[email]>=2.9.2", # openai-harmony depends on a later pydantic, but that causes the stainless gen to hit an infinite loop + "ray; sys_platform == 'never'", # ray has an unfixed critical CVE, as for why we're removing it this way see https://github.com/astral-sh/uv/issues/9174 "fschat; sys_platform == 'never'", # fschat is unmaintained with unfixed High/Medium CVEs; transitive via garak but never imported - "botocore>=1.40.46,<1.40.62", # ngcsdk requires botocore>=1.37; upper bound required by aiobotocore 2.25.1 (aioboto3 15.5.0) + "botocore>=1.40.46,<1.40.62", # ngcsdk requires botocore>=1.37; upper bound required by aiobotocore 2.25.1 (aioboto3 15.5.0) "langchain>=1.0.0", - "langchain-core>=1.3.3", # LangChain Core CVEs through GHSA-qh6h-p6c9-ff54 and CVE-2026-44843 - "nltk>=3.9.4", # NLTK CVEs with available patches; remaining NLTK alerts have no upstream fix - "Pillow>=12.2.0", # Pillow CVEs through CVE-2026-48624; override: fastembed (via nemoguardrails) pins Pillow<12 + "langchain-core>=1.3.3", # LangChain Core CVEs through GHSA-qh6h-p6c9-ff54 and CVE-2026-44843 + "nltk>=3.9.4", # NLTK CVEs with available patches; remaining NLTK alerts have no upstream fix + "Pillow>=12.2.0", # Pillow CVEs through CVE-2026-48624; override: fastembed (via nemoguardrails) pins Pillow<12 # todo(dn-v2): unpin these. this was originally causing tests to fail. "aiodns==3.5.0", "pycares==4.11.0", - "authlib>=1.6.9", # Critical – nmp-api/core/cpu-tasks/gpu-tasks - "aiohttp>=3.13.4", # High/Medium/Low OOS – auditor-tasks + customizer - "jaraco-context>=6.1.0", # High OOS – auditor-tasks + customizer - "pyasn1>=0.6.3", # High OOS – auditor-tasks + customizer - "setuptools>=78.1.1", # High OOS – nmp-cpu-tasks/gpu-tasks/customizer-tasks - "urllib3>=2.7.0", # High OOS – auditor-tasks + customizer - "wheel>=0.46.2", # High OOS – widespread - "cbor2>=5.9.0", # High – customizer + nmp-gpu-tasks - "simpleeval>=1.0.5", # High – nmp-api + nmp-gpu-tasks - "ujson>=5.12.0", # High – nmp-api/cpu-tasks/gpu-tasks - "xgrammar>=0.1.32", # High – customizer + nmp-gpu-tasks - "fastmcp>=3.2.0", # GHSA-vv7q-7jx5-f767 (Critical) + GHSA-rww4-4w9c-7733 (High); overrides vendored sdk/python/nemo-platform <3 constraint - "wandb>=0.25.1", # CVE-2026-33186 - "click>=8.2.0", # Below CVEs are based on constraints that garak has + "authlib>=1.6.9", # Critical – nmp-api/core/cpu-tasks/gpu-tasks + "aiohttp>=3.13.4", # High/Medium/Low OOS – auditor-tasks + customizer + "jaraco-context>=6.1.0", # High OOS – auditor-tasks + customizer + "pyasn1>=0.6.3", # High OOS – auditor-tasks + customizer + "setuptools>=78.1.1", # High OOS – nmp-cpu-tasks/gpu-tasks/customizer-tasks + "urllib3>=2.7.0", # High OOS – auditor-tasks + customizer + "wheel>=0.46.2", # High OOS – widespread + "cbor2>=5.9.0", # High – customizer + nmp-gpu-tasks + "simpleeval>=1.0.5", # High – nmp-api + nmp-gpu-tasks + "ujson>=5.12.0", # High – nmp-api/cpu-tasks/gpu-tasks + "xgrammar>=0.1.32", # High – customizer + nmp-gpu-tasks + "fastmcp>=3.2.0", # GHSA-vv7q-7jx5-f767 (Critical) + GHSA-rww4-4w9c-7733 (High); overrides vendored sdk/python/nemo-platform <3 constraint + "wandb>=0.25.1", # CVE-2026-33186 + "click>=8.2.0", # Below CVEs are based on constraints that garak has "langchain-openai>=1.1.14", - "litellm>=1.83.10", # CVE-2026-42208 (Critical SQL injection), CVE-2026-40217, and 5 other High CVEs + "litellm>=1.83.10", # CVE-2026-42208 (Critical SQL injection), CVE-2026-40217, and 5 other High CVEs "python-dotenv>=1.2.2", "openai>=2.26.0", - "sqlfluff>=4.1.0", # CVE-2026-46373 + "sqlfluff>=4.1.0", # CVE-2026-46373 ] @@ -502,13 +508,16 @@ output = "coverage.xml" [tool.pyright] extraPaths = [ - "plugins/nemo-guardrails/src", - "plugins/nemo-evaluator/src", - "tests/agentic-use", - "tests/agentic-use/shared", + "plugins/nemo-guardrails/src", + "plugins/nemo-evaluator/src", + "tests/agentic-use", + "tests/agentic-use/shared", ] executionEnvironments = [ - { root = "tests/agentic-use", extraPaths = ["tests/agentic-use", "tests/agentic-use/shared"] }, + { root = "tests/agentic-use", extraPaths = [ + "tests/agentic-use", + "tests/agentic-use/shared", + ] }, ] [tool.ty.environment] @@ -562,6 +571,11 @@ exclude = [ "./services/customizer/", + # GPU-container training drivers import torch/peft/nemo_automodel/unsloth at runtime only. + "services/automodel/src/nmp/automodel/tasks/training/backends/", + "services/automodel/src/nmp/automodel/tasks/training/utils.py", + "services/unsloth/src/nmp/unsloth/tasks/training/backends/", + "./services/guardrails/", "./services/intake/", diff --git a/pytest.ini b/pytest.ini index fc2276291d..266cf09b7d 100644 --- a/pytest.ini +++ b/pytest.ini @@ -58,8 +58,9 @@ markers = gpu_integration: GPU integration tests - test individual services that utilize AI dependencies/ a GPU smoke_gpu_tasks: Import smoke tests for the nmp-gpu-tasks image smoke_customizer_tasks: Import smoke tests for the customizer-tasks image - smoke_customizer_automodel: Import smoke tests for the customizer-automodel image smoke_customizer_rl: Import smoke tests for the customizer-rl image + smoke_nmp_automodel_tasks: Import smoke tests for the nmp-automodel-tasks image + smoke_nmp_automodel_training: Import smoke tests for the nmp-automodel-training image e2e: End-to-end tests - test complete customer workflows on deployed infrastructure (Helm/Docker Compose) regression: Regression tests - test individual functional microservices for baseline functionality infrastructure: Infrastructure tests - ensure services are compatible with customer infrastructure diff --git a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml index 8135fb7c79..675abf90e1 100644 --- a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml +++ b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml @@ -11881,6 +11881,8 @@ components: for the general entity reference pattern used across the platform.' + examples: + - workspace/benchmark-name BenchmarkRequest: properties: name: @@ -11978,7 +11980,7 @@ components: - ragas/amnesty_qa title: BuiltInDataset description: Well-known dataset (BEIR or RAGAS) referenced by its identifier. - CPUExecutionProviderInput: + CPUExecutionProvider: properties: provider: type: string @@ -11998,34 +12000,7 @@ components: type: object required: - container - title: CPUExecutionProviderInput - description: 'CPU-based execution provider. - - - Provides configuration for running jobs on CPU resources with - - resource requests and limits.' - CPUExecutionProviderOutput: - properties: - provider: - type: string - const: cpu - title: Provider - default: cpu - profile: - type: string - title: Profile - default: default - container: - $ref: '#/components/schemas/ContainerSpec' - resources: - allOf: - - $ref: '#/components/schemas/ComputeResources' - description: Resource requests and limits for CPU execution. - type: object - required: - - container - title: CPUExecutionProviderOutput + title: CPUExecutionProvider description: 'CPU-based execution provider. @@ -13630,7 +13605,7 @@ components: default: generic metadata: allOf: - - $ref: '#/components/schemas/FilesetMetadataInput' + - $ref: '#/components/schemas/FilesetMetadata' description: 'Purpose-specific metadata. Use the purpose as the key (e.g., {dataset: {...}}).' custom_fields: @@ -13963,7 +13938,7 @@ components: type: object title: Spec platform_spec: - $ref: '#/components/schemas/PlatformJobSpecInput' + $ref: '#/components/schemas/PlatformJobSpec' source: type: string title: Source @@ -14165,34 +14140,7 @@ components: type: object title: DialogRails description: Configuration of topical rails. - DistributedGPUExecutionProviderInput: - properties: - provider: - type: string - const: gpu_distributed - title: Provider - default: gpu_distributed - profile: - type: string - title: Profile - default: default - container: - $ref: '#/components/schemas/ContainerSpec' - resources: - allOf: - - $ref: '#/components/schemas/ComputeResources' - description: Resource requests and limits for distributed GPU execution. - type: object - required: - - container - title: DistributedGPUExecutionProviderInput - description: 'GPU-based execution provider. - - - Provides configuration for running jobs on GPU resources with - - resource requests and limits.' - DistributedGPUExecutionProviderOutput: + DistributedGPUExecutionProvider: properties: provider: type: string @@ -14212,7 +14160,7 @@ components: type: object required: - container - title: DistributedGPUExecutionProviderOutput + title: DistributedGPUExecutionProvider description: 'GPU-based execution provider. @@ -15619,7 +15567,7 @@ components: dataset: anyOf: - $ref: '#/components/schemas/DatasetRows' - - $ref: '#/components/schemas/FilesetOutput' + - $ref: '#/components/schemas/Fileset' - $ref: '#/components/schemas/FilesetRef' title: Dataset description: Dataset containing the test cases for this benchmark. @@ -16365,6 +16313,34 @@ components: enum: - fileset title: FileStorageType + Fileset: + properties: + path: + title: Path + description: The relative path to file/directory in the storage. + type: string + minLength: 1 + storage: + anyOf: + - $ref: '#/components/schemas/NGCStorageConfig' + - $ref: '#/components/schemas/HuggingfaceStorageConfig' + title: Storage + description: The storage configuration for the fileset. + metadata: + allOf: + - $ref: '#/components/schemas/FilesetMetadata' + description: Purpose-specific metadata for the fileset. + custom_fields: + additionalProperties: true + type: object + title: Custom Fields + description: Custom fields for the fileset. + additionalProperties: false + type: object + required: + - storage + title: Fileset + description: Fileset definition for use without persisting to the Files API. FilesetFileOutput: properties: file_ref: @@ -16420,53 +16396,14 @@ components: (on or before) datetime filters. title: FilesetFilter type: object - FilesetInput: - properties: - path: - title: Path - description: The relative path to file/directory in the storage. - type: string - minLength: 1 - storage: - anyOf: - - $ref: '#/components/schemas/NGCStorageConfig' - - $ref: '#/components/schemas/HuggingfaceStorageConfig' - title: Storage - description: The storage configuration for the fileset. - metadata: - allOf: - - $ref: '#/components/schemas/FilesetMetadataInput' - description: Purpose-specific metadata for the fileset. - custom_fields: - additionalProperties: true - type: object - title: Custom Fields - description: Custom fields for the fileset. - additionalProperties: false - type: object - required: - - storage - title: FilesetInput - description: Fileset definition for use without persisting to the Files API. - FilesetMetadataInput: - properties: - dataset: - $ref: '#/components/schemas/DatasetMetadataContent' - model: - $ref: '#/components/schemas/ModelMetadataContent' - type: object - title: FilesetMetadataInput - description: "Tagged metadata container - the key indicates the type.\n\nExample:\n\ - \ metadata = FilesetMetadata(\n dataset=DatasetMetadataContent(\n\ - \ schema={\"columns\": [\"id\", \"name\"]},\n )\n )" - FilesetMetadataOutput: + FilesetMetadata: properties: dataset: $ref: '#/components/schemas/DatasetMetadataContent' model: $ref: '#/components/schemas/ModelMetadataContent' type: object - title: FilesetMetadataOutput + title: FilesetMetadata description: "Tagged metadata container - the key indicates the type.\n\nExample:\n\ \ metadata = FilesetMetadata(\n dataset=DatasetMetadataContent(\n\ \ schema={\"columns\": [\"id\", \"name\"]},\n )\n )" @@ -16494,7 +16431,7 @@ components: - $ref: '#/components/schemas/S3StorageConfig' title: Storage metadata: - $ref: '#/components/schemas/FilesetMetadataOutput' + $ref: '#/components/schemas/FilesetMetadata' custom_fields: additionalProperties: true type: object @@ -16718,34 +16655,7 @@ components: type: object title: GLiNERDetectionOptions description: Configuration options for GLiNER. - GPUExecutionProviderInput: - properties: - provider: - type: string - const: gpu - title: Provider - default: gpu - profile: - type: string - title: Profile - default: default - container: - $ref: '#/components/schemas/ContainerSpec' - resources: - allOf: - - $ref: '#/components/schemas/ComputeResources' - description: Resource requests and limits for GPU execution. - type: object - required: - - container - title: GPUExecutionProviderInput - description: 'GPU-based execution provider. - - - Provides configuration for running jobs on GPU resources with - - resource requests and limits.' - GPUExecutionProviderOutput: + GPUExecutionProvider: properties: provider: type: string @@ -16765,7 +16675,7 @@ components: type: object required: - container - title: GPUExecutionProviderOutput + title: GPUExecutionProvider description: 'GPU-based execution provider. @@ -17196,7 +17106,7 @@ components: type: string data: allOf: - - $ref: '#/components/schemas/RailsConfigOutput' + - $ref: '#/components/schemas/RailsConfig' type: object description: Guardrail configuration data additionalProperties: true @@ -17375,7 +17285,7 @@ components: - type: string title: Reference description: A reference to RailsConfig. - - $ref: '#/components/schemas/RailsConfigInput' + - $ref: '#/components/schemas/RailsConfig' title: Config description: The id of the configuration or its dict representation to be used. @@ -19462,7 +19372,7 @@ components: anyOf: - $ref: '#/components/schemas/DatasetRows' - $ref: '#/components/schemas/FilesetRef' - - $ref: '#/components/schemas/FilesetInput' + - $ref: '#/components/schemas/Fileset' title: Dataset description: The dataset to evaluate which may represent generated outputs from a model. @@ -19552,7 +19462,7 @@ components: anyOf: - $ref: '#/components/schemas/DatasetRows' - $ref: '#/components/schemas/FilesetRef' - - $ref: '#/components/schemas/FilesetInput' + - $ref: '#/components/schemas/Fileset' title: Dataset description: The dataset to use for agent prompts and evaluation. params: @@ -19676,7 +19586,7 @@ components: anyOf: - $ref: '#/components/schemas/DatasetRows' - $ref: '#/components/schemas/FilesetRef' - - $ref: '#/components/schemas/FilesetInput' + - $ref: '#/components/schemas/Fileset' title: Dataset description: The dataset to use for model prompts and evaluation. params: @@ -19751,6 +19661,8 @@ components: for the general entity reference pattern used across the platform.' + examples: + - workspace/metric-name MetricRetrieverJob: properties: metric: @@ -19772,14 +19684,14 @@ components: to dataset column paths for this job. retriever_pipeline: allOf: - - $ref: '#/components/schemas/RetrieverPipelineInput' + - $ref: '#/components/schemas/RetrieverPipeline' description: The pipeline configuration for retriever-based evaluation. dataset: anyOf: - $ref: '#/components/schemas/BuiltInDataset' - $ref: '#/components/schemas/DatasetRows' - $ref: '#/components/schemas/FilesetRef' - - $ref: '#/components/schemas/FilesetInput' + - $ref: '#/components/schemas/Fileset' title: Dataset description: The dataset to use for evaluation. params: @@ -20848,6 +20760,8 @@ components: the general entity reference pattern used across the platform.' + examples: + - workspace/model_name ModelSpec: properties: context_size: @@ -22355,23 +22269,14 @@ components: type: object title: PatronusEvaluateApiParams description: Config to parameterize the Patronus Evaluate API call - PatronusEvaluateConfigInput: - properties: - evaluate_config: - allOf: - - $ref: '#/components/schemas/PatronusEvaluateApiParams' - description: Configuration passed to the Patronus Evaluate API - type: object - title: PatronusEvaluateConfigInput - description: Config for the Patronus Evaluate API call - PatronusEvaluateConfigOutput: + PatronusEvaluateConfig: properties: evaluate_config: allOf: - $ref: '#/components/schemas/PatronusEvaluateApiParams' description: Configuration passed to the Patronus Evaluate API type: object - title: PatronusEvaluateConfigOutput + title: PatronusEvaluateConfig description: Config for the Patronus Evaluate API call PatronusEvaluationSuccessStrategy: type: string @@ -22388,31 +22293,18 @@ components: ALL_PASS requires all evaluators to pass for success. ANY_PASS requires only one evaluator to pass for success.' - PatronusRailConfigInput: - properties: - input: - allOf: - - $ref: '#/components/schemas/PatronusEvaluateConfigInput' - description: Patronus Evaluate API configuration for an Input Guardrail - output: - allOf: - - $ref: '#/components/schemas/PatronusEvaluateConfigInput' - description: Patronus Evaluate API configuration for an Output Guardrail - type: object - title: PatronusRailConfigInput - description: Configuration data for the Patronus Evaluate API - PatronusRailConfigOutput: + PatronusRailConfig: properties: input: allOf: - - $ref: '#/components/schemas/PatronusEvaluateConfigOutput' + - $ref: '#/components/schemas/PatronusEvaluateConfig' description: Patronus Evaluate API configuration for an Input Guardrail output: allOf: - - $ref: '#/components/schemas/PatronusEvaluateConfigOutput' + - $ref: '#/components/schemas/PatronusEvaluateConfig' description: Patronus Evaluate API configuration for an Output Guardrail type: object - title: PatronusRailConfigOutput + title: PatronusRailConfig description: Configuration data for the Patronus Evaluate API Percentiles: properties: @@ -22614,7 +22506,7 @@ components: title: Spec description: Job Spec platform_spec: - $ref: '#/components/schemas/PlatformJobSpecOutput' + $ref: '#/components/schemas/PlatformJobSpec' fileset: type: string title: Fileset @@ -22753,31 +22645,18 @@ components: - updated_at - -updated_at title: PlatformJobSortField - PlatformJobSpecInput: - properties: - steps: - items: - $ref: '#/components/schemas/PlatformJobStepSpecInput' - type: array - title: Steps - description: List of steps to be executed in the job - type: object - required: - - steps - title: PlatformJobSpecInput - description: Specification for a platform job, containing steps and secrets. - PlatformJobSpecOutput: + PlatformJobSpec: properties: steps: items: - $ref: '#/components/schemas/PlatformJobStepSpecOutput' + $ref: '#/components/schemas/PlatformJobStepSpec' type: array title: Steps description: List of steps to be executed in the job type: object required: - steps - title: PlatformJobSpecOutput + title: PlatformJobSpec description: Specification for a platform job, containing steps and secrets. PlatformJobStatus: type: string @@ -22952,57 +22831,7 @@ components: Parent-scoped: unique within (workspace, entity_type, parent=attempt_id).' - PlatformJobStepSpecInput: - properties: - name: - type: string - pattern: ^[a-z](?!.*--)[a-z0-9\-@.+_]{1,62}(? tuple[str, str] | None: return None # Get the argument name (metavar) and strip any surrounding brackets - metavar = self.make_metavar() + metavar = self.make_metavar(ctx) # Remove square brackets that Click adds for optional arguments metavar = metavar.strip("[]") diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/evaluation/api.md b/sdk/python/nemo-platform/src/nemo_platform/resources/evaluation/api.md index 98c4a2f3fe..70791ebc74 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/evaluation/api.md +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/evaluation/api.md @@ -28,6 +28,7 @@ from nemo_platform.types.evaluation import ( FaithfulnessMetric, FaithfulnessMetricParam, FieldMapping, + Fileset, FilesetRef, InferenceParams, JsonScoreParser, @@ -237,7 +238,6 @@ Types: ```python from nemo_platform.types.evaluation import ( BuiltInDataset, - Fileset, MetricEvaluationJob, MetricEvaluationJobRequest, MetricEvaluationJobsListFilter, diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/files/api.md b/sdk/python/nemo-platform/src/nemo_platform/resources/files/api.md index 24e25c3dca..805e38f424 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/files/api.md +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/files/api.md @@ -34,7 +34,7 @@ Methods: Types: ```python -from nemo_platform.types.files import FilesetFilter, FilesetMetadataParam +from nemo_platform.types.files import FilesetFilter ``` Methods: diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/files/filesets.py b/sdk/python/nemo-platform/src/nemo_platform/resources/files/filesets.py index 9eaf9bda5e..f8fb167cf2 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/files/filesets.py +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/files/filesets.py @@ -43,7 +43,7 @@ from ...types.files.fileset_purpose import FilesetPurpose from ...types.shared.generic_sort_field import GenericSortField from ...types.files.fileset_filter_param import FilesetFilterParam -from ...types.files.fileset_metadata_param_param import FilesetMetadataParamParam +from ...types.shared_params.fileset_metadata import FilesetMetadata from ..._exceptions import ConflictError __all__ = ["FilesetsResource", "AsyncFilesetsResource"] @@ -77,7 +77,7 @@ def create( cache: bool | Omit = omit, custom_fields: Dict[str, object] | Omit = omit, description: str | Omit = omit, - metadata: FilesetMetadataParamParam | Omit = omit, + metadata: FilesetMetadata | Omit = omit, project: str | Omit = omit, purpose: FilesetPurpose | Omit = omit, storage: fileset_create_params.Storage | Omit = omit, @@ -206,7 +206,7 @@ def update( workspace: str | None = None, custom_fields: Dict[str, object] | Omit = omit, description: str | Omit = omit, - metadata: FilesetMetadataParamParam | Omit = omit, + metadata: FilesetMetadata | Omit = omit, project: str | Omit = omit, purpose: FilesetPurpose | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -402,7 +402,7 @@ async def create( cache: bool | Omit = omit, custom_fields: Dict[str, object] | Omit = omit, description: str | Omit = omit, - metadata: FilesetMetadataParamParam | Omit = omit, + metadata: FilesetMetadata | Omit = omit, project: str | Omit = omit, purpose: FilesetPurpose | Omit = omit, storage: fileset_create_params.Storage | Omit = omit, @@ -531,7 +531,7 @@ async def update( workspace: str | None = None, custom_fields: Dict[str, object] | Omit = omit, description: str | Omit = omit, - metadata: FilesetMetadataParamParam | Omit = omit, + metadata: FilesetMetadata | Omit = omit, project: str | Omit = omit, purpose: FilesetPurpose | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/api.md b/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/api.md index a4c81d15f0..52c2cf31fd 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/api.md +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/api.md @@ -59,15 +59,15 @@ from nemo_platform.types.guardrail import ( PangeaRailConfig, PangeaRailOptions, PatronusEvaluateAPIParams, - PatronusEvaluateConfigParam, + PatronusEvaluateConfig, PatronusEvaluationSuccessStrategy, - PatronusRailConfigParam, + PatronusRailConfig, PrivateAIDetection, PrivateAIDetectionOptions, RailStatus, - RailsConfigDataParam, - RailsConfigParam, - RailsParam, + Rails, + RailsConfig, + RailsConfigData, ReasoningConfig, RegexDetection, RegexDetectionOptions, @@ -99,11 +99,6 @@ from nemo_platform.types.guardrail import ( GuardrailConfigParam, GuardrailConfigUpdate, GuardrailConfigsPage, - PatronusEvaluateConfig, - PatronusRailConfig, - Rails, - RailsConfig, - RailsConfigData, ) ``` diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/jobs/api.md b/sdk/python/nemo-platform/src/nemo_platform/resources/jobs/api.md index f5bfd41e0c..99f688895d 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/jobs/api.md +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/jobs/api.md @@ -8,10 +8,8 @@ from nemo_platform.types.jobs import ( ComputeResources, ContainerSpec, CPUExecutionProvider, - CPUExecutionProviderParam, CreatePlatformJobRequest, DistributedGPUExecutionProvider, - DistributedGPUExecutionProviderParam, DockerJobExecutionProfile, DockerJobExecutionProfileConfig, DockerJobNetworkConfig, @@ -19,7 +17,6 @@ from nemo_platform.types.jobs import ( DockerVolumeMount, E2EJobExecutionProfile, GPUExecutionProvider, - GPUExecutionProviderParam, ImagePullSecret, JobExecutionProfileConfig, KubernetesEmptyDirVolume, @@ -36,9 +33,7 @@ from nemo_platform.types.jobs import ( PlatformJobSecretEnvironmentVariableRef, PlatformJobSortField, PlatformJobSpec, - PlatformJobSpecParam, PlatformJobStepSpec, - PlatformJobStepSpecParam, PlatformJobsListFilter, StepLifecycle, SubprocessExecutionProvider, diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/jobs/jobs.py b/sdk/python/nemo-platform/src/nemo_platform/resources/jobs/jobs.py index 9c5652b216..3a7aff2408 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/jobs/jobs.py +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/jobs/jobs.py @@ -58,7 +58,6 @@ from ...pagination import SyncLogsPagination, AsyncLogsPagination, SyncDefaultPagination, AsyncDefaultPagination from ...types.jobs import ( PlatformJobSortField, - PlatformJobSpecParam, job_list_params, job_create_params, job_get_logs_params, diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/evaluation/extended_benchmark.py b/sdk/python/nemo-platform/src/nemo_platform/types/evaluation/extended_benchmark.py index f5b13f35d3..cfb25fa67e 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/evaluation/extended_benchmark.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/evaluation/extended_benchmark.py @@ -19,6 +19,7 @@ from datetime import datetime from typing_extensions import Annotated, TypeAlias +from .fileset import Fileset from ..._utils import PropertyInfo from ..._models import BaseModel from .f1_metric import F1Metric @@ -29,7 +30,6 @@ from .field_mapping import FieldMapping from .remote_metric import RemoteMetric from .system_metric import SystemMetric -from ..files.fileset import Fileset from .llm_judge_metric import LLMJudgeMetric from .exact_match_metric import ExactMatchMetric from .faithfulness_metric import FaithfulnessMetric diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/evaluation/fileset.py b/sdk/python/nemo-platform/src/nemo_platform/types/evaluation/fileset.py index 31a86fa8b9..dfa280dc13 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/evaluation/fileset.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/evaluation/fileset.py @@ -19,8 +19,8 @@ from typing_extensions import TypeAlias from ..._models import BaseModel +from ..shared.fileset_metadata import FilesetMetadata from ..files.ngc_storage_config import NGCStorageConfig -from ..files.fileset_metadata_param import FilesetMetadataParam from ..files.huggingface_storage_config import HuggingfaceStorageConfig __all__ = ["Fileset", "Storage"] @@ -37,7 +37,7 @@ class Fileset(BaseModel): custom_fields: Optional[Dict[str, object]] = None """Custom fields for the fileset.""" - metadata: Optional[FilesetMetadataParam] = None + metadata: Optional[FilesetMetadata] = None """Tagged metadata container - the key indicates the type. Example: metadata = FilesetMetadata( dataset=DatasetMetadataContent( diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/evaluation/fileset_param.py b/sdk/python/nemo-platform/src/nemo_platform/types/evaluation/fileset_param.py index ec6edf8f8b..c202d12f81 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/evaluation/fileset_param.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/evaluation/fileset_param.py @@ -21,7 +21,7 @@ from typing_extensions import Required, TypeAlias, TypedDict from ..files.ngc_storage_config_param import NGCStorageConfigParam -from ..files.fileset_metadata_param_param import FilesetMetadataParamParam +from ..shared_params.fileset_metadata import FilesetMetadata from ..files.huggingface_storage_config_param import HuggingfaceStorageConfigParam __all__ = ["FilesetParam", "Storage"] @@ -38,7 +38,7 @@ class FilesetParam(TypedDict, total=False): custom_fields: Dict[str, object] """Custom fields for the fileset.""" - metadata: FilesetMetadataParamParam + metadata: FilesetMetadata """Tagged metadata container - the key indicates the type. Example: metadata = FilesetMetadata( dataset=DatasetMetadataContent( diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/files/__init__.py b/sdk/python/nemo-platform/src/nemo_platform/types/files/__init__.py index 8256b63c17..e860a6d91a 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/files/__init__.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/files/__init__.py @@ -32,7 +32,6 @@ from .fileset_create_params import FilesetCreateParams as FilesetCreateParams from .fileset_update_params import FilesetUpdateParams as FilesetUpdateParams from .file_list_files_params import FileListFilesParams as FileListFilesParams -from .fileset_metadata_param import FilesetMetadataParam as FilesetMetadataParam from .file_upload_file_params import FileUploadFileParams as FileUploadFileParams from .s3_storage_config_param import S3StorageConfigParam as S3StorageConfigParam from .dataset_metadata_content import DatasetMetadataContent as DatasetMetadataContent @@ -40,6 +39,5 @@ from .huggingface_storage_config import HuggingfaceStorageConfig as HuggingfaceStorageConfig from .local_storage_config_param import LocalStorageConfigParam as LocalStorageConfigParam from .list_fileset_files_response import ListFilesetFilesResponse as ListFilesetFilesResponse -from .fileset_metadata_param_param import FilesetMetadataParamParam as FilesetMetadataParamParam from .dataset_metadata_content_param import DatasetMetadataContentParam as DatasetMetadataContentParam from .huggingface_storage_config_param import HuggingfaceStorageConfigParam as HuggingfaceStorageConfigParam diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/files/fileset_create_params.py b/sdk/python/nemo-platform/src/nemo_platform/types/files/fileset_create_params.py index 29292c6d6e..06715b1c74 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/files/fileset_create_params.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/files/fileset_create_params.py @@ -24,7 +24,7 @@ from .s3_storage_config_param import S3StorageConfigParam from .ngc_storage_config_param import NGCStorageConfigParam from .local_storage_config_param import LocalStorageConfigParam -from .fileset_metadata_param_param import FilesetMetadataParamParam +from ..shared_params.fileset_metadata import FilesetMetadata from .huggingface_storage_config_param import HuggingfaceStorageConfigParam __all__ = ["FilesetCreateParams", "Storage"] @@ -49,7 +49,7 @@ class FilesetCreateParams(TypedDict, total=False): description: str """The description of the fileset.""" - metadata: FilesetMetadataParamParam + metadata: FilesetMetadata """Tagged metadata container - the key indicates the type. Example: metadata = FilesetMetadata( dataset=DatasetMetadataContent( diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/files/fileset_metadata_param.py b/sdk/python/nemo-platform/src/nemo_platform/types/files/fileset_metadata_param.py deleted file mode 100644 index e24a37897f..0000000000 --- a/sdk/python/nemo-platform/src/nemo_platform/types/files/fileset_metadata_param.py +++ /dev/null @@ -1,46 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from ..._models import BaseModel -from .dataset_metadata_content import DatasetMetadataContent -from ..shared.model_metadata_content import ModelMetadataContent - -__all__ = ["FilesetMetadataParam"] - - -class FilesetMetadataParam(BaseModel): - """Tagged metadata container - the key indicates the type. - - Example: - metadata = FilesetMetadata( - dataset=DatasetMetadataContent( - schema={"columns": ["id", "name"]}, - ) - ) - """ - - dataset: Optional[DatasetMetadataContent] = None - """Content for dataset-type filesets.""" - - model: Optional[ModelMetadataContent] = None - """Content for model-type filesets. - - Contains tool calling configuration that is merged into the ModelSpec during - checkpoint analysis. - """ diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/files/fileset_update_params.py b/sdk/python/nemo-platform/src/nemo_platform/types/files/fileset_update_params.py index d91614409a..3f8699dda8 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/files/fileset_update_params.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/files/fileset_update_params.py @@ -21,7 +21,7 @@ from typing_extensions import TypedDict from .fileset_purpose import FilesetPurpose -from .fileset_metadata_param_param import FilesetMetadataParamParam +from ..shared_params.fileset_metadata import FilesetMetadata __all__ = ["FilesetUpdateParams"] @@ -35,7 +35,7 @@ class FilesetUpdateParams(TypedDict, total=False): description: str """The description of the fileset.""" - metadata: FilesetMetadataParamParam + metadata: FilesetMetadata """Tagged metadata container - the key indicates the type. Example: metadata = FilesetMetadata( dataset=DatasetMetadataContent( diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/shared_params/__init__.py b/sdk/python/nemo-platform/src/nemo_platform/types/shared_params/__init__.py index 9ca01aca8b..0f3f40f5cc 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/shared_params/__init__.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/shared_params/__init__.py @@ -22,6 +22,7 @@ from .backend_format import BackendFormat as BackendFormat from .datetime_filter import DatetimeFilter as DatetimeFilter from .finetuning_type import FinetuningType as FinetuningType +from .fileset_metadata import FilesetMetadata as FilesetMetadata from .tool_call_config import ToolCallConfig as ToolCallConfig from .api_endpoint_data import APIEndpointData as APIEndpointData from .file_storage_type import FileStorageType as FileStorageType diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/files/fileset_metadata_param_param.py b/sdk/python/nemo-platform/src/nemo_platform/types/shared_params/fileset_metadata.py similarity index 84% rename from sdk/python/nemo-platform/src/nemo_platform/types/files/fileset_metadata_param_param.py rename to sdk/python/nemo-platform/src/nemo_platform/types/shared_params/fileset_metadata.py index 502d30fbe8..5e5bd4857a 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/files/fileset_metadata_param_param.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/shared_params/fileset_metadata.py @@ -19,13 +19,13 @@ from typing_extensions import TypedDict -from .dataset_metadata_content_param import DatasetMetadataContentParam -from ..shared_params.model_metadata_content import ModelMetadataContent +from .model_metadata_content import ModelMetadataContent +from ..files.dataset_metadata_content_param import DatasetMetadataContentParam -__all__ = ["FilesetMetadataParamParam"] +__all__ = ["FilesetMetadata"] -class FilesetMetadataParamParam(TypedDict, total=False): +class FilesetMetadata(TypedDict, total=False): """Tagged metadata container - the key indicates the type. Example: diff --git a/sdk/python/nemo-platform/tests/api_resources/evaluation/test_benchmark_job_results.py b/sdk/python/nemo-platform/tests/api_resources/evaluation/test_benchmark_job_results.py index 6727ebe554..55f584ff82 100644 --- a/sdk/python/nemo-platform/tests/api_resources/evaluation/test_benchmark_job_results.py +++ b/sdk/python/nemo-platform/tests/api_resources/evaluation/test_benchmark_job_results.py @@ -114,14 +114,14 @@ def test_method_list_with_all_params(self, client: NeMoPlatform) -> None: workspace="workspace", aggregate_fields=["nan_count"], filter={ - "benchmark": "26f1kl_-n-71/4m_-__-35-", + "benchmark": "workspace/benchmark-name", "created_at": { "gte": parse_datetime("2019-12-27T18:11:19.117Z"), "lte": parse_datetime("2019-12-27T18:11:19.117Z"), }, "dataset": "string", "metrics": "metrics", - "model": "26f1kl_-n-71/4m_-__-35-", + "model": "workspace/model_name", "name": "name", }, page=0, @@ -299,14 +299,14 @@ async def test_method_list_with_all_params(self, async_client: AsyncNeMoPlatform workspace="workspace", aggregate_fields=["nan_count"], filter={ - "benchmark": "26f1kl_-n-71/4m_-__-35-", + "benchmark": "workspace/benchmark-name", "created_at": { "gte": parse_datetime("2019-12-27T18:11:19.117Z"), "lte": parse_datetime("2019-12-27T18:11:19.117Z"), }, "dataset": "string", "metrics": "metrics", - "model": "26f1kl_-n-71/4m_-__-35-", + "model": "workspace/model_name", "name": "name", }, page=0, diff --git a/sdk/python/nemo-platform/tests/api_resources/evaluation/test_benchmark_jobs.py b/sdk/python/nemo-platform/tests/api_resources/evaluation/test_benchmark_jobs.py index fca1a59aaa..df701f9a3f 100644 --- a/sdk/python/nemo-platform/tests/api_resources/evaluation/test_benchmark_jobs.py +++ b/sdk/python/nemo-platform/tests/api_resources/evaluation/test_benchmark_jobs.py @@ -47,7 +47,7 @@ class TestBenchmarkJobs: def test_method_create(self, client: NeMoPlatform) -> None: benchmark_job = client.evaluation.benchmark_jobs.create( workspace="workspace", - spec={"benchmark": "26f1kl_-n-71/4m_-__-35-"}, + spec={"benchmark": "workspace/benchmark-name"}, ) assert_matches_type(BenchmarkEvaluationJob, benchmark_job, path=["response"]) @@ -57,7 +57,7 @@ def test_method_create_with_all_params(self, client: NeMoPlatform) -> None: benchmark_job = client.evaluation.benchmark_jobs.create( workspace="workspace", spec={ - "benchmark": "26f1kl_-n-71/4m_-__-35-", + "benchmark": "workspace/benchmark-name", "params": { "limit_samples": 1, "parallelism": 1, @@ -76,7 +76,7 @@ def test_method_create_with_all_params(self, client: NeMoPlatform) -> None: def test_raw_response_create(self, client: NeMoPlatform) -> None: response = client.evaluation.benchmark_jobs.with_raw_response.create( workspace="workspace", - spec={"benchmark": "26f1kl_-n-71/4m_-__-35-"}, + spec={"benchmark": "workspace/benchmark-name"}, ) assert response.is_closed is True @@ -89,7 +89,7 @@ def test_raw_response_create(self, client: NeMoPlatform) -> None: def test_streaming_response_create(self, client: NeMoPlatform) -> None: with client.evaluation.benchmark_jobs.with_streaming_response.create( workspace="workspace", - spec={"benchmark": "26f1kl_-n-71/4m_-__-35-"}, + spec={"benchmark": "workspace/benchmark-name"}, ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -105,7 +105,7 @@ def test_path_params_create(self, client: NeMoPlatform) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `workspace` but received ''"): client.evaluation.benchmark_jobs.with_raw_response.create( workspace="", - spec={"benchmark": "26f1kl_-n-71/4m_-__-35-"}, + spec={"benchmark": "workspace/benchmark-name"}, ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -457,7 +457,7 @@ class TestAsyncBenchmarkJobs: async def test_method_create(self, async_client: AsyncNeMoPlatform) -> None: benchmark_job = await async_client.evaluation.benchmark_jobs.create( workspace="workspace", - spec={"benchmark": "26f1kl_-n-71/4m_-__-35-"}, + spec={"benchmark": "workspace/benchmark-name"}, ) assert_matches_type(BenchmarkEvaluationJob, benchmark_job, path=["response"]) @@ -467,7 +467,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncNeMoPlatfo benchmark_job = await async_client.evaluation.benchmark_jobs.create( workspace="workspace", spec={ - "benchmark": "26f1kl_-n-71/4m_-__-35-", + "benchmark": "workspace/benchmark-name", "params": { "limit_samples": 1, "parallelism": 1, @@ -486,7 +486,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncNeMoPlatfo async def test_raw_response_create(self, async_client: AsyncNeMoPlatform) -> None: response = await async_client.evaluation.benchmark_jobs.with_raw_response.create( workspace="workspace", - spec={"benchmark": "26f1kl_-n-71/4m_-__-35-"}, + spec={"benchmark": "workspace/benchmark-name"}, ) assert response.is_closed is True @@ -499,7 +499,7 @@ async def test_raw_response_create(self, async_client: AsyncNeMoPlatform) -> Non async def test_streaming_response_create(self, async_client: AsyncNeMoPlatform) -> None: async with async_client.evaluation.benchmark_jobs.with_streaming_response.create( workspace="workspace", - spec={"benchmark": "26f1kl_-n-71/4m_-__-35-"}, + spec={"benchmark": "workspace/benchmark-name"}, ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -515,7 +515,7 @@ async def test_path_params_create(self, async_client: AsyncNeMoPlatform) -> None with pytest.raises(ValueError, match=r"Expected a non-empty value for `workspace` but received ''"): await async_client.evaluation.benchmark_jobs.with_raw_response.create( workspace="", - spec={"benchmark": "26f1kl_-n-71/4m_-__-35-"}, + spec={"benchmark": "workspace/benchmark-name"}, ) @pytest.mark.skip(reason="Mock server tests are disabled") diff --git a/sdk/python/nemo-platform/tests/api_resources/evaluation/test_benchmarks.py b/sdk/python/nemo-platform/tests/api_resources/evaluation/test_benchmarks.py index a9af4247ef..9b02144c1b 100644 --- a/sdk/python/nemo-platform/tests/api_resources/evaluation/test_benchmarks.py +++ b/sdk/python/nemo-platform/tests/api_resources/evaluation/test_benchmarks.py @@ -46,7 +46,7 @@ def test_method_create(self, client: NeMoPlatform) -> None: workspace="workspace", dataset="string", description="description", - metrics=["26f1kl_-n-71/4m_-__-35-"], + metrics=["workspace/metric-name"], name="name", ) assert_matches_type(BenchmarkCreateResponse, benchmark, path=["response"]) @@ -58,7 +58,7 @@ def test_method_create_with_all_params(self, client: NeMoPlatform) -> None: workspace="workspace", dataset="string", description="description", - metrics=["26f1kl_-n-71/4m_-__-35-"], + metrics=["workspace/metric-name"], name="name", extended_response=True, field_mapping={ @@ -83,7 +83,7 @@ def test_raw_response_create(self, client: NeMoPlatform) -> None: workspace="workspace", dataset="string", description="description", - metrics=["26f1kl_-n-71/4m_-__-35-"], + metrics=["workspace/metric-name"], name="name", ) @@ -99,7 +99,7 @@ def test_streaming_response_create(self, client: NeMoPlatform) -> None: workspace="workspace", dataset="string", description="description", - metrics=["26f1kl_-n-71/4m_-__-35-"], + metrics=["workspace/metric-name"], name="name", ) as response: assert not response.is_closed @@ -118,7 +118,7 @@ def test_path_params_create(self, client: NeMoPlatform) -> None: workspace="", dataset="string", description="description", - metrics=["26f1kl_-n-71/4m_-__-35-"], + metrics=["workspace/metric-name"], name="name", ) @@ -317,7 +317,7 @@ async def test_method_create(self, async_client: AsyncNeMoPlatform) -> None: workspace="workspace", dataset="string", description="description", - metrics=["26f1kl_-n-71/4m_-__-35-"], + metrics=["workspace/metric-name"], name="name", ) assert_matches_type(BenchmarkCreateResponse, benchmark, path=["response"]) @@ -329,7 +329,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncNeMoPlatfo workspace="workspace", dataset="string", description="description", - metrics=["26f1kl_-n-71/4m_-__-35-"], + metrics=["workspace/metric-name"], name="name", extended_response=True, field_mapping={ @@ -354,7 +354,7 @@ async def test_raw_response_create(self, async_client: AsyncNeMoPlatform) -> Non workspace="workspace", dataset="string", description="description", - metrics=["26f1kl_-n-71/4m_-__-35-"], + metrics=["workspace/metric-name"], name="name", ) @@ -370,7 +370,7 @@ async def test_streaming_response_create(self, async_client: AsyncNeMoPlatform) workspace="workspace", dataset="string", description="description", - metrics=["26f1kl_-n-71/4m_-__-35-"], + metrics=["workspace/metric-name"], name="name", ) as response: assert not response.is_closed @@ -389,7 +389,7 @@ async def test_path_params_create(self, async_client: AsyncNeMoPlatform) -> None workspace="", dataset="string", description="description", - metrics=["26f1kl_-n-71/4m_-__-35-"], + metrics=["workspace/metric-name"], name="name", ) diff --git a/sdk/python/nemo-platform/tests/api_resources/evaluation/test_metric_job_results.py b/sdk/python/nemo-platform/tests/api_resources/evaluation/test_metric_job_results.py index b3d4a2530e..b0b305f50c 100644 --- a/sdk/python/nemo-platform/tests/api_resources/evaluation/test_metric_job_results.py +++ b/sdk/python/nemo-platform/tests/api_resources/evaluation/test_metric_job_results.py @@ -119,8 +119,8 @@ def test_method_list_with_all_params(self, client: NeMoPlatform) -> None: "lte": parse_datetime("2019-12-27T18:11:19.117Z"), }, "dataset": "string", - "metric": "26f1kl_-n-71/4m_-__-35-", - "model": "26f1kl_-n-71/4m_-__-35-", + "metric": "workspace/metric-name", + "model": "workspace/model_name", "name": "name", }, page=0, @@ -303,8 +303,8 @@ async def test_method_list_with_all_params(self, async_client: AsyncNeMoPlatform "lte": parse_datetime("2019-12-27T18:11:19.117Z"), }, "dataset": "string", - "metric": "26f1kl_-n-71/4m_-__-35-", - "model": "26f1kl_-n-71/4m_-__-35-", + "metric": "workspace/metric-name", + "model": "workspace/model_name", "name": "name", }, page=0, diff --git a/sdk/python/nemo-platform/tests/api_resources/evaluation/test_metric_jobs.py b/sdk/python/nemo-platform/tests/api_resources/evaluation/test_metric_jobs.py index 7d085750af..b0e55c6859 100644 --- a/sdk/python/nemo-platform/tests/api_resources/evaluation/test_metric_jobs.py +++ b/sdk/python/nemo-platform/tests/api_resources/evaluation/test_metric_jobs.py @@ -49,7 +49,7 @@ def test_method_create(self, client: NeMoPlatform) -> None: workspace="workspace", spec={ "dataset": {"rows": [{"foo": "bar"}]}, - "metric": "26f1kl_-n-71/4m_-__-35-", + "metric": "workspace/metric-name", }, ) assert_matches_type(MetricEvaluationJob, metric_job, path=["response"]) @@ -61,7 +61,7 @@ def test_method_create_with_all_params(self, client: NeMoPlatform) -> None: workspace="workspace", spec={ "dataset": {"rows": [{"foo": "bar"}]}, - "metric": "26f1kl_-n-71/4m_-__-35-", + "metric": "workspace/metric-name", "field_mapping": { "context": "context", "custom": {"foo": "J!"}, @@ -94,7 +94,7 @@ def test_raw_response_create(self, client: NeMoPlatform) -> None: workspace="workspace", spec={ "dataset": {"rows": [{"foo": "bar"}]}, - "metric": "26f1kl_-n-71/4m_-__-35-", + "metric": "workspace/metric-name", }, ) @@ -110,7 +110,7 @@ def test_streaming_response_create(self, client: NeMoPlatform) -> None: workspace="workspace", spec={ "dataset": {"rows": [{"foo": "bar"}]}, - "metric": "26f1kl_-n-71/4m_-__-35-", + "metric": "workspace/metric-name", }, ) as response: assert not response.is_closed @@ -129,7 +129,7 @@ def test_path_params_create(self, client: NeMoPlatform) -> None: workspace="", spec={ "dataset": {"rows": [{"foo": "bar"}]}, - "metric": "26f1kl_-n-71/4m_-__-35-", + "metric": "workspace/metric-name", }, ) @@ -484,7 +484,7 @@ async def test_method_create(self, async_client: AsyncNeMoPlatform) -> None: workspace="workspace", spec={ "dataset": {"rows": [{"foo": "bar"}]}, - "metric": "26f1kl_-n-71/4m_-__-35-", + "metric": "workspace/metric-name", }, ) assert_matches_type(MetricEvaluationJob, metric_job, path=["response"]) @@ -496,7 +496,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncNeMoPlatfo workspace="workspace", spec={ "dataset": {"rows": [{"foo": "bar"}]}, - "metric": "26f1kl_-n-71/4m_-__-35-", + "metric": "workspace/metric-name", "field_mapping": { "context": "context", "custom": {"foo": "J!"}, @@ -529,7 +529,7 @@ async def test_raw_response_create(self, async_client: AsyncNeMoPlatform) -> Non workspace="workspace", spec={ "dataset": {"rows": [{"foo": "bar"}]}, - "metric": "26f1kl_-n-71/4m_-__-35-", + "metric": "workspace/metric-name", }, ) @@ -545,7 +545,7 @@ async def test_streaming_response_create(self, async_client: AsyncNeMoPlatform) workspace="workspace", spec={ "dataset": {"rows": [{"foo": "bar"}]}, - "metric": "26f1kl_-n-71/4m_-__-35-", + "metric": "workspace/metric-name", }, ) as response: assert not response.is_closed @@ -564,7 +564,7 @@ async def test_path_params_create(self, async_client: AsyncNeMoPlatform) -> None workspace="", spec={ "dataset": {"rows": [{"foo": "bar"}]}, - "metric": "26f1kl_-n-71/4m_-__-35-", + "metric": "workspace/metric-name", }, ) diff --git a/sdk/python/nemo-platform/tests/api_resources/evaluation/test_metrics.py b/sdk/python/nemo-platform/tests/api_resources/evaluation/test_metrics.py index cac2df447f..54db2784c4 100644 --- a/sdk/python/nemo-platform/tests/api_resources/evaluation/test_metrics.py +++ b/sdk/python/nemo-platform/tests/api_resources/evaluation/test_metrics.py @@ -46,10 +46,7 @@ def test_method_create_overload_1(self, client: NeMoPlatform) -> None: metric = client.evaluation.metrics.create( name="name", workspace="workspace", - model={ - "name": "name", - "url": "url", - }, + model="workspace/model_name", scores=[ { "name": "name", @@ -74,12 +71,7 @@ def test_method_create_with_all_params_overload_1(self, client: NeMoPlatform) -> metric = client.evaluation.metrics.create( name="name", workspace="workspace", - model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + model="workspace/model_name", scores=[ { "name": "name", @@ -135,10 +127,7 @@ def test_raw_response_create_overload_1(self, client: NeMoPlatform) -> None: response = client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - model={ - "name": "name", - "url": "url", - }, + model="workspace/model_name", scores=[ { "name": "name", @@ -167,10 +156,7 @@ def test_streaming_response_create_overload_1(self, client: NeMoPlatform) -> Non with client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - model={ - "name": "name", - "url": "url", - }, + model="workspace/model_name", scores=[ { "name": "name", @@ -202,10 +188,7 @@ def test_path_params_create_overload_1(self, client: NeMoPlatform) -> None: client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - model={ - "name": "name", - "url": "url", - }, + model="workspace/model_name", scores=[ { "name": "name", @@ -227,10 +210,7 @@ def test_path_params_create_overload_1(self, client: NeMoPlatform) -> None: client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - model={ - "name": "name", - "url": "url", - }, + model="workspace/model_name", scores=[ { "name": "name", @@ -254,10 +234,7 @@ def test_method_create_overload_2(self, client: NeMoPlatform) -> None: metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -267,12 +244,7 @@ def test_method_create_with_all_params_overload_2(self, client: NeMoPlatform) -> metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -296,10 +268,7 @@ def test_raw_response_create_overload_2(self, client: NeMoPlatform) -> None: response = client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -313,10 +282,7 @@ def test_streaming_response_create_overload_2(self, client: NeMoPlatform) -> Non with client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -333,20 +299,14 @@ def test_path_params_create_overload_2(self, client: NeMoPlatform) -> None: client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -355,10 +315,7 @@ def test_method_create_overload_3(self, client: NeMoPlatform) -> None: metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -368,12 +325,7 @@ def test_method_create_with_all_params_overload_3(self, client: NeMoPlatform) -> metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -397,10 +349,7 @@ def test_raw_response_create_overload_3(self, client: NeMoPlatform) -> None: response = client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -414,10 +363,7 @@ def test_streaming_response_create_overload_3(self, client: NeMoPlatform) -> Non with client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -434,20 +380,14 @@ def test_path_params_create_overload_3(self, client: NeMoPlatform) -> None: client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -456,10 +396,7 @@ def test_method_create_overload_4(self, client: NeMoPlatform) -> None: metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -469,12 +406,7 @@ def test_method_create_with_all_params_overload_4(self, client: NeMoPlatform) -> metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -497,10 +429,7 @@ def test_raw_response_create_overload_4(self, client: NeMoPlatform) -> None: response = client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -514,10 +443,7 @@ def test_streaming_response_create_overload_4(self, client: NeMoPlatform) -> Non with client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -534,20 +460,14 @@ def test_path_params_create_overload_4(self, client: NeMoPlatform) -> None: client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -556,10 +476,7 @@ def test_method_create_overload_5(self, client: NeMoPlatform) -> None: metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -569,12 +486,7 @@ def test_method_create_with_all_params_overload_5(self, client: NeMoPlatform) -> metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -597,10 +509,7 @@ def test_raw_response_create_overload_5(self, client: NeMoPlatform) -> None: response = client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -614,10 +523,7 @@ def test_streaming_response_create_overload_5(self, client: NeMoPlatform) -> Non with client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -634,20 +540,14 @@ def test_path_params_create_overload_5(self, client: NeMoPlatform) -> None: client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -656,10 +556,7 @@ def test_method_create_overload_6(self, client: NeMoPlatform) -> None: metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -669,12 +566,7 @@ def test_method_create_with_all_params_overload_6(self, client: NeMoPlatform) -> metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -697,10 +589,7 @@ def test_raw_response_create_overload_6(self, client: NeMoPlatform) -> None: response = client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -714,10 +603,7 @@ def test_streaming_response_create_overload_6(self, client: NeMoPlatform) -> Non with client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -734,20 +620,14 @@ def test_path_params_create_overload_6(self, client: NeMoPlatform) -> None: client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -756,10 +636,7 @@ def test_method_create_overload_7(self, client: NeMoPlatform) -> None: metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -769,12 +646,7 @@ def test_method_create_with_all_params_overload_7(self, client: NeMoPlatform) -> metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -797,10 +669,7 @@ def test_raw_response_create_overload_7(self, client: NeMoPlatform) -> None: response = client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -814,10 +683,7 @@ def test_streaming_response_create_overload_7(self, client: NeMoPlatform) -> Non with client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -834,20 +700,14 @@ def test_path_params_create_overload_7(self, client: NeMoPlatform) -> None: client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -856,10 +716,7 @@ def test_method_create_overload_8(self, client: NeMoPlatform) -> None: metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -869,12 +726,7 @@ def test_method_create_with_all_params_overload_8(self, client: NeMoPlatform) -> metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -897,10 +749,7 @@ def test_raw_response_create_overload_8(self, client: NeMoPlatform) -> None: response = client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -914,10 +763,7 @@ def test_streaming_response_create_overload_8(self, client: NeMoPlatform) -> Non with client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -934,20 +780,14 @@ def test_path_params_create_overload_8(self, client: NeMoPlatform) -> None: client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -956,10 +796,7 @@ def test_method_create_overload_9(self, client: NeMoPlatform) -> None: metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -969,12 +806,7 @@ def test_method_create_with_all_params_overload_9(self, client: NeMoPlatform) -> metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -997,10 +829,7 @@ def test_raw_response_create_overload_9(self, client: NeMoPlatform) -> None: response = client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -1014,10 +843,7 @@ def test_streaming_response_create_overload_9(self, client: NeMoPlatform) -> Non with client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -1034,20 +860,14 @@ def test_path_params_create_overload_9(self, client: NeMoPlatform) -> None: client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -1056,14 +876,8 @@ def test_method_create_overload_10(self, client: NeMoPlatform) -> None: metric = client.evaluation.metrics.create( name="name", workspace="workspace", - embeddings_model={ - "name": "name", - "url": "url", - }, - judge_model={ - "name": "name", - "url": "url", - }, + embeddings_model="workspace/model_name", + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -1073,18 +887,8 @@ def test_method_create_with_all_params_overload_10(self, client: NeMoPlatform) - metric = client.evaluation.metrics.create( name="name", workspace="workspace", - embeddings_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + embeddings_model="workspace/model_name", + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -1108,14 +912,8 @@ def test_raw_response_create_overload_10(self, client: NeMoPlatform) -> None: response = client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - embeddings_model={ - "name": "name", - "url": "url", - }, - judge_model={ - "name": "name", - "url": "url", - }, + embeddings_model="workspace/model_name", + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -1129,14 +927,8 @@ def test_streaming_response_create_overload_10(self, client: NeMoPlatform) -> No with client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - embeddings_model={ - "name": "name", - "url": "url", - }, - judge_model={ - "name": "name", - "url": "url", - }, + embeddings_model="workspace/model_name", + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -1153,28 +945,16 @@ def test_path_params_create_overload_10(self, client: NeMoPlatform) -> None: client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - embeddings_model={ - "name": "name", - "url": "url", - }, - judge_model={ - "name": "name", - "url": "url", - }, + embeddings_model="workspace/model_name", + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - embeddings_model={ - "name": "name", - "url": "url", - }, - judge_model={ - "name": "name", - "url": "url", - }, + embeddings_model="workspace/model_name", + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -1183,10 +963,7 @@ def test_method_create_overload_11(self, client: NeMoPlatform) -> None: metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -1196,12 +973,7 @@ def test_method_create_with_all_params_overload_11(self, client: NeMoPlatform) - metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -1224,10 +996,7 @@ def test_raw_response_create_overload_11(self, client: NeMoPlatform) -> None: response = client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -1241,10 +1010,7 @@ def test_streaming_response_create_overload_11(self, client: NeMoPlatform) -> No with client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -1261,20 +1027,14 @@ def test_path_params_create_overload_11(self, client: NeMoPlatform) -> None: client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -1283,10 +1043,7 @@ def test_method_create_overload_12(self, client: NeMoPlatform) -> None: metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -1296,12 +1053,7 @@ def test_method_create_with_all_params_overload_12(self, client: NeMoPlatform) - metric = client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -1324,10 +1076,7 @@ def test_raw_response_create_overload_12(self, client: NeMoPlatform) -> None: response = client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -1341,10 +1090,7 @@ def test_streaming_response_create_overload_12(self, client: NeMoPlatform) -> No with client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -1361,20 +1107,14 @@ def test_path_params_create_overload_12(self, client: NeMoPlatform) -> None: client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -1775,7 +1515,7 @@ def test_method_create_with_all_params_overload_18(self, client: NeMoPlatform) - } ], url="url", - api_key_secret="api_key_secret", + api_key_secret="my-secret", description="description", labels={"foo": "string"}, max_retries=0, @@ -1859,7 +1599,7 @@ def test_method_create_with_all_params_overload_19(self, client: NeMoPlatform) - workspace="workspace", evaluator_name="evaluator_name", url="url", - api_key_secret="api_key_secret", + api_key_secret="my-secret", description="description", labels={"foo": "string"}, max_retries=0, @@ -2323,7 +2063,7 @@ def test_method_evaluate(self, client: NeMoPlatform) -> None: metric = client.evaluation.metrics.evaluate( workspace="workspace", dataset={"rows": [{"foo": "bar"}]}, - metric="26f1kl_-n-71/4m_-__-35-", + metric="workspace/metric-name", ) assert_matches_type(MetricEvaluationResponse, metric, path=["response"]) @@ -2333,7 +2073,7 @@ def test_method_evaluate_with_all_params(self, client: NeMoPlatform) -> None: metric = client.evaluation.metrics.evaluate( workspace="workspace", dataset={"rows": [{"foo": "bar"}]}, - metric="26f1kl_-n-71/4m_-__-35-", + metric="workspace/metric-name", aggregate_fields=["nan_count"], ) assert_matches_type(MetricEvaluationResponse, metric, path=["response"]) @@ -2344,7 +2084,7 @@ def test_raw_response_evaluate(self, client: NeMoPlatform) -> None: response = client.evaluation.metrics.with_raw_response.evaluate( workspace="workspace", dataset={"rows": [{"foo": "bar"}]}, - metric="26f1kl_-n-71/4m_-__-35-", + metric="workspace/metric-name", ) assert response.is_closed is True @@ -2358,7 +2098,7 @@ def test_streaming_response_evaluate(self, client: NeMoPlatform) -> None: with client.evaluation.metrics.with_streaming_response.evaluate( workspace="workspace", dataset={"rows": [{"foo": "bar"}]}, - metric="26f1kl_-n-71/4m_-__-35-", + metric="workspace/metric-name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -2375,7 +2115,7 @@ def test_path_params_evaluate(self, client: NeMoPlatform) -> None: client.evaluation.metrics.with_raw_response.evaluate( workspace="", dataset={"rows": [{"foo": "bar"}]}, - metric="26f1kl_-n-71/4m_-__-35-", + metric="workspace/metric-name", ) @@ -2390,10 +2130,7 @@ async def test_method_create_overload_1(self, async_client: AsyncNeMoPlatform) - metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - model={ - "name": "name", - "url": "url", - }, + model="workspace/model_name", scores=[ { "name": "name", @@ -2418,12 +2155,7 @@ async def test_method_create_with_all_params_overload_1(self, async_client: Asyn metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + model="workspace/model_name", scores=[ { "name": "name", @@ -2479,10 +2211,7 @@ async def test_raw_response_create_overload_1(self, async_client: AsyncNeMoPlatf response = await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - model={ - "name": "name", - "url": "url", - }, + model="workspace/model_name", scores=[ { "name": "name", @@ -2511,10 +2240,7 @@ async def test_streaming_response_create_overload_1(self, async_client: AsyncNeM async with async_client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - model={ - "name": "name", - "url": "url", - }, + model="workspace/model_name", scores=[ { "name": "name", @@ -2546,10 +2272,7 @@ async def test_path_params_create_overload_1(self, async_client: AsyncNeMoPlatfo await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - model={ - "name": "name", - "url": "url", - }, + model="workspace/model_name", scores=[ { "name": "name", @@ -2571,10 +2294,7 @@ async def test_path_params_create_overload_1(self, async_client: AsyncNeMoPlatfo await async_client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - model={ - "name": "name", - "url": "url", - }, + model="workspace/model_name", scores=[ { "name": "name", @@ -2598,10 +2318,7 @@ async def test_method_create_overload_2(self, async_client: AsyncNeMoPlatform) - metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -2611,12 +2328,7 @@ async def test_method_create_with_all_params_overload_2(self, async_client: Asyn metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -2640,10 +2352,7 @@ async def test_raw_response_create_overload_2(self, async_client: AsyncNeMoPlatf response = await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -2657,10 +2366,7 @@ async def test_streaming_response_create_overload_2(self, async_client: AsyncNeM async with async_client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -2677,20 +2383,14 @@ async def test_path_params_create_overload_2(self, async_client: AsyncNeMoPlatfo await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): await async_client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -2699,10 +2399,7 @@ async def test_method_create_overload_3(self, async_client: AsyncNeMoPlatform) - metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -2712,12 +2409,7 @@ async def test_method_create_with_all_params_overload_3(self, async_client: Asyn metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -2741,10 +2433,7 @@ async def test_raw_response_create_overload_3(self, async_client: AsyncNeMoPlatf response = await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -2758,10 +2447,7 @@ async def test_streaming_response_create_overload_3(self, async_client: AsyncNeM async with async_client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -2778,20 +2464,14 @@ async def test_path_params_create_overload_3(self, async_client: AsyncNeMoPlatfo await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): await async_client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -2800,10 +2480,7 @@ async def test_method_create_overload_4(self, async_client: AsyncNeMoPlatform) - metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -2813,12 +2490,7 @@ async def test_method_create_with_all_params_overload_4(self, async_client: Asyn metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -2841,10 +2513,7 @@ async def test_raw_response_create_overload_4(self, async_client: AsyncNeMoPlatf response = await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -2858,10 +2527,7 @@ async def test_streaming_response_create_overload_4(self, async_client: AsyncNeM async with async_client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -2878,20 +2544,14 @@ async def test_path_params_create_overload_4(self, async_client: AsyncNeMoPlatfo await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): await async_client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -2900,10 +2560,7 @@ async def test_method_create_overload_5(self, async_client: AsyncNeMoPlatform) - metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -2913,12 +2570,7 @@ async def test_method_create_with_all_params_overload_5(self, async_client: Asyn metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -2941,10 +2593,7 @@ async def test_raw_response_create_overload_5(self, async_client: AsyncNeMoPlatf response = await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -2958,10 +2607,7 @@ async def test_streaming_response_create_overload_5(self, async_client: AsyncNeM async with async_client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -2978,20 +2624,14 @@ async def test_path_params_create_overload_5(self, async_client: AsyncNeMoPlatfo await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): await async_client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -3000,10 +2640,7 @@ async def test_method_create_overload_6(self, async_client: AsyncNeMoPlatform) - metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -3013,12 +2650,7 @@ async def test_method_create_with_all_params_overload_6(self, async_client: Asyn metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -3041,10 +2673,7 @@ async def test_raw_response_create_overload_6(self, async_client: AsyncNeMoPlatf response = await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -3058,10 +2687,7 @@ async def test_streaming_response_create_overload_6(self, async_client: AsyncNeM async with async_client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -3078,20 +2704,14 @@ async def test_path_params_create_overload_6(self, async_client: AsyncNeMoPlatfo await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): await async_client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -3100,10 +2720,7 @@ async def test_method_create_overload_7(self, async_client: AsyncNeMoPlatform) - metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -3113,12 +2730,7 @@ async def test_method_create_with_all_params_overload_7(self, async_client: Asyn metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -3141,10 +2753,7 @@ async def test_raw_response_create_overload_7(self, async_client: AsyncNeMoPlatf response = await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -3158,10 +2767,7 @@ async def test_streaming_response_create_overload_7(self, async_client: AsyncNeM async with async_client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -3178,20 +2784,14 @@ async def test_path_params_create_overload_7(self, async_client: AsyncNeMoPlatfo await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): await async_client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -3200,10 +2800,7 @@ async def test_method_create_overload_8(self, async_client: AsyncNeMoPlatform) - metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -3213,12 +2810,7 @@ async def test_method_create_with_all_params_overload_8(self, async_client: Asyn metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -3241,10 +2833,7 @@ async def test_raw_response_create_overload_8(self, async_client: AsyncNeMoPlatf response = await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -3258,10 +2847,7 @@ async def test_streaming_response_create_overload_8(self, async_client: AsyncNeM async with async_client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -3278,20 +2864,14 @@ async def test_path_params_create_overload_8(self, async_client: AsyncNeMoPlatfo await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): await async_client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -3300,10 +2880,7 @@ async def test_method_create_overload_9(self, async_client: AsyncNeMoPlatform) - metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -3313,12 +2890,7 @@ async def test_method_create_with_all_params_overload_9(self, async_client: Asyn metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -3341,10 +2913,7 @@ async def test_raw_response_create_overload_9(self, async_client: AsyncNeMoPlatf response = await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -3358,10 +2927,7 @@ async def test_streaming_response_create_overload_9(self, async_client: AsyncNeM async with async_client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -3378,20 +2944,14 @@ async def test_path_params_create_overload_9(self, async_client: AsyncNeMoPlatfo await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): await async_client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -3400,14 +2960,8 @@ async def test_method_create_overload_10(self, async_client: AsyncNeMoPlatform) metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - embeddings_model={ - "name": "name", - "url": "url", - }, - judge_model={ - "name": "name", - "url": "url", - }, + embeddings_model="workspace/model_name", + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -3417,18 +2971,8 @@ async def test_method_create_with_all_params_overload_10(self, async_client: Asy metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - embeddings_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + embeddings_model="workspace/model_name", + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -3452,14 +2996,8 @@ async def test_raw_response_create_overload_10(self, async_client: AsyncNeMoPlat response = await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - embeddings_model={ - "name": "name", - "url": "url", - }, - judge_model={ - "name": "name", - "url": "url", - }, + embeddings_model="workspace/model_name", + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -3473,14 +3011,8 @@ async def test_streaming_response_create_overload_10(self, async_client: AsyncNe async with async_client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - embeddings_model={ - "name": "name", - "url": "url", - }, - judge_model={ - "name": "name", - "url": "url", - }, + embeddings_model="workspace/model_name", + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -3497,28 +3029,16 @@ async def test_path_params_create_overload_10(self, async_client: AsyncNeMoPlatf await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - embeddings_model={ - "name": "name", - "url": "url", - }, - judge_model={ - "name": "name", - "url": "url", - }, + embeddings_model="workspace/model_name", + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): await async_client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - embeddings_model={ - "name": "name", - "url": "url", - }, - judge_model={ - "name": "name", - "url": "url", - }, + embeddings_model="workspace/model_name", + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -3527,10 +3047,7 @@ async def test_method_create_overload_11(self, async_client: AsyncNeMoPlatform) metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -3540,12 +3057,7 @@ async def test_method_create_with_all_params_overload_11(self, async_client: Asy metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -3568,10 +3080,7 @@ async def test_raw_response_create_overload_11(self, async_client: AsyncNeMoPlat response = await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -3585,10 +3094,7 @@ async def test_streaming_response_create_overload_11(self, async_client: AsyncNe async with async_client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -3605,20 +3111,14 @@ async def test_path_params_create_overload_11(self, async_client: AsyncNeMoPlatf await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): await async_client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -3627,10 +3127,7 @@ async def test_method_create_overload_12(self, async_client: AsyncNeMoPlatform) metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert_matches_type(MetricCreateResponse, metric, path=["response"]) @@ -3640,12 +3137,7 @@ async def test_method_create_with_all_params_overload_12(self, async_client: Asy metric = await async_client.evaluation.metrics.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - "api_key_secret": "api_key_secret", - "format": "nim", - }, + judge_model="workspace/model_name", description="description", ignore_request_failure=True, inference={ @@ -3668,10 +3160,7 @@ async def test_raw_response_create_overload_12(self, async_client: AsyncNeMoPlat response = await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) assert response.is_closed is True @@ -3685,10 +3174,7 @@ async def test_streaming_response_create_overload_12(self, async_client: AsyncNe async with async_client.evaluation.metrics.with_streaming_response.create( name="name", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -3705,20 +3191,14 @@ async def test_path_params_create_overload_12(self, async_client: AsyncNeMoPlatf await async_client.evaluation.metrics.with_raw_response.create( name="name", workspace="", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `name` but received ''"): await async_client.evaluation.metrics.with_raw_response.create( name="", workspace="workspace", - judge_model={ - "name": "name", - "url": "url", - }, + judge_model="workspace/model_name", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -4119,7 +3599,7 @@ async def test_method_create_with_all_params_overload_18(self, async_client: Asy } ], url="url", - api_key_secret="api_key_secret", + api_key_secret="my-secret", description="description", labels={"foo": "string"}, max_retries=0, @@ -4203,7 +3683,7 @@ async def test_method_create_with_all_params_overload_19(self, async_client: Asy workspace="workspace", evaluator_name="evaluator_name", url="url", - api_key_secret="api_key_secret", + api_key_secret="my-secret", description="description", labels={"foo": "string"}, max_retries=0, @@ -4667,7 +4147,7 @@ async def test_method_evaluate(self, async_client: AsyncNeMoPlatform) -> None: metric = await async_client.evaluation.metrics.evaluate( workspace="workspace", dataset={"rows": [{"foo": "bar"}]}, - metric="26f1kl_-n-71/4m_-__-35-", + metric="workspace/metric-name", ) assert_matches_type(MetricEvaluationResponse, metric, path=["response"]) @@ -4677,7 +4157,7 @@ async def test_method_evaluate_with_all_params(self, async_client: AsyncNeMoPlat metric = await async_client.evaluation.metrics.evaluate( workspace="workspace", dataset={"rows": [{"foo": "bar"}]}, - metric="26f1kl_-n-71/4m_-__-35-", + metric="workspace/metric-name", aggregate_fields=["nan_count"], ) assert_matches_type(MetricEvaluationResponse, metric, path=["response"]) @@ -4688,7 +4168,7 @@ async def test_raw_response_evaluate(self, async_client: AsyncNeMoPlatform) -> N response = await async_client.evaluation.metrics.with_raw_response.evaluate( workspace="workspace", dataset={"rows": [{"foo": "bar"}]}, - metric="26f1kl_-n-71/4m_-__-35-", + metric="workspace/metric-name", ) assert response.is_closed is True @@ -4702,7 +4182,7 @@ async def test_streaming_response_evaluate(self, async_client: AsyncNeMoPlatform async with async_client.evaluation.metrics.with_streaming_response.evaluate( workspace="workspace", dataset={"rows": [{"foo": "bar"}]}, - metric="26f1kl_-n-71/4m_-__-35-", + metric="workspace/metric-name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -4719,5 +4199,5 @@ async def test_path_params_evaluate(self, async_client: AsyncNeMoPlatform) -> No await async_client.evaluation.metrics.with_raw_response.evaluate( workspace="", dataset={"rows": [{"foo": "bar"}]}, - metric="26f1kl_-n-71/4m_-__-35-", + metric="workspace/metric-name", ) diff --git a/sdk/python/nemo-platform/tests/vendored/nemo_platform_ext/cli/test_app.py b/sdk/python/nemo-platform/tests/vendored/nemo_platform_ext/cli/test_app.py index 1d065c4d2e..69f59cb4c3 100644 --- a/sdk/python/nemo-platform/tests/vendored/nemo_platform_ext/cli/test_app.py +++ b/sdk/python/nemo-platform/tests/vendored/nemo_platform_ext/cli/test_app.py @@ -51,7 +51,8 @@ def test_help_includes_getting_started(): assert "Getting started:" in result.stdout assert "nemo docs --list" in result.stdout assert "nemo services run --help" in result.stdout - assert "Set up NeMo Platform: start services, configure a provider, install skills." in result.stdout + # Help panel truncates long command descriptions; match the visible prefix. + assert "Set up NeMo Platform: start services" in result.stdout assert "--help, -h" in result.stdout assert "nemo auth login --base-url" not in result.stdout assert "nemo quickstart configure" not in result.stdout diff --git a/sdk/stainless.yaml b/sdk/stainless.yaml index 2500eed76e..6da6fad7c6 100644 --- a/sdk/stainless.yaml +++ b/sdk/stainless.yaml @@ -280,6 +280,7 @@ resources: response_groundedness_metric_param: ResponseGroundednessMetricInput response_relevancy_metric: ResponseRelevancyMetric response_relevancy_metric_param: ResponseRelevancyMetricInput + fileset: Fileset rouge_metric: ROUGEMetric rouge_metric_param: ROUGEMetricInput row_score: RowScore @@ -345,15 +346,15 @@ resources: benchmark_evaluation_result: BenchmarkEvaluationResult benchmark_metric_result: BenchmarkMetricResult methods: - download: get + download: get /apis/evaluation/v2/workspaces/{workspace}/benchmark-jobs/{job}/results/aggregate-scores/download row_scores: methods: - download: get + download: get /apis/evaluation/v2/workspaces/{workspace}/benchmark-jobs/{job}/results/row-scores/download artifacts: methods: - download: get + download: get /apis/evaluation/v2/workspaces/{workspace}/benchmark-jobs/{job}/results/artifacts/download benchmark_job_results: models: @@ -410,7 +411,6 @@ resources: metric_jobs: models: built_in_dataset: BuiltInDataset - fileset: FilesetInput metric_evaluation_job: MetricEvaluationJob metric_evaluation_job_request: MetricEvaluationJobRequest metric_evaluation_jobs_list_filter: MetricEvaluationJobsListFilter @@ -420,7 +420,7 @@ resources: metric_online_agent_job: MetricOnlineAgentJob metric_online_job: MetricOnlineJob metric_retriever_job: MetricRetrieverJob - retriever_pipeline: RetrieverPipelineInput + retriever_pipeline: RetrieverPipeline system_metric_param: SystemMetricInput methods: retrieve: get /apis/evaluation/v2/workspaces/{workspace}/metric-jobs/{name} @@ -441,7 +441,7 @@ resources: subresources: aggregate_scores: methods: - download: get + download: get /apis/evaluation/v2/workspaces/{workspace}/metric-jobs/{job}/results/aggregate-scores/download row_scores: methods: @@ -484,7 +484,6 @@ resources: filesets: models: fileset_filter: FilesetFilter - fileset_metadata_param: FilesetMetadataInput methods: create: post /apis/files/v2/workspaces/{workspace}/filesets list: get /apis/files/v2/workspaces/{workspace}/filesets @@ -559,19 +558,19 @@ resources: pangea_rail_config: PangeaRailConfig pangea_rail_options: PangeaRailOptions patronus_evaluate_api_params: PatronusEvaluateApiParams - patronus_evaluate_config_param: PatronusEvaluateConfigInput patronus_evaluation_success_strategy: PatronusEvaluationSuccessStrategy - patronus_rail_config_param: PatronusRailConfigInput private_ai_detection: PrivateAIDetection private_ai_detection_options: PrivateAIDetectionOptions rail_status: RailStatus - rails_config_data_param: RailsConfigDataInput - rails_config_param: RailsConfigInput - rails_param: RailsInput reasoning_config: ReasoningConfig regex_detection: RegexDetection regex_detection_options: RegexDetectionOptions retrieval_rails: RetrievalRails + patronus_evaluate_config: PatronusEvaluateConfig + patronus_rail_config: PatronusRailConfig + rails: Rails + rails_config: RailsConfig + rails_config_data: RailsConfigData sensitive_data_detection: SensitiveDataDetection sensitive_data_detection_options: SensitiveDataDetectionOptions single_call_config: SingleCallConfig @@ -591,11 +590,6 @@ resources: guardrail_config_param: GuardrailConfigInput guardrail_config_update: GuardrailConfigUpdate guardrail_configs_page: GuardrailConfigsPage - patronus_evaluate_config: PatronusEvaluateConfigOutput - patronus_rail_config: PatronusRailConfigOutput - rails: RailsOutput - rails_config: RailsConfigOutput - rails_config_data: RailsConfigDataOutput methods: list: get /apis/guardrails/v2/workspaces/{workspace}/configs create: post /apis/guardrails/v2/workspaces/{workspace}/configs @@ -731,19 +725,13 @@ resources: compute_resource_spec: ComputeResourceSpec compute_resources: ComputeResources container_spec: ContainerSpec - cpu_execution_provider: CPUExecutionProviderOutput - cpu_execution_provider_param: CPUExecutionProviderInput create_platform_job_request: CreatePlatformJobRequest - distributed_gpu_execution_provider: DistributedGPUExecutionProviderOutput - distributed_gpu_execution_provider_param: DistributedGPUExecutionProviderInput docker_job_execution_profile: DockerJobExecutionProfile docker_job_execution_profile_config: DockerJobExecutionProfileConfig docker_job_network_config: DockerJobNetworkConfig docker_job_storage_config: DockerJobStorageConfig docker_volume_mount: DockerVolumeMount e2e_job_execution_profile: E2EJobExecutionProfile - gpu_execution_provider: GPUExecutionProviderOutput - gpu_execution_provider_param: GPUExecutionProviderInput image_pull_secret: ImagePullSecret job_execution_profile_config: JobExecutionProfileConfig kubernetes_empty_dir_volume: KubernetesEmptyDirVolume @@ -759,11 +747,12 @@ resources: platform_job_responses_page: PlatformJobResponsesPage platform_job_secret_environment_variable_ref: PlatformJobSecretEnvironmentVariableRef platform_job_sort_field: PlatformJobSortField - platform_job_spec: PlatformJobSpecOutput - platform_job_spec_param: PlatformJobSpecInput - platform_job_step_spec: PlatformJobStepSpecOutput - platform_job_step_spec_param: PlatformJobStepSpecInput platform_jobs_list_filter: PlatformJobsListFilter + cpu_execution_provider: CPUExecutionProvider + distributed_gpu_execution_provider: DistributedGPUExecutionProvider + gpu_execution_provider: GPUExecutionProvider + platform_job_spec: PlatformJobSpec + platform_job_step_spec: PlatformJobStepSpec step_lifecycle: StepLifecycle subprocess_execution_provider: SubprocessExecutionProvider subprocess_job_execution_profile: SubprocessJobExecutionProfile @@ -930,10 +919,10 @@ resources: auth_context: AuthContext tool_call_config: ToolCallConfig model_metadata_content: ModelMetadataContent - fileset_metadata: FilesetMetadataOutput tool_calling_metadata_content: ToolCallingMetadataContent backend_format: BackendFormat finetuning_type: FinetuningType + fileset_metadata: FilesetMetadata iam: standalone_api: true subresources: diff --git a/services/automodel/README.md b/services/automodel/README.md index da10845815..0b6f872234 100644 --- a/services/automodel/README.md +++ b/services/automodel/README.md @@ -1,3 +1,5 @@ # nmp-automodel Compiler and task entrypoints for NeMo Automodel training jobs on the platform. **No HTTP server** — consumed by `nemo-automodel-plugin` and Jobs task images (`nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-tasks`, `.../nmp-automodel-training`). + +Runtime exceptions from `nemo_automodel` are mapped to user-facing error types via `src/nmp/automodel/tasks/training/errors/error_rules.yaml`. See [docs/automodel_errors.md](docs/automodel_errors.md) for the full catalog and validation status of each Automodel error. diff --git a/services/automodel/docker/Dockerfile.mamba-wheel b/services/automodel/docker/Dockerfile.mamba-wheel index 3574283585..c4b62e79d5 100644 --- a/services/automodel/docker/Dockerfile.mamba-wheel +++ b/services/automodel/docker/Dockerfile.mamba-wheel @@ -9,7 +9,7 @@ # Both only ship source distributions on PyPI and require nvcc to compile. # The two builds are independent stages so BuildKit runs them in parallel. # Each image stores its wheel at /wheels/*.whl. -# Build via Platform bake group: docker buildx bake -f docker-bake.automodel.hcl nmp-automodel-gpu-wheels +# Build via Platform bake group: docker buildx bake -f docker-bake.hcl nmp-automodel-gpu-wheels # # Build args: # CAUSAL_CONV1D_VERSION - git tag to build (default: v1.5.3) diff --git a/services/automodel/docker/Dockerfile.nmp-automodel-base b/services/automodel/docker/Dockerfile.nmp-automodel-base index 512b488988..5103bf6361 100644 --- a/services/automodel/docker/Dockerfile.nmp-automodel-base +++ b/services/automodel/docker/Dockerfile.nmp-automodel-base @@ -2,7 +2,7 @@ # nmp-automodel base - PyTorch NGC image + Automodel + CUDA extension wheels. # # Mirrors nmp/docker/Dockerfile.nmp-customizer customizer-automodel-base-builder. -# Publish target: nmp-automodel-base-builder (tags as nmp-automodel-base). +# Publish target: nmp-automodel-base (slim image; builder is build-only). ARG CAUSAL_CONV1D_WHEEL_IMAGE=local ARG MAMBA_SSM_WHEEL_IMAGE=local @@ -33,15 +33,30 @@ ENV PATH="/opt/venv/bin:/root/.local/bin:$PATH" RUN uv venv ${UV_PROJECT_ENVIRONMENT} --system-site-packages COPY --from=automodel-clone /opt/Automodel /opt/Automodel -COPY services/customizer/src/cherry-picks /opt/cherry-picks +COPY services/automodel/docker/cherry-picks /opt/cherry-picks RUN cd /opt/Automodel && patch -p1 < /opt/cherry-picks/e6d2930a.diff RUN cd /opt/Automodel && \ bash docker/common/update_pyproject_pytorch.sh /opt/Automodel +# Sync all Automodel extras except cuda — causal-conv1d, mamba-ssm, nv-grouped-gemm, +# and bitsandbytes are installed from prebuilt wheels / source in the steps below. RUN --mount=type=cache,target=/root/.cache/uv \ cd /opt/Automodel && \ - UV_HTTP_TIMEOUT=120 uv sync --locked --extra all --all-groups + UV_HTTP_TIMEOUT=120 uv sync --locked \ + --extra extra \ + --extra vlm \ + --extra delta-databricks \ + --all-groups + +# cuda extra deps not covered above (onnxscript) plus prebuilt / compiled extensions. +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install "onnxscript>=0.5.6" + + +# RUN --mount=type=cache,target=/root/.cache/uv \ +# cd /opt/Automodel && \ +# UV_HTTP_TIMEOUT=120 uv sync --locked --extra all --all-groups # Install AFTER Automodel sync - uv sync drops packages not in its lockfile. RUN --mount=from=causal-conv1d-wheel-src,target=/tmp/causal-conv1d-wheel-src,readonly \ @@ -90,3 +105,8 @@ ENV VIRTUAL_ENV=/opt/venv \ HF_HUB_ENABLE_HF_TRANSFER=1 ENV PATH="/bin:/opt/venv/bin:/root/.local/bin:$PATH" WORKDIR /opt + +# duplicated to preserve torchrun in published image +RUN if [ -f /usr/local/bin/torchrun ]; then \ + sed -i '1c\#!/opt/venv/bin/python' /usr/local/bin/torchrun; \ + fi diff --git a/services/automodel/docker/Dockerfile.nmp-automodel-tasks b/services/automodel/docker/Dockerfile.nmp-automodel-tasks index 5e42558a5f..65c2814a6e 100644 --- a/services/automodel/docker/Dockerfile.nmp-automodel-tasks +++ b/services/automodel/docker/Dockerfile.nmp-automodel-tasks @@ -47,3 +47,6 @@ USER 0 COPY tests/smoke_gpu/ /smoke_test/ RUN uv pip install --python ${VIRTUAL_ENV}/bin/python --no-cache --reinstall pytest && \ ${VIRTUAL_ENV}/bin/pytest /smoke_test/ -m ${SMOKE_MARKER} -v + +# Default stage for untargeted builds (bake uses --target runtime; smoke-test is opt-in). +FROM runtime diff --git a/services/automodel/docker/Dockerfile.nmp-automodel-training b/services/automodel/docker/Dockerfile.nmp-automodel-training index 75a4fcdc44..429b38548d 100644 --- a/services/automodel/docker/Dockerfile.nmp-automodel-training +++ b/services/automodel/docker/Dockerfile.nmp-automodel-training @@ -52,3 +52,6 @@ USER 0 COPY tests/smoke_gpu/ /smoke_test/ RUN uv pip install --python ${VIRTUAL_ENV}/bin/python --no-cache --reinstall pytest && \ ${VIRTUAL_ENV}/bin/pytest /smoke_test/ -m ${SMOKE_MARKER} -v + +# Default stage for untargeted builds (bake uses --target runtime; smoke-test is opt-in). +FROM runtime diff --git a/services/automodel/docker/README.md b/services/automodel/docker/README.md index 13e0ec2c71..31268b0160 100644 --- a/services/automodel/docker/README.md +++ b/services/automodel/docker/README.md @@ -14,7 +14,7 @@ Full references (default tag `local`): - `nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-tasks:local` - `nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-training:local` -Bake file: **`docker-bake.automodel.hcl`** at the Platform repo root (`context = "."`). Run all commands from the Platform repo root. +Bake file: **`docker-bake.hcl`** at the Platform repo root (`context = "."`). Run all commands from the Platform repo root. ## `docker buildx bake --print` @@ -39,10 +39,10 @@ export WHEELS_TAG="$(git rev-parse --short HEAD)" # export WHEELS_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev # export IMAGE_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev -docker buildx bake --print -f docker-bake.automodel.hcl nmp-automodel-gpu-wheels +docker buildx bake --print -f docker-bake.hcl nmp-automodel-gpu-wheels docker buildx bake \ - -f docker-bake.automodel.hcl \ + -f docker-bake.hcl \ nmp-automodel-gpu-wheels \ --push \ --set "*.platform=linux/amd64" @@ -59,13 +59,13 @@ export WHEELS_TAG="${WHEELS_TAG:-3fd6986ff173b598446ffac06d9be3f84b482495}" export BAKE_TAG="${WHEELS_TAG}" docker buildx bake \ - -f docker-bake.automodel.hcl \ + -f docker-bake.hcl \ nmp-automodel-base-builder \ --push \ --set "*.platform=linux/amd64" docker buildx bake \ - -f docker-bake.automodel.hcl \ + -f docker-bake.hcl \ nmp-automodel \ --push \ --set "*.platform=linux/amd64" diff --git a/services/customizer/src/cherry-picks/e6d2930a.diff b/services/automodel/docker/cherry-picks/e6d2930a.diff similarity index 100% rename from services/customizer/src/cherry-picks/e6d2930a.diff rename to services/automodel/docker/cherry-picks/e6d2930a.diff diff --git a/services/automodel/docker/docker-bake.hcl b/services/automodel/docker/docker-bake.hcl index 47cb2b0c46..d961287205 100644 --- a/services/automodel/docker/docker-bake.hcl +++ b/services/automodel/docker/docker-bake.hcl @@ -1,4 +1,4 @@ -# Moved to Platform repo root (same pattern as nmp/docker-bake.hcl): -# docker buildx bake -f docker-bake.automodel.hcl +# Moved to Platform repo root: +# docker buildx bake -f docker-bake.hcl # # Context is "." (repo root when run from Platform/). Do not use ../../.. here. diff --git a/services/automodel/docker/locks/README.md b/services/automodel/docker/locks/README.md index 1dbcfdc3c9..7b30378a50 100644 --- a/services/automodel/docker/locks/README.md +++ b/services/automodel/docker/locks/README.md @@ -1,6 +1,6 @@ # Mamba / causal-conv1d wheel build locks -Copied from `nmp/docker/locks/` for building `causal-conv1d-wheel` and `mamba-ssm-wheel` images from the Platform repo (see `Dockerfile.mamba-wheel` and `docker-bake.automodel.hcl` group `nmp-automodel-gpu-wheels`). +Copied from `nmp/docker/locks/` for building `causal-conv1d-wheel` and `mamba-ssm-wheel` images from the Platform repo (see `Dockerfile.mamba-wheel` and `docker-bake.hcl` group `nmp-automodel-gpu-wheels`). To refresh locks after dependency changes: diff --git a/services/customizer/docs/automodel_errors.md b/services/automodel/docs/automodel_errors.md similarity index 82% rename from services/customizer/docs/automodel_errors.md rename to services/automodel/docs/automodel_errors.md index 94b766cd6b..227d95520f 100644 --- a/services/customizer/docs/automodel_errors.md +++ b/services/automodel/docs/automodel_errors.md @@ -1,16 +1,18 @@ -# Automodel Error Table for Customizer Implementation +# Automodel Error Table for nmp-automodel This table maps Automodel errors to Custom Exception Classes for implementation. +**Implementation:** rules live in `src/nmp/automodel/tasks/training/errors/error_rules.yaml`; conversion runs in the training task runner via `create_error_details()`. Tests: `tests/tasks/training/test_errors.py`. + ## Validation Status Legend These markers indicate whether an error needs a rule in `error_rules.yaml`. Reviewed the code and categorized each potential error: -> **`[VALIDATED]`** = Pre-validated in Customizer before Automodel execution (e.g., by `prepare_dataset()`, `validate_datasets()`, or API validation). These errors cannot reach the training backend. +> **`[VALIDATED]`** = Pre-validated in nmp-automodel before Automodel execution (e.g., by `prepare_dataset()`, `validate_datasets()`, or API validation). These errors cannot reach the training backend. > > **`[ADD]`** = May occur at runtime and needs an error handling rule in `error_rules.yaml`. These are the errors we care about. > -> **`[NEVER OCCUR]`** = Will never occur with current Customizer configuration (e.g., uses Megatron dataset which we don't use, or NanoGPT which isn't supported). +> **`[NEVER OCCUR]`** = Will never occur with current nmp-automodel configuration (e.g., uses Megatron dataset which we don't use, or NanoGPT which isn't supported). --- @@ -36,7 +38,7 @@ These markers indicate whether an error needs a rule in `error_rules.yaml`. Revi | Exception Class | HTTP Status | Reason Not Needed | |----------------|-------------|-------------------| | `DatasetNotFoundError` | 404 | All errors pre-validated by `prepare_dataset()` or never occur (Megatron/NanoGPT not used) | -| `DatasetPermissionError` | 403 | Never occurs - Megatron not used, Customizer creates files with correct permissions | +| `DatasetPermissionError` | 403 | Never occurs - Megatron not used, nmp-automodel creates files with correct permissions | | `ModelNotFoundError` | 404 | All errors pre-validated by API before training starts | --- @@ -52,8 +54,8 @@ These markers indicate whether an error needs a rule in `error_rules.yaml`. Revi | `ValueError("Expected path to be of string or Path type.")` | `[NEVER OCCUR]` Megatron dataset not used | Dataset path must be a string or Path object, got wrong type | `/opt/Automodel/nemo_automodel/components/datasets/llm/megatron_dataset.py:321` | | `ValueError(f"No files matching glob {path} found")` | `[NEVER OCCUR]` Megatron dataset not used | The glob pattern for dataset files matched no files | `/opt/Automodel/nemo_automodel/components/datasets/llm/megatron_dataset.py:351` | | `FileNotFoundError(f"Expected {str(file_path)} to exist.")` | `[NEVER OCCUR]` Megatron dataset not used | A specific dataset file does not exist at the given path | `/opt/Automodel/nemo_automodel/components/datasets/llm/megatron_dataset.py:338` | -| `RuntimeError("No data files provided")` | `[NEVER OCCUR]` Customizer always provides files in config | No data files were specified for the chat dataset | `/opt/Automodel/nemo_automodel/components/datasets/llm/chat_dataset.py:70` | -| `ValueError("data_files entries must be strings")` | `[NEVER OCCUR]` Customizer always provides strings | Data file paths must be strings, but got wrong type | `/opt/Automodel/nemo_automodel/components/datasets/llm/chat_dataset.py:45` | +| `RuntimeError("No data files provided")` | `[NEVER OCCUR]` nmp-automodel always provides files in config | No data files were specified for the chat dataset | `/opt/Automodel/nemo_automodel/components/datasets/llm/chat_dataset.py:70` | +| `ValueError("data_files entries must be strings")` | `[NEVER OCCUR]` nmp-automodel always provides strings | Data file paths must be strings, but got wrong type | `/opt/Automodel/nemo_automodel/components/datasets/llm/chat_dataset.py:45` | | `FileNotFoundError(f"No files matched pattern {file_pattern}")` | `[NEVER OCCUR]` NanoGPT dataset not used | No files match the specified file pattern for NanoGPT dataset | `/opt/Automodel/nemo_automodel/components/datasets/llm/nanogpt_dataset.py:309` | **User Message**: `Dataset not found: {details}. Please verify the dataset path exists and is accessible.` @@ -66,8 +68,8 @@ These markers indicate whether an error needs a rule in `error_rules.yaml`. Revi |------------------------|-------------------|---------------|--------------| | `ValueError("Each sample must contain a 'messages' list in OpenAI format")` | `[VALIDATED]` in `validate_datasets` | Dataset samples are not in the expected OpenAI chat format | `/opt/Automodel/nemo_automodel/components/datasets/llm/chat_dataset.py:171` | | `RuntimeError(f"no sample to consume: {total_samples}")` | `[VALIDATED]` in `validate_batch_size` | The dataset has zero samples to train on | `/opt/Automodel/nemo_automodel/components/datasets/llm/megatron/sampler.py:59` | -| `DatasetFormatError("...")` | `[VALIDATED]` in `validate_datasets` | Dataset doesn't match expected JSON schema | Customizer `datasets.py` | -| `DatasetFormatError("{file} is empty")` | `[VALIDATED]` in `validate_dataset` | Dataset file is empty | Customizer `datasets.py` | +| `DatasetFormatError("...")` | `[VALIDATED]` in `validate_datasets` | Dataset doesn't match expected JSON schema | nmp-automodel dataset validation | +| `DatasetFormatError("{file} is empty")` | `[VALIDATED]` in `validate_dataset` | Dataset file is empty | nmp-automodel dataset validation | | `ValueError(f"Unsupported role in messages: {role}")` | `[ADD]` Schema only validates role is string, not value | A message has an invalid role (not system/user/assistant/tool) | `/opt/Automodel/nemo_automodel/components/datasets/llm/chat_dataset.py:114` | | `ValueError("ChatDataset requires a tokenizer with chat template support.")` | `[NEVER OCCUR]` `set_chat_template()` provides or errors first | The tokenizer does not have a chat template defined | `/opt/Automodel/nemo_automodel/components/datasets/llm/chat_dataset.py:150` | | `ValueError(f"Dataset sample is too long ({seq_len} > {packed_sequence_size})...")` | `[NEVER OCCUR]` Truncation to `packed_size` matches `packed_sequence_size` | A single sample exceeds the maximum allowed sequence length | `/opt/Automodel/nemo_automodel/components/datasets/llm/packed_sequence.py:259` | @@ -75,8 +77,8 @@ These markers indicate whether an error needs a rule in `error_rules.yaml`. Revi | `ValueError(f"Invalid JSON in blend file {path}: {e}")` | `[NEVER OCCUR]` Megatron dataset not used | Blend JSON file has invalid JSON syntax or wrong structure | `/opt/Automodel/nemo_automodel/components/datasets/llm/megatron_dataset.py:400,403` | | `ValueError("Tokenizer is required")` | `[NEVER OCCUR]` Tokenizer always provided | Chat dataset was not provided a tokenizer during initialization | `/opt/Automodel/nemo_automodel/components/datasets/llm/chat_dataset.py:142` | | `ValueError(f"Expected {n_bytes} to be equal to 2 (uint16) or 4 (uint32).")` | `[NEVER OCCUR]` NanoGPT dataset not used | Binary dataset uses unsupported byte size per token | `/opt/Automodel/nemo_automodel/components/datasets/llm/nanogpt_dataset.py:149` | -| `AssertionError("Expected answer to be in column_mapping")` | `[NEVER OCCUR]` Customizer sets column mapping correctly | Column mapping is missing required fields | `/opt/Automodel/nemo_automodel/components/datasets/llm/column_mapped_text_instruction_dataset.py:213-229` | -| `ValueError("All elements must be strings")` | `[NEVER OCCUR]` Customizer always provides strings | Dataset file paths must be strings or list of strings | `/opt/Automodel/nemo_automodel/components/datasets/llm/column_mapped_text_instruction_dataset.py:70,73` | +| `AssertionError("Expected answer to be in column_mapping")` | `[NEVER OCCUR]` nmp-automodel sets column mapping correctly | Column mapping is missing required fields | `/opt/Automodel/nemo_automodel/components/datasets/llm/column_mapped_text_instruction_dataset.py:213-229` | +| `ValueError("All elements must be strings")` | `[NEVER OCCUR]` nmp-automodel always provides strings | Dataset file paths must be strings or list of strings | `/opt/Automodel/nemo_automodel/components/datasets/llm/column_mapped_text_instruction_dataset.py:70,73` | | `ValueError(f"Missing required fields: {missing}...")` | `[NEVER OCCUR]` Retrieval dataset not used | Dataset item is missing required fields for retrieval | `/opt/Automodel/nemo_automodel/components/datasets/llm/retrieval_dataset.py:127` | **User Message**: `Dataset format error: {details}. Please check your dataset matches the expected schema.` @@ -87,7 +89,7 @@ These markers indicate whether an error needs a rule in `error_rules.yaml`. Revi | Automodel Error Raised | Validation Status | What It Means | Code Pointer | |------------------------|-------------------|---------------|--------------| -| `PermissionError(f"Expected {str(path)} to be readable.")` | `[NEVER OCCUR]` Megatron not used; Customizer files have correct permissions | Cannot read the dataset file due to permission issues | `/opt/Automodel/nemo_automodel/components/datasets/llm/megatron_dataset.py:328,333,340` | +| `PermissionError(f"Expected {str(path)} to be readable.")` | `[NEVER OCCUR]` Megatron not used; nmp-automodel files have correct permissions | Cannot read the dataset file due to permission issues | `/opt/Automodel/nemo_automodel/components/datasets/llm/megatron_dataset.py:328,333,340` | **User Message**: `Dataset access denied: Cannot read {path}. Please check file permissions.` @@ -99,7 +101,7 @@ These markers indicate whether an error needs a rule in `error_rules.yaml`. Revi |------------------------|-------------------|---------------|--------------| | `FileNotFoundError(f"Model path {model_path} does not exist")` | `[VALIDATED]` API validates model exists | The specified model directory or HuggingFace model ID does not exist | `/opt/Automodel/nemo_automodel/components/checkpoint/checkpointing.py:296` | | `FileNotFoundError(f"No snapshot directories found in {snapshots_root}")` | `[VALIDATED]` model pre-downloaded | Model not found in HuggingFace cache | `/opt/Automodel/nemo_automodel/components/checkpoint/checkpointing.py:691` | -| `FileNotFoundError(file_path)` | `[NEVER OCCUR]` Config generated by Customizer, always valid | A required configuration file is missing | `/opt/Automodel/nemo_automodel/_cli/app.py:53,86` | +| `FileNotFoundError(file_path)` | `[NEVER OCCUR]` Config generated by nmp-automodel compiler, always valid | A required configuration file is missing | `/opt/Automodel/nemo_automodel/_cli/app.py:53,86` | **User Message**: `Model not found: {path}. Please verify the model path is correct.` @@ -113,7 +115,7 @@ These markers indicate whether an error needs a rule in `error_rules.yaml`. Revi | `RuntimeError("Failed to patch model")` | `[ADD]` May occur at runtime | Could not apply model optimizations/patches | `/opt/Automodel/nemo_automodel/_transformers/auto_model.py:124` | | `AssertionError(f"Signature mismatch:\n original: {sig_orig}\n patched : {sig_patch}")` | `[ADD]` May occur at runtime | Method signature doesn't match expected signature | `/opt/Automodel/nemo_automodel/_transformers/auto_model.py:55` | | `ValueError("lm_head.weight not found in model")` | `[ADD]` May occur at runtime (model corruption) | Model is missing the language model head weight | `/opt/Automodel/nemo_automodel/recipes/llm/train_ft.py:760` | -| `AssertionError("model_name is required when loading base model")` | `[NEVER OCCUR]` Customizer always provides model_name | Model name must be specified when loading base model | `/opt/Automodel/nemo_automodel/components/checkpoint/checkpointing.py:367` | +| `AssertionError("model_name is required when loading base model")` | `[NEVER OCCUR]` nmp-automodel always provides model_name | Model name must be specified when loading base model | `/opt/Automodel/nemo_automodel/components/checkpoint/checkpointing.py:367` | **User Message**: `Model loading failed: {details}. The model may be corrupted or incompatible.` @@ -134,8 +136,8 @@ These markers indicate whether an error needs a rule in `error_rules.yaml`. Revi | `ValueError(...)` | `[VALIDATED]` in `customizer_automodel_config.py` | Data parallel size must be positive | `/opt/Automodel/nemo_automodel/components/distributed/fsdp2.py:176` | | `AssertionError("dp_size must be a multiple of dp_replicate_size")` | `[NEVER OCCUR]` dp_replicate_size not configurable | Data parallel size must be evenly divisible by dp_replicate_size | `/opt/Automodel/nemo_automodel/components/distributed/fsdp2.py:192` | | `AssertionError("Expected {name} to be an int...")` | `[NEVER OCCUR]` Config always produces valid integers | Parallelism dimension values must be positive integers | `/opt/Automodel/nemo_automodel/components/distributed/fsdp2.py:214-215` | -| `AssertionError("MegatronFSDPManager is not supported...")` | `[NEVER OCCUR]` Not used in Customizer | MegatronFSDP cannot be used with pipeline parallelism | `/opt/Automodel/nemo_automodel/recipes/llm/train_ft.py:919-921` | -| `ValueError("Packed sequence is only supported with CP size 1")` | `[NEVER OCCUR]` Customizer doesn't use CP with packing | Packed sequences cannot be used with context parallelism | `/opt/Automodel/nemo_automodel/_transformers/auto_model.py:201` | +| `AssertionError("MegatronFSDPManager is not supported...")` | `[NEVER OCCUR]` Not used in nmp-automodel | MegatronFSDP cannot be used with pipeline parallelism | `/opt/Automodel/nemo_automodel/recipes/llm/train_ft.py:919-921` | +| `ValueError("Packed sequence is only supported with CP size 1")` | `[NEVER OCCUR]` nmp-automodel doesn't use CP with packing | Packed sequences cannot be used with context parallelism | `/opt/Automodel/nemo_automodel/_transformers/auto_model.py:201` | | `ValueError("Student and teacher tokenizers have different vocab sizes...")` | `[NEVER OCCUR]` KD not used in finetune path | Student and teacher models have incompatible tokenizers | `/opt/Automodel/nemo_automodel/recipes/llm/kd.py:107,115,119` | | `ValueError("Pipeline parallelism support will be added in the future...")` | `[NEVER OCCUR]` KD not used in finetune path | PP cannot be used with knowledge distillation | `/opt/Automodel/nemo_automodel/recipes/llm/kd.py:135` | @@ -145,9 +147,9 @@ These markers indicate whether an error needs a rule in `error_rules.yaml`. Revi |------------------------|-------------------|---------------|--------------| | `ImportError("triton is not installed. Please install it with `pip install triton`.")` | `[ADD]` Environment-dependent, may occur | Triton library required for optimized LoRA kernels | `/opt/Automodel/nemo_automodel/components/_peft/lora_kernel.py:65,105,153,215,270` | | `AssertionError("Incompatible X and LoRA A dimensions")` | `[ADD]` May occur at runtime | LoRA adapter dimensions don't match base model layer | `/opt/Automodel/nemo_automodel/components/_peft/lora_kernel.py:272-276` | -| `ValueError("QAT with PEFT is not supported in 25.11")` | `[NEVER OCCUR]` QAT not supported in Customizer | Quantization-Aware Training cannot be used with PEFT | `/opt/Automodel/nemo_automodel/recipes/llm/train_ft.py:216` | -| `ValueError("PEFT checkpointing is not supported for torch_save format...")` | `[NEVER OCCUR]` Customizer uses safetensors (hardcoded) | PEFT checkpoints must use safetensors format | `/opt/Automodel/nemo_automodel/recipes/llm/train_ft.py:377` | -| `ValueError("Expected match_all_linear to be true or target_modules...")` | `[NEVER OCCUR]` Customizer uses match_all_linear=True | PEFT config must specify which modules to apply LoRA to | `/opt/Automodel/nemo_automodel/components/_peft/module_matcher.py:87` | +| `ValueError("QAT with PEFT is not supported in 25.11")` | `[NEVER OCCUR]` QAT not supported in nmp-automodel | Quantization-Aware Training cannot be used with PEFT | `/opt/Automodel/nemo_automodel/recipes/llm/train_ft.py:216` | +| `ValueError("PEFT checkpointing is not supported for torch_save format...")` | `[NEVER OCCUR]` nmp-automodel uses safetensors (hardcoded) | PEFT checkpoints must use safetensors format | `/opt/Automodel/nemo_automodel/recipes/llm/train_ft.py:377` | +| `ValueError("Expected match_all_linear to be true or target_modules...")` | `[NEVER OCCUR]` nmp-automodel uses match_all_linear=True | PEFT config must specify which modules to apply LoRA to | `/opt/Automodel/nemo_automodel/components/_peft/module_matcher.py:87` | | `AssertionError("exclude_modules must be empty when target_modules is used.")` | `[NEVER OCCUR]` Config never uses both | Cannot use both target_modules and exclude_modules | `/opt/Automodel/nemo_automodel/components/_peft/module_matcher.py:108` | #### 6c. Batch Config Errors (ALL VALIDATED/NEVER OCCUR) @@ -155,19 +157,19 @@ These markers indicate whether an error needs a rule in `error_rules.yaml`. Revi | Automodel Error Raised | Validation Status | What It Means | Code Pointer | |------------------------|-------------------|---------------|--------------| | `AssertionError("warmup_steps < lr_decay_steps")` | `[VALIDATED]` in `customizer_automodel_config.py` | Warmup steps must be less than total training steps | lr_scheduler assertion | -| `DatasetFormatError("Batch size cannot be larger than...")` | `[VALIDATED]` in `datasets.py` | Batch size exceeds validation sample count | Customizer `datasets.py:353-359` | +| `DatasetFormatError("Batch size cannot be larger than...")` | `[VALIDATED]` in dataset validation | Batch size exceeds validation sample count | nmp-automodel dataset validation | | `RuntimeError(f"micro_batch_size must be greater than 0...")` | `[NEVER OCCUR]` Megatron sampler not used | Micro batch size must be a positive number | `/opt/Automodel/nemo_automodel/components/datasets/llm/megatron/sampler.py:61` | | `RuntimeError(f"global_batch_size ({gbs}) is not divisible by...")` | `[NEVER OCCUR]` Megatron sampler not used | Global batch size must be divisible by (micro_batch × dp_size) | `/opt/Automodel/nemo_automodel/components/datasets/llm/megatron/sampler.py:70` | -| `AssertionError(f"grad_acc_steps ({steps}) must be >= 1...")` | `[NEVER OCCUR]` Customizer does not set grad_acc_steps | Gradient accumulation steps must be at least 1 | `/opt/Automodel/nemo_automodel/components/training/step_scheduler.py:74-76` | -| `AssertionError("epoch_len must be provided if max_steps is not provided")` | `[NEVER OCCUR]` Customizer always provides max_steps | Cannot determine epoch length without max_steps | `/opt/Automodel/nemo_automodel/components/training/step_scheduler.py:92` | +| `AssertionError(f"grad_acc_steps ({steps}) must be >= 1...")` | `[NEVER OCCUR]` nmp-automodel does not set grad_acc_steps | Gradient accumulation steps must be at least 1 | `/opt/Automodel/nemo_automodel/components/training/step_scheduler.py:74-76` | +| `AssertionError("epoch_len must be provided if max_steps is not provided")` | `[NEVER OCCUR]` nmp-automodel always provides max_steps | Cannot determine epoch length without max_steps | `/opt/Automodel/nemo_automodel/components/training/step_scheduler.py:92` | | `AssertionError("num_epochs must be greater than 0")` etc. | `[NEVER OCCUR]` Epochs calculated in `customizer_automodel_config.py` | Training parameters have invalid values | `/opt/Automodel/nemo_automodel/components/training/step_scheduler.py:79,83,90,96` | #### 6d. MoE Config Errors (ALL NEVER OCCUR) | Automodel Error Raised | Validation Status | What It Means | Code Pointer | |------------------------|-------------------|---------------|--------------| -| `ValueError(f"Invalid expert activation: {config.expert_activation}")` | `[NEVER OCCUR]` Customizer uses model's default activation | MoE expert FFN activation function (gelu/relu/silu) is not supported | `/opt/Automodel/nemo_automodel/components/moe/layers.py:171,368` | -| `ValueError(f"{tensor_name} has shape {tensor.shape[0]} experts, expected {expected}")` | `[NEVER OCCUR]` Customizer uses base model's expert count | Checkpoint expert count doesn't match model config (e.g., loading 8-expert weights into 4-expert model) | `/opt/Automodel/nemo_automodel/components/moe/state_dict_utils.py:181,187` | +| `ValueError(f"Invalid expert activation: {config.expert_activation}")` | `[NEVER OCCUR]` nmp-automodel uses model's default activation | MoE expert FFN activation function (gelu/relu/silu) is not supported | `/opt/Automodel/nemo_automodel/components/moe/layers.py:171,368` | +| `ValueError(f"{tensor_name} has shape {tensor.shape[0]} experts, expected {expected}")` | `[NEVER OCCUR]` nmp-automodel uses base model's expert count | Checkpoint expert count doesn't match model config (e.g., loading 8-expert weights into 4-expert model) | `/opt/Automodel/nemo_automodel/components/moe/state_dict_utils.py:181,187` | | `ValueError("Two Different Datasets have the same corpus id...")` | `[NEVER OCCUR]` Retrieval dataset not used | Multiple datasets have same corpus ID but different paths | `/opt/Automodel/nemo_automodel/components/datasets/llm/retrieval_dataset.py:89` | **User Message**: `Training configuration error: {details}. Please check your parallelism or PEFT settings.` @@ -182,7 +184,7 @@ These markers indicate whether an error needs a rule in `error_rules.yaml`. Revi | `ValueError("Failed to validate global plan")` | `[ADD]` May occur at runtime | When saving/loading checkpoints across multiple GPUs, PyTorch creates a "global plan" that coordinates which GPU handles which model shards. This error occurs when the plan validation fails, typically due to: 1)Mismatch between GPU topology when saving vs loading (e.g., saved on 8 GPUs, loading on 4), 2)Corrupted checkpoint metadata 3)Inconsistent distributed state across ranks | `/opt/Automodel/nemo_automodel/components/checkpoint/_backports/default_planner.py:156` | | `RuntimeError(f"Missing key in checkpoint state_dict: {fqn}.")` | `[ADD]` May occur at runtime | When loading in strict mode (default), every weight in the model must exist in the checkpoint. This error means the checkpoint is missing a weight the model expects, may be because of incomplete/corrupted checkpoint download | `/opt/Automodel/nemo_automodel/components/checkpoint/_backports/default_planner.py:462` | | `RuntimeError(f"Expert weights missing from checkpoint...")` | `[ADD]` May occur at runtime | Specific to MoE (Mixture of Experts) models, the code validates that all expert weights exist. If any are missing, the checkpoint is likely corrupted or incomplete | `/opt/Automodel/nemo_automodel/components/moe/state_dict_mixin.py:105-110` | -| `AssertionError(f"Unsupported model save format: {format}")` | `[NEVER OCCUR]` Customizer uses safetensors | Model save format not supported | `/opt/Automodel/nemo_automodel/components/checkpoint/checkpointing.py:101` | +| `AssertionError(f"Unsupported model save format: {format}")` | `[NEVER OCCUR]` nmp-automodel uses safetensors | Model save format not supported | `/opt/Automodel/nemo_automodel/components/checkpoint/checkpointing.py:101` | | `Exception("Failed to write dataset materials to the data cache directory...")` | `[NEVER OCCUR]` Megatron dataset not used | Megatron dataset builder failed to write cache files due to disk full or permission issues | `/opt/Automodel/nemo_automodel/components/datasets/llm/megatron/builder.py:647` | **User Message**: `Checkpoint error: {details}. The checkpoint may be corrupted or incompatible.` @@ -216,7 +218,7 @@ These markers indicate whether an error needs a rule in `error_rules.yaml`. Revi | Automodel Error Raised | Validation Status | What It Means | Code Pointer | |------------------------|-------------------|---------------|--------------| -| `subprocess.TimeoutExpired` | `[ADD]` May occur at runtime | Training subprocess exceeded `training_timeout` from API config | Customizer `train_automodel.py:328` (not Automodel) | +| `subprocess.TimeoutExpired` | `[ADD]` May occur at runtime | Training subprocess exceeded `training_timeout` from API config | nmp-automodel training runner (subprocess wait timeout; not Automodel) | **User Message**: `Training exceeded time limit. Consider reducing training steps or increasing timeout.` @@ -231,9 +233,9 @@ These markers indicate whether an error needs a rule in `error_rules.yaml`. Revi | `AssertionError("We only support 1D mesh for MoE")` | `[ADD]` May occur at runtime | MoE expert parallelism only supports 1D device mesh, got multi-dimensional mesh | `/opt/Automodel/nemo_automodel/components/moe/layers.py:245` | | `ValueError(f"{tensor_name} has unsupported DTensor placement: {placement}. Expected Shard(dim=0) or Replicate for expert parallelism.")` | `[ADD]` May occur at runtime | DTensor has wrong placement type for expert parallelism - must be Shard(0) or Replicate | `/opt/Automodel/nemo_automodel/components/moe/state_dict_utils.py:196-198` | | `ValueError("FusedLinearCrossEntropy requires the model to output hidden states. Set model.output_hidden_states=True in the config.")` | `[ADD]` May occur at runtime | Fused loss optimization requires hidden states output but model config doesn't enable it | `/opt/Automodel/nemo_automodel/recipes/llm/train_ft.py:1222` | -| `AssertionError("AutoPipeline configuration is required when pipeline parallelism is enabled")` | `[NEVER OCCUR]` Customizer configures PP correctly | Pipeline parallelism requires autopipeline configuration | `/opt/Automodel/nemo_automodel/recipes/llm/train_ft.py:916-918` | -| `ImportError(f"Cannot resolve target (blocked or not found): {dotted_path}")` | `[NEVER OCCUR]` Config generated by Customizer | Config references a module/class that's blocked or doesn't exist | `/opt/Automodel/nemo_automodel/components/config/loader.py:246` | -| `ImportError("Access to private or dunder attributes is disabled by default. To allow out-of-tree code, set NEMO_ENABLE_USER_MODULES=1...")` | `[NEVER OCCUR]` Config generated by Customizer | Config tries to access private (_) or dunder (__) attributes - blocked for security | `/opt/Automodel/nemo_automodel/components/config/loader.py:210-213,234-237` | +| `AssertionError("AutoPipeline configuration is required when pipeline parallelism is enabled")` | `[NEVER OCCUR]` nmp-automodel configures PP correctly | Pipeline parallelism requires autopipeline configuration | `/opt/Automodel/nemo_automodel/recipes/llm/train_ft.py:916-918` | +| `ImportError(f"Cannot resolve target (blocked or not found): {dotted_path}")` | `[NEVER OCCUR]` Config generated by nmp-automodel compiler | Config references a module/class that's blocked or doesn't exist | `/opt/Automodel/nemo_automodel/components/config/loader.py:246` | +| `ImportError("Access to private or dunder attributes is disabled by default. To allow out-of-tree code, set NEMO_ENABLE_USER_MODULES=1...")` | `[NEVER OCCUR]` Config generated by nmp-automodel compiler | Config tries to access private (_) or dunder (__) attributes - blocked for security | `/opt/Automodel/nemo_automodel/components/config/loader.py:210-213,234-237` | **User Message**: `An internal error occurred: {details}.` diff --git a/services/automodel/src/nmp/automodel/adapter.py b/services/automodel/src/nmp/automodel/adapter.py index ccb3bfa7a8..27491354f7 100644 --- a/services/automodel/src/nmp/automodel/adapter.py +++ b/services/automodel/src/nmp/automodel/adapter.py @@ -18,6 +18,7 @@ WandBParams, ) from nmp.common.api.common import SecretRef +from pydantic import BaseModel def _map_finetuning_type(value: str) -> str: @@ -99,9 +100,9 @@ def _build_integrations(spec: dict[str, Any]) -> IntegrationParams | None: return IntegrationParams(wandb=wandb_params, mlflow=raw.get("mlflow")) -def automodel_spec_to_compiler_output(spec: dict[str, Any] | Any) -> CustomizationJobOutput: +def automodel_spec_to_compiler_output(spec: dict[str, Any] | BaseModel) -> CustomizationJobOutput: """Map simplified Automodel job output (plugin schema) to ``CustomizationJobOutput``.""" - if hasattr(spec, "model_dump"): + if isinstance(spec, BaseModel): data = spec.model_dump(mode="python") else: data = dict(spec) @@ -116,13 +117,11 @@ def automodel_spec_to_compiler_output(spec: dict[str, Any] | Any) -> Customizati name=output["name"], type=out_type, fileset=output["fileset"], - description=output.get("description"), ) else: output_resp = output return CustomizationJobOutput( - name=data.get("name"), model=data["model"], dataset=training_uri, training=_build_training_block(data), diff --git a/services/automodel/src/nmp/automodel/app/__init__.py b/services/automodel/src/nmp/automodel/app/__init__.py index 35a0c9116b..7198afdac1 100644 --- a/services/automodel/src/nmp/automodel/app/__init__.py +++ b/services/automodel/src/nmp/automodel/app/__init__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Customizer application module.""" +"""Automodel application module.""" diff --git a/services/automodel/src/nmp/automodel/app/jobs/compiler.py b/services/automodel/src/nmp/automodel/app/jobs/compiler.py index f43b11906d..5b3b0625f9 100644 --- a/services/automodel/src/nmp/automodel/app/jobs/compiler.py +++ b/services/automodel/src/nmp/automodel/app/jobs/compiler.py @@ -7,6 +7,16 @@ from nemo_platform import AsyncNeMoPlatform, NotFoundError from nemo_platform.types.models.model_entity import ModelEntity +from nemo_platform_plugin.jobs.api_factory import ( + ContainerSpec, + CPUExecutionProviderSpec, + EnvironmentVariable, + PlatformJobSpec, + PlatformJobStep, + ResourcesLimitsSpec, + ResourcesRequestsSpec, + ResourcesSpec, +) from nmp.automodel.api.v2.jobs.schemas import ( CustomizationJobOutput, DeploymentParams, @@ -46,16 +56,6 @@ from nmp.automodel.platform_client import fetch_model_entity from nmp.common.auth import AuthClient, auth_client_context from nmp.common.entities.utils import parse_entity_ref -from nmp.common.jobs.api_factory import ( - ContainerSpec, - CPUExecutionProviderSpec, - EnvironmentVariable, - PlatformJobSpec, - PlatformJobStep, - ResourcesLimitsSpec, - ResourcesRequestsSpec, - ResourcesSpec, -) from nmp.common.jobs.constants import DEFAULT_JOB_STORAGE_PATH, PERSISTENT_JOB_STORAGE_PATH_ENVVAR from nmp.common.jobs.exceptions import PlatformJobCompilationError diff --git a/services/automodel/src/nmp/automodel/app/jobs/training/compiler.py b/services/automodel/src/nmp/automodel/app/jobs/training/compiler.py index 1af931f92e..e25e63e894 100644 --- a/services/automodel/src/nmp/automodel/app/jobs/training/compiler.py +++ b/services/automodel/src/nmp/automodel/app/jobs/training/compiler.py @@ -6,6 +6,16 @@ import logging from nemo_platform.types.models.model_entity import ModelEntity +from nemo_platform_plugin.jobs.api_factory import ( + ContainerSpec, + DistributedGPUExecutionProviderSpec, + EnvironmentVariable, + EnvironmentVariableFromSecret, + GPUExecutionProviderSpec, + PlatformJobStep, + ResourcesSpec, + StepLifecycle, +) from nmp.automodel.api.v2.jobs.schemas import ( AnyTraining, CustomizationJobOutput, @@ -31,16 +41,6 @@ from nmp.automodel.config import config from nmp.automodel.entities.values import Precision, TrainingType from nmp.automodel.images import AUTOMODEL_PYTHON_ENTRYPOINT, get_training_image -from nmp.common.jobs.api_factory import ( - ContainerSpec, - DistributedGPUExecutionProviderSpec, - EnvironmentVariable, - EnvironmentVariableFromSecret, - GPUExecutionProviderSpec, - PlatformJobStep, - ResourcesSpec, - StepLifecycle, -) from nmp.common.model_utils import is_embedding_model logger = logging.getLogger(__name__) diff --git a/services/automodel/src/nmp/automodel/compile.py b/services/automodel/src/nmp/automodel/compile.py index 2781fb9e65..a5476fbef6 100644 --- a/services/automodel/src/nmp/automodel/compile.py +++ b/services/automodel/src/nmp/automodel/compile.py @@ -27,7 +27,7 @@ async def platform_job_config_compiler( return await _compile_canonical( workspace, job_spec, - sdk, # type: ignore[arg-type] + sdk, ) diff --git a/services/automodel/src/nmp/automodel/tasks/docker/docker-compose.yaml b/services/automodel/src/nmp/automodel/tasks/docker/docker-compose.yaml index eae2b4a163..0eaa2f56ae 100644 --- a/services/automodel/src/nmp/automodel/tasks/docker/docker-compose.yaml +++ b/services/automodel/src/nmp/automodel/tasks/docker/docker-compose.yaml @@ -9,7 +9,7 @@ # # Prerequisites: # - Build the image first (from Platform repo root): -# docker buildx bake -f docker-bake.automodel.hcl nmp-automodel-tasks-docker +# docker buildx bake -f docker-bake.hcl nmp-automodel-tasks-docker # - Have NeMo Platform running at http://localhost:8080 # - Create sample_config.json (or use the one provided) diff --git a/services/automodel/src/nmp/automodel/tasks/file_io/run.py b/services/automodel/src/nmp/automodel/tasks/file_io/run.py index f14acd7b25..5cd23fa6ce 100644 --- a/services/automodel/src/nmp/automodel/tasks/file_io/run.py +++ b/services/automodel/src/nmp/automodel/tasks/file_io/run.py @@ -219,7 +219,7 @@ def _download_with_retry( fileset=fileset_name, workspace=fileset_workspace, local_path=dest_dir, - callback=callback, # type: ignore[arg-type] + callback=callback, ) def upload_fileset( @@ -312,7 +312,7 @@ def _upload_with_retry( remote_path=remote_path, fileset=fileset_name, workspace=fileset_workspace, - callback=callback, # type: ignore[arg-type] + callback=callback, ) def run_download(self, downloads: list[DownloadItem]) -> None: diff --git a/services/automodel/src/nmp/automodel/tasks/training/backends/finetune.py b/services/automodel/src/nmp/automodel/tasks/training/backends/finetune.py index abaf469ba1..291f36d8ce 100644 --- a/services/automodel/src/nmp/automodel/tasks/training/backends/finetune.py +++ b/services/automodel/src/nmp/automodel/tasks/training/backends/finetune.py @@ -102,9 +102,9 @@ def __init__(self, recipe: AutomodelRecipe, job_ctx: NMPJobContext | None = None self._original_save_checkpoint = recipe.save_checkpoint # Monkey-patch the recipe's methods to add our callbacks - recipe.log_train_metrics = self._log_train_metrics # type: ignore[method-assign] - recipe.log_val_metrics = self._log_val_metrics # type: ignore[method-assign] - recipe.save_checkpoint = self._save_checkpoint # type: ignore[method-assign] + recipe.log_train_metrics = self._log_train_metrics + recipe.log_val_metrics = self._log_val_metrics + recipe.save_checkpoint = self._save_checkpoint @property def recipe(self) -> AutomodelRecipe: diff --git a/tests/customizer-automodel-contract/README.md b/services/automodel/tests/contract/README.md similarity index 91% rename from tests/customizer-automodel-contract/README.md rename to services/automodel/tests/contract/README.md index cc3eb3626b..77bdbe318a 100644 --- a/tests/customizer-automodel-contract/README.md +++ b/services/automodel/tests/contract/README.md @@ -1,6 +1,6 @@ # Automodel Contract Tests -These configs are generated by Customizer's `compile_automodel_config()` — the exact same code path used in production. They serve as **contract tests**: if any of these configs stop working with Automodel's `finetune.py`, it means a breaking change was introduced. +These configs are generated by nmp-automodel's `compile_automodel_config()` — the same code path used in production. They serve as **contract tests**: if any of these configs stop working with Automodel's `finetune.py`, it means a breaking change was introduced. ## Test Matrix @@ -63,7 +63,7 @@ output_configs/ # flat — one YAML per input JSON - **Sequence packing**: Packing configs have `packed_sequence.packed_sequence_size` computed from actual dataset token length statistics. - **Nemotron Nano**: Uses `trust_remote_code: true` since the model has custom architecture code (`NemotronHForCausalLM`). - **Embedding model**: Uses Automodel's biencoder recipe (`TrainBiencoderRecipe`) with retrieval dataset format, not the LLM recipe. -- **Generation environment**: Run `generate_configs.py` in the **customizer-automodel** image on a GPU pod. Compiling configs requires `nemo_automodel` and its dependencies which are only available in that image. +- **Generation environment**: Run `generate_configs.py` in the **nmp-automodel-training** image on a GPU pod. Compiling configs requires `nemo_automodel` and its dependencies which are only available in that image. ## Running the Configs @@ -130,11 +130,11 @@ python3 generate_configs.py input_configs/nemotron-nano/nemotron_nano_lora.json ### Prerequisites -The script needs `nemo_automodel` and its dependencies. Run it inside the **customizer-automodel** container image, which has everything pre-installed: +The script needs `nemo_automodel` and its dependencies. Run it inside the **nmp-automodel-training** container image, which has everything pre-installed: ```bash -# On a GPU pod with the customizer-automodel image -cd tests/customizer-automodel-contract +# On a GPU pod with the nmp-automodel-training image +cd services/automodel/tests/contract python generate_configs.py --all ``` @@ -148,11 +148,11 @@ These fixtures are retained for the Platform-Deploy contract-test pipeline. Plat The job fails when the committed output configs don't match what the current code generates. This means the config compilation code changed but the output configs weren't regenerated. To fix it: -1. **Get a GPU pod** with the `customizer-automodel` image (the script needs `nemo_automodel` which is only available in that image). +1. **Get a GPU pod** with the `nmp-automodel-training` image (the script needs `nemo_automodel` which is only available in that image). 2. **Regenerate all configs:** ```bash - cd tests/customizer-automodel-contract + cd services/automodel/tests/contract python generate_configs.py --all ``` diff --git a/tests/customizer-automodel-contract/generate_configs.py b/services/automodel/tests/contract/generate_configs.py similarity index 97% rename from tests/customizer-automodel-contract/generate_configs.py rename to services/automodel/tests/contract/generate_configs.py index 65a8b9a567..1b352003ad 100644 --- a/tests/customizer-automodel-contract/generate_configs.py +++ b/services/automodel/tests/contract/generate_configs.py @@ -3,11 +3,10 @@ # SPDX-License-Identifier: Apache-2.0 """ -Generate Automodel YAML configs from Customizer TrainingStepConfig JSONs. +Generate Automodel YAML configs from TrainingStepConfig JSON fixtures. -Uses the same compile_automodel_config() that Customizer uses at runtime. -Input configs are grouped by model in subdirectories of input_configs/ so -each model is downloaded only once. +Uses compile_automodel_config() from nmp-automodel. Input configs are grouped +by model in subdirectories of input_configs/ so each model is downloaded only once. Directory layout: input_configs/ @@ -47,14 +46,11 @@ import yaml SCRIPT_DIR = Path(__file__).resolve().parent -REPO_ROOT = SCRIPT_DIR.parent.parent +REPO_ROOT = SCRIPT_DIR.parents[3] AUTOMODEL_SRC = REPO_ROOT / "services" / "automodel" / "src" -CUSTOMIZER_SRC = REPO_ROOT / "services" / "customizer" / "src" if AUTOMODEL_SRC.is_dir(): sys.path.insert(0, str(AUTOMODEL_SRC)) -elif CUSTOMIZER_SRC.is_dir(): - sys.path.insert(0, str(CUSTOMIZER_SRC)) else: sys.path.insert(0, "/app/services/automodel/src") diff --git a/tests/customizer-automodel-contract/input_configs/embed-1b/embed_1b_full_sft.json b/services/automodel/tests/contract/input_configs/embed-1b/embed_1b_full_sft.json similarity index 100% rename from tests/customizer-automodel-contract/input_configs/embed-1b/embed_1b_full_sft.json rename to services/automodel/tests/contract/input_configs/embed-1b/embed_1b_full_sft.json diff --git a/tests/customizer-automodel-contract/input_configs/embed-1b/embed_1b_lora.json b/services/automodel/tests/contract/input_configs/embed-1b/embed_1b_lora.json similarity index 100% rename from tests/customizer-automodel-contract/input_configs/embed-1b/embed_1b_lora.json rename to services/automodel/tests/contract/input_configs/embed-1b/embed_1b_lora.json diff --git a/tests/customizer-automodel-contract/input_configs/gpt-oss/gpt_oss_full_sft.json b/services/automodel/tests/contract/input_configs/gpt-oss/gpt_oss_full_sft.json similarity index 100% rename from tests/customizer-automodel-contract/input_configs/gpt-oss/gpt_oss_full_sft.json rename to services/automodel/tests/contract/input_configs/gpt-oss/gpt_oss_full_sft.json diff --git a/tests/customizer-automodel-contract/input_configs/gpt-oss/gpt_oss_full_sft_chat.json b/services/automodel/tests/contract/input_configs/gpt-oss/gpt_oss_full_sft_chat.json similarity index 100% rename from tests/customizer-automodel-contract/input_configs/gpt-oss/gpt_oss_full_sft_chat.json rename to services/automodel/tests/contract/input_configs/gpt-oss/gpt_oss_full_sft_chat.json diff --git a/tests/customizer-automodel-contract/input_configs/gpt-oss/gpt_oss_lora.json b/services/automodel/tests/contract/input_configs/gpt-oss/gpt_oss_lora.json similarity index 100% rename from tests/customizer-automodel-contract/input_configs/gpt-oss/gpt_oss_lora.json rename to services/automodel/tests/contract/input_configs/gpt-oss/gpt_oss_lora.json diff --git a/tests/customizer-automodel-contract/input_configs/gpt-oss/gpt_oss_lora_packing.json b/services/automodel/tests/contract/input_configs/gpt-oss/gpt_oss_lora_packing.json similarity index 100% rename from tests/customizer-automodel-contract/input_configs/gpt-oss/gpt_oss_lora_packing.json rename to services/automodel/tests/contract/input_configs/gpt-oss/gpt_oss_lora_packing.json diff --git a/tests/customizer-automodel-contract/input_configs/llama-3.1-8b/llama_3_1_8b_full_sft_tp.json b/services/automodel/tests/contract/input_configs/llama-3.1-8b/llama_3_1_8b_full_sft_tp.json similarity index 100% rename from tests/customizer-automodel-contract/input_configs/llama-3.1-8b/llama_3_1_8b_full_sft_tp.json rename to services/automodel/tests/contract/input_configs/llama-3.1-8b/llama_3_1_8b_full_sft_tp.json diff --git a/tests/customizer-automodel-contract/input_configs/llama-3.2-1b/llama_3_2_1b_full_sft.json b/services/automodel/tests/contract/input_configs/llama-3.2-1b/llama_3_2_1b_full_sft.json similarity index 100% rename from tests/customizer-automodel-contract/input_configs/llama-3.2-1b/llama_3_2_1b_full_sft.json rename to services/automodel/tests/contract/input_configs/llama-3.2-1b/llama_3_2_1b_full_sft.json diff --git a/tests/customizer-automodel-contract/input_configs/llama-3.2-1b/llama_3_2_1b_full_sft_chat.json b/services/automodel/tests/contract/input_configs/llama-3.2-1b/llama_3_2_1b_full_sft_chat.json similarity index 100% rename from tests/customizer-automodel-contract/input_configs/llama-3.2-1b/llama_3_2_1b_full_sft_chat.json rename to services/automodel/tests/contract/input_configs/llama-3.2-1b/llama_3_2_1b_full_sft_chat.json diff --git a/tests/customizer-automodel-contract/input_configs/llama-3.2-1b/llama_3_2_1b_lora.json b/services/automodel/tests/contract/input_configs/llama-3.2-1b/llama_3_2_1b_lora.json similarity index 100% rename from tests/customizer-automodel-contract/input_configs/llama-3.2-1b/llama_3_2_1b_lora.json rename to services/automodel/tests/contract/input_configs/llama-3.2-1b/llama_3_2_1b_lora.json diff --git a/tests/customizer-automodel-contract/input_configs/llama-3.2-1b/llama_3_2_1b_lora_packing.json b/services/automodel/tests/contract/input_configs/llama-3.2-1b/llama_3_2_1b_lora_packing.json similarity index 100% rename from tests/customizer-automodel-contract/input_configs/llama-3.2-1b/llama_3_2_1b_lora_packing.json rename to services/automodel/tests/contract/input_configs/llama-3.2-1b/llama_3_2_1b_lora_packing.json diff --git a/tests/customizer-automodel-contract/input_configs/nemotron-nano/nemotron_nano_full_sft.json b/services/automodel/tests/contract/input_configs/nemotron-nano/nemotron_nano_full_sft.json similarity index 100% rename from tests/customizer-automodel-contract/input_configs/nemotron-nano/nemotron_nano_full_sft.json rename to services/automodel/tests/contract/input_configs/nemotron-nano/nemotron_nano_full_sft.json diff --git a/tests/customizer-automodel-contract/input_configs/nemotron-nano/nemotron_nano_full_sft_chat.json b/services/automodel/tests/contract/input_configs/nemotron-nano/nemotron_nano_full_sft_chat.json similarity index 100% rename from tests/customizer-automodel-contract/input_configs/nemotron-nano/nemotron_nano_full_sft_chat.json rename to services/automodel/tests/contract/input_configs/nemotron-nano/nemotron_nano_full_sft_chat.json diff --git a/tests/customizer-automodel-contract/input_configs/nemotron-nano/nemotron_nano_lora.json b/services/automodel/tests/contract/input_configs/nemotron-nano/nemotron_nano_lora.json similarity index 100% rename from tests/customizer-automodel-contract/input_configs/nemotron-nano/nemotron_nano_lora.json rename to services/automodel/tests/contract/input_configs/nemotron-nano/nemotron_nano_lora.json diff --git a/tests/customizer-automodel-contract/input_configs/nemotron-nano/nemotron_nano_lora_packing.json b/services/automodel/tests/contract/input_configs/nemotron-nano/nemotron_nano_lora_packing.json similarity index 100% rename from tests/customizer-automodel-contract/input_configs/nemotron-nano/nemotron_nano_lora_packing.json rename to services/automodel/tests/contract/input_configs/nemotron-nano/nemotron_nano_lora_packing.json diff --git a/tests/customizer-automodel-contract/output_configs/embed_1b_full_sft.yaml b/services/automodel/tests/contract/output_configs/embed_1b_full_sft.yaml similarity index 100% rename from tests/customizer-automodel-contract/output_configs/embed_1b_full_sft.yaml rename to services/automodel/tests/contract/output_configs/embed_1b_full_sft.yaml diff --git a/tests/customizer-automodel-contract/output_configs/embed_1b_lora.yaml b/services/automodel/tests/contract/output_configs/embed_1b_lora.yaml similarity index 100% rename from tests/customizer-automodel-contract/output_configs/embed_1b_lora.yaml rename to services/automodel/tests/contract/output_configs/embed_1b_lora.yaml diff --git a/tests/customizer-automodel-contract/output_configs/gpt_oss_full_sft.yaml b/services/automodel/tests/contract/output_configs/gpt_oss_full_sft.yaml similarity index 100% rename from tests/customizer-automodel-contract/output_configs/gpt_oss_full_sft.yaml rename to services/automodel/tests/contract/output_configs/gpt_oss_full_sft.yaml diff --git a/tests/customizer-automodel-contract/output_configs/gpt_oss_full_sft_chat.yaml b/services/automodel/tests/contract/output_configs/gpt_oss_full_sft_chat.yaml similarity index 100% rename from tests/customizer-automodel-contract/output_configs/gpt_oss_full_sft_chat.yaml rename to services/automodel/tests/contract/output_configs/gpt_oss_full_sft_chat.yaml diff --git a/tests/customizer-automodel-contract/output_configs/gpt_oss_lora.yaml b/services/automodel/tests/contract/output_configs/gpt_oss_lora.yaml similarity index 100% rename from tests/customizer-automodel-contract/output_configs/gpt_oss_lora.yaml rename to services/automodel/tests/contract/output_configs/gpt_oss_lora.yaml diff --git a/tests/customizer-automodel-contract/output_configs/gpt_oss_lora_packing.yaml b/services/automodel/tests/contract/output_configs/gpt_oss_lora_packing.yaml similarity index 100% rename from tests/customizer-automodel-contract/output_configs/gpt_oss_lora_packing.yaml rename to services/automodel/tests/contract/output_configs/gpt_oss_lora_packing.yaml diff --git a/tests/customizer-automodel-contract/output_configs/llama_3_1_8b_full_sft_tp.yaml b/services/automodel/tests/contract/output_configs/llama_3_1_8b_full_sft_tp.yaml similarity index 100% rename from tests/customizer-automodel-contract/output_configs/llama_3_1_8b_full_sft_tp.yaml rename to services/automodel/tests/contract/output_configs/llama_3_1_8b_full_sft_tp.yaml diff --git a/tests/customizer-automodel-contract/output_configs/llama_3_2_1b_full_sft.yaml b/services/automodel/tests/contract/output_configs/llama_3_2_1b_full_sft.yaml similarity index 100% rename from tests/customizer-automodel-contract/output_configs/llama_3_2_1b_full_sft.yaml rename to services/automodel/tests/contract/output_configs/llama_3_2_1b_full_sft.yaml diff --git a/tests/customizer-automodel-contract/output_configs/llama_3_2_1b_full_sft_chat.yaml b/services/automodel/tests/contract/output_configs/llama_3_2_1b_full_sft_chat.yaml similarity index 100% rename from tests/customizer-automodel-contract/output_configs/llama_3_2_1b_full_sft_chat.yaml rename to services/automodel/tests/contract/output_configs/llama_3_2_1b_full_sft_chat.yaml diff --git a/tests/customizer-automodel-contract/output_configs/llama_3_2_1b_lora.yaml b/services/automodel/tests/contract/output_configs/llama_3_2_1b_lora.yaml similarity index 100% rename from tests/customizer-automodel-contract/output_configs/llama_3_2_1b_lora.yaml rename to services/automodel/tests/contract/output_configs/llama_3_2_1b_lora.yaml diff --git a/tests/customizer-automodel-contract/output_configs/llama_3_2_1b_lora_packing.yaml b/services/automodel/tests/contract/output_configs/llama_3_2_1b_lora_packing.yaml similarity index 100% rename from tests/customizer-automodel-contract/output_configs/llama_3_2_1b_lora_packing.yaml rename to services/automodel/tests/contract/output_configs/llama_3_2_1b_lora_packing.yaml diff --git a/tests/customizer-automodel-contract/output_configs/nemotron_nano_full_sft.yaml b/services/automodel/tests/contract/output_configs/nemotron_nano_full_sft.yaml similarity index 100% rename from tests/customizer-automodel-contract/output_configs/nemotron_nano_full_sft.yaml rename to services/automodel/tests/contract/output_configs/nemotron_nano_full_sft.yaml diff --git a/tests/customizer-automodel-contract/output_configs/nemotron_nano_full_sft_chat.yaml b/services/automodel/tests/contract/output_configs/nemotron_nano_full_sft_chat.yaml similarity index 100% rename from tests/customizer-automodel-contract/output_configs/nemotron_nano_full_sft_chat.yaml rename to services/automodel/tests/contract/output_configs/nemotron_nano_full_sft_chat.yaml diff --git a/tests/customizer-automodel-contract/output_configs/nemotron_nano_lora.yaml b/services/automodel/tests/contract/output_configs/nemotron_nano_lora.yaml similarity index 100% rename from tests/customizer-automodel-contract/output_configs/nemotron_nano_lora.yaml rename to services/automodel/tests/contract/output_configs/nemotron_nano_lora.yaml diff --git a/tests/customizer-automodel-contract/output_configs/nemotron_nano_lora_packing.yaml b/services/automodel/tests/contract/output_configs/nemotron_nano_lora_packing.yaml similarity index 100% rename from tests/customizer-automodel-contract/output_configs/nemotron_nano_lora_packing.yaml rename to services/automodel/tests/contract/output_configs/nemotron_nano_lora_packing.yaml diff --git a/tests/customizer-automodel-contract/sample-datasets/chat/training.jsonl b/services/automodel/tests/contract/sample-datasets/chat/training.jsonl similarity index 100% rename from tests/customizer-automodel-contract/sample-datasets/chat/training.jsonl rename to services/automodel/tests/contract/sample-datasets/chat/training.jsonl diff --git a/tests/customizer-automodel-contract/sample-datasets/chat/validation.jsonl b/services/automodel/tests/contract/sample-datasets/chat/validation.jsonl similarity index 100% rename from tests/customizer-automodel-contract/sample-datasets/chat/validation.jsonl rename to services/automodel/tests/contract/sample-datasets/chat/validation.jsonl diff --git a/tests/customizer-automodel-contract/sample-datasets/embedding/training.jsonl b/services/automodel/tests/contract/sample-datasets/embedding/training.jsonl similarity index 100% rename from tests/customizer-automodel-contract/sample-datasets/embedding/training.jsonl rename to services/automodel/tests/contract/sample-datasets/embedding/training.jsonl diff --git a/tests/customizer-automodel-contract/sample-datasets/embedding/validation.jsonl b/services/automodel/tests/contract/sample-datasets/embedding/validation.jsonl similarity index 100% rename from tests/customizer-automodel-contract/sample-datasets/embedding/validation.jsonl rename to services/automodel/tests/contract/sample-datasets/embedding/validation.jsonl diff --git a/tests/customizer-automodel-contract/sample-datasets/prompt_completion/training.jsonl b/services/automodel/tests/contract/sample-datasets/prompt_completion/training.jsonl similarity index 100% rename from tests/customizer-automodel-contract/sample-datasets/prompt_completion/training.jsonl rename to services/automodel/tests/contract/sample-datasets/prompt_completion/training.jsonl diff --git a/tests/customizer-automodel-contract/sample-datasets/prompt_completion/validation.jsonl b/services/automodel/tests/contract/sample-datasets/prompt_completion/validation.jsonl similarity index 100% rename from tests/customizer-automodel-contract/sample-datasets/prompt_completion/validation.jsonl rename to services/automodel/tests/contract/sample-datasets/prompt_completion/validation.jsonl diff --git a/services/customizer/tests/tasks/training/backends/automodel/test_backend.py b/services/automodel/tests/tasks/training/backends/test_backend.py similarity index 87% rename from services/customizer/tests/tasks/training/backends/automodel/test_backend.py rename to services/automodel/tests/tasks/training/backends/test_backend.py index 675581838a..938b06e86c 100644 --- a/services/customizer/tests/tasks/training/backends/automodel/test_backend.py +++ b/services/automodel/tests/tasks/training/backends/test_backend.py @@ -15,8 +15,8 @@ sys.modules["nemo_automodel._transformers"] = MagicMock() sys.modules["nemo_automodel._transformers.registry"] = MagicMock() -from nmp.customizer.tasks.training.backends.automodel.backend import AutomodelBackend # noqa: E402 -from nmp.customizer.tasks.training.backends.automodel.checkpoints import ModelType # noqa: E402 +from nmp.automodel.tasks.training.backends.backend import AutomodelBackend # noqa: E402 +from nmp.automodel.tasks.training.backends.checkpoints import ModelType # noqa: E402 class TestAutomodelBackend: @@ -35,7 +35,7 @@ def test_find_best_checkpoint_uses_model_embedding_flag( expected_path = tmp_path / "best.ckpt" mock_find_best_checkpoint = mocker.patch( - "nmp.customizer.tasks.training.backends.automodel.backend.find_best_checkpoint", + "nmp.automodel.tasks.training.backends.backend.find_best_checkpoint", return_value=expected_path, ) @@ -61,7 +61,7 @@ def test_process_checkpoint_uses_model_embedding_flag_not_model_name( checkpoint_info = MagicMock() mock_process_checkpoint = mocker.patch( - "nmp.customizer.tasks.training.backends.automodel.backend.process_checkpoint", + "nmp.automodel.tasks.training.backends.backend.process_checkpoint", return_value=checkpoint_info, ) diff --git a/services/customizer/tests/tasks/training/backends/automodel/test_callbacks.py b/services/automodel/tests/tasks/training/backends/test_callbacks.py similarity index 90% rename from services/customizer/tests/tasks/training/backends/automodel/test_callbacks.py rename to services/automodel/tests/tasks/training/backends/test_callbacks.py index d9ae004b21..ea3627fe76 100644 --- a/services/customizer/tests/tasks/training/backends/automodel/test_callbacks.py +++ b/services/automodel/tests/tasks/training/backends/test_callbacks.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock -from nmp.customizer.tasks.training.backends.automodel.callbacks import TrainingProgressCallback +from nmp.automodel.tasks.training.backends.callbacks import TrainingProgressCallback class TestTrainingProgressCallback: @@ -23,8 +23,6 @@ def _make_callback(self, prior_metrics: dict | None = None) -> tuple[TrainingPro def _last_report_kwargs(self, mock_reporter: MagicMock) -> dict: return mock_reporter.report_running.call_args.kwargs - # --- Accumulation --- - def test_train_step_accumulates_metrics(self): callback, reporter = self._make_callback() @@ -65,7 +63,6 @@ def test_mixed_train_and_val_both_present(self): assert len(kwargs["metrics"]["val_loss"]) == 1 def test_metrics_included_in_every_update(self): - """Each report_running call should contain the full accumulated metrics.""" callback, reporter = self._make_callback() callback.report_train_step(step=1, epoch=1, loss=3.21) @@ -77,10 +74,7 @@ def test_metrics_included_in_every_update(self): second_call_kwargs = reporter.report_running.call_args_list[1].kwargs assert len(second_call_kwargs["metrics"]["train_loss"]) == 2 - # --- Flat field naming --- - def test_train_step_uses_train_loss_flat_field(self): - """The flat field should be 'train_loss', not 'loss'.""" callback, reporter = self._make_callback() callback.report_train_step(step=1, epoch=1, loss=3.21) @@ -98,10 +92,7 @@ def test_train_step_passes_optional_fields(self): assert kwargs["lr"] == 0.0002 assert kwargs["grad_norm"] == 1.5 - # --- Server seeding (pause/resume) --- - def test_seeds_from_server_on_init(self): - """Callback should pre-populate metrics from the server.""" prior = { "train_loss": [ {"step": 1, "epoch": 1, "value": 3.21}, @@ -118,7 +109,6 @@ def test_seeds_from_server_on_init(self): reporter.fetch_current_metrics.assert_called_once() def test_seeded_metrics_included_in_first_report(self): - """After seeding, the first report should include both old and new metrics.""" prior = { "train_loss": [{"step": 1, "epoch": 1, "value": 3.21}], "val_loss": [], @@ -134,7 +124,6 @@ def test_seeded_metrics_included_in_first_report(self): ] def test_seeded_val_metrics_preserved_across_train_steps(self): - """Val metrics from a prior run should appear in subsequent train step reports.""" prior = { "train_loss": [{"step": 1, "epoch": 1, "value": 3.21}], "val_loss": [{"step": 1, "epoch": 1, "value": 3.50}], @@ -147,8 +136,6 @@ def test_seeded_val_metrics_preserved_across_train_steps(self): assert len(kwargs["metrics"]["val_loss"]) == 1 assert kwargs["metrics"]["val_loss"][0]["value"] == 3.50 - # --- Delegation --- - def test_report_training_start_delegates(self): callback, reporter = self._make_callback() diff --git a/services/customizer/tests/tasks/training/backends/automodel/test_config.py b/services/automodel/tests/tasks/training/backends/test_config.py similarity index 81% rename from services/customizer/tests/tasks/training/backends/automodel/test_config.py rename to services/automodel/tests/tasks/training/backends/test_config.py index 5efb2d47bb..49aaef6c2d 100644 --- a/services/customizer/tests/tasks/training/backends/automodel/test_config.py +++ b/services/automodel/tests/tasks/training/backends/test_config.py @@ -17,12 +17,16 @@ sys.modules["nemo_automodel._transformers.registry"] = MagicMock() sys.modules.setdefault("transformers", MagicMock()) -from nmp.customizer.tasks.training.backends.automodel.config import ( # noqa: E402 +from nmp.automodel.tasks.training.backends.config import ( # noqa: E402 _configure_chat_dataset, _configure_moe_backend, _configure_sft_dataset, ) +CONFIG_MODULE = "nmp.automodel.tasks.training.backends.config" +AUTOCONFIG_PATCH = "transformers.AutoConfig" +MODEL_REGISTRY_PATCH = f"{CONFIG_MODULE}.ModelRegistry" + @pytest.fixture def mock_customizer_config() -> MagicMock: @@ -52,23 +56,13 @@ def test_chat_dataset_includes_split_attribute( temp_dataset_files: tuple[Path, Path], mocker, ) -> None: - """Test that chat dataset config includes 'split' attribute for sequence packing. - - The 'split' attribute is required by Automodel's pack_dataset() when sequence - packing is enabled. Without it, build_dataloader() raises AttributeError. - """ train_file, val_file = temp_dataset_files cfg: dict[str, Any] = {} - # Mock resolve_chat_template to avoid external dependencies - mocker.patch( - "nmp.customizer.tasks.training.backends.automodel.config.resolve_chat_template", - return_value="mock_template", - ) + mocker.patch(f"{CONFIG_MODULE}.resolve_chat_template", return_value="mock_template") mock_customizer_config.parallelism.pipeline_parallel_size = 1 _configure_chat_dataset(cfg, mock_customizer_config, train_file, val_file, seq_length=2048) - # Verify split is set for both train and validation datasets assert "dataset" in cfg assert "validation_dataset" in cfg assert cfg["dataset"]["split"] == "train" @@ -80,18 +74,13 @@ def test_chat_dataset_includes_required_fields( temp_dataset_files: tuple[Path, Path], mocker, ) -> None: - """Test that chat dataset config includes all required fields.""" train_file, val_file = temp_dataset_files cfg: dict[str, Any] = {} - mocker.patch( - "nmp.customizer.tasks.training.backends.automodel.config.resolve_chat_template", - return_value="mock_template", - ) + mocker.patch(f"{CONFIG_MODULE}.resolve_chat_template", return_value="mock_template") mock_customizer_config.parallelism.pipeline_parallel_size = 1 _configure_chat_dataset(cfg, mock_customizer_config, train_file, val_file, seq_length=2048) - # Verify required fields are present assert cfg["dataset"]["_target_"] == "nemo_automodel.components.datasets.llm.chat_dataset.ChatDataset" assert cfg["dataset"]["path_or_dataset_id"] == str(train_file) assert cfg["dataset"]["seq_length"] == 2048 @@ -106,11 +95,6 @@ def test_sft_dataset_includes_split_attribute( temp_dataset_files: tuple[Path, Path], mock_customizer_config: MagicMock, ) -> None: - """Test that SFT dataset config includes 'split' attribute for sequence packing. - - The 'split' attribute is required by Automodel's pack_dataset() when sequence - packing is enabled. Without it, build_dataloader() raises AttributeError. - """ train_file, val_file = temp_dataset_files cfg: dict[str, Any] = {} @@ -125,7 +109,6 @@ def test_sft_dataset_includes_split_attribute( seq_length=2048, ) - # Verify split is set for both train and validation datasets assert "dataset" in cfg assert "validation_dataset" in cfg assert cfg["dataset"]["split"] == "train" @@ -136,7 +119,6 @@ def test_sft_dataset_includes_required_fields( temp_dataset_files: tuple[Path, Path], mock_customizer_config: MagicMock, ) -> None: - """Test that SFT dataset config includes all required fields.""" train_file, val_file = temp_dataset_files cfg: dict[str, Any] = {} @@ -151,7 +133,6 @@ def test_sft_dataset_includes_required_fields( seq_length=2048, ) - # Verify required fields are present assert ( cfg["dataset"]["_target_"] == "nemo_automodel.components.datasets.llm.column_mapped_text_instruction_dataset.ColumnMappedTextInstructionDataset" @@ -165,18 +146,8 @@ def test_sft_dataset_includes_required_fields( assert cfg["dataset"]["truncation"] == "longest_first" -AUTOCONFIG_PATCH = "transformers.AutoConfig" -MODEL_REGISTRY_PATCH = "nmp.customizer.tasks.training.backends.automodel.config.ModelRegistry" - - class TestConfigureMoeBackend: - """Tests for _configure_moe_backend function. - - Validates MoE model detection and parallelism constraints: - - MoE models get backend + parallelizer configs - - Multi-GPU MoE requires tp == 1 and ep > 1 - - Dense models and standard HF models are unaffected - """ + """Tests for _configure_moe_backend function.""" def _make_config( self, @@ -200,13 +171,9 @@ def _make_hf_config( num_local_experts: int | None = None, num_experts: int | None = None, ) -> MagicMock: - """Create a mock HF config with explicit getattr behavior for expert attributes.""" hf_config = MagicMock() hf_config.architectures = architectures - # Override getattr to match real HF config behavior: - # getattr(config, "num_local_experts", None) returns None when not set, - # not a MagicMock (which would be truthy and break MoE detection) original_getattr = type(hf_config).__getattr__ def _controlled_getattr(self, name): @@ -222,7 +189,6 @@ def _controlled_getattr(self, name): @patch(MODEL_REGISTRY_PATCH) @patch(AUTOCONFIG_PATCH) def test_moe_model_gets_backend_and_parallelizer(self, mock_autoconfig_cls, mock_registry) -> None: - """MoE models with correct parallelism get backend and parallelizer configs.""" mock_autoconfig_cls.from_pretrained.return_value = self._make_hf_config( architectures=["NemotronHForCausalLM"], num_local_experts=8, @@ -240,7 +206,6 @@ def test_moe_model_gets_backend_and_parallelizer(self, mock_autoconfig_cls, mock @patch(MODEL_REGISTRY_PATCH) @patch(AUTOCONFIG_PATCH) def test_moe_multi_gpu_tp_gt1_raises(self, mock_autoconfig_cls, mock_registry) -> None: - """MoE model on multi-GPU with tp > 1 must raise ValueError.""" mock_autoconfig_cls.from_pretrained.return_value = self._make_hf_config( architectures=["NemotronHForCausalLM"], num_local_experts=8, @@ -257,7 +222,6 @@ def test_moe_multi_gpu_tp_gt1_raises(self, mock_autoconfig_cls, mock_registry) - @patch(MODEL_REGISTRY_PATCH) @patch(AUTOCONFIG_PATCH) def test_moe_multi_gpu_ep_not_set_raises(self, mock_autoconfig_cls, mock_registry) -> None: - """MoE model on multi-GPU without ep > 1 must raise ValueError.""" mock_autoconfig_cls.from_pretrained.return_value = self._make_hf_config( architectures=["NemotronHForCausalLM"], num_local_experts=8, @@ -274,7 +238,6 @@ def test_moe_multi_gpu_ep_not_set_raises(self, mock_autoconfig_cls, mock_registr @patch(MODEL_REGISTRY_PATCH) @patch(AUTOCONFIG_PATCH) def test_moe_multi_gpu_ep_eq1_raises(self, mock_autoconfig_cls, mock_registry) -> None: - """MoE model on multi-GPU with ep == 1 must raise ValueError.""" mock_autoconfig_cls.from_pretrained.return_value = self._make_hf_config( architectures=["NemotronHForCausalLM"], num_local_experts=8, @@ -291,7 +254,6 @@ def test_moe_multi_gpu_ep_eq1_raises(self, mock_autoconfig_cls, mock_registry) - @patch(MODEL_REGISTRY_PATCH) @patch(AUTOCONFIG_PATCH) def test_moe_single_gpu_skips_multi_gpu_validation(self, mock_autoconfig_cls, mock_registry) -> None: - """MoE model on single GPU skips multi-GPU parallelism constraints.""" mock_autoconfig_cls.from_pretrained.return_value = self._make_hf_config( architectures=["NemotronHForCausalLM"], num_local_experts=8, @@ -299,7 +261,6 @@ def test_moe_single_gpu_skips_multi_gpu_validation(self, mock_autoconfig_cls, mo mock_registry.model_arch_name_to_cls = {"NemotronHForCausalLM": MagicMock()} cfg: dict[str, Any] = {"model": {}} - # Single GPU: no multi-GPU constraints apply _configure_moe_backend(cfg, self._make_config(num_gpus_per_node=1, expert_parallel_size=None)) assert cfg["model"]["backend"]["_target_"] == "nemo_automodel.components.models.common.utils.BackendConfig" @@ -307,7 +268,6 @@ def test_moe_single_gpu_skips_multi_gpu_validation(self, mock_autoconfig_cls, mo @patch(MODEL_REGISTRY_PATCH) @patch(AUTOCONFIG_PATCH) def test_dense_custom_model_no_moe_config(self, mock_autoconfig_cls, mock_registry) -> None: - """Dense models with custom implementations don't get MoE configs.""" mock_autoconfig_cls.from_pretrained.return_value = self._make_hf_config( architectures=["LlamaForCausalLM"], ) @@ -322,7 +282,6 @@ def test_dense_custom_model_no_moe_config(self, mock_autoconfig_cls, mock_regist @patch(MODEL_REGISTRY_PATCH) @patch(AUTOCONFIG_PATCH) def test_standard_hf_model_no_custom_config(self, mock_autoconfig_cls, mock_registry) -> None: - """Standard HF models not in ModelRegistry get no custom configs.""" mock_autoconfig_cls.from_pretrained.return_value = self._make_hf_config( architectures=["LlamaForCausalLM"], ) @@ -337,7 +296,6 @@ def test_standard_hf_model_no_custom_config(self, mock_autoconfig_cls, mock_regi @patch(MODEL_REGISTRY_PATCH) @patch(AUTOCONFIG_PATCH) def test_autoconfig_exception_handled_gracefully(self, mock_autoconfig_cls, _mock_registry) -> None: - """AutoConfig errors don't crash training (logged as warning).""" mock_autoconfig_cls.from_pretrained.side_effect = OSError("Model not found") cfg: dict[str, Any] = {"model": {}} @@ -349,7 +307,6 @@ def test_autoconfig_exception_handled_gracefully(self, mock_autoconfig_cls, _moc @patch(MODEL_REGISTRY_PATCH) @patch(AUTOCONFIG_PATCH) def test_moe_validation_error_propagates(self, mock_autoconfig_cls, mock_registry) -> None: - """ValueError from MoE validation is NOT swallowed by the generic except.""" mock_autoconfig_cls.from_pretrained.return_value = self._make_hf_config( architectures=["NemotronHForCausalLM"], num_local_experts=8, diff --git a/services/automodel/tests/tasks/training/test_errors.py b/services/automodel/tests/tasks/training/test_errors.py new file mode 100644 index 0000000000..6f380de1b1 --- /dev/null +++ b/services/automodel/tests/tasks/training/test_errors.py @@ -0,0 +1,273 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for nmp-automodel training error handling. + +Maps Automodel runtime exceptions to user-facing error types via error_rules.yaml. +See services/automodel/docs/automodel_errors.md for the full error catalog. +""" + +import subprocess + +from nmp.automodel.tasks.training.errors.converter import create_error_details, get_error_converter + + +class TestGetErrorConverter: + """Tests for error converter initialization.""" + + def test_converter_loads_rules(self): + converter = get_error_converter() + assert converter.rule_count > 0 + + +class TestAutomodelDatasetErrors: + """Tests for Automodel dataset error conversion.""" + + def test_unsupported_role_error(self): + original = ValueError("Unsupported role in messages: invalid_role") + details = create_error_details(original) + + assert details["type"] == "DatasetFormatError" + assert "invalid role" in details["message"].lower() + + def test_unrelated_value_error_uses_fallback(self): + original = ValueError("Something completely different") + details = create_error_details(original) + + assert details["type"] == "InternalError" + + +class TestAutomodelModelLoadErrors: + """Tests for Automodel model load error conversion.""" + + def test_weight_swap_failure(self): + original = RuntimeError("_apply(): Couldn't swap Linear.weight") + details = create_error_details(original) + + assert details["type"] == "ModelLoadError" + assert "weights could not be applied" in details["message"].lower() + + def test_patch_failure(self): + original = RuntimeError("Failed to patch model") + details = create_error_details(original) + + assert details["type"] == "ModelLoadError" + assert "optimizations" in details["message"].lower() + + def test_signature_mismatch(self): + original = AssertionError("Signature mismatch:\n original: foo\n patched : bar") + details = create_error_details(original) + + assert details["type"] == "ModelLoadError" + assert "signature" in details["message"].lower() + + def test_missing_lm_head(self): + original = ValueError("lm_head.weight not found in model") + details = create_error_details(original) + + assert details["type"] == "ModelLoadError" + assert "language model head" in details["message"].lower() + + +class TestAutomodelTrainingConfigErrors: + """Tests for Automodel training config error conversion.""" + + def test_tied_embeddings_error(self): + original = ValueError( + "Model 'test-model' is not compatible with pipeline parallelism:\n\n" + "1. tie_word_embeddings=True is not supported for pipelining." + ) + details = create_error_details(original) + + assert details["type"] == "TrainingConfigError" + assert "tied embeddings" in details["message"].lower() + + def test_encoder_decoder_error(self): + original = ValueError( + "Model 'test-model' is not compatible with pipeline parallelism:\n\n" + "1. Encoder-Decoder models with cross-attention are not supported yet." + ) + details = create_error_details(original) + + assert details["type"] == "TrainingConfigError" + assert "encoder-decoder" in details["message"].lower() + + def test_pp_batch_size_error(self): + original = AssertionError("pp_batch_size // pp_microbatch_size must be >= pp_size") + details = create_error_details(original) + + assert details["type"] == "TrainingConfigError" + assert "pipeline parallelism" in details["message"].lower() + + def test_sdpa_error(self): + original = ValueError("Model does not support SDPA required for context parallelism") + details = create_error_details(original) + + assert details["type"] == "TrainingConfigError" + assert "SDPA" in details["message"] or "context parallelism" in details["message"].lower() + + def test_triton_not_installed(self): + original = ImportError("triton is not installed. Please install it.") + details = create_error_details(original) + + assert details["type"] == "TrainingConfigError" + assert "triton" in details["message"].lower() + + def test_lora_dimensions_mismatch(self): + original = AssertionError("Incompatible X and LoRA A dimensions") + details = create_error_details(original) + + assert details["type"] == "TrainingConfigError" + assert "LoRA" in details["message"] + + +class TestAutomodelCheckpointErrors: + """Tests for Automodel checkpoint error conversion.""" + + def test_checkpoint_directory_exists(self): + original = AssertionError("Checkpoint directory /path/to/ckpt already exists") + details = create_error_details(original) + + assert details["type"] == "CheckpointError" + assert "already exists" in details["message"].lower() + + def test_global_plan_validation(self): + original = ValueError("Failed to validate global plan") + details = create_error_details(original) + + assert details["type"] == "CheckpointError" + assert "validation failed" in details["message"].lower() + + def test_missing_checkpoint_key(self): + original = RuntimeError("Missing key in checkpoint state_dict: model.layer.weight") + details = create_error_details(original) + + assert details["type"] == "CheckpointError" + assert "missing" in details["message"].lower() + + def test_moe_expert_weights_missing(self): + original = RuntimeError("Expert weights missing from checkpoint for layer 0") + details = create_error_details(original) + + assert details["type"] == "CheckpointError" + assert "MoE" in details["message"] or "expert" in details["message"].lower() + + +class TestAutomodelCudaErrors: + """Tests for Automodel CUDA error conversion.""" + + def test_cuda_oom_message(self): + original = RuntimeError("CUDA out of memory. Tried to allocate 2.00 GiB") + details = create_error_details(original) + + assert details["type"] == "CudaError" + assert "memory" in details["message"].lower() + + def test_out_of_memory_generic(self): + original = RuntimeError("out of memory") + details = create_error_details(original) + + assert details["type"] == "CudaError" + + def test_cuda_error_generic(self): + original = RuntimeError("CUDA error: device-side assert triggered") + details = create_error_details(original) + + assert details["type"] == "CudaError" + + +class TestAutomodelDistributedErrors: + """Tests for Automodel distributed error conversion.""" + + def test_distributed_not_available(self): + original = RuntimeError("torch.distributed not available") + details = create_error_details(original) + + assert details["type"] == "DistributedError" + assert "not available" in details["message"].lower() + + def test_distributed_not_initialized(self): + original = RuntimeError("expected torch.distributed to be initialized") + details = create_error_details(original) + + assert details["type"] == "DistributedError" + assert "not properly initialized" in details["message"].lower() + + def test_nccl_error(self): + original = RuntimeError("NCCL error in: ncclAllReduce") + details = create_error_details(original) + + assert details["type"] == "DistributedError" + assert "NCCL" in details["message"] + + def test_timeout_in_cause_chain(self): + timeout_exc = TimeoutError("Timed out waiting for worker") + original = RuntimeError("Distributed operation failed") + original.__cause__ = timeout_exc + + details = create_error_details(original) + + assert details["type"] == "DistributedError" + assert "timed out" in details["message"].lower() + + def test_timeout_in_nested_cause_chain(self): + timeout_exc = TimeoutError("Connection timed out") + middle_exc = ValueError("Worker communication failed") + middle_exc.__cause__ = timeout_exc + original = RuntimeError("Training failed") + original.__cause__ = middle_exc + + details = create_error_details(original) + + assert details["type"] == "DistributedError" + assert "timed out" in details["message"].lower() + + +class TestAutomodelTimeoutError: + """Tests for training timeout error conversion.""" + + def test_subprocess_timeout(self): + original = subprocess.TimeoutExpired(cmd="torchrun", timeout=3600) + details = create_error_details(original) + + assert details["type"] == "TrainingTimeoutError" + assert "time limit" in details["message"].lower() + + +class TestAutomodelInternalErrors: + """Tests for Automodel internal error conversion.""" + + def test_pipeline_missing_inputs(self): + original = ValueError("You must provide either input_ids or inputs_embeds") + details = create_error_details(original) + + assert details["type"] == "InternalError" + assert "pipeline" in details["message"].lower() + + def test_pipeline_missing_embeddings(self): + original = ValueError("inputs_embeds must be provided for pipeline stages without embed_tokens") + details = create_error_details(original) + + assert details["type"] == "InternalError" + assert "pipeline" in details["message"].lower() + + def test_moe_mesh_error(self): + original = AssertionError("We only support 1D mesh for MoE") + details = create_error_details(original) + + assert details["type"] == "ParallelismConfigError" + assert "moe" in details["message"].lower() + + def test_dtensor_placement_error(self): + original = ValueError("tensor has unsupported DTensor placement: Partial") + details = create_error_details(original) + + assert details["type"] == "ParallelismConfigError" + assert "moe" in details["message"].lower() or "expert" in details["message"].lower() + + def test_fused_loss_error(self): + original = ValueError("FusedLinearCrossEntropy requires the model to output hidden states") + details = create_error_details(original) + + assert details["type"] == "InternalError" + assert "hidden states" in details["message"].lower() diff --git a/services/automodel/tests/test_compiler.py b/services/automodel/tests/test_compiler.py index 6805b61112..306cfbfcb8 100644 --- a/services/automodel/tests/test_compiler.py +++ b/services/automodel/tests/test_compiler.py @@ -76,7 +76,7 @@ async def test_platform_job_config_compiler_sft_lora(mock_sdk, monkeypatch): "nmp.automodel.app.jobs.compiler.fetch_model_entity", AsyncMock(return_value=_make_mock_model_entity()), ) - contract_dir = Path(__file__).resolve().parents[3] / "tests" / "customizer-automodel-contract" / "input_configs" + contract_dir = Path(__file__).resolve().parent / "contract" / "input_configs" input_path = contract_dir / "llama-3.2-1b" / "llama_3_2_1b_lora.json" if not input_path.exists(): pytest.skip("contract configs not present") diff --git a/services/automodel/tests/test_contract_configs.py b/services/automodel/tests/test_contract_configs.py index 0d808033fb..6f008030bd 100644 --- a/services/automodel/tests/test_contract_configs.py +++ b/services/automodel/tests/test_contract_configs.py @@ -13,7 +13,7 @@ import pytest REPO_ROOT = Path(__file__).resolve().parents[3] -CONTRACT_DIR = REPO_ROOT / "tests" / "customizer-automodel-contract" +CONTRACT_DIR = Path(__file__).resolve().parent / "contract" GENERATE_SCRIPT = CONTRACT_DIR / "generate_configs.py" # v1 excludes embedding SFT until product expands scope. diff --git a/services/core/entities/src/nmp/core/entities/api/v2/entities/endpoints.py b/services/core/entities/src/nmp/core/entities/api/v2/entities/endpoints.py index 54d7f74b31..cfccc6be3e 100644 --- a/services/core/entities/src/nmp/core/entities/api/v2/entities/endpoints.py +++ b/services/core/entities/src/nmp/core/entities/api/v2/entities/endpoints.py @@ -236,6 +236,10 @@ async def create_entity( project=entity.project, created_by=auth_client.principal.effective_id, ) + if entity_type == ROLE_BINDING_ENTITY_TYPE: + principal = entity.data.get("principal") + if principal is not None: + await bindings_cache_delete(str(principal)) return new_entity except IntegrityError as e: error_msg = str(e.orig) if hasattr(e, "orig") else str(e) diff --git a/services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py b/services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py index f87b29a52d..8971d36127 100644 --- a/services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py +++ b/services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py @@ -29,7 +29,12 @@ from nmp.common.auth.models import Principal from nmp.core.entities.api.dependencies import AuthClientDep, EntityRepository, WorkspaceRepository from nmp.core.entities.api.v2.schemas import DeleteResponse, GenericSortField -from nmp.core.entities.api.v2.utils import ROLE_BINDING_ENTITY_TYPE, add_workspace_filtering, get_accessible_workspaces +from nmp.core.entities.api.v2.utils import ( + ROLE_BINDING_ENTITY_TYPE, + add_workspace_filtering, + bindings_cache_delete, + get_accessible_workspaces, +) from nmp.core.entities.api.v2.workspaces.schemas import ( WorkspaceInput, WorkspaceMember, @@ -244,6 +249,7 @@ async def create_workspace( "revoked_at": None, }, ) + await bindings_cache_delete(binding_principal) # Wait for Admin role to propagate if requested if wait_role_propagation: @@ -450,6 +456,10 @@ async def delete_workspace( # Delete all role bindings immediately (revoke access) deleted_bindings = await _delete_all_role_bindings(entity_repository, name) + for binding in deleted_bindings: + principal = binding.data.get("principal") + if principal is not None: + await bindings_cache_delete(str(principal)) logger.info( "Deleted role bindings for workspace deletion", extra={"workspace": name, "deleted_count": len(deleted_bindings)}, @@ -628,6 +638,8 @@ async def add_workspace_member( }, ) + await bindings_cache_delete(member.principal) + # Wait for roles to propagate if requested if wait_role_propagation and member.roles: for role in member.roles: @@ -790,6 +802,8 @@ async def update_workspace_member( }, ) + await bindings_cache_delete(principal_id) + # Wait for roles to propagate if requested if wait_role_propagation: # Wait for all added roles to be granted @@ -922,6 +936,8 @@ async def remove_workspace_member( }, ) + await bindings_cache_delete(principal_id) + # Wait for roles to be revoked if requested if wait_role_propagation and revoked_roles: for role in revoked_roles: diff --git a/services/customizer/pyproject.toml b/services/customizer/pyproject.toml index 966fb3c9c2..99e8a2f5e0 100644 --- a/services/customizer/pyproject.toml +++ b/services/customizer/pyproject.toml @@ -20,9 +20,6 @@ test = [ "pytest-asyncio>=0.23.0", ] -[project.scripts] -customizer-server = "nmp.customizer.main:run_standalone" - [tool.uv.sources] nmp-common = { workspace = true } nmp-testing = { workspace = true } diff --git a/services/customizer/src/nmp/customizer/app/jobs/training/compiler.py b/services/customizer/src/nmp/customizer/app/jobs/training/compiler.py index 2d2e35423c..463de2c978 100644 --- a/services/customizer/src/nmp/customizer/app/jobs/training/compiler.py +++ b/services/customizer/src/nmp/customizer/app/jobs/training/compiler.py @@ -16,6 +16,7 @@ ResourcesSpec, StepLifecycle, ) +from nemo_platform_plugin.jobs.exceptions import PlatformJobCompilationError from nemo_platform_plugin.jobs.image import get_qualified_image from nmp.common.model_utils import is_embedding_model from nmp.customizer.api.v2.jobs.schemas import ( @@ -412,21 +413,20 @@ def _extract_model_name(job_spec: CustomizationJobOutput) -> str | None: def _determine_backend(job_spec: CustomizationJobOutput) -> TrainingBackend: """Determine which backend to use based on the training type. - Decision logic: - 1. DPO → nemo_rl (RL training requires nemo-rl library) - 2. Everything else (SFT, Distillation) → automodel + Legacy customizer only compiles DPO jobs (nemo_rl). SFT, LoRA, and distillation + are handled by the nmp-automodel plugin. """ if TrainingType(job_spec.training.type) == TrainingType.DPO: return TrainingBackend.NEMO_RL - return TrainingBackend.AUTOMODEL + raise PlatformJobCompilationError( + f"Training type '{job_spec.training.type}' is not supported by legacy customizer. " + "Use the nmp-automodel plugin for SFT, LoRA, and distillation jobs." + ) def _get_training_image(backend: TrainingBackend) -> str: """Get the training image for a backend.""" - if backend == TrainingBackend.AUTOMODEL: - return config.training_automodel_image or get_qualified_image("customizer-automodel") - elif backend == TrainingBackend.NEMO_RL: + if backend == TrainingBackend.NEMO_RL: return config.training_rl_image or get_qualified_image("customizer-rl") - else: - raise ValueError(f"No training image configured for backend: {backend}") + raise ValueError(f"No training image configured for backend: {backend}") diff --git a/services/customizer/src/nmp/customizer/config.py b/services/customizer/src/nmp/customizer/config.py index 4dfe91c3ea..19f412aef4 100644 --- a/services/customizer/src/nmp/customizer/config.py +++ b/services/customizer/src/nmp/customizer/config.py @@ -24,12 +24,6 @@ class CustomizerConfig(create_service_config_class("customizer")): # type: igno description="Enable debug mode", ) - # Container image overrides for training tasks. - training_automodel_image: str | None = Field( - default=None, - description="Override container image for Automodel training. If not set, uses platform defaults.", - ) - # Container image overrides for training tasks. training_rl_image: str | None = Field( default=None, diff --git a/services/customizer/src/nmp/customizer/main.py b/services/customizer/src/nmp/customizer/main.py index f77d9122c5..e8884e183d 100644 --- a/services/customizer/src/nmp/customizer/main.py +++ b/services/customizer/src/nmp/customizer/main.py @@ -3,18 +3,7 @@ """Customizer service entry point.""" -import uvicorn -from nmp.customizer.config import config from nmp.customizer.service import CustomizerService # Global service instance for platform integration service = CustomizerService() - - -def run_standalone(): - """Run the customizer service as a standalone server.""" - uvicorn.run(service.app, host="0.0.0.0", port=config.port) - - -if __name__ == "__main__": - run_standalone() diff --git a/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/__init__.py b/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/__init__.py deleted file mode 100644 index 13ea2859f5..0000000000 --- a/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import warnings - -from pydantic.warnings import UnsupportedFieldAttributeWarning - -warnings.filterwarnings("ignore", category=UnsupportedFieldAttributeWarning) - -warnings.filterwarnings( - "ignore", - category=UserWarning, - module="torch.distributed.device_mesh", -) - -warnings.filterwarnings( - "ignore", - category=UserWarning, - module="nemo_automodel.components.moe.state_dict_utils", -) diff --git a/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/backend.py b/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/backend.py deleted file mode 100644 index 107663b82c..0000000000 --- a/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/backend.py +++ /dev/null @@ -1,194 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import logging -import signal -import subprocess -import threading -import time -from collections import deque -from pathlib import Path -from typing import Any, Optional - -from nmp.customizer.app.jobs.context import NMPJobContext -from nmp.customizer.tasks.training.errors.parser import ( - MAX_OUTPUT_LINES, - parse_error_from_output, - read_subprocess_output, -) -from nmp.customizer.tasks.training.progress import JobsServiceProgressReporter -from nmp.customizer.tasks.training.protocol import LibraryConfig, TrainingBackend -from nmp.customizer.tasks.training.schemas import ( - CheckpointInfo, - TrainingMetrics, - TrainingStepConfig, -) -from nmp.customizer.tasks.training.schemas import ( - TrainingBackend as TrainingBackendEnum, -) -from nmp.customizer.tasks.training.utils import generate_torchrun_flags_from_env - -from .checkpoints import ModelType, find_best_checkpoint, process_checkpoint -from .config import compile_automodel_config - -logger = logging.getLogger(__name__) - - -class AutomodelBackend(TrainingBackend): - """ - Implements the TrainingBackend protocol for nemo-automodel library. - """ - - def __init__(self, job_ctx: NMPJobContext): - self.job_ctx = job_ctx - - @property - def backend_type(self) -> TrainingBackendEnum: - return TrainingBackendEnum.AUTOMODEL - - def compile_config( - self, - config: TrainingStepConfig, - workspace_dir: Path, - ) -> dict[str, Any]: - """ - Compile Automodel-specific configuration. - - Pure transformation - no file I/O. The runner handles writing to disk. - """ - return compile_automodel_config(config, workspace_dir, self.job_ctx) - - def execute_training( - self, - customizer_config: TrainingStepConfig, - library_config: LibraryConfig, - progress: JobsServiceProgressReporter, - ) -> TrainingMetrics: - """Execute training using CustomizerTrainFinetuneRecipe or CustomizerBiencoderRecipe. - - The config file has already been written to disk by the runner. - Progress reporting happens within the training subprocess via - TrainingProgressCallback, which reads job context from environment - variables. - """ - progress.report_running("training", backend=self.backend_type.value) - - # Run training with our custom recipe - # Note: The progress parameter is not passed to run_training_with_customizer_recipe - # because progress reporting now happens inside the subprocess via - # TrainingProgressCallback using environment variables. - command = ["torchrun"] - command.extend(generate_torchrun_flags_from_env()) - command.extend( - [ - "-m", - "nmp.customizer.tasks.training.backends.automodel.finetune", - "--config", - str(library_config.config_path), - ] - ) - - logger.info(f"Executing: {' '.join(command)}") - - training_process: subprocess.Popen | None = None - - # Rolling buffer to keep recent output lines for error extraction - output_lines: deque[str] = deque(maxlen=MAX_OUTPUT_LINES) - reader_thread: threading.Thread | None = None - - def cleanup(signum, frame): - logger.warning(f"Signal {signum} received, terminating...") - if training_process: - training_process.send_signal(signum) - try: - training_process.wait(timeout=30) - except subprocess.TimeoutExpired: - training_process.kill() - raise SystemExit(signum) - - signal.signal(signal.SIGINT, cleanup) - signal.signal(signal.SIGTERM, cleanup) - - start_time = time.time() - - training_process = subprocess.Popen( - command, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - bufsize=1, # Line buffered - ) - - # Start reader thread to capture output without blocking - reader_thread = threading.Thread( - target=read_subprocess_output, - args=(training_process, output_lines), - daemon=True, - ) - reader_thread.start() - - try: - training_process.wait(timeout=customizer_config.training_timeout) - except subprocess.TimeoutExpired: - logger.exception("Training timed out") - training_process.kill() - # Reap the killed process to avoid zombies - try: - training_process.wait(timeout=30) - except subprocess.TimeoutExpired: - logger.warning( - "Killed training process did not terminate within 30s - " - "process may be stuck in uninterruptible state" - ) - # Wait for reader thread to capture any remaining output before re-raising - if reader_thread and reader_thread.is_alive(): - reader_thread.join(timeout=5) - raise # Let runner.py convert via create_error_details() - - # Wait for reader thread to finish capturing output - if reader_thread and reader_thread.is_alive(): - reader_thread.join(timeout=5) - - duration = time.time() - start_time - logger.info(f"Training finished in {duration:.1f} seconds") - - if training_process.returncode != 0: - parsed = parse_error_from_output(output_lines, training_process.returncode) - raise parsed.to_exception() - - # Return empty metrics (actual metrics are reported via callbacks during training) - # TODO: Consider parsing training logs or checkpoints to extract final metrics. - return TrainingMetrics(total_steps=0, total_epochs=0) - - def find_best_checkpoint( - self, - workspace_dir: Path, - customizer_config: TrainingStepConfig, - library_config: Optional[LibraryConfig] = None, - ) -> Path: - """Find best Automodel checkpoint.""" - model_type = ModelType.EMBEDDING if customizer_config.model.is_embedding_model else ModelType.LLM - return find_best_checkpoint(workspace_dir, customizer_config, model_type=model_type) - - def process_checkpoint( - self, - checkpoint_path: Path, - output_path: Path, - customizer_config: TrainingStepConfig, - library_config: LibraryConfig | None = None, - ) -> CheckpointInfo: - """Process Automodel checkpoint.""" - model_type = ModelType.EMBEDDING if customizer_config.model.is_embedding_model else ModelType.LLM - - # Extract resolved chat template from library config if available (LLM only) - resolved_template = None - if model_type == ModelType.LLM and library_config and library_config.config_dict: - resolved_template = library_config.config_dict.get("_resolved_chat_template") - - return process_checkpoint( - checkpoint_path, - output_path, - customizer_config, - model_type=model_type, - resolved_chat_template=resolved_template, - ) diff --git a/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/checkpoints.py b/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/checkpoints.py deleted file mode 100644 index 5d49d1f8d9..0000000000 --- a/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/checkpoints.py +++ /dev/null @@ -1,522 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -""" -Checkpoint processing for Automodel backend. - -This module handles: -- Finding the best checkpoint after training -- LoRA adapter merging -- Chat template preservation -- FSDP2 architecture fix -- HF export and format conversion -- ONNX export for embedding models - -Supports both LLM and embedding (biencoder) models through unified functions. -""" - -import json -import logging -import re -import shutil -from enum import StrEnum -from pathlib import Path - -from nmp.customizer.tasks.training.chat_templates import ( - apply_chat_template_to_checkpoint, - resolve_chat_template, -) -from nmp.customizer.tasks.training.schemas import ( - CheckpointFormat, - CheckpointInfo, - FinetuningType, - Precision, - TrainingStepConfig, -) - -logger = logging.getLogger(__name__) - - -class ModelType(StrEnum): - """Type of model for checkpoint processing.""" - - LLM = "llm" - EMBEDDING = "embedding" - - -def extract_precision_from_model_config(model_path: str | Path) -> Precision | None: - """ - Extract precision from a HuggingFace model's config.json. - - HuggingFace models store their torch_dtype in config.json (e.g., "bfloat16"). - This function reads that value and maps it to our Precision enum. - - This is used to determine the actual training precision when "auto" was used - for torch_dtype. The precision comes from the base model's config, not from - the output checkpoint (which may only contain adapter weights for LoRA). - - Args: - model_path: Path to the model directory containing config.json - - Returns: - Precision enum value if found, None otherwise - """ - config_path = Path(model_path) / "config.json" - if not config_path.exists(): - logger.warning(f"config.json not found at {config_path}, cannot extract precision") - return None - - try: - with open(config_path, "r") as f: - config = json.load(f) - - torch_dtype = config.get("torch_dtype") - if torch_dtype is None: - logger.warning("torch_dtype not found in config.json") - return None - - try: - precision = Precision.from_hf_dtype(torch_dtype) - logger.info(f"Extracted precision from model config: {torch_dtype} -> {precision.value}") - return precision - except ValueError: - logger.warning(f"Unknown torch_dtype '{torch_dtype}' in config.json, cannot map to Precision") - return None - - except (json.JSONDecodeError, IOError) as e: - logger.warning(f"Failed to read config.json: {e}") - return None - - -def extract_step_number(path: Path) -> int: - """Extract step number from directory name like 'epoch_0_step_99'""" - match = re.search(r"step_(\d+)", path.name) - return int(match.group(1)) if match else -1 - - -def get_model_dir_from_checkpoint(checkpoint_dir: Path, is_peft: bool) -> Path: - """ - Extract model directory from checkpoint directory. - """ - if is_peft: - # For LoRA, checkpoint is saved directly under model/ directory - model_dir = checkpoint_dir / "model" - if model_dir.exists() and model_dir.is_dir(): - logger.info(f"Found LoRA checkpoint at: {model_dir}") - return model_dir.resolve() - else: - # For full-sft, check for consolidated directory first - consolidated_dir = checkpoint_dir / "model" / "consolidated" - if consolidated_dir.exists() and consolidated_dir.is_dir(): - logger.info(f"Found consolidated checkpoint at: {consolidated_dir}") - return consolidated_dir.resolve() - - # Fallback to model/ directory if consolidated doesn't exist - model_dir = checkpoint_dir / "model" - if model_dir.exists() and model_dir.is_dir(): - logger.info(f"Found sharded checkpoint at: {model_dir}") - return model_dir.resolve() - - raise FileNotFoundError(f"Model directory not found in checkpoint {checkpoint_dir}") - - -def find_best_checkpoint( - workspace_dir: Path, - config: TrainingStepConfig, - model_type: ModelType = ModelType.LLM, -) -> Path: - """ - Find the best checkpoint directory. - """ - base_dir = workspace_dir / "checkpoints" - is_peft = config.training.finetuning_type in (FinetuningType.LORA, FinetuningType.LORA_MERGED) - type_label = "embedding" if model_type == ModelType.EMBEDDING else "" - - # Order of preference: - # 1. LOWEST_VAL symlink - # 2. LATEST symlink - # 3. Highest step number - - for link_name in ["LOWEST_VAL", "LATEST"]: - link = base_dir / link_name - if link.exists() and link.is_symlink(): - try: - target = link.resolve() - if target.exists(): - logger.info(f"Using {link_name} {type_label} checkpoint: {target.name}".replace(" ", " ")) - return get_model_dir_from_checkpoint(target, is_peft) - except Exception as e: - logger.warning(f"Failed to resolve {link_name} symlink: {e}") - - # Fallback: scan directories - epoch_step_dirs = list(base_dir.glob("epoch_*_step_*")) - if not epoch_step_dirs: - raise FileNotFoundError(f"No {type_label} checkpoint directories found in {base_dir}".replace(" ", " ")) - - best_checkpoint = max(epoch_step_dirs, key=extract_step_number) - logger.info(f"Using latest {type_label} checkpoint by step number: {best_checkpoint.name}".replace(" ", " ")) - return get_model_dir_from_checkpoint(best_checkpoint, is_peft) - - -def fix_fsdp2_architecture(model_path: Path) -> None: - """ - Fix FSDP2 architecture naming issue in HuggingFace config. - - FSDP2 adds "FSDP" prefix to architecture names (e.g., "FSDPLlamaForCausalLM" - instead of "LlamaForCausalLM"). This function removes that prefix to ensure - the checkpoint is compatible with standard HuggingFace/vLLM loading. - - Reference: https://github.com/huggingface/transformers/commit/dc262ee6f57f2154f5233e53482da14dbe3be834 - """ - config_path = model_path / "config.json" - if not config_path.exists(): - logger.warning(f"config.json not found at {config_path}, skipping FSDP2 fix") - return - - with open(config_path, "r") as f: - config = json.load(f) - - if "architectures" not in config: - return - - original_archs = config["architectures"] - fixed_archs = [arch.removeprefix("FSDP") for arch in original_archs] - - if original_archs != fixed_archs: - config["architectures"] = fixed_archs - with open(config_path, "w") as f: - json.dump(config, f, indent=2) - logger.info(f"Fixed FSDP2 architecture names: {original_archs} -> {fixed_archs}") - - -def merge_lora_adapter( - adapter_path: Path, - base_model_path: str, - output_path: Path, -) -> None: - """ - Merge LoRA adapter weights into the base model. - - Uses HuggingFace's PEFT library to: - 1. Load the base model - 2. Attach the LoRA adapter - 3. Merge weights using merge_and_unload() - 4. Save as a standard HuggingFace checkpoint - - Note: This function only supports LLM models. For embedding models, - use merge_lora_embedding_adapter() instead. - - Args: - adapter_path: Path to the LoRA adapter checkpoint - base_model_path: Path to the base model (for loading weights) - output_path: Where to save the merged model - """ - try: - import torch - from peft import PeftModel - from transformers import AutoModelForCausalLM, AutoTokenizer - except ImportError as e: - raise ImportError( - "LoRA merge requires 'peft' and 'transformers' packages. Ensure they are installed in the container." - ) from e - - logger.info(f"Merging LoRA adapter from {adapter_path} with base model {base_model_path}") - - # Use scratch directory if available for better I/O performance - tmp_path = Path("/scratch/merged_lora") if Path("/scratch").is_dir() else Path("/tmp/merged_lora") - shutil.rmtree(tmp_path, ignore_errors=True) - tmp_path.mkdir(parents=True, exist_ok=True) - - try: - # 1. Load base model in mergeable dtype (not quantized) - logger.info("Loading base model...") - model = AutoModelForCausalLM.from_pretrained( - base_model_path, - torch_dtype=torch.bfloat16, - device_map="auto", - trust_remote_code=True, - ) - - # 2. Attach the LoRA adapter - logger.info("Loading LoRA adapter...") - model = PeftModel.from_pretrained(model, str(adapter_path)) - - # 3. Merge LoRA weights into base model - logger.info("Merging LoRA weights...") - model = model.merge_and_unload() - - # 4. Save merged model - logger.info(f"Saving merged model to {tmp_path}...") - model.save_pretrained(tmp_path, safe_serialization=True) - - # 5. Save tokenizer from base model - tokenizer = AutoTokenizer.from_pretrained(base_model_path, trust_remote_code=True) - tokenizer.save_pretrained(tmp_path) - - # 6. Copy to output path - output_path.mkdir(parents=True, exist_ok=True) - shutil.copytree(tmp_path, output_path, dirs_exist_ok=True) - - logger.info(f"Successfully merged LoRA adapter to {output_path}") - - finally: - # Cleanup temp directory - shutil.rmtree(tmp_path, ignore_errors=True) - - -def merge_lora_embedding_adapter( - adapter_path: Path, - base_model_path: str, - output_path: Path, -) -> None: - """Merge a LoRA adapter into a base embedding model. - - This intentionally mirrors the logic in Automodel's `tools/merge_lora.py`, - but is implemented locally because the customizer container may not have - that module on `PYTHONPATH`. - - Args: - adapter_path: Path to the PEFT adapter directory. - base_model_path: HuggingFace model name or path for the base encoder. - output_path: Where to write the merged model. - """ - try: - import gc - - import torch - from peft import PeftModel - from transformers import AutoModel, AutoTokenizer - except ImportError as e: - raise ImportError( - "LoRA merge requires 'peft' and 'transformers' packages. Ensure they are installed in the container." - ) from e - - logger.info("Merging embedding LoRA adapter from %s with base model %s", adapter_path, base_model_path) - - # Use scratch directory if available for better I/O performance - tmp_path = Path("/scratch/merged_lora") if Path("/scratch").is_dir() else Path("/tmp/merged_lora") - shutil.rmtree(tmp_path, ignore_errors=True) - tmp_path.mkdir(parents=True, exist_ok=True) - - try: - logger.info("Loading base model (AutoModel): %s", base_model_path) - model = AutoModel.from_pretrained( - base_model_path, - torch_dtype=torch.float16, - device_map="auto", - trust_remote_code=True, - ) - - logger.info("Loading adapter from %s", adapter_path) - model = PeftModel.from_pretrained(model, str(adapter_path)) - - logger.info("Merging adapter into base model") - model = model.merge_and_unload() - - logger.info("Saving merged model to %s", tmp_path) - model.save_pretrained(tmp_path, safe_serialization=True) - - try: - tokenizer = AutoTokenizer.from_pretrained(base_model_path, trust_remote_code=True) - tokenizer.save_pretrained(tmp_path) - logger.info("Tokenizer saved to %s", tmp_path) - except Exception as e: - logger.warning("Could not save tokenizer: %s", e) - - output_path.mkdir(parents=True, exist_ok=True) - shutil.copytree(tmp_path, output_path, dirs_exist_ok=True) - logger.info("Successfully merged embedding LoRA adapter to %s", output_path) - - finally: - shutil.rmtree(tmp_path, ignore_errors=True) - try: - del model - except Exception: - pass - torch.cuda.empty_cache() - gc.collect() - - -def export_onnx( - model_path: Path, - output_path: Path, - tokenizer_path: str, -) -> Path: - """Export an embedding model to ONNX format. - - Uses Automodel's export_to_onnx to export to ONNX format. - The resulting `model.onnx` is written into *output_path* alongside - the existing HuggingFace checkpoint files. - - Args: - model_path: Path to the HuggingFace model directory (config.json + weights). - output_path: Directory where ``model.onnx`` will be written. - tokenizer_path: Fallback tokenizer location (base model path). Used when - the checkpoint directory does not contain tokenizer files. - - Returns: - Path to the exported ``model.onnx`` file. - """ - # need to import here for the tests - from nemo_automodel.components.models.biencoder.export_onnx import export_to_onnx - - logger.info(f"Exporting embedding model at path {model_path} to ONNX format at path {output_path}") - - try: - onnx_path = export_to_onnx( - model_path=str(model_path), - output_dir=str(output_path), - tokenizer_path=tokenizer_path, - pooling="avg", - normalize=True, - opset=17, - export_dtype="fp16", - verify=True, - ) - except Exception: - logger.exception(f"ONNX export failed for model at {model_path}") - raise - - logger.info(f"ONNX model exported to {onnx_path}") - return Path(onnx_path) - - -_ONNX_TOP_LEVEL_PATTERNS = {"model.onnx", "model.onnx.data", "tokenizer"} - - -def _restructure_embedding_output(output_path: Path) -> None: - """Move HF artifacts into ``alternates/hf/`` so the NIM selects the ONNX profile. - - NIM scans the top-level directory to choose the model backend. If it sees - ``.safetensors`` files it creates a PyTorch profile, which is unsupported - for custom models in many NIM versions. The legacy customizer kept only - ``model.onnx`` (+ tokenizer/) at the root and placed HF weights under - ``alternates/hf/``. This function reproduces that layout. - """ - alternates_hf = output_path / "alternates" / "hf" - alternates_hf.mkdir(parents=True, exist_ok=True) - - for entry in list(output_path.iterdir()): - if entry.name in _ONNX_TOP_LEVEL_PATTERNS or entry.name == "alternates": - continue - dest = alternates_hf / entry.name - logger.info("Moving %s -> %s", entry, dest) - shutil.move(str(entry), str(dest)) - - logger.info("Restructured embedding output: ONNX at top level, HF in alternates/hf/") - - -def process_checkpoint( - checkpoint_path: Path, - output_path: Path, - customizer_config: TrainingStepConfig, - model_type: ModelType = ModelType.LLM, - resolved_chat_template: str | None = None, -) -> CheckpointInfo: - """ - Process checkpoint to standard output format. - - Works for both LLM and embedding (biencoder) models. - - Handles three scenarios: - 1. Full weights training: Copy checkpoint, fix FSDP2 arch, preserve chat template (LLM only) - 2. LoRA (unmerged): Copy adapter, preserve format as hf-peft - 3. LoRA merged: Merge adapter with base model, output as standard HF - - Args: - checkpoint_path: Path to the checkpoint directory (model files) - output_path: Where to write the processed checkpoint - customizer_config: Training configuration with model paths and settings - model_type: Type of model ("llm" or "embedding") - resolved_chat_template: Pre-resolved chat template from training config (LLM only). - If provided, this template is used. Otherwise, falls back to - priority-based resolution using model.name and model.path. - - Returns: - CheckpointInfo with output path, format, and precision - """ - output_path.mkdir(parents=True, exist_ok=True) - - finetuning_type = customizer_config.training.finetuning_type - base_model_path = customizer_config.model.path - is_embedding = model_type == ModelType.EMBEDDING - type_label = "embedding" if is_embedding else "" - - # Resolve chat template using the same priority logic as training: - # 1. Use pre-resolved template if provided (ensures consistency with training) - # 2. Otherwise, resolve using priority-based selection - chat_template: str | None = None - if not is_embedding: - if resolved_chat_template is not None: - chat_template = resolved_chat_template - logger.info("Using pre-resolved chat template from training config") - else: - # Fall back to priority-based resolution (user_template from fileset metadata takes priority) - chat_template = resolve_chat_template( - model_path=base_model_path, - model_name=customizer_config.model.name, - user_template=customizer_config.model.chat_template, - ) - - if finetuning_type == FinetuningType.LORA_MERGED: - # LoRA merged: merge adapter weights into base model - # For embedding models, this produces a full-weight model compatible with ONNX export and NIM serving. - if is_embedding: - merge_lora_embedding_adapter( - adapter_path=checkpoint_path, - base_model_path=base_model_path, - output_path=output_path, - ) - else: - merge_lora_adapter( - adapter_path=checkpoint_path, - base_model_path=base_model_path, - output_path=output_path, - ) - checkpoint_format = CheckpointFormat.HF - - # Fix FSDP2 architecture naming - fix_fsdp2_architecture(output_path) - # Apply chat template for LLM models only - if chat_template: - apply_chat_template_to_checkpoint(output_path, chat_template) - - elif finetuning_type == FinetuningType.LORA: - # LoRA unmerged: just copy the adapter files - logger.info(f"Copying {type_label} LoRA adapter from {checkpoint_path} to {output_path}".replace(" ", " ")) - shutil.copytree(checkpoint_path, output_path, dirs_exist_ok=True) - checkpoint_format = CheckpointFormat.HF_PEFT - # Note: For hf-peft, chat template is inherited from base model at inference time - - else: - # Full weights training: copy and process - logger.info( - f"Copying {type_label} full weights checkpoint from {checkpoint_path} to {output_path}".replace(" ", " ") - ) - shutil.copytree(checkpoint_path, output_path, dirs_exist_ok=True) - checkpoint_format = CheckpointFormat.HF - - # Fix FSDP2 architecture naming - fix_fsdp2_architecture(output_path) - # Apply chat template for LLM models only - if chat_template: - apply_chat_template_to_checkpoint(output_path, chat_template) - - if is_embedding: - export_onnx( - model_path=output_path, - output_path=output_path, - tokenizer_path=base_model_path, - ) - _restructure_embedding_output(output_path) - - # Determine precision: use explicit config value, or extract from base model - precision = customizer_config.model.precision - if precision is None: - precision = extract_precision_from_model_config(customizer_config.model.path) - - return CheckpointInfo( - path=str(output_path), - format=checkpoint_format, - precision=precision, - ) diff --git a/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/config.py b/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/config.py deleted file mode 100644 index b369eb5d19..0000000000 --- a/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/config.py +++ /dev/null @@ -1,848 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -""" -Automodel configuration compiler. - -This module transforms the standardized TrainingStepConfig into the format -expected by nemo_automodel's TrainFinetuneRecipeForNextTokenPrediction -or KnowledgeDistillationRecipeForNextTokenPrediction. -""" - -import logging -import os -from pathlib import Path -from typing import Any - -from nemo_automodel._transformers.registry import ModelRegistry -from nmp.customizer.app.jobs.context import NMPJobContext -from nmp.customizer.tasks.training.chat_templates import resolve_chat_template -from nmp.customizer.tasks.training.datasets.preparation import ( - DatasetSchema, - PreparedDataset, - compute_val_check_interval, - detect_dataset_schema, - prepare_dataset, -) -from nmp.customizer.tasks.training.datasets.validation import DatasetValidator -from nmp.customizer.tasks.training.integrations import ( - build_mlflow_config, - build_wandb_config, -) -from nmp.customizer.tasks.training.schemas import ( - EmbeddingConfig, - FinetuningType, - LoRAConfig, - TrainingStepConfig, - TrainingType, -) -from nmp.customizer.tasks.training.sequence_packing import ( - calculate_optimal_pack_size, - estimate_dataset_sequence_lengths, -) - -logger = logging.getLogger(__name__) - - -def compile_automodel_config( - customizer_config: TrainingStepConfig, - workspace_dir: Path, - job_ctx: NMPJobContext, -) -> dict[str, Any]: - """ - Compile Automodel-specific configuration. - - This transforms the standardized TrainingStepConfig into the format - expected by nemo_automodel's TrainFinetuneRecipeForNextTokenPrediction. - """ - cfg: dict[str, Any] = {} - _is_embedding_model = customizer_config.model.is_embedding_model - trust_remote_code = customizer_config.model.trust_remote_code - embedding_config = EmbeddingConfig() - - # === Distributed Environment === - # Required for torch.distributed initialization - cfg["dist_env"] = { - "backend": "nccl", - "timeout_minutes": 30, # Higher timeout for large model loading - } - - # === Random Number Generator === - # Both recipes use StatefulRNG for reproducibility across restarts and multi-node training, - # but they expect the config in different formats: - # - Biencoder recipe: expects cfg["seed"] and creates StatefulRNG internally - # - LLM recipe: expects cfg["rng"] with full StatefulRNG config - seed = int(os.environ.get("PL_GLOBAL_SEED", customizer_config.seed)) - - if _is_embedding_model: - # Biencoder recipe creates StatefulRNG from seed value internally - # See: nemo_automodel/recipes/biencoder/train_biencoder.py - cfg["seed"] = seed - else: - # LLM recipe expects the full rng config object - cfg["rng"] = { - "_target_": "nemo_automodel.components.training.rng.StatefulRNG", - "seed": seed, - "ranked": True, # Different seed per rank for data augmentation - } - - # === Model Configuration === - # Common fields shared by both embedding and causal LM models - cfg["model"] = { - "pretrained_model_name_or_path": customizer_config.model.path, - "torch_dtype": customizer_config.model.precision.to_torch_dtype() - if customizer_config.model.precision - else "auto", - # trust_remote_code is required for models like nvidia/llama-nemotron-embed-1b-v2 - # which use custom model_type "llama_bidirec" with custom modeling code. - "trust_remote_code": trust_remote_code, - } - if customizer_config.model.override_custom_impl: - cfg["model"]["force_hf"] = True - - if _is_embedding_model: - cfg["model"].update( - { - "_target_": "nemo_automodel.components.models.biencoder.NeMoAutoModelBiencoder.from_pretrained", - # Use the same encoder for both queries and passages. default value taken from Automodel example - "share_encoder": True, - # Add a trainable linear layer after pooling to reduce embedding dimension. default value taken from Automodel example - "add_linear_pooler": False, - # How to combine token embeddings into a single document/query embedding. default value taken from Automodel example - "pooling": "avg", - # Normalize embeddings to unit length (length = 1). default value taken from Automodel example - "l2_normalize": True, - # When training an embedding model, we want it to learn that similar things should have similar embeddings - # and different things should have different embeddings. - # Temperature controls how "strict" the model is when learning these relationships. - # Low value (0.02), tells the model to pick the correct doc and penalizes near-misses. - # High value (like 1.0 that's Automodel default) tells the model to be more lenient and allows for near-misses. - # 0.02 is taken from the Automodel example for biencoder training. - "t": 0.02, - # Total number of passages per query during training: 1 positive + (n-1) negatives. - # For example, train_n_passages=5 means 1 positive and 4 negative passages per query. - # This differs from legacy Customizer's 'num_hard_negatives' which only counted negatives - # (num_hard_negatives=4 is equivalent to train_n_passages=5). - "train_n_passages": embedding_config.train_n_passages, - # Number of negative passages per query during validation. - "eval_negative_size": get_eval_negative_size(embedding_config), - # Gradient checkpointing saves memory by not storing all activations during forward pass. - # Instead, it recomputes them during backward pass with a memory trade-off - less memory, slower training. - # Useful for large models or limited GPU memory. - # TODO: consider exposing this in CustomizationJobInput - "do_gradient_checkpointing": embedding_config.do_gradient_checkpointing, - "use_liger_kernel": True, - "use_sdpa_patching": True, - } - ) - - # === Tokenizer === - cfg["tokenizer"] = { - "_target_": "nemo_automodel._transformers.auto_tokenizer.NeMoAutoTokenizer.from_pretrained", - "pretrained_model_name_or_path": customizer_config.model.path, - } - else: - cfg["model"].update( - { - "_target_": "nemo_automodel.NeMoAutoModelForCausalLM.from_pretrained", - "attn_implementation": customizer_config.model.attn_implementation, - } - ) - - # === Distributed Configuration === - p = customizer_config.parallelism - total_gpus = p.num_nodes * p.num_gpus_per_node - # Note dp_size is typically auto-derived by Automodel (world_size / (tp * pp * cp)), - # but we calculate it explicitly here because: - # 1. It's validated upstream in validators.py - # 2. We need it for warmup_steps validation below - # 3. Passing an explicit value ensures consistency rather than relying on Automodel's derivation - dp = total_gpus // (p.tensor_parallel_size * p.pipeline_parallel_size * p.context_parallel_size) - - cfg["distributed"] = { - "_target_": "nemo_automodel.components.distributed.fsdp2.FSDP2Manager", - "dp_size": dp, - "tp_size": p.tensor_parallel_size, - "pp_size": p.pipeline_parallel_size, - "cp_size": p.context_parallel_size, - "ep_size": p.expert_parallel_size, - "sequence_parallel": p.sequence_parallel, - } - if p.pipeline_parallel_size > 1: - cfg["distributed"]["pipeline"] = { - "pp_schedule": "interleaved1f1b", - "pp_microbatch_size": 1, - "scale_grads_in_schedule": False, - } - - # === Dataset Preparation === - # Discover, merge, and optionally split dataset files - prepared = prepare_dataset( - dataset_path=Path(customizer_config.dataset.path), - output_dir=workspace_dir / "dataset", - seed=customizer_config.seed, - ) - logger.info( - f"Prepared dataset: train={prepared.train_samples} samples, validation={prepared.validation_samples} samples, files: " - f"train={prepared.train_file.absolute()}, validation={prepared.validation_file.absolute()}" - ) - validator = DatasetValidator(training_type=customizer_config.training.training_type) - validator.validate_dataset(str(prepared.train_file)) - validator.validate_dataset(str(prepared.validation_file)) - logger.info("Validated datasets successfully") - - # === Step Scheduler (with val_check_interval conversion) === - batch_size = customizer_config.batch.global_batch_size - epochs = customizer_config.schedule.epochs - - # Compute steps per epoch (round up to ensure all samples are used) - steps_per_epoch = (prepared.train_samples + batch_size - 1) // batch_size - total_steps = steps_per_epoch * epochs - - # Determine effective max_steps - user_max_steps = customizer_config.schedule.max_steps - if user_max_steps and user_max_steps > 0: - max_steps = min(user_max_steps, total_steps) - else: - max_steps = total_steps - - logger.info( - f"Training schedule: {prepared.train_samples} samples, batch_size={batch_size}, " - f"steps_per_epoch={steps_per_epoch}, epochs={epochs}, max_steps={max_steps}" - ) - - cfg["step_scheduler"] = { - "global_batch_size": batch_size, - "local_batch_size": customizer_config.batch.micro_batch_size, - "max_steps": max_steps, - "num_epochs": epochs, - } - - val_every_steps = compute_val_check_interval( - steps_per_epoch=steps_per_epoch, - max_steps=max_steps, - val_check_interval=customizer_config.schedule.val_check_interval, - ) - cfg["step_scheduler"]["val_every_steps"] = val_every_steps - cfg["step_scheduler"]["ckpt_every_steps"] = val_every_steps - logger.info(f"Validation interval: {customizer_config.schedule.val_check_interval} -> {val_every_steps} steps") - - # === Validate warmup_steps === - # Automodel requires: lr_warmup_steps < lr_decay_steps (scheduler.py line 96) - # lr_decay_steps = total_optimizer_steps (accounting for gradient accumulation) - warmup_steps = customizer_config.optimizer.warmup_steps - if warmup_steps > 0: - micro_batch_size = customizer_config.batch.micro_batch_size - - # Calculate gradient accumulation steps (how StepScheduler computes it) - grad_acc_steps = batch_size // (micro_batch_size * dp) - - # Calculate total optimizer steps (accounting for gradient accumulation) - total_optimizer_steps = (epochs * prepared.train_samples) // grad_acc_steps - - # lr_decay_steps will be min(max_steps, total_optimizer_steps) - lr_decay_steps = min(total_optimizer_steps, max_steps) - - if warmup_steps >= lr_decay_steps: - raise ValueError( - f"warmup_steps ({warmup_steps}) must be less than lr_decay_steps ({lr_decay_steps}). " - f"Calculation: grad_acc_steps={grad_acc_steps} (batch_size={batch_size} / " - f"(micro_batch_size={micro_batch_size} * dp_size={dp})), " - f"total_optimizer_steps={total_optimizer_steps} (epochs={epochs} * " - f"steps_per_epoch={prepared.train_samples} / grad_acc_steps={grad_acc_steps}), " - f"lr_decay_steps=min({total_optimizer_steps}, {max_steps})={lr_decay_steps}" - ) - - # === Optimizer === - cfg["optimizer"] = { - "_target_": "torch.optim.Adam", - "lr": customizer_config.optimizer.learning_rate, - "weight_decay": customizer_config.optimizer.weight_decay, - "betas": [customizer_config.optimizer.beta1, customizer_config.optimizer.beta2], - "eps": customizer_config.optimizer.eps, # Adam epsilon for numerical stability - } - - cfg["lr_scheduler"] = { - "lr_decay_style": "cosine", - "lr_warmup_steps": customizer_config.optimizer.warmup_steps, - } - if customizer_config.optimizer.min_learning_rate: - cfg["lr_scheduler"]["min_lr"] = customizer_config.optimizer.min_learning_rate - - # === Checkpoint === - cfg["checkpoint"] = { - "enabled": True, - "model_save_format": "safetensors", - "checkpoint_dir": str(workspace_dir / "checkpoints"), - "save_consolidated": True, - # Required for models with quantized base weights (e.g., GPT-OSS) - # Safe to enable even for non-quantized models - "dequantize_base_checkpoint": True, - "v4_compatible": customizer_config.model.v4_compatible, - } - - # === Sequence Packing (must be computed before dataset config) === - # When packing is enabled, we use the pack size as the effective sequence length - # for dataset configuration. This ensures samples are truncated appropriately. - effective_seq_length = customizer_config.model.max_seq_length - if not _is_embedding_model: - if customizer_config.batch.sequence_packing: - # Calculate optimal pack size based on dataset statistics - packing_estimate = estimate_dataset_sequence_lengths( - customizer_config, - train_file=prepared.train_file, - max_samples=customizer_config.batch.sequence_packing_max_samples, - seed=customizer_config.seed, - trust_remote_code=trust_remote_code, - ) - - if packing_estimate is not None: - optimal_pack_size = packing_estimate.pack_size - logger.info( - f"Sequence packing enabled: pack_size={optimal_pack_size}, " - f"avg_seq={packing_estimate.avg_seq_length}, max_seq={packing_estimate.max_seq_length}, " - f"packing_factor={packing_estimate.packing_factor}, samples={packing_estimate.samples_analyzed}" - ) - else: - # Fallback to conservative default (model max_seq_length) - optimal_pack_size = calculate_optimal_pack_size(customizer_config) - logger.info(f"Sequence packing enabled with conservative pack_size={optimal_pack_size}") - - cfg["packed_sequence"] = { - "packed_sequence_size": optimal_pack_size, - "split_across_pack": False, - } - - # Use pack size as the effective sequence length for datasets - effective_seq_length = optimal_pack_size - - # === Dataset Configuration (with schema detection) === - _configure_datasets( - cfg, - customizer_config, - prepared, - effective_seq_length, - seed, - _is_embedding_model, - embedding_config, - ) - - # === Dataloader === - # Embedding datasets configure their own specialized dataloaders in _configure_embedding_dataset - if not _is_embedding_model: - cfg["dataloader"] = { - "_target_": "torchdata.stateful_dataloader.StatefulDataLoader", - "collate_fn": "nemo_automodel.components.datasets.utils.default_collater", - "shuffle": True, - } - cfg["validation_dataloader"] = { - "_target_": "torchdata.stateful_dataloader.StatefulDataLoader", - "collate_fn": "nemo_automodel.components.datasets.utils.default_collater", - } - - # === PEFT (LoRA) === - if customizer_config.training.training_type in ( - TrainingType.SFT, - TrainingType.DISTILLATION, - ) and customizer_config.training.finetuning_type in (FinetuningType.LORA, FinetuningType.LORA_MERGED): - lora = customizer_config.training.lora - if lora is None: - lora = LoRAConfig() - peft_cfg: dict[str, Any] = { - "_target_": "nemo_automodel.components._peft.lora.PeftConfig", - "dim": lora.rank, - "alpha": lora.alpha, - "dropout": lora.dropout, - "use_triton": lora.use_triton, - "target_modules": lora.target_modules, - } - # TODO: Support exclude_modules via the API - # if lora.exclude_modules: - # peft_cfg["exclude_modules"] = lora.exclude_modules - cfg["peft"] = peft_cfg - - # === Loss === - if not _is_embedding_model: - cfg["loss_fn"] = { - "_target_": "nemo_automodel.components.loss.masked_ce.MaskedCrossEntropy", - } - - # === Custom Model Configuration === - # Check for custom Automodel implementations (e.g., MoE models) - # and configure backend/parallelizer settings - if not _is_embedding_model: - _configure_moe_backend(cfg, customizer_config, trust_remote_code=trust_remote_code) - - # === Knowledge Distillation === - if customizer_config.training.training_type == TrainingType.DISTILLATION: - _configure_kd(cfg, customizer_config, trust_remote_code=trust_remote_code) - - # === Integrations (Runtime Environment) === - - # WandB - check for API key in environment - wandb_config = build_wandb_config( - customizer_config=customizer_config, - job_ctx=job_ctx, - framework="automodel", - ) - if wandb_config: - cfg["wandb"] = wandb_config - logger.info(f"WandB enabled: project={wandb_config.get('project')}") - - # MLflow - mlflow_config = build_mlflow_config( - customizer_config=customizer_config, - job_ctx=job_ctx, - framework="automodel", - ) - if mlflow_config: - cfg["mlflow"] = mlflow_config - logger.info(f"MLflow enabled: {mlflow_config.get('tracking_uri')}") - - return cfg - - -def _configure_moe_backend( - cfg: dict[str, Any], customizer_config: TrainingStepConfig, trust_remote_code: bool = False -) -> None: - """ - Configure custom Automodel model implementations for MoE models. - - Automodel has optimized implementations for certain model architectures. - Only MoE models (those with num_local_experts, num_experts, or n_routed_experts in config) - require additional backend and parallelizer configuration. - - Dense models like LlamaForCausalLM may have custom Automodel implementations - (for combined QKV projections, etc.) but don't need MoE-specific config. - - This function: - 1. Detects if the model is an MoE model via config attributes - 2. Only for MoE: Configures the backend (with deepep disabled for stability) - 3. Only for MoE: Configures the parallelizer for expert distribution - """ - # Import here to avoid ModuleNotFoundError in environments where - # transformers is not installed (e.g., during test collection) - from transformers import AutoConfig - - model_path = customizer_config.model.path - - try: - hf_config = AutoConfig.from_pretrained(model_path, trust_remote_code=trust_remote_code) - architectures = getattr(hf_config, "architectures", None) - - # Check if model has a custom Automodel implementation - has_custom_impl = ( - architectures and len(architectures) > 0 and architectures[0] in ModelRegistry.model_arch_name_to_cls - ) - - if has_custom_impl: - # Check if model is MoE by looking for expert-related config attributes - # MoE models use num_local_experts (Mixtral-style), num_experts (older), or n_routed_experts (NemotronH) - num_experts = ( - getattr(hf_config, "num_local_experts", None) - or getattr(hf_config, "num_experts", None) - or getattr(hf_config, "n_routed_experts", None) - ) - is_moe_model = num_experts is not None and num_experts > 1 - if is_moe_model: - logger.info( - f"Detected MoE model with custom Automodel implementation for architecture: {architectures[0]}. " - f"Adding MoE-specific configurations (num_experts={num_experts})." - ) - - # Validate MoE parallelism constraints. - # Automodel's MoE parallelizer does not support tensor parallelism: - # assert tp_axis_name is None or world_mesh[tp_axis_name].size() == 1 - # See: nemo_automodel/components/moe/parallelizer.py - p = customizer_config.parallelism - total_gpus = p.num_nodes * p.num_gpus_per_node - if total_gpus > 1: - if p.tensor_parallel_size > 1: - raise ValueError( - f"Tensor parallelism (tensor_parallel_size={p.tensor_parallel_size}) is not supported for MoE models." - ) - ep = p.expert_parallel_size - if ep is None or ep <= 1: - raise ValueError( - f"MoE model detected (num_experts={num_experts}) but expert_parallel_size " - f"is {ep or 'not set'}. Multi-GPU MoE training requires expert_parallel_size > 1." - ) - - # Backend configuration for MoE models - # DeepEP is disabled for stability - it's a newer feature that can cause issues - cfg.setdefault("model", {})["backend"] = { - "_target_": "nemo_automodel.components.models.common.utils.BackendConfig", - "enable_deepep": False, - } - - else: - logger.info( - f"Detected custom Automodel implementation for architecture: {architectures[0]}. " - "Not an MoE model, skipping MoE-specific configurations." - ) - else: - logger.debug( - f"No custom Automodel implementation found for {model_path}. " - "Using standard HuggingFace model implementation." - ) - except ValueError: - raise # Re-raise validation errors - except Exception as e: - # Don't fail training if we can't check for custom implementations - logger.warning( - f"Failed to check for custom model implementation: {e}. Using standard HuggingFace model implementation." - ) - - -def _configure_datasets( - cfg: dict[str, Any], - customizer_config: TrainingStepConfig, - prepared: PreparedDataset, - seq_length: int, - seed: int, - is_embedding_model: bool = False, - embedding_config: EmbeddingConfig | None = None, -) -> None: - """ - Configure dataset sections based on detected schema. - - Supports: - - Chat format (OpenAI messages): Uses ChatDataset - - SFT format (prompt/completion): Uses ColumnMappedTextInstructionDataset - - Custom format (via prompt_template): Uses ColumnMappedTextInstructionDataset with custom columns - - Embedding format (query/pos_doc/neg_doc): Uses inline retrieval dataset - - Args: - cfg: Configuration dictionary to populate. - customizer_config: Training step configuration. - prepared: Prepared dataset with merged train/val files. - seq_length: Effective sequence length for dataset configuration. - When sequence packing is enabled, this is the pack size. - Otherwise, this is the model's max_seq_length. - seed: Random seed for reproducibility. - is_embedding_model: Whether this is an embedding model (for dataset format hints). - embedding_config: Embedding model configuration (required for embedding datasets). - """ - train_file = prepared.train_file - validation_file = prepared.validation_file - - # Detect schema from training data - schema, column_keys = detect_dataset_schema( - train_file, - prompt_template=customizer_config.dataset.prompt_template, - ) - - # Validate that embedding models use embedding datasets and vice versa - if is_embedding_model and schema != DatasetSchema.EMBEDDING: - raise ValueError( - f"Model '{customizer_config.model.name}' is detected as an embedding model but the dataset " - f"is in '{schema.value}' format. Embedding models require datasets with 'query', 'pos_doc', " - "and 'neg_doc' fields. Please provide a dataset in embedding format." - ) - if schema == DatasetSchema.EMBEDDING and not is_embedding_model: - raise ValueError( - f"Dataset is in embedding format (query/pos_doc/neg_doc) but model " - f"'{customizer_config.model.name}' is not detected as an embedding model. " - "Embedding datasets can only be used with embedding models." - ) - - if schema == DatasetSchema.EMBEDDING: - # Embedding/retrieval dataset - uses inline format directly - if embedding_config is None: - raise ValueError("embedding_config is required for embedding dataset configuration") - _configure_embedding_dataset(cfg, customizer_config, train_file, validation_file, seed, embedding_config) - elif schema == DatasetSchema.CHAT: - # Chat dataset (OpenAI messages format) - _configure_chat_dataset(cfg, customizer_config, train_file, validation_file, seq_length) - else: - # SFT/Custom dataset (prompt/completion or custom columns) - assert column_keys is not None, "column_keys must be set for SFT/CUSTOM schema" - question_col, answer_col = column_keys - _configure_sft_dataset( - cfg, - customizer_config, - train_file, - validation_file, - question_col, - answer_col, - seq_length, - ) - - -def _configure_chat_dataset( - cfg: dict[str, Any], - customizer_config: TrainingStepConfig, - train_file: Path, - val_file: Path, - seq_length: int, -) -> None: - """Configure ChatDataset for OpenAI messages format.""" - logger.info(f"Configuring ChatDataset for chat format data with seq_length={seq_length}") - - # Resolve chat template using priority-based selection: - # 1. Fileset metadata chat_template (from model entity spec, highest priority) - # 2. Custom template from DEFAULT_CHAT_TEMPLATES (if model.name matches) - # 3. Model's built-in tokenizer template (fallback) - chat_template = resolve_chat_template( - model_path=customizer_config.model.path, - model_name=customizer_config.model.name, - user_template=customizer_config.model.chat_template, - ) - pp_enabled = customizer_config.parallelism.pipeline_parallel_size > 1 - # Note: "split" is required by Automodel's pack_dataset() when sequence packing is enabled. - # Without it, build_dataloader() raises AttributeError accessing cfg_ds.split. - cfg["dataset"] = { - "_target_": "nemo_automodel.components.datasets.llm.chat_dataset.ChatDataset", - "path_or_dataset_id": str(train_file), - "split": "train", - "seq_length": seq_length, - "padding": "do_not_pad" if not pp_enabled else "max_length", - } - cfg["validation_dataset"] = { - "_target_": "nemo_automodel.components.datasets.llm.chat_dataset.ChatDataset", - "path_or_dataset_id": str(val_file), - "split": "validation", - "seq_length": seq_length, - "padding": "do_not_pad" if not pp_enabled else "max_length", - } - - # Add chat template if available - if chat_template: - cfg["dataset"]["chat_template"] = chat_template - cfg["validation_dataset"]["chat_template"] = chat_template - logger.info("Added chat template to dataset config") - else: - logger.warning("No chat template found - ChatDataset may fail") - - # Store resolved template in config for checkpoint processing - # This ensures the same template is used during training and applied to output - cfg["_resolved_chat_template"] = chat_template - - -def _configure_sft_dataset( - cfg: dict[str, Any], - customizer_config: TrainingStepConfig, - train_file: Path, - val_file: Path, - question_col: str, - answer_col: str, - seq_length: int, -) -> None: - """Configure ColumnMappedTextInstructionDataset for SFT/custom format.""" - logger.info( - f"Configuring SFT dataset with columns: question={question_col}, answer={answer_col}, seq_length={seq_length}" - ) - pp_enabled = customizer_config.parallelism.pipeline_parallel_size > 1 - # Note: "split" is required by Automodel's pack_dataset() when sequence packing is enabled. - # Without it, build_dataloader() raises AttributeError accessing cfg_ds.split. - cfg["dataset"] = { - "_target_": "nemo_automodel.components.datasets.llm.column_mapped_text_instruction_dataset.ColumnMappedTextInstructionDataset", - "path_or_dataset_id": str(train_file), - "split": "train", - "column_mapping": { - "question": question_col, - "answer": answer_col, - }, - "seq_length": seq_length, - "answer_only_loss_mask": True, - "padding": "do_not_pad" if not pp_enabled else "max_length", - "truncation": "longest_first", - } - cfg["validation_dataset"] = { - "_target_": "nemo_automodel.components.datasets.llm.column_mapped_text_instruction_dataset.ColumnMappedTextInstructionDataset", - "path_or_dataset_id": str(val_file), - "split": "validation", - "column_mapping": { - "question": question_col, - "answer": answer_col, - }, - "seq_length": seq_length, - "answer_only_loss_mask": True, - "padding": "do_not_pad" if not pp_enabled else "max_length", - "truncation": "longest_first", - } - - -def _configure_embedding_dataset( - cfg: dict[str, Any], - customizer_config: TrainingStepConfig, - train_file: Path, - val_file: Path, - seed: int, - embedding_config: EmbeddingConfig, -) -> None: - """Configure embedding/retrieval dataset for biencoder training. - - Uses Automodel's inline retrieval dataset format which directly accepts - Customizer's embedding format without conversion: - {"query": "...", "pos_doc": "...", "neg_doc": ["...", "..."]} - - This uses retrieval_dataset_inline.make_retrieval_dataset which handles: - - Loading inline text directly from JSONL - - RetrievalBiencoderCollator for tokenization and batching - - Args: - cfg: Configuration dictionary to populate. - customizer_config: Training step configuration. - train_file: Path to training JSONL file. - val_file: Path to validation JSONL file. - seed: Random seed for reproducibility. - embedding_config: Embedding model configuration. - """ - - logger.info(f"Configuring embedding dataset with train_n_passages={embedding_config.train_n_passages}") - - cfg["dataloader"] = { - "_target_": "torchdata.stateful_dataloader.StatefulDataLoader", - "dataset": { - "_target_": "nemo_automodel.components.datasets.llm.retrieval_dataset_inline.make_retrieval_dataset", - "data_dir_list": [str(train_file)], - "data_type": "train", - "train_n_passages": embedding_config.train_n_passages, - "seed": seed, - "do_shuffle": True, - }, - "collate_fn": { - "_target_": "nemo_automodel.components.datasets.llm.RetrievalBiencoderCollator", - "q_max_len": embedding_config.query_max_length, - "p_max_len": embedding_config.passage_max_length, - "query_prefix": embedding_config.query_prefix, - "passage_prefix": embedding_config.passage_prefix, - "pad_to_multiple_of": 8, - }, - "shuffle": True, - "num_workers": 0, - } - - if val_file and val_file.exists(): - cfg["validation_dataloader"] = { - "_target_": "torchdata.stateful_dataloader.StatefulDataLoader", - "dataset": { - "_target_": "nemo_automodel.components.datasets.llm.retrieval_dataset_inline.make_retrieval_dataset", - "data_dir_list": [str(val_file)], - "data_type": "eval", - "train_n_passages": embedding_config.train_n_passages, - "eval_negative_size": get_eval_negative_size(embedding_config), - "seed": seed, - "do_shuffle": False, - }, - "collate_fn": { - "_target_": "nemo_automodel.components.datasets.llm.RetrievalBiencoderCollator", - "q_max_len": embedding_config.query_max_length, - "p_max_len": embedding_config.passage_max_length, - "query_prefix": embedding_config.query_prefix, - "passage_prefix": embedding_config.passage_prefix, - "padding": "longest", - "pad_to_multiple_of": 8, - }, - "batch_size": customizer_config.batch.micro_batch_size, - "shuffle": False, - "num_workers": 0, - } - - -def _verify_tokenizer_compatibility(student_path: str, teacher_path: str, trust_remote_code: bool = False) -> None: - """ - Verify that student and teacher models have compatible tokenizers. - - Knowledge distillation requires the student and teacher to have the same - vocabulary so their logit spaces are aligned. This check prevents subtle - bugs where training appears to work but produces garbage outputs. - - Raises: - ValueError: If tokenizers are incompatible - """ - # Import here to avoid ModuleNotFoundError in environments where - # transformers is not installed (e.g., during test collection) - from transformers import AutoTokenizer - - try: - student_tokenizer = AutoTokenizer.from_pretrained(student_path, trust_remote_code=trust_remote_code) - teacher_tokenizer = AutoTokenizer.from_pretrained(teacher_path, trust_remote_code=trust_remote_code) - - if student_tokenizer.vocab_size != teacher_tokenizer.vocab_size: - raise ValueError( - f"Tokenizer vocabulary size mismatch: student has {student_tokenizer.vocab_size} tokens, " - f"teacher has {teacher_tokenizer.vocab_size} tokens. " - "Knowledge distillation requires matching vocabularies." - ) - - # Optional: Could also check for specific token mismatches - logger.info(f"Tokenizer compatibility verified: both models have vocab_size={student_tokenizer.vocab_size}") - - except Exception as e: - if "vocabulary size mismatch" in str(e): - raise - # Log but don't fail for other tokenizer loading issues - # (e.g., network issues, missing files) - the training will fail later with a clearer error - logger.warning(f"Could not verify tokenizer compatibility: {e}") - - -def _configure_kd(cfg: dict[str, Any], customizer_config: TrainingStepConfig, trust_remote_code: bool = False) -> None: - """ - Configure Knowledge Distillation for Automodel's KD recipe. - - Automodel's KnowledgeDistillationRecipeForNextTokenPrediction requires: - - teacher_model: Frozen teacher model for soft targets - - kd_ratio: Balance between CE and KD loss (0=CE only, 1=KD only) - - kd_loss_fn: KL-divergence loss with temperature scaling - - offload_teacher_model: Optional CPU offloading for memory efficiency - """ - kd_config = customizer_config.training.kd - if not kd_config or not kd_config.teacher_model: - raise ValueError( - "Knowledge distillation requires training.kd.teacher to be set. " - "Ensure the job input includes a teacher model." - ) - - # Verify tokenizer compatibility before proceeding - _verify_tokenizer_compatibility( - customizer_config.model.path, - kd_config.teacher_model.path, - trust_remote_code=trust_remote_code, - ) - - # Teacher model (frozen, same architecture loading as student) - # Use teacher's precision if specified, otherwise fall back to student's precision - teacher_precision = kd_config.teacher_model.precision or customizer_config.model.precision - cfg["teacher_model"] = { - "_target_": "nemo_automodel.NeMoAutoModelForCausalLM.from_pretrained", - "pretrained_model_name_or_path": kd_config.teacher_model.path, - "torch_dtype": teacher_precision.to_torch_dtype() if teacher_precision else "auto", - "attn_implementation": kd_config.teacher_model.attn_implementation, - "trust_remote_code": kd_config.teacher_model.trust_remote_code, - } - - # KD loss function with temperature - cfg["kd_loss_fn"] = { - "_target_": "nemo_automodel.components.loss.kd_loss.KDLoss", - "ignore_index": -100, - "temperature": kd_config.temperature, - "fp32_upcast": True, # Recommended for numerical stability - } - - # KD ratio (blend between CE and KD loss) - cfg["kd_ratio"] = kd_config.ratio - - # Optional: Offload teacher to CPU for memory efficiency - if kd_config.offload_teacher: - cfg["offload_teacher_model"] = True - logger.info("Teacher model will be offloaded to CPU between forward passes") - - -def get_eval_negative_size(embedding_config: EmbeddingConfig) -> int: - """Get the effective eval_negative_size value from embedding config. - - Returns the user-specified eval_negative_size if set, otherwise defaults - to train_n_passages - 1 for consistent train/eval behavior. - - The -1 relationship exists because: - - train_n_passages = total passages = 1 positive + N negatives - - eval_negative_size = just the negative count = N - - So: eval_negative_size = train_n_passages - 1 (subtracting the positive) - - Example: train_n_passages=5 (1 pos + 4 neg) -> eval_negative_size=4 - """ - if embedding_config.eval_negative_size is not None: - return embedding_config.eval_negative_size - return embedding_config.train_n_passages - 1 diff --git a/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/finetune.py b/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/finetune.py deleted file mode 100644 index e986c1c9e6..0000000000 --- a/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/finetune.py +++ /dev/null @@ -1,297 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -""" -Customizer training script for Automodel fine-tuning. - -This module wraps nemo_automodel's training recipes to add Customizer-specific -callbacks and logging using the Composition pattern. Supports SFT, Knowledge -Distillation, and Biencoder (embedding model) training. - -Architecture: - - AutomodelRecipe: Protocol defining the interface we need from Automodel recipes - - CustomizerRecipeWrapper: Wraps any AutomodelRecipe and adds progress reporting - - create_customizer_recipe(): Factory function that auto-detects and creates the appropriate recipe -""" - -from __future__ import annotations - -import logging -from typing import Any, Protocol, runtime_checkable - -from nemo_automodel.components.checkpoint.checkpointing import Checkpointer -from nemo_automodel.components.config._arg_parser import parse_args_and_load_config -from nemo_automodel.components.training.step_scheduler import StepScheduler -from nemo_automodel.recipes.biencoder.train_biencoder import TrainBiencoderRecipe -from nemo_automodel.recipes.llm.kd import KnowledgeDistillationRecipeForNextTokenPrediction -from nemo_automodel.recipes.llm.train_ft import TrainFinetuneRecipeForNextTokenPrediction -from nmp.customizer.app.jobs.context import NMPJobContext -from nmp.customizer.tasks.training.backends.automodel.callbacks import TrainingProgressCallback -from nmp.customizer.tasks.training.progress import JobsServiceProgressReporter - -logger = logging.getLogger(__name__) - - -@runtime_checkable -class AutomodelRecipe(Protocol): - """Protocol defining the interface we need from Automodel recipes. - - This makes the dependencies explicit and enables type checking, unlike - the previous mixin approach that relied on implicit attributes. - """ - - cfg: Any - step_scheduler: StepScheduler - checkpointer: Checkpointer - dist_env: Any - - def setup(self) -> None: - """Build all components needed for training.""" - ... - - def run_train_validation_loop(self) -> None: - """Run the main training/validation loop.""" - ... - - def log_train_metrics(self, log_data: Any) -> None: - """Log training metrics.""" - ... - - def log_val_metrics(self, *args: Any, **kwargs: Any) -> None: - """Log validation metrics. - - Note: Signature varies across Automodel recipes: - - LLM/KD: (val_name, log_data, metric_logger=None) - - VLM/biencoder/seq_cls: (log_data) - """ - ... - - def save_checkpoint( - self, - epoch: int, - step: int, - train_loss: float, - val_loss: dict[str, float] | None = None, - best_metric_key: str = "default", - ) -> None: - """Save a checkpoint.""" - ... - - -class CustomizerRecipeWrapper: - """Wraps an Automodel recipe and adds Customizer-specific behavior. - - This wrapper uses composition to add progress reporting to any Automodel recipe, - including SFT, Knowledge Distillation, and Biencoder (embedding) training. - It monkey-patches the recipe's methods to intercept logging and checkpoint calls, - forwarding them to the JobsServiceProgressReporter. - """ - - def __init__(self, recipe: AutomodelRecipe, job_ctx: NMPJobContext | None = None): - """Initialize the wrapper with an Automodel recipe. - - Args: - recipe: Any recipe implementing the AutomodelRecipe protocol - (SFT, KD, biencoder, etc.). - job_ctx: NeMo Platform job context for progress reporting (optional, - defaults to environment variables). - """ - self._job_ctx = job_ctx or NMPJobContext.from_env() - self._reporter = JobsServiceProgressReporter(self._job_ctx) - self._reporter.report_running("automodel_recipe_setup") - - self._recipe = recipe - self._recipe.setup() - - self.max_steps = getattr(self._recipe.step_scheduler, "max_steps", None) or 100 - self.num_epochs = getattr(self._recipe.step_scheduler, "num_epochs", None) or 1 - - self.callback = TrainingProgressCallback(self._reporter) - logger.info(f"Customizer wrapper initialized: max_steps={self.max_steps}, num_epochs={self.num_epochs}") - - # Store original methods before patching - self._original_log_train_metrics = recipe.log_train_metrics - self._original_log_val_metrics = recipe.log_val_metrics - self._original_save_checkpoint = recipe.save_checkpoint - - # Monkey-patch the recipe's methods to add our callbacks - recipe.log_train_metrics = self._log_train_metrics # type: ignore[method-assign] - recipe.log_val_metrics = self._log_val_metrics # type: ignore[method-assign] - recipe.save_checkpoint = self._save_checkpoint # type: ignore[method-assign] - - @property - def recipe(self) -> AutomodelRecipe: - """Access the underlying recipe.""" - return self._recipe - - def run_train_validation_loop(self) -> None: - """Run training with proper cleanup of Customizer resources.""" - try: - self.callback.report_training_start(self.max_steps, self.num_epochs) - self._recipe.run_train_validation_loop() - finally: - if self.callback: - self.callback.close() - logger.info("Customizer callback closed") - - def _log_train_metrics(self, log_data: Any) -> None: - """Wrapped log_train_metrics with Customizer reporting.""" - # Call original method first - self._original_log_train_metrics(log_data) - - # Report to Customizer - if self.callback and log_data: - try: - metrics = getattr(log_data, "metrics", {}) - self.callback.report_train_step( - step=getattr(log_data, "step", 0) + 1, # Convert to 1-based - epoch=getattr(log_data, "epoch", 0) + 1, # Convert to 1-based - loss=metrics.get("loss", 0.0), - lr=metrics.get("lr"), - grad_norm=metrics.get("grad_norm"), - ) - except Exception as e: - logger.warning(f"Failed to report training progress: {e}") - - try: - if self._recipe.step_scheduler.is_last_batch: - self.callback.report_epoch_end( - step=self._recipe.step_scheduler.step + 1, - epoch=self._recipe.step_scheduler.epoch + 1, - ) - except Exception as e: - logger.warning(f"Failed to report epoch end: {e}") - - def _log_val_metrics(self, *args: Any, **kwargs: Any) -> None: - """Wrapped log_val_metrics with Customizer reporting. - - Handles different Automodel recipe signatures: - - LLM/KD: (val_name, log_data, metric_logger=None) - - VLM/biencoder/seq_cls: (log_data) - """ - # Call original method first with whatever args were passed - self._original_log_val_metrics(*args, **kwargs) - - # Extract log_data from args (it's always the last positional arg before kwargs) - # LLM signature: (val_name, log_data, metric_logger=None) -> log_data is args[1] - # VLM/biencoder signature: (log_data) -> log_data is args[0] - log_data = None - if len(args) >= 2: - # LLM/KD style: (val_name, log_data, ...) - log_data = args[1] - elif len(args) == 1: - # VLM/biencoder style: (log_data) - log_data = args[0] - - # Report to Customizer - if self.callback and log_data: - try: - metrics = getattr(log_data, "metrics", {}) - self.callback.report_validation( - step=getattr(log_data, "step", 0) + 1, # Convert to 1-based - epoch=getattr(log_data, "epoch", 0) + 1, # Convert to 1-based - val_loss=metrics.get("val_loss", 0.0), - ) - except Exception as e: - logger.warning(f"Failed to report validation progress: {e}") - - def _save_checkpoint( - self, - epoch: int, - step: int, - train_loss: float, - val_loss: dict[str, float] | None = None, - best_metric_key: str = "default", - ) -> None: - """Wrapped save_checkpoint with Customizer reporting.""" - # Call original method first - self._original_save_checkpoint(epoch, step, train_loss, val_loss, best_metric_key) - - # Report to Customizer - if self.callback: - try: - checkpoint_dir = getattr( - getattr(self._recipe.checkpointer, "config", None), - "checkpoint_dir", - None, - ) - self.callback.report_checkpoint_saved( - step=step + 1, # Convert to 1-based - epoch=epoch + 1, # Convert to 1-based - checkpoint_path=str(checkpoint_dir) if checkpoint_dir else None, - ) - except Exception as e: - logger.warning(f"Failed to report checkpoint save: {e}") - - -def _is_kd_config(cfg: Any) -> bool: - """Check if config is for knowledge distillation.""" - return cfg.get("teacher_model") is not None or cfg.get("kd_ratio") is not None - - -def _is_biencoder_config(cfg: Any) -> bool: - """Check if config is for biencoder/embedding model training. - - Detects biencoder configs by checking if model._target_ contains 'biencoder'. - - Note: ConfigNode automatically resolves _target_ to the actual function/class, - so we check the function's __module__ or __qualname__ for 'biencoder'. - """ - try: - model_cfg = cfg.get("model", {}) - if model_cfg is None: - return False - - target = model_cfg.get("_target_") - if target is None: - return False - - # target is resolved to the actual function/class by ConfigNode - # Check its module path or qualified name - module = getattr(target, "__module__", "") or "" - qualname = getattr(target, "__qualname__", "") or "" - return "biencoder" in module.lower() or "biencoder" in qualname.lower() - except (AttributeError, TypeError): - return False - - -def create_customizer_recipe(cfg: Any) -> CustomizerRecipeWrapper: - """Factory function to create the appropriate wrapped recipe. - - Auto-detects the training type from the config: - - Biencoder configs (model._target_ contains 'biencoder') -> biencoder recipe - - KD configs (has teacher_model or kd_ratio) -> KD recipe - - Otherwise -> SFT recipe - - Args: - cfg: Configuration object from parse_args_and_load_config(). - - Returns: - A CustomizerRecipeWrapper wrapping the appropriate recipe for the training type. - """ - if _is_biencoder_config(cfg): - logger.info("Detected biencoder config, using embedding model recipe") - base_recipe = TrainBiencoderRecipe(cfg) - elif _is_kd_config(cfg): - logger.info("Detected Knowledge Distillation config, using KD recipe") - base_recipe = KnowledgeDistillationRecipeForNextTokenPrediction(cfg) - else: - logger.info("Using SFT fine-tuning recipe") - base_recipe = TrainFinetuneRecipeForNextTokenPrediction(cfg) - - return CustomizerRecipeWrapper(base_recipe) - - -def main() -> None: - """Main entry point for Customizer-enhanced training. - - Parses configuration, auto-detects the training type (biencoder, KD, or SFT), - creates the appropriate recipe with Customizer integration, and runs training. - """ - cfg = parse_args_and_load_config() - recipe = create_customizer_recipe(cfg) - recipe.run_train_validation_loop() - - -if __name__ == "__main__": - main() diff --git a/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/requirements.txt b/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/requirements.txt deleted file mode 100644 index 1ac43ef3a7..0000000000 --- a/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -nemo_automodel==0.2.0 diff --git a/services/customizer/src/nmp/customizer/tasks/training/backends/nemo_rl/dpo_config.py b/services/customizer/src/nmp/customizer/tasks/training/backends/nemo_rl/dpo_config.py index 0a4df10cab..c18443f43a 100644 --- a/services/customizer/src/nmp/customizer/tasks/training/backends/nemo_rl/dpo_config.py +++ b/services/customizer/src/nmp/customizer/tasks/training/backends/nemo_rl/dpo_config.py @@ -14,7 +14,7 @@ converting the internal TrainingStepConfig format to NeMo RL's YAML format. Example of similar config but for AutoModel training -- services/customizer/src/nmp/customizer/tasks/training/backends/automodel/config.py +- services/automodel/src/nmp/automodel/tasks/training/backends/config.py """ import logging diff --git a/services/customizer/src/nmp/customizer/tasks/training/runner.py b/services/customizer/src/nmp/customizer/tasks/training/runner.py index 2dfe2aef9e..ae1add2c85 100644 --- a/services/customizer/src/nmp/customizer/tasks/training/runner.py +++ b/services/customizer/src/nmp/customizer/tasks/training/runner.py @@ -156,9 +156,9 @@ def run(self) -> TrainingResult: def _load_backend(self, backend_type: TrainingBackendEnum) -> TrainingBackend: """Load the backend for the given backend type.""" if backend_type == TrainingBackendEnum.AUTOMODEL: - from .backends.automodel.backend import AutomodelBackend - - return AutomodelBackend(self._job_ctx) + raise ValueError( + "Automodel training is no longer supported in legacy customizer; use the nmp-automodel plugin." + ) if backend_type == TrainingBackendEnum.MEGATRON_BRIDGE: # TODO: Implement megatron_bridge backend diff --git a/services/customizer/tests/constants.py b/services/customizer/tests/constants.py index 3636945d7a..1357f53924 100644 --- a/services/customizer/tests/constants.py +++ b/services/customizer/tests/constants.py @@ -2,8 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 NMP_CUSTOMIZER_FILE_IO_IMAGE_ENVVAR = "NMP_CUSTOMIZER_FILE_IO_IMAGE" -NMP_CUSTOMIZER_TRAINING_AUTOMODEL_IMAGE_ENVVAR = "NMP_CUSTOMIZER_TRAINING_AUTOMODEL_IMAGE" FILE_IO_IMAGE = "test-file-io:v1" -TRAINING_AUTOMODEL_IMAGE = "test-training-automodel:v1" TRAINING_RL_IMAGE = "test-training-rl:v1" diff --git a/services/customizer/tests/tasks/training/test_errors.py b/services/customizer/tests/tasks/training/test_errors.py index b4d5e376ba..7b2851c523 100644 --- a/services/customizer/tests/tasks/training/test_errors.py +++ b/services/customizer/tests/tasks/training/test_errors.py @@ -4,16 +4,16 @@ """ Tests for Customizer training error handling. -Tests the error mapping framework integration with Customizer: +Tests the error mapping framework integration with Customizer (NeMo-RL / DPO): - Exception classes and EXCEPTION_REGISTRY - Error rules loading from YAML -- Exception conversion for Automodel errors via create_error_details() - Subprocess error parsing (parse_error_from_output) - End-to-end subprocess→parser→converter flow for NeMo-RL errors - Error details generation for Jobs service reporting + +Automodel error conversion tests live in services/automodel/tests/tasks/training/test_errors.py. """ -import subprocess from collections import deque from nmp.customizer.tasks.training.errors.converter import ( @@ -159,297 +159,6 @@ def test_returns_same_instance(self): assert converter1 is converter2 -# ============================================================================= -# AUTOMODEL ERROR CONVERSION TESTS (via create_error_details) -# ============================================================================= - - -class TestAutomodelDatasetErrors: - """Tests for Automodel dataset error conversion.""" - - def test_unsupported_role_error(self): - """Should convert unsupported role ValueError to DatasetFormatError.""" - original = ValueError("Unsupported role in messages: invalid_role") - details = create_error_details(original) - - assert details["type"] == "DatasetFormatError" - assert "invalid role" in details["message"].lower() - - def test_unrelated_value_error_uses_fallback(self): - """Unrelated ValueError should use InternalError fallback.""" - original = ValueError("Something completely different") - details = create_error_details(original) - - # Should fall back to InternalError - assert details["type"] == "InternalError" - - -class TestAutomodelModelLoadErrors: - """Tests for Automodel model load error conversion.""" - - def test_weight_swap_failure(self): - """Should convert weight swap RuntimeError to ModelLoadError.""" - original = RuntimeError("_apply(): Couldn't swap Linear.weight") - details = create_error_details(original) - - assert details["type"] == "ModelLoadError" - assert "weights could not be applied" in details["message"].lower() - - def test_patch_failure(self): - """Should convert patch failure to ModelLoadError.""" - original = RuntimeError("Failed to patch model") - details = create_error_details(original) - - assert details["type"] == "ModelLoadError" - assert "optimizations" in details["message"].lower() - - def test_signature_mismatch(self): - """Should convert signature mismatch to ModelLoadError.""" - original = AssertionError("Signature mismatch:\n original: foo\n patched : bar") - details = create_error_details(original) - - assert details["type"] == "ModelLoadError" - assert "signature" in details["message"].lower() - - def test_missing_lm_head(self): - """Should convert missing lm_head to ModelLoadError.""" - original = ValueError("lm_head.weight not found in model") - details = create_error_details(original) - - assert details["type"] == "ModelLoadError" - assert "language model head" in details["message"].lower() - - -class TestAutomodelTrainingConfigErrors: - """Tests for Automodel training config error conversion.""" - - def test_tied_embeddings_error(self): - """Should convert tied embeddings error to TrainingConfigError.""" - original = ValueError( - "Model 'test-model' is not compatible with pipeline parallelism:\n\n" - "1. tie_word_embeddings=True is not supported for pipelining." - ) - details = create_error_details(original) - - assert details["type"] == "TrainingConfigError" - assert "tied embeddings" in details["message"].lower() - - def test_encoder_decoder_error(self): - """Should convert encoder-decoder error to TrainingConfigError.""" - original = ValueError( - "Model 'test-model' is not compatible with pipeline parallelism:\n\n" - "1. Encoder-Decoder models with cross-attention are not supported yet." - ) - details = create_error_details(original) - - assert details["type"] == "TrainingConfigError" - assert "encoder-decoder" in details["message"].lower() - - def test_pp_batch_size_error(self): - """Should convert PP batch size error to TrainingConfigError.""" - original = AssertionError("pp_batch_size // pp_microbatch_size must be >= pp_size") - details = create_error_details(original) - - assert details["type"] == "TrainingConfigError" - assert "pipeline parallelism" in details["message"].lower() - - def test_sdpa_error(self): - """Should convert SDPA error to TrainingConfigError.""" - original = ValueError("Model does not support SDPA required for context parallelism") - details = create_error_details(original) - - assert details["type"] == "TrainingConfigError" - assert "SDPA" in details["message"] or "context parallelism" in details["message"].lower() - - def test_triton_not_installed(self): - """Should convert triton import error to TrainingConfigError.""" - original = ImportError("triton is not installed. Please install it.") - details = create_error_details(original) - - assert details["type"] == "TrainingConfigError" - assert "triton" in details["message"].lower() - - def test_lora_dimensions_mismatch(self): - """Should convert LoRA dimensions error to TrainingConfigError.""" - original = AssertionError("Incompatible X and LoRA A dimensions") - details = create_error_details(original) - - assert details["type"] == "TrainingConfigError" - assert "LoRA" in details["message"] - - -class TestAutomodelCheckpointErrors: - """Tests for Automodel checkpoint error conversion.""" - - def test_checkpoint_directory_exists(self): - """Should convert checkpoint exists error to CheckpointError.""" - original = AssertionError("Checkpoint directory /path/to/ckpt already exists") - details = create_error_details(original) - - assert details["type"] == "CheckpointError" - assert "already exists" in details["message"].lower() - - def test_global_plan_validation(self): - """Should convert global plan error to CheckpointError.""" - original = ValueError("Failed to validate global plan") - details = create_error_details(original) - - assert details["type"] == "CheckpointError" - assert "validation failed" in details["message"].lower() - - def test_missing_checkpoint_key(self): - """Should convert missing key error to CheckpointError.""" - original = RuntimeError("Missing key in checkpoint state_dict: model.layer.weight") - details = create_error_details(original) - - assert details["type"] == "CheckpointError" - assert "missing" in details["message"].lower() - - def test_moe_expert_weights_missing(self): - """Should convert MoE expert weights error to CheckpointError.""" - original = RuntimeError("Expert weights missing from checkpoint for layer 0") - details = create_error_details(original) - - assert details["type"] == "CheckpointError" - assert "MoE" in details["message"] or "expert" in details["message"].lower() - - -class TestAutomodelCudaErrors: - """Tests for Automodel CUDA error conversion.""" - - def test_cuda_oom_message(self): - """Should convert CUDA OOM message to CudaError.""" - original = RuntimeError("CUDA out of memory. Tried to allocate 2.00 GiB") - details = create_error_details(original) - - assert details["type"] == "CudaError" - assert "memory" in details["message"].lower() - - def test_out_of_memory_generic(self): - """Should convert generic OOM to CudaError.""" - original = RuntimeError("out of memory") - details = create_error_details(original) - - assert details["type"] == "CudaError" - - def test_cuda_error_generic(self): - """Should convert generic CUDA error to CudaError.""" - original = RuntimeError("CUDA error: device-side assert triggered") - details = create_error_details(original) - - assert details["type"] == "CudaError" - - -class TestAutomodelDistributedErrors: - """Tests for Automodel distributed error conversion.""" - - def test_distributed_not_available(self): - """Should convert distributed not available to DistributedError.""" - original = RuntimeError("torch.distributed not available") - details = create_error_details(original) - - assert details["type"] == "DistributedError" - assert "not available" in details["message"].lower() - - def test_distributed_not_initialized(self): - """Should convert not initialized to DistributedError.""" - original = RuntimeError("expected torch.distributed to be initialized") - details = create_error_details(original) - - assert details["type"] == "DistributedError" - assert "not properly initialized" in details["message"].lower() - - def test_nccl_error(self): - """Should convert NCCL error to DistributedError.""" - original = RuntimeError("NCCL error in: ncclAllReduce") - details = create_error_details(original) - - assert details["type"] == "DistributedError" - assert "NCCL" in details["message"] - - def test_timeout_in_cause_chain(self): - """Should convert exception with TimeoutError in cause chain to DistributedError.""" - # Create an exception chain: RuntimeError caused by TimeoutError - timeout_exc = TimeoutError("Timed out waiting for worker") - original = RuntimeError("Distributed operation failed") - original.__cause__ = timeout_exc - - details = create_error_details(original) - - assert details["type"] == "DistributedError" - assert "timed out" in details["message"].lower() - - def test_timeout_in_nested_cause_chain(self): - """Should find TimeoutError recursively in nested cause chain.""" - # Create a deeper chain: RuntimeError -> ValueError -> TimeoutError - timeout_exc = TimeoutError("Connection timed out") - middle_exc = ValueError("Worker communication failed") - middle_exc.__cause__ = timeout_exc - original = RuntimeError("Training failed") - original.__cause__ = middle_exc - - details = create_error_details(original) - - assert details["type"] == "DistributedError" - assert "timed out" in details["message"].lower() - - -class TestAutomodelTimeoutError: - """Tests for training timeout error conversion.""" - - def test_subprocess_timeout(self): - """Should convert subprocess.TimeoutExpired to TrainingTimeoutError.""" - original = subprocess.TimeoutExpired(cmd="torchrun", timeout=3600) - details = create_error_details(original) - - assert details["type"] == "TrainingTimeoutError" - assert "time limit" in details["message"].lower() - - -class TestAutomodelInternalErrors: - """Tests for Automodel internal error conversion.""" - - def test_pipeline_missing_inputs(self): - """Should convert missing inputs to InternalError.""" - original = ValueError("You must provide either input_ids or inputs_embeds") - details = create_error_details(original) - - assert details["type"] == "InternalError" - assert "pipeline" in details["message"].lower() - - def test_pipeline_missing_embeddings(self): - """Should convert missing embeddings to InternalError.""" - original = ValueError("inputs_embeds must be provided for pipeline stages without embed_tokens") - details = create_error_details(original) - - assert details["type"] == "InternalError" - assert "pipeline" in details["message"].lower() - - def test_moe_mesh_error(self): - """Should convert MoE mesh error to ParallelismConfigError.""" - original = AssertionError("We only support 1D mesh for MoE") - details = create_error_details(original) - - assert details["type"] == "ParallelismConfigError" - assert "moe" in details["message"].lower() - - def test_dtensor_placement_error(self): - """Should convert DTensor placement error to ParallelismConfigError.""" - original = ValueError("tensor has unsupported DTensor placement: Partial") - details = create_error_details(original) - - assert details["type"] == "ParallelismConfigError" - assert "moe" in details["message"].lower() or "expert" in details["message"].lower() - - def test_fused_loss_error(self): - """Should convert fused loss error to InternalError.""" - original = ValueError("FusedLinearCrossEntropy requires the model to output hidden states") - details = create_error_details(original) - - assert details["type"] == "InternalError" - assert "hidden states" in details["message"].lower() - - # ============================================================================= # SUBPROCESS ERROR PARSING TESTS # ============================================================================= diff --git a/services/customizer/tests/tasks/training/test_integrations.py b/services/customizer/tests/tasks/training/test_integrations.py index 4b55fef18a..62f6bf5c2d 100644 --- a/services/customizer/tests/tasks/training/test_integrations.py +++ b/services/customizer/tests/tasks/training/test_integrations.py @@ -54,7 +54,7 @@ def job_ctx_minimal(tmp_path: Path) -> NMPJobContext: def training_step_config(tmp_path: Path) -> TrainingStepConfig: """Create a minimal real TrainingStepConfig for integrations tests.""" return TrainingStepConfig( - backend=TrainingBackend.AUTOMODEL, + backend=TrainingBackend.NEMO_RL, model=ModelConfig(path="/models/test-model", name="meta/llama-test"), dataset=TrainingStepConfig.DatasetConfig(path="/datasets/train"), training=TrainingStepConfig.TrainingConfig(training_type=TrainingType.SFT), diff --git a/services/customizer/tests/tasks/training/test_runner.py b/services/customizer/tests/tasks/training/test_runner.py index 120c046df4..963d9e50b8 100644 --- a/services/customizer/tests/tasks/training/test_runner.py +++ b/services/customizer/tests/tasks/training/test_runner.py @@ -59,7 +59,7 @@ def config_file(workspace_dir: Path) -> Path: """Create a minimal training config file.""" config_path = workspace_dir / "config.json" config_data = { - "backend": "automodel", + "backend": "nemo_rl", "model": {"path": "/models/test-model", "name": "test/model"}, "dataset": {"path": "/data/train.jsonl"}, "training": {"training_type": "sft", "finetuning_type": "lora"}, @@ -120,7 +120,7 @@ def mock_dist_ctx_worker() -> MagicMock: def mock_backend() -> MagicMock: """Create a mock training backend.""" backend = MagicMock() - backend.backend_type = TrainingBackend.AUTOMODEL + backend.backend_type = TrainingBackend.NEMO_RL backend.compile_config.return_value = {"model": {"path": "/test"}, "training": {"lr": 1e-4}} backend.execute_training.return_value = TrainingMetrics(total_steps=100, total_epochs=1, final_loss=0.5) backend.find_best_checkpoint.return_value = Path("/checkpoints/best") @@ -184,7 +184,7 @@ def test_load_config_parses_json( ): runner = TrainingRunner(backend=mock_backend) - assert runner._config.backend == TrainingBackend.AUTOMODEL + assert runner._config.backend == TrainingBackend.NEMO_RL assert runner._config.model.path == "/models/test-model" assert runner._config.seed == 42 @@ -236,7 +236,7 @@ def test_worker_waits_and_loads_config( ): """Test that worker waits for coordinator and loads config from disk.""" # Pre-create the config file that coordinator would have written - config_path = workspace_dir / f"{TrainingBackend.AUTOMODEL.value}_config.yaml" + config_path = workspace_dir / f"{TrainingBackend.NEMO_RL.value}_config.yaml" config_path.write_text("model:\n path: /test\ntraining:\n lr: 0.0001\n") with ( @@ -488,7 +488,7 @@ def test_run_worker_exits_after_training_sync( ): """Test that workers return success immediately after training sync without entering postprocessing.""" # Pre-create the library config that the coordinator would have written - config_path = workspace_dir / f"{TrainingBackend.AUTOMODEL.value}_config.yaml" + config_path = workspace_dir / f"{TrainingBackend.NEMO_RL.value}_config.yaml" config_path.write_text("model:\n path: /test\ntraining:\n lr: 0.0001\n") with ( diff --git a/services/customizer/tests/test_compiler.py b/services/customizer/tests/test_compiler.py index 6deef1e607..90f079e08d 100644 --- a/services/customizer/tests/test_compiler.py +++ b/services/customizer/tests/test_compiler.py @@ -91,7 +91,6 @@ def mock_auth_client(mocker): FILE_IO_IMAGE = get_qualified_image(CPU_IMAGE_NAMESPACE) GPU_TASKS_IMAGE = get_qualified_image(GPU_IMAGE_NAMESPACE) -TRAINING_AUTOMODEL_IMAGE = get_qualified_image("customizer-automodel") TRAINING_RL_IMAGE = get_qualified_image("customizer-rl") JobOutputType, transformer_func = _validate_and_resolve_job_output( @@ -127,6 +126,25 @@ def _compiler_args(original_spec: CustomizationJobInput, workspace: str, entity_ return asyncio.run(_compiler_args_async(original_spec, workspace, entity_client, sdk)) +def make_valid_dpo_job_input_dict() -> dict: + """Create a valid DPO CustomizationJobInput as a dictionary.""" + return { + "model": "default/test-target", + "training": { + "type": "dpo", + "epochs": 1, + "batch_size": 4, + "learning_rate": 0.0001, + }, + "dataset": "fileset://default/my-dataset", + } + + +def make_valid_dpo_job_input() -> CustomizationJobInput: + """Create a validated DPO CustomizationJobInput.""" + return CustomizationJobInput.model_validate(make_valid_dpo_job_input_dict()) + + def make_valid_job_input_dict() -> dict: """Create a valid CustomizationJobInput as a dictionary.""" return { @@ -180,14 +198,6 @@ async def make_valid_job_output_async( "integrations_input", ), [ - ( - {"type": "sft", "peft": {"type": "lora"}, "epochs": 1, "batch_size": 4, "learning_rate": 0.0001}, - TRAINING_AUTOMODEL_IMAGE, - "automodel", - "lora", - False, - None, - ), ( {"type": "dpo", "epochs": 1, "batch_size": 4, "learning_rate": 0.0001}, TRAINING_RL_IMAGE, @@ -197,27 +207,13 @@ async def make_valid_job_output_async( None, ), ( - {"type": "sft", "peft": {"type": "lora"}, "epochs": 1, "batch_size": 4, "learning_rate": 0.0001}, - TRAINING_AUTOMODEL_IMAGE, - "automodel", - "lora", - False, + {"type": "dpo", "epochs": 1, "batch_size": 4, "learning_rate": 0.0001}, + TRAINING_RL_IMAGE, + "nemo_rl", + "all_weights", + True, {"wandb": {"project": "my-project", "api_key_secret": "my-wandb-secret"}}, ), - ( - { - "type": "sft", - "peft": {"type": "lora", "merge": True}, - "epochs": 1, - "batch_size": 4, - "learning_rate": 0.0001, - }, - TRAINING_AUTOMODEL_IMAGE, - "automodel", - "lora_merged", - False, - None, - ), ], ) async def test_platform_job_config_compiler( @@ -496,7 +492,7 @@ async def test_platform_job_config_compiler_distributed(mocker, mock_sdk, mock_a new_callable=mocker.AsyncMock, return_value=_make_mock_model_entity(), ) - job_input_dict = make_valid_job_input_dict() + job_input_dict = make_valid_dpo_job_input_dict() job_input_dict["training"]["parallelism"] = { "num_nodes": 2, "num_gpus_per_node": 4, @@ -549,7 +545,7 @@ async def test_platform_job_config_compiler_distributed(mocker, mock_sdk, mock_a @pytest.mark.asyncio async def test_platform_job_config_compiler_distillation(mocker, mock_sdk, mock_auth_client): - """Test that distillation jobs include teacher model download and kd config.""" + """Distillation jobs are routed to nmp-automodel, not legacy customizer.""" student_me = _make_mock_model_entity(fileset="fileset://default/base-model") teacher_me = _make_mock_model_entity( workspace="meta", @@ -581,47 +577,21 @@ async def test_platform_job_config_compiler_distillation(mocker, mock_sdk, mock_ transformed_spec, job_name = await _compiler_args_async( job_input, "workspace", entity_client=object(), sdk=mock_sdk ) - result = await platform_job_config_compiler( - "workspace", - job_input, - transformed_spec, - entity_client=object(), - job_name=job_name, - sdk=mock_sdk, - ) - # Step 1: download step should include student model, dataset, AND teacher model - download_step = result["steps"][0] - assert download_step["name"] == "model-and-dataset-download" - downloads = download_step["config"]["download"] - assert len(downloads) == 3 - assert downloads[0]["dest"] == DEFAULT_MODEL_PATH - assert downloads[1]["dest"] == DEFAULT_DATASET_PATH - assert downloads[2] == { - "src": {"workspace": "meta", "name": "llama-3.1-70b-instruct"}, - "dest": DEFAULT_TEACHER_MODEL_PATH, - } - - # Step 2: training step should have kd config populated - training_step = result["steps"][1] - assert training_step["config"]["backend"] == "automodel" - training_config = training_step["config"]["training"] - assert training_config["training_type"] == "distillation" - assert training_config["finetuning_type"] == "all_weights" - assert training_config["kd"] is not None - assert training_config["kd"]["teacher_model"]["path"] == DEFAULT_TEACHER_MODEL_PATH - assert training_config["kd"]["teacher_model"]["name"] == "meta/llama-3.1-70b-instruct" - assert training_config["kd"]["teacher_model"]["precision"] == "bf16" - assert training_config["kd"]["teacher_model"]["trust_remote_code"] is False - assert training_config["kd"]["ratio"] == 0.7 - assert training_config["kd"]["temperature"] == 2.0 - assert training_config["kd"]["offload_teacher"] is False - assert training_config["dpo"] is None + with pytest.raises(PlatformJobCompilationError, match="nmp-automodel"): + await platform_job_config_compiler( + "workspace", + job_input, + transformed_spec, + entity_client=object(), + job_name=job_name, + sdk=mock_sdk, + ) @pytest.mark.asyncio async def test_platform_job_config_compiler_distillation_with_lora(mocker, mock_sdk, mock_auth_client): - """Test that distillation + LoRA produces correct kd + lora config.""" + """Distillation + LoRA is not compiled by legacy customizer.""" student_me = _make_mock_model_entity(fileset="fileset://default/base-model") teacher_me = _make_mock_model_entity( workspace="meta", @@ -650,21 +620,40 @@ async def test_platform_job_config_compiler_distillation_with_lora(mocker, mock_ transformed_spec, job_name = await _compiler_args_async( job_input, "workspace", entity_client=object(), sdk=mock_sdk ) - result = await platform_job_config_compiler( - "workspace", - job_input, - transformed_spec, - entity_client=object(), - job_name=job_name, - sdk=mock_sdk, + + with pytest.raises(PlatformJobCompilationError, match="nmp-automodel"): + await platform_job_config_compiler( + "workspace", + job_input, + transformed_spec, + entity_client=object(), + job_name=job_name, + sdk=mock_sdk, + ) + + +@pytest.mark.asyncio +async def test_platform_job_config_compiler_rejects_sft(mocker, mock_sdk, mock_auth_client): + """SFT jobs are routed to nmp-automodel, not legacy customizer.""" + mocker.patch( + "nmp.customizer.app.jobs.compiler.fetch_model_entity", + new_callable=mocker.AsyncMock, + return_value=_make_mock_model_entity(), + ) + job_input = make_valid_job_input() + transformed_spec, job_name = await _compiler_args_async( + job_input, "workspace", entity_client=object(), sdk=mock_sdk ) - training_step = result["steps"][1] - training_config = training_step["config"]["training"] - assert training_config["kd"] is not None - assert training_config["lora"] is not None - assert training_config["lora"]["rank"] == 16 - assert training_config["finetuning_type"] == "lora" + with pytest.raises(PlatformJobCompilationError, match="nmp-automodel"): + await platform_job_config_compiler( + "workspace", + job_input, + transformed_spec, + entity_client=object(), + job_name=job_name, + sdk=mock_sdk, + ) @pytest.mark.asyncio @@ -715,7 +704,7 @@ async def test_platform_job_config_compiler_explicit_execution_profile(mocker, m new_callable=mocker.AsyncMock, return_value=_make_mock_model_entity(), ) - job_input_dict = make_valid_job_input_dict() + job_input_dict = make_valid_dpo_job_input_dict() job_input_dict["training"]["execution_profile"] = "a100" job_input = CustomizationJobInput.model_validate(job_input_dict) @@ -743,7 +732,7 @@ async def test_platform_job_config_compiler_distributed_explicit_execution_profi new_callable=mocker.AsyncMock, return_value=_make_mock_model_entity(), ) - job_input_dict = make_valid_job_input_dict() + job_input_dict = make_valid_dpo_job_input_dict() job_input_dict["training"]["parallelism"] = {"num_nodes": 2, "num_gpus_per_node": 4} job_input_dict["training"]["batch_size"] = 8 job_input_dict["training"]["execution_profile"] = "high_priority" @@ -775,7 +764,7 @@ async def test_platform_job_config_compiler_config_default_execution_profile(moc return_value=_make_mock_model_entity(), ) mocker.patch("nmp.customizer.app.jobs.training.compiler.config.default_training_execution_profile", "a100") - job_input = make_valid_job_input() + job_input = make_valid_dpo_job_input() transformed_spec, job_name = await _compiler_args_async( job_input, "workspace", entity_client=object(), sdk=mock_sdk @@ -802,7 +791,7 @@ async def test_platform_job_config_compiler_user_profile_overrides_config_defaul return_value=_make_mock_model_entity(), ) mocker.patch("nmp.customizer.app.jobs.training.compiler.config.default_training_execution_profile", "a100") - job_input_dict = make_valid_job_input_dict() + job_input_dict = make_valid_dpo_job_input_dict() job_input_dict["training"]["execution_profile"] = "spot" job_input = CustomizationJobInput.model_validate(job_input_dict) @@ -880,8 +869,7 @@ async def test_platform_job_config_compiler_uses_name_fallback_when_spec_flag_is return_value=model_entity, ) - job_input_dict = make_valid_job_input_dict() - del job_input_dict["training"]["peft"] + job_input_dict = make_valid_dpo_job_input_dict() job_input = CustomizationJobInput.model_validate(job_input_dict) transformed_spec, job_name = await _compiler_args_async( job_input, "workspace", entity_client=object(), sdk=mock_sdk @@ -926,7 +914,7 @@ async def test_platform_job_config_compiler_passes_chat_template_from_model_spec return_value=model_entity, ) - job_input = make_valid_job_input() + job_input = make_valid_dpo_job_input() transformed_spec, job_name = await _compiler_args_async( job_input, "workspace", entity_client=object(), sdk=mock_sdk ) @@ -970,7 +958,7 @@ async def test_platform_job_config_compiler_chat_template_none_when_spec_missing return_value=model_entity, ) - job_input = make_valid_job_input() + job_input = make_valid_dpo_job_input() transformed_spec, job_name = await _compiler_args_async( job_input, "workspace", entity_client=object(), sdk=mock_sdk ) diff --git a/services/evaluator/pyproject.toml b/services/evaluator/pyproject.toml index 6dad8c5f3a..eb7a8bc64f 100644 --- a/services/evaluator/pyproject.toml +++ b/services/evaluator/pyproject.toml @@ -42,7 +42,7 @@ dependencies = [ "kubernetes>=31.0.0", "openai>=1.61.0", "ragas==0.3.5", - "langchain-community>=0.3.27,<0.4", + "langchain-community>=0.3.31,<0.4", "pymilvus==2.6.9", "langchain-nvidia-ai-endpoints>=1.0.0,<2.0.0", "nemo-evaluator-sdk", diff --git a/services/intake/tests/integration/spans/test_atif_ingest.py b/services/intake/tests/integration/spans/test_atif_ingest.py index 39f0f0c417..1013d42274 100644 --- a/services/intake/tests/integration/spans/test_atif_ingest.py +++ b/services/intake/tests/integration/spans/test_atif_ingest.py @@ -4,6 +4,7 @@ """ATIF ingest tests.""" import json +from datetime import datetime, timedelta, timezone from decimal import Decimal import pytest @@ -12,6 +13,25 @@ _HISTORICAL_GTE = "2024-01-01T00:00:00Z" +def _recent_base_time() -> datetime: + """Return a UTC timestamp safely inside the spans list 30-day default lookback.""" + return datetime.now(timezone.utc) - timedelta(hours=2) + + +def _atif_timestamp(dt: datetime) -> str: + """Format a UTC datetime the way ATIF ingest payloads expect.""" + return dt.strftime("%Y-%m-%dT%H:%M:%S") + f".{dt.microsecond:06d}Z" + + +def _span_started_at(dt: datetime) -> str: + """Format a UTC datetime the way span list/get APIs return started_at.""" + return dt.replace(tzinfo=None).isoformat(timespec="microseconds") + + +def _span_ended_at(dt: datetime) -> str: + return _span_started_at(dt) + + def test_atif_ingest_rejects_loose_steps_payload(client: TestClient): body = { "session_id": "atif-session", @@ -176,6 +196,13 @@ def test_atif_ingest_accepts_example_trajectory_and_reconstructs_read_side_data( "metadata": {"trial": "sample-test-case-a__trial-a"}, } _create_experiment(client, evaluation_context["evaluation_id"]) + base_time = _recent_base_time() + user_step_time = base_time + agent_step_2_time = base_time + timedelta(seconds=5, milliseconds=636) + agent_step_3_time = base_time + timedelta(seconds=10, milliseconds=528) + verifier_started_at = base_time + timedelta(minutes=3, seconds=1, microseconds=657282) + verifier_finished_at = base_time + timedelta(minutes=8, seconds=45, microseconds=570079) + another_session_step_time = user_step_time - timedelta(minutes=4, seconds=29) tool_call = { "tool_call_id": "tooluse_tuIapjh62ZTI1pildiC9sg", "function_name": "Bash", @@ -209,8 +236,8 @@ def test_atif_ingest_accepts_example_trajectory_and_reconstructs_read_side_data( "trial_name": "sample-test-case-a__trial-a", "trial_uri": "artifact://sample-run/sample-test-case-a__trial-a", "verifier": { - "started_at": "2026-05-04T19:01:01.657282Z", - "finished_at": "2026-05-04T19:06:45.570079Z", + "started_at": _atif_timestamp(verifier_started_at), + "finished_at": _atif_timestamp(verifier_finished_at), }, "verifier_result": {"rewards": {"reward": 0.0}}, }, @@ -235,7 +262,7 @@ def test_atif_ingest_accepts_example_trajectory_and_reconstructs_read_side_data( "steps": [ { "step_id": 1, - "timestamp": "2026-05-04T18:57:59.943Z", + "timestamp": _atif_timestamp(user_step_time), "source": "user", "message": 'You need to create a file called "/app/solution.txt" with the word found in ' '"secret_file.txt" in the "secrets.7z" archive.', @@ -243,7 +270,7 @@ def test_atif_ingest_accepts_example_trajectory_and_reconstructs_read_side_data( }, { "step_id": 2, - "timestamp": "2026-05-04T18:58:05.579Z", + "timestamp": _atif_timestamp(agent_step_2_time), "source": "agent", "model_name": "provider/sample-model", "reasoning_content": "The archive exists; inspect available tooling.", @@ -285,7 +312,7 @@ def test_atif_ingest_accepts_example_trajectory_and_reconstructs_read_side_data( }, { "step_id": 3, - "timestamp": "2026-05-04T18:58:10.471Z", + "timestamp": _atif_timestamp(agent_step_3_time), "source": "agent", "model_name": "provider/sample-model", "message": "The archive exists but 7z isn't installed. Let me install it and extract the file.", @@ -367,8 +394,8 @@ def test_atif_ingest_accepts_example_trajectory_and_reconstructs_read_side_data( assert "evaluation_context" not in trajectory_raw assert "experiment.metadata" not in trajectory_raw assert "evaluation.metadata" not in trajectory_raw - assert trajectory["started_at"] == "2026-05-04T18:57:59.943000" - assert trajectory["ended_at"] == "2026-05-04T19:06:45.570079" + assert trajectory["started_at"] == _span_started_at(user_step_time) + assert trajectory["ended_at"] == _span_ended_at(verifier_finished_at) for span in spans: if span["name"] == "sample-agent": @@ -489,7 +516,7 @@ def test_atif_ingest_accepts_example_trajectory_and_reconstructs_read_side_data( "steps": [ { "step_id": 1, - "timestamp": "2026-05-04T18:53:30.000Z", + "timestamp": _atif_timestamp(another_session_step_time), "source": "user", "message": "Run the second sample task.", } @@ -566,6 +593,10 @@ def test_atif_trace_tokens_do_not_double_count_when_trajectory_and_steps_both_ca per-step metrics that sum to it. The trajectory span must NOT carry token attributes, or the trace-level rollup would sum them and report 2x the real total. """ + base_time = _recent_base_time() + user_step_time = base_time + agent_step_2_time = base_time + timedelta(seconds=5) + agent_step_3_time = base_time + timedelta(seconds=10) body = { "schema_version": "ATIF-v1.6", "session_id": "atif-rollup-no-double-count", @@ -586,13 +617,13 @@ def test_atif_trace_tokens_do_not_double_count_when_trajectory_and_steps_both_ca "steps": [ { "step_id": 1, - "timestamp": "2026-05-04T19:00:00Z", + "timestamp": _atif_timestamp(user_step_time), "source": "user", "message": "Help me with a task.", }, { "step_id": 2, - "timestamp": "2026-05-04T19:00:05Z", + "timestamp": _atif_timestamp(agent_step_2_time), "source": "agent", "model_name": "test-provider/test-model", "message": "Here is the first response.", @@ -605,7 +636,7 @@ def test_atif_trace_tokens_do_not_double_count_when_trajectory_and_steps_both_ca }, { "step_id": 3, - "timestamp": "2026-05-04T19:00:10Z", + "timestamp": _atif_timestamp(agent_step_3_time), "source": "agent", "model_name": "test-provider/test-model", "message": "Here is the follow-up.", diff --git a/services/unsloth/docker/Dockerfile.nmp-unsloth-training b/services/unsloth/docker/Dockerfile.nmp-unsloth-training index 7247980328..4858f7bc68 100644 --- a/services/unsloth/docker/Dockerfile.nmp-unsloth-training +++ b/services/unsloth/docker/Dockerfile.nmp-unsloth-training @@ -3,7 +3,7 @@ # an unsloth customization job (file_io download, training, file_io upload, # model_entity). # -# Two install steps: +# Install steps: # 1. `uv pip install unsloth --torch-backend=auto`. This is unsloth's # canonical install command (per their README). It pulls unsloth + # unsloth_zoo + the entire HF stack (transformers, trl, peft, accelerate, @@ -11,17 +11,19 @@ # pyproject.toml has constrained — including explicit !=X.Y.Z blocklists # for known-broken transformers/trl releases. We deliberately don't # second-guess these pins; they're tested upstream. +# 1b. flash-attn — optional for unsloth and not installed by step 1. Without +# it Unsloth falls back when xformers is also missing (common on newer CUDA +# stacks), logging "FA2 = False / Xformers = None". Installed immediately +# after unsloth so pip does not re-resolve the HF stack. # 2. Editable installs of the platform glue (nemo-platform SDK, plugin, # nmp-common, nmp-unsloth) from the in-repo workspace slice. # # Publish target: nmp-unsloth-training -# Default tag: `local` (override via IMAGE_TAG at build time). +# Default tag: `local` (override via BAKE_TAG at build time). -# NGC PyTorch base. 25.04-py3 ships PyTorch 2.7 + CUDA 12.8 + Python 3.12, -# which is the newest combination with first-class bitsandbytes wheel support. -# Bumping to 25.06-py3 (CUDA 12.9) or 26.02-py3 (CUDA 13.x) loses bitsandbytes -# because its published wheels currently top out at CUDA 12.8. -ARG PYTORCH_BASE=nvcr.io/nvidia/pytorch:25.04-py3 +# NGC PyTorch base. 26.02-py3 ships PyTorch 2.11 + CUDA 13.1 + Python 3.12. +# Override at build time: --set nmp-unsloth-training.args.PYTORCH_BASE=... +ARG PYTORCH_BASE=nvcr.io/nvidia/pytorch:26.02-py3 FROM ${PYTORCH_BASE} AS base @@ -38,9 +40,9 @@ ENV VIRTUAL_ENV=/opt/venv \ OTEL_PYTHON_EXCLUDED_URLS="health" ENV PATH="/opt/venv/bin:/root/.local/bin:${PATH}" -# --system-site-packages lets the venv inherit the NGC base's pre-built torch -# (CUDA 12.8 build). Without this, `uv pip install unsloth --torch-backend=auto` -# would have to download a fresh torch wheel from PyPI, ballooning image size. +# --system-site-packages lets the venv inherit the NGC base's pre-built torch. +# Without this, `uv pip install unsloth --torch-backend=auto` would download a +# fresh torch wheel from PyPI, ballooning image size. RUN uv venv ${UV_PROJECT_ENVIRONMENT} --system-site-packages # ────────────────────────────────────────────────────────────────────────── @@ -52,11 +54,10 @@ ARG USERNAME=ubuntu ARG USER_UID=1000 ARG USER_GID=1000 -COPY --from=platform-workspace / /app WORKDIR /app RUN mkdir -p /home/${USERNAME}/.cache && \ - chown -R ${USER_UID}:${USER_GID} /home/${USERNAME} /app/services/unsloth + chown -R ${USER_UID}:${USER_GID} /home/${USERNAME} # Step 1: install unsloth via its own resolver. --torch-backend=auto tells uv # to detect the existing torch's CUDA build (from --system-site-packages @@ -66,6 +67,26 @@ RUN --mount=type=cache,target=/root/.cache/uv \ --torch-backend=auto \ unsloth +# TODO: Step 1b: Flash Attention 2 — compiled from source against the NGC 26.02 torch. +# /usr/local/cuda symlinks to an older toolkit; use /usr/local/cuda-13.1 instead. +# Cap parallel nvcc/ninja work — default uses all CPUs and OOMs typical build hosts. +# Put flash attention back in when we have a working wheel in a separate image. +# ARG FLASH_ATTN_MAX_JOBS=40 +# ARG NVCC_THREADS=1 +# RUN --mount=type=cache,target=/root/.cache/uv \ +# --mount=type=cache,target=/root/.cache/pip \ +# uv pip install --python ${VIRTUAL_ENV}/bin/python --no-cache \ +# ninja einops packaging && \ +# CUDA_HOME=/usr/local/cuda-13.1 \ +# MAX_JOBS=${FLASH_ATTN_MAX_JOBS} \ +# NVCC_THREADS=${NVCC_THREADS} \ +# FLASH_ATTN_FORCE_BUILD=TRUE \ +# uv pip install --python ${VIRTUAL_ENV}/bin/python --no-cache \ +# --no-build-isolation flash-attn==2.8.3 + +COPY --from=platform-workspace / /app +RUN chown -R ${USER_UID}:${USER_GID} /app + # Step 2: install the platform glue editably. No [unsloth] extra here — that's # what we just installed in step 1, and re-triggering it would force the # resolver to re-evaluate the whole HF stack. diff --git a/services/unsloth/docker/README.md b/services/unsloth/docker/README.md index abb4db11f1..571a1d726c 100644 --- a/services/unsloth/docker/README.md +++ b/services/unsloth/docker/README.md @@ -8,12 +8,11 @@ model_entity). |-------|------------|------| | `nmp-unsloth-training` | `Dockerfile.nmp-unsloth-training` | NGC PyTorch base + Unsloth ML stack + platform glue. ENTRYPOINT is `/opt/venv/bin/python`. | -Default tag is `nmp-unsloth-training:local` (no registry prefix) — suitable for -in-daemon builds via `--load`. Set `IMAGE_REGISTRY` to add a prefix when you -want to push: +Bake file: **`docker-bake.hcl`** at the Platform repo root (`context = "."`). Run all commands from the Platform repo root. -- Local: `nmp-unsloth-training:local` -- Pushed: `${IMAGE_REGISTRY}/nmp-unsloth-training:${IMAGE_TAG}` +Tags use the same bake variables as automodel (`IMAGE_REGISTRY`, `BAKE_TAG`; defaults in `docker-bake.hcl`): + +- Default: `${IMAGE_REGISTRY}/nmp-unsloth-training:${BAKE_TAG}` (e.g. `…/nmp-unsloth-training:local` with `--load`) Future-proofing: a leaner CPU image (`nmp-unsloth-tasks`) can be added later for the file_io / model_entity steps. The compiler already routes @@ -29,22 +28,22 @@ cd /path/to/Platform # --- Option A: local build (loads into the local daemon, no registry needed) --- docker buildx bake \ - -f services/unsloth/docker/docker-bake.hcl \ + -f docker-bake.hcl \ nmp-unsloth-training \ --load \ --set "*.platform=linux/amd64" -# Result: `nmp-unsloth-training:local` in `docker images`. +# Result: `${IMAGE_REGISTRY}/nmp-unsloth-training:${BAKE_TAG}` in `docker images`. # --- Option B: push to a registry --- docker login nvcr.io export IMAGE_REGISTRY="nvcr.io/0921617854601259/nemo-platform-dev" -export IMAGE_TAG="$(git rev-parse --short HEAD)" +export BAKE_TAG="$(git rev-parse --short HEAD)" docker buildx bake \ - -f services/unsloth/docker/docker-bake.hcl \ + -f docker-bake.hcl \ nmp-unsloth-training \ --push \ --set "*.platform=linux/amd64" -# Result: `${IMAGE_REGISTRY}/nmp-unsloth-training:${IMAGE_TAG}` in the registry. +# Result: `${IMAGE_REGISTRY}/nmp-unsloth-training:${BAKE_TAG}` in the registry. ``` The build pulls the NGC PyTorch base, then runs unsloth's canonical install @@ -56,6 +55,12 @@ in two steps: xformers) at versions tested upstream — we deliberately don't pin any of them on our end, because unsloth's pyproject already has precise `!=X.Y.Z` blocklists for known-broken releases. +1b. Flash Attention 2 (Dockerfile step 1b) — source build with + `--no-build-isolation` against the NGC base torch (cached Docker layer). + Parallelism is capped via `MAX_JOBS` (default `2`, override with bake arg + `FLASH_ATTN_MAX_JOBS`) and `NVCC_THREADS=1` to avoid OOM during nvcc. + Unsloth does not depend on `flash-attn`; without it you may see + `FA2 = False` and `Xformers = None` on newer CUDA stacks. 2. Editable install of the platform glue: `nemo-platform-sdk`, `nemo-platform-plugin`, `nmp-common`, `nmp-unsloth`. @@ -109,7 +114,7 @@ Pick **one** of the following depending on where the GPU host will pull from: ```bash docker buildx bake \ - -f services/unsloth/docker/docker-bake.hcl \ + -f docker-bake.hcl \ nmp-unsloth-training \ --load \ --set "*.platform=linux/amd64" @@ -120,25 +125,26 @@ docker buildx bake \ ```bash export IMAGE_REGISTRY="nvcr.io/0921617854601259/nemo-platform-dev" -export IMAGE_TAG="$(git rev-parse --short HEAD)" +export BAKE_TAG="$(git rev-parse --short HEAD)" docker buildx bake \ - -f services/unsloth/docker/docker-bake.hcl \ + -f docker-bake.hcl \ nmp-unsloth-training \ --push \ --set "*.platform=linux/amd64" -# → ${IMAGE_REGISTRY}/nmp-unsloth-training:${IMAGE_TAG} in the registry. +# → ${IMAGE_REGISTRY}/nmp-unsloth-training:${BAKE_TAG} in the registry. ``` **C) Air-gapped GPU host** (save + `scp` + `docker load`): ```bash -export IMAGE_TAG="$(git rev-parse --short HEAD)" +export IMAGE_REGISTRY="nvcr.io/0921617854601259/nemo-platform-dev" +export BAKE_TAG="$(git rev-parse --short HEAD)" docker buildx build \ -f services/unsloth/docker/Dockerfile.nmp-unsloth-training \ --output type=docker,dest=/tmp/nmp-unsloth-training.tar \ --target runtime \ - -t "nmp-unsloth-training:${IMAGE_TAG}" \ + -t "${IMAGE_REGISTRY}/nmp-unsloth-training:${BAKE_TAG}" \ --build-context "platform-workspace=path-to-platform-workspace" \ . @@ -152,11 +158,11 @@ On the host running `nemo services run`, set the full image ref. For a local build this is just the bare name: ```bash -# Local build: -export NMP_UNSLOTH_TRAINING_IMAGE="nmp-unsloth-training:local" +# Local bake (--load; matches docker-bake.hcl defaults): +export NMP_UNSLOTH_TRAINING_IMAGE="${IMAGE_REGISTRY:-nvcr.io/0921617854601259/nemo-platform-dev}/nmp-unsloth-training:${BAKE_TAG:-local}" # Or, when you pushed (Option B above): -export NMP_UNSLOTH_TRAINING_IMAGE="${IMAGE_REGISTRY}/nmp-unsloth-training:${IMAGE_TAG}" +export NMP_UNSLOTH_TRAINING_IMAGE="${IMAGE_REGISTRY}/nmp-unsloth-training:${BAKE_TAG}" # Restart so the env var takes effect. nemo services restart @@ -166,7 +172,7 @@ Or persist in `~/.nemo/config.yaml`: ```yaml unsloth: - training_image: nmp-unsloth-training:local + training_image: nvcr.io/0921617854601259/nemo-platform-dev/nmp-unsloth-training:local ``` ### 3. Prepare model + dataset filesets diff --git a/services/unsloth/docker/docker-bake.hcl b/services/unsloth/docker/docker-bake.hcl index 30764d2c9e..d961287205 100644 --- a/services/unsloth/docker/docker-bake.hcl +++ b/services/unsloth/docker/docker-bake.hcl @@ -1,56 +1,4 @@ -# nmp-unsloth bake — single image target. +# Moved to Platform repo root: +# docker buildx bake -f docker-bake.hcl # -# Run from the Platform repo root: -# -# # Local build (no registry prefix, --load into local daemon): -# docker buildx bake -f services/unsloth/docker/docker-bake.hcl nmp-unsloth-training --load -# -# # Push to a registry: -# IMAGE_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev \ -# docker buildx bake -f services/unsloth/docker/docker-bake.hcl nmp-unsloth-training --push -# -# Override via env vars: -# IMAGE_REGISTRY = registry+repo prefix (default: empty → bare `nmp-unsloth-training:TAG`) -# IMAGE_TAG = image tag (default: local) -# BUILD_PLATFORM = OCI platform (default: linux/amd64) - -variable "IMAGE_REGISTRY" { - default = "" -} - -variable "IMAGE_TAG" { - default = "local" -} - -variable "BUILD_PLATFORM" { - default = "linux/amd64" -} - -# Named platform-workspace build context (built from the repo root). Each -# target consumes it via `contexts.platform-workspace`. -target "platform-workspace" { - context = "." - dockerfile = "services/unsloth/docker/Dockerfile.platform-workspace" - target = "platform-workspace" - output = ["type=cacheonly"] -} - -target "nmp-unsloth-training" { - context = "." - dockerfile = "services/unsloth/docker/Dockerfile.nmp-unsloth-training" - target = "runtime" - contexts = { - platform-workspace = "target:platform-workspace" - } - # Tag with the registry prefix only when one is supplied; otherwise emit the - # bare local name so the image lands as `nmp-unsloth-training:TAG` in the - # daemon's image store. - tags = [ - IMAGE_REGISTRY != "" ? "${IMAGE_REGISTRY}/nmp-unsloth-training:${IMAGE_TAG}" : "nmp-unsloth-training:${IMAGE_TAG}", - ] - platforms = ["${BUILD_PLATFORM}"] -} - -group "default" { - targets = ["nmp-unsloth-training"] -} +# Context is "." (repo root when run from Platform/). Do not use ../../.. here. diff --git a/services/unsloth/src/nmp/unsloth/app/constants.py b/services/unsloth/src/nmp/unsloth/app/constants.py index fdeaf64ee3..32e8c97041 100644 --- a/services/unsloth/src/nmp/unsloth/app/constants.py +++ b/services/unsloth/src/nmp/unsloth/app/constants.py @@ -15,6 +15,7 @@ # Subdirectory names under the job's persistent storage root. DEFAULT_MODEL_OUTPUT_DIR_NAME = "model" DEFAULT_DATASET_OUTPUT_DIR_NAME = "dataset" +DEFAULT_VALIDATION_DATASET_OUTPUT_DIR_NAME = "validation_dataset" DEFAULT_OUTPUT_MODEL_DIR_NAME = "output_model" # Absolute paths used by the compiler when wiring step-to-step file sharing. @@ -22,6 +23,7 @@ # ``ctx.storage.persistent`` so it does not depend on these values. DEFAULT_MODEL_PATH = f"{DEFAULT_JOB_STORAGE_PATH}/{DEFAULT_MODEL_OUTPUT_DIR_NAME}" DEFAULT_DATASET_PATH = f"{DEFAULT_JOB_STORAGE_PATH}/{DEFAULT_DATASET_OUTPUT_DIR_NAME}" +DEFAULT_VALIDATION_DATASET_PATH = f"{DEFAULT_JOB_STORAGE_PATH}/{DEFAULT_VALIDATION_DATASET_OUTPUT_DIR_NAME}" DEFAULT_OUTPUT_MODEL_PATH = f"{DEFAULT_JOB_STORAGE_PATH}/{DEFAULT_OUTPUT_MODEL_DIR_NAME}" NMP_JOBS_URL_ENVVAR = "NMP_JOBS_URL" diff --git a/services/unsloth/src/nmp/unsloth/app/jobs/compiler.py b/services/unsloth/src/nmp/unsloth/app/jobs/compiler.py index 6b3dc33038..b4b3083986 100644 --- a/services/unsloth/src/nmp/unsloth/app/jobs/compiler.py +++ b/services/unsloth/src/nmp/unsloth/app/jobs/compiler.py @@ -34,6 +34,7 @@ DEFAULT_DATASET_PATH, DEFAULT_MODEL_PATH, DEFAULT_OUTPUT_MODEL_PATH, + DEFAULT_VALIDATION_DATASET_PATH, ) from nmp.unsloth.app.jobs.file_io.schemas import ( DownloadItem, @@ -97,6 +98,29 @@ def _build_peft_config(spec: UnslothJobOutput) -> PEFTConfig | None: ) +def _same_fileset_ref(a: str, b: str, *, workspace: str) -> bool: + """Return True when two platform fileset refs denote the same fileset.""" + ra = FileSetRef.model_validate(a) + rb = FileSetRef.model_validate(b) + return (ra.workspace or workspace) == (rb.workspace or workspace) and ra.name == rb.name + + +def _resolve_validation_dataset_path( + job_spec: UnslothJobOutput, + workspace: str, +) -> str | None: + """Map ``spec.dataset.validation_path`` to the local PVC path the download step uses.""" + if not job_spec.dataset.validation_path: + return None + if _same_fileset_ref( + job_spec.dataset.path, + job_spec.dataset.validation_path, + workspace=workspace, + ): + return DEFAULT_DATASET_PATH + return DEFAULT_VALIDATION_DATASET_PATH + + def _require_fileset(name: str | None, *, label: str) -> str: if not name or not str(name).strip(): raise PlatformJobCompilationError( @@ -109,24 +133,36 @@ def _require_fileset(name: str | None, *, label: str) -> str: def _build_file_download_config( job_spec: UnslothJobOutput, me: ModelEntity, + *, + workspace: str, ) -> FileIOTaskConfig: """Compile the download step: model fileset + dataset fileset.""" model_fileset = _require_fileset( me.fileset, label=f"Model '{me.workspace}/{me.name}'", ) - return FileIOTaskConfig( - download=[ - DownloadItem( - src=FileSetRef.model_validate(model_fileset), - dest=DEFAULT_MODEL_PATH, - ), + downloads = [ + DownloadItem( + src=FileSetRef.model_validate(model_fileset), + dest=DEFAULT_MODEL_PATH, + ), + DownloadItem( + src=FileSetRef.model_validate(job_spec.dataset.path), + dest=DEFAULT_DATASET_PATH, + ), + ] + if job_spec.dataset.validation_path and not _same_fileset_ref( + job_spec.dataset.path, + job_spec.dataset.validation_path, + workspace=workspace, + ): + downloads.append( DownloadItem( - src=FileSetRef.model_validate(job_spec.dataset.path), - dest=DEFAULT_DATASET_PATH, + src=FileSetRef.model_validate(job_spec.dataset.validation_path), + dest=DEFAULT_VALIDATION_DATASET_PATH, ), - ], - ) + ) + return FileIOTaskConfig(download=downloads) def _build_file_upload_config(output_fileset_name: str) -> FileIOTaskConfig: @@ -192,7 +228,8 @@ async def platform_job_config_compiler( cpu_resources = _get_cpu_resources() base_env = _get_base_environment() - download_config = _build_file_download_config(job_spec, me) + validation_dataset_path = _resolve_validation_dataset_path(job_spec, workspace=workspace) + download_config = _build_file_download_config(job_spec, me, workspace=workspace) upload_config = _build_file_upload_config(job_spec.output.fileset) model_entity_config = _build_model_entity_config( workspace, @@ -215,7 +252,12 @@ async def platform_job_config_compiler( environment=base_env, config=download_config.model_dump(mode="json"), ), - compile_training_step(job_spec, base_env, profile=profile), + compile_training_step( + job_spec, + base_env, + validation_dataset_path=validation_dataset_path, + profile=profile, + ), PlatformJobStep( name="model-upload", executor=CPUExecutionProviderSpec( diff --git a/services/unsloth/src/nmp/unsloth/app/jobs/training/compiler.py b/services/unsloth/src/nmp/unsloth/app/jobs/training/compiler.py index e0be00c9a0..8321fb63de 100644 --- a/services/unsloth/src/nmp/unsloth/app/jobs/training/compiler.py +++ b/services/unsloth/src/nmp/unsloth/app/jobs/training/compiler.py @@ -36,7 +36,7 @@ def compile_training_step( job_spec: UnslothJobOutput, base_env: list[EnvironmentVariable], - *, + validation_dataset_path: str | None = None, profile: str | None = None, ) -> PlatformJobStep: """Build the GPU training :class:`PlatformJobStep`. @@ -49,6 +49,9 @@ def compile_training_step( job_spec: Canonical job spec to serialize into the step config. base_env: Environment variables carried into every step (e.g. ``PERSISTENT_JOB_STORAGE_PATH``). + validation_dataset_path: Local path the file_io download step + populated for validation. ``None`` when the job has no + validation split. profile: GPU execution profile (e.g. ``gpu`` or ``gpu_distributed``). When ``None`` the executor's default is used. @@ -57,10 +60,11 @@ def compile_training_step( spec=job_spec, model_path=DEFAULT_MODEL_PATH, dataset_path=DEFAULT_DATASET_PATH, + validation_path=validation_dataset_path, output_path=DEFAULT_OUTPUT_MODEL_PATH, ) - executor_kwargs: dict = { + executor: GPUExecutionProviderSpec = { "provider": "gpu", "container": ContainerSpec( image=get_training_image(), @@ -72,11 +76,11 @@ def compile_training_step( "resources": ResourcesSpec(), } if profile is not None: - executor_kwargs["profile"] = profile + executor["profile"] = profile return PlatformJobStep( name="training", - executor=GPUExecutionProviderSpec(**executor_kwargs), + executor=executor, environment=base_env, config=step_config.model_dump(mode="json"), ) diff --git a/services/unsloth/src/nmp/unsloth/app/jobs/training/schemas.py b/services/unsloth/src/nmp/unsloth/app/jobs/training/schemas.py index 5417ff83d4..fc11936fc3 100644 --- a/services/unsloth/src/nmp/unsloth/app/jobs/training/schemas.py +++ b/services/unsloth/src/nmp/unsloth/app/jobs/training/schemas.py @@ -24,4 +24,12 @@ class TrainingStepConfig(BaseModel): spec: UnslothJobOutput = Field(description="Canonical job spec for the training run.") model_path: str = Field(description="Local filesystem path where the model weights were downloaded.") dataset_path: str = Field(description="Local filesystem path where the training dataset was downloaded.") + validation_path: str | None = Field( + default=None, + description=( + "Local filesystem path where the validation dataset was downloaded. " + "When set, overrides ``spec.dataset.validation_path`` so the trainer " + "reads on-disk JSONL instead of treating the platform ref as an HF id." + ), + ) output_path: str = Field(description="Local filesystem path the training driver should save the checkpoint to.") diff --git a/services/unsloth/src/nmp/unsloth/compile.py b/services/unsloth/src/nmp/unsloth/compile.py index 48fbaeb9ca..58145187cd 100644 --- a/services/unsloth/src/nmp/unsloth/compile.py +++ b/services/unsloth/src/nmp/unsloth/compile.py @@ -11,24 +11,20 @@ from __future__ import annotations -from typing import TYPE_CHECKING - +from nemo_platform import AsyncNeMoPlatform +from nemo_platform_plugin.jobs.api_factory import PlatformJobSpec from nmp.unsloth.app.jobs.compiler import platform_job_config_compiler as _compile_canonical - -if TYPE_CHECKING: - from nemo_platform import AsyncNeMoPlatform - from nemo_platform_plugin.jobs.api_factory import PlatformJobSpec - from nmp.unsloth.schemas import UnslothJobOutput +from nmp.unsloth.schemas import UnslothJobOutput async def platform_job_config_compiler( *, workspace: str, - spec: "UnslothJobOutput", - sdk: "AsyncNeMoPlatform", + spec: UnslothJobOutput, + sdk: AsyncNeMoPlatform, job_name: str | None = None, profile: str | None = None, -) -> "PlatformJobSpec": +) -> PlatformJobSpec: """Compile a canonical unsloth job spec to a ``PlatformJobSpec``. Used by :meth:`UnslothJob.compile`. Container submit only — Unsloth diff --git a/services/unsloth/src/nmp/unsloth/schemas.py b/services/unsloth/src/nmp/unsloth/schemas.py index bcc419eccc..2b165e32dd 100644 --- a/services/unsloth/src/nmp/unsloth/schemas.py +++ b/services/unsloth/src/nmp/unsloth/schemas.py @@ -18,7 +18,7 @@ - ``unsloth.FastLanguageModel.from_pretrained(...)`` → :class:`ModelLoadSpec` - ``unsloth.FastLanguageModel.get_peft_model(...)`` → :class:`LoRAParams` -- ``transformers.TrainingArguments`` + ``trl.SFTTrainer(...)`` → +- ``trl.SFTConfig`` + ``trl.SFTTrainer(...)`` → :class:`ScheduleSpec`, :class:`BatchSpec`, :class:`OptimizerSpec`, :class:`HardwareSpec`, :class:`IntegrationsSpec` - Output saving (``model.save_pretrained{,_merged}``) → :class:`OutputResponse` diff --git a/services/unsloth/src/nmp/unsloth/tasks/file_io/run.py b/services/unsloth/src/nmp/unsloth/tasks/file_io/run.py index 052af33194..4a9f4acaf4 100644 --- a/services/unsloth/src/nmp/unsloth/tasks/file_io/run.py +++ b/services/unsloth/src/nmp/unsloth/tasks/file_io/run.py @@ -198,7 +198,7 @@ def _download_with_retry( fileset=fileset_name, workspace=fileset_workspace, local_path=dest_dir, - callback=callback, # type: ignore[arg-type] + callback=callback, ) def upload_fileset(self, fileset: FileSetRef, src_path: Path) -> UploadStats: @@ -270,7 +270,7 @@ def _upload_with_retry( remote_path=remote_path, fileset=fileset_name, workspace=fileset_workspace, - callback=callback, # type: ignore[arg-type] + callback=callback, ) def create_fileset(self, fileset: FileSetRef, metadata: dict | None = None) -> None: diff --git a/services/unsloth/src/nmp/unsloth/tasks/training/__main__.py b/services/unsloth/src/nmp/unsloth/tasks/training/__main__.py index d2c5886bf1..924657ad92 100644 --- a/services/unsloth/src/nmp/unsloth/tasks/training/__main__.py +++ b/services/unsloth/src/nmp/unsloth/tasks/training/__main__.py @@ -88,7 +88,7 @@ def main() -> int: ctx = JobContext( workspace=os.environ.get("NEMO_JOB_WORKSPACE", "default"), storage=storage, - results=None, # type: ignore[arg-type] + results=None, job_id=os.environ.get("NEMO_JOB_ID"), ) @@ -109,7 +109,9 @@ def main() -> int: hf_home = storage.ephemeral / "hf" hf_home.mkdir(parents=True, exist_ok=True) os.environ.setdefault("HF_HOME", str(hf_home)) - logger.info(f"Container: UNSLOTH_COMPILE_LOCATION={os.environ['UNSLOTH_COMPILE_LOCATION']} HF_HOME={os.environ['HF_HOME']}") + logger.info( + f"Container: UNSLOTH_COMPILE_LOCATION={os.environ['UNSLOTH_COMPILE_LOCATION']} HF_HOME={os.environ['HF_HOME']}" + ) try: result = train_sft( @@ -117,6 +119,7 @@ def main() -> int: ctx, model_path=config.model_path, dataset_path=config.dataset_path, + validation_path=config.validation_path, output_path=config.output_path, ) except Exception: diff --git a/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/callbacks.py b/services/unsloth/src/nmp/unsloth/tasks/training/backends/callbacks.py similarity index 64% rename from services/customizer/src/nmp/customizer/tasks/training/backends/automodel/callbacks.py rename to services/unsloth/src/nmp/unsloth/tasks/training/backends/callbacks.py index 51f3148ccd..4a3da9e631 100644 --- a/services/customizer/src/nmp/customizer/tasks/training/backends/automodel/callbacks.py +++ b/services/unsloth/src/nmp/unsloth/tasks/training/backends/callbacks.py @@ -1,23 +1,20 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +"""Training progress callbacks for Unsloth Jobs-service reporting.""" + import logging -from nmp.customizer.tasks.training.progress import JobsServiceProgressReporter +from nmp.unsloth.tasks.training.progress import JobsServiceProgressReporter logger = logging.getLogger(__name__) class TrainingProgressCallback: - """ - Callback for reporting Automodel training progress to the Jobs service. + """Report Unsloth training progress to the Jobs service. - This class composes JobsServiceProgressReporter and provides training-specific - methods for reporting detailed metrics during training. - - Metric accumulation: train_loss and val_loss are accumulated as time-series - lists and included in every status_details update under a ``metrics`` key, - enabling loss-curve reconstruction from job status. + Metric accumulation matches Automodel: ``train_loss`` and ``val_loss`` + time series are included under ``metrics`` on every update. """ def __init__(self, reporter: JobsServiceProgressReporter): @@ -34,16 +31,20 @@ def __init__(self, reporter: JobsServiceProgressReporter): ) def _build_metrics_summary(self) -> dict[str, list[dict[str, float | int]]]: - """Build the accumulated metrics payload for inclusion in status_details.""" return { "train_loss": list(self._train_metrics), "val_loss": list(self._val_metrics), } - def report_training_start(self, max_steps: int, num_epochs: int) -> None: - """Report that training has started with schedule information.""" + def report_training_start(self, max_steps: int, num_epochs: int, *, backend: str = "unsloth") -> None: self._reporter.configure_progress_tracking(max_steps, num_epochs) - self._reporter.report_running(phase="training", step=0, max_steps=max_steps, num_epochs=num_epochs) + self._reporter.report_running( + phase="training", + step=0, + max_steps=max_steps, + num_epochs=num_epochs, + backend=backend, + ) def report_train_step( self, @@ -52,8 +53,9 @@ def report_train_step( loss: float, lr: float | None = None, grad_norm: float | None = None, + *, + backend: str = "unsloth", ) -> None: - """Report training step with metrics.""" self._train_metrics.append({"step": step, "epoch": epoch, "value": loss}) self._reporter.report_running( phase="training", @@ -62,33 +64,46 @@ def report_train_step( train_loss=loss, lr=lr, grad_norm=grad_norm, + backend=backend, metrics=self._build_metrics_summary(), ) - def report_validation(self, step: int, epoch: int, val_loss: float) -> None: - """Report validation results.""" + def report_validation( + self, + step: int, + epoch: int, + val_loss: float, + *, + backend: str = "unsloth", + ) -> None: self._val_metrics.append({"step": step, "epoch": epoch, "value": val_loss}) self._reporter.report_running( phase="validation", step=step, epoch=epoch, val_loss=val_loss, + backend=backend, metrics=self._build_metrics_summary(), ) - def report_checkpoint_saved(self, step: int, epoch: int, checkpoint_path: str | None = None) -> None: - """Report that a checkpoint was saved.""" + def report_checkpoint_saved( + self, + step: int, + epoch: int, + checkpoint_path: str | None = None, + *, + backend: str = "unsloth", + ) -> None: self._reporter.report_running( phase="checkpoint_saved", step=step, epoch=epoch, checkpoint_path=checkpoint_path, + backend=backend, ) - def report_epoch_end(self, step: int, epoch: int) -> None: - """Report that an epoch has completed.""" - self._reporter.report_running(phase="epoch_end", step=step, epoch=epoch) + def report_epoch_end(self, step: int, epoch: int, *, backend: str = "unsloth") -> None: + self._reporter.report_running(phase="epoch_end", step=step, epoch=epoch, backend=backend) def close(self) -> None: - """Clean up resources.""" self._reporter.close() diff --git a/services/unsloth/src/nmp/unsloth/tasks/training/backends/hf_trainer_callback.py b/services/unsloth/src/nmp/unsloth/tasks/training/backends/hf_trainer_callback.py new file mode 100644 index 0000000000..c8c498b871 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/tasks/training/backends/hf_trainer_callback.py @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Bridge HuggingFace Trainer callbacks to Jobs-service progress reporting.""" + +import math +from typing import Any + +from nmp.unsloth.tasks.training.backends.callbacks import TrainingProgressCallback + + +def _epoch_from_value(raw_epoch: float | int, num_epochs: int) -> int: + """Map HF fractional epoch values to a 1-based epoch index.""" + return max(1, min(num_epochs, math.ceil(float(raw_epoch)))) + + +def create_hf_trainer_progress_callback( + progress_callback: TrainingProgressCallback, + *, + backend: str = "unsloth", +) -> Any: + """Build a HuggingFace :class:`~transformers.TrainerCallback` for Jobs reporting. + + Import is deferred so this module stays importable without ``transformers``. + """ + from transformers import TrainerCallback + + class HfTrainerProgressCallback(TrainerCallback): + def __init__(self) -> None: + self._progress = progress_callback + self._backend = backend + self._num_epochs = 1 + + def on_train_begin(self, args: Any, state: Any, control: Any, **kwargs: Any) -> None: + self._num_epochs = max(1, int(args.num_train_epochs)) + max_steps = max(1, int(state.max_steps)) + self._progress.report_training_start( + max_steps=max_steps, + num_epochs=self._num_epochs, + backend=self._backend, + ) + + def on_log( + self, args: Any, state: Any, control: Any, logs: dict[str, Any] | None = None, **kwargs: Any + ) -> None: + if not logs or "loss" not in logs: + return + + epoch_raw = logs.get("epoch", state.epoch if state.epoch is not None else 0) + self._progress.report_train_step( + step=int(state.global_step), + epoch=_epoch_from_value(epoch_raw, self._num_epochs), + loss=float(logs["loss"]), + lr=float(logs["learning_rate"]) if logs.get("learning_rate") is not None else None, + grad_norm=float(logs["grad_norm"]) if logs.get("grad_norm") is not None else None, + backend=self._backend, + ) + + def on_evaluate( + self, + args: Any, + state: Any, + control: Any, + metrics: dict[str, Any] | None = None, + **kwargs: Any, + ) -> None: + if not metrics or "eval_loss" not in metrics: + return + + epoch_raw = metrics.get("epoch", state.epoch if state.epoch is not None else 0) + self._progress.report_validation( + step=int(state.global_step), + epoch=_epoch_from_value(epoch_raw, self._num_epochs), + val_loss=float(metrics["eval_loss"]), + backend=self._backend, + ) + + def on_save(self, args: Any, state: Any, control: Any, **kwargs: Any) -> None: + checkpoint_path = kwargs.get("checkpoint_folder", args.output_dir) + epoch_raw = state.epoch if state.epoch is not None else 0 + self._progress.report_checkpoint_saved( + step=int(state.global_step), + epoch=_epoch_from_value(epoch_raw, self._num_epochs), + checkpoint_path=str(checkpoint_path) if checkpoint_path is not None else None, + backend=self._backend, + ) + + return HfTrainerProgressCallback() diff --git a/services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py b/services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py index 41154f9524..6c65f9022f 100644 --- a/services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py +++ b/services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py @@ -19,15 +19,13 @@ imports silently degrade performance. """ -from __future__ import annotations - import logging import os -from typing import TYPE_CHECKING, Any +from typing import Any, Literal -if TYPE_CHECKING: - from nemo_platform_plugin.job_context import JobContext - from nmp.unsloth.schemas import UnslothJobOutput +from nemo_platform_plugin.job_context import JobContext +from nmp.unsloth.schemas import UnslothJobOutput +from nmp.unsloth.tasks.training.backends.callbacks import TrainingProgressCallback logger = logging.getLogger(__name__) @@ -40,6 +38,7 @@ def train_sft( dataset_path: str | None = None, validation_path: str | None = None, output_path: str | None = None, + progress_callback: TrainingProgressCallback | None = None, ) -> dict[str, Any]: """Run SFT with Unsloth's FastLanguageModel + LoRA. @@ -61,6 +60,9 @@ def train_sft( step's expected location. When ``None`` falls back to ``ctx.storage.persistent / spec.output.name`` (matches the historical local-run layout — kept for tests). + progress_callback: Optional Jobs-service progress reporter. + When ``None``, a callback is created from platform job + environment variables (no-op when Jobs context is absent). Returns: A result dict with final training loss, step count, output @@ -76,8 +78,7 @@ def train_sft( # reorder. import unsloth # noqa: F401 (import-side-effects required) from datasets import Dataset, load_dataset - from transformers import TrainingArguments - from trl import SFTTrainer + from trl import SFTConfig, SFTTrainer from unsloth import FastLanguageModel if spec.training.training_type != "sft": @@ -163,6 +164,7 @@ def train_sft( tokenizer=tokenizer, load_dataset=load_dataset, Dataset=Dataset, + split="train", ) eval_ds = None resolved_validation_path = validation_path or spec.dataset.validation_path @@ -174,9 +176,10 @@ def train_sft( tokenizer=tokenizer, load_dataset=load_dataset, Dataset=Dataset, + split="validation", ) - # ── TrainingArguments ───────────────────────────────────────────── + # ── SFTConfig ─────────────────────────────────────────────────── bf16 = spec.hardware.precision == "bf16" fp16 = spec.hardware.precision == "fp16" @@ -196,6 +199,10 @@ def train_sft( "report_to": list( spec.integrations.report_to if spec.integrations is not None else ["none"], ), + # SFT-specific — belong on SFTConfig in trl>=0.13, not on SFTTrainer. + "dataset_text_field": spec.dataset.text_field, + "max_length": spec.model.max_seq_length, + "packing": spec.dataset.packing, } if spec.schedule.warmup_ratio is not None: args_kwargs["warmup_ratio"] = spec.schedule.warmup_ratio @@ -206,6 +213,8 @@ def train_sft( if spec.schedule.save_steps is not None: args_kwargs["save_steps"] = spec.schedule.save_steps args_kwargs["save_strategy"] = "steps" + else: + args_kwargs["save_strategy"] = "epoch" if spec.schedule.eval_steps is not None: args_kwargs["eval_steps"] = spec.schedule.eval_steps args_kwargs["eval_strategy"] = "steps" @@ -218,19 +227,25 @@ def train_sft( if spec.integrations.wandb.project: os.environ.setdefault("WANDB_PROJECT", spec.integrations.wandb.project) - args = TrainingArguments(**args_kwargs) + args = SFTConfig(**args_kwargs) + + progress = progress_callback or _create_progress_callback() + from nmp.unsloth.tasks.training.backends.hf_trainer_callback import ( + create_hf_trainer_progress_callback, + ) trainer = SFTTrainer( model=model, - tokenizer=tokenizer, + processing_class=tokenizer, train_dataset=train_ds, eval_dataset=eval_ds, - dataset_text_field=spec.dataset.text_field, - max_seq_length=spec.model.max_seq_length, - packing=spec.dataset.packing, args=args, + callbacks=[create_hf_trainer_progress_callback(progress)], ) - train_result = trainer.train() + try: + train_result = trainer.train() + finally: + progress.close() # ── Save ────────────────────────────────────────────────────────── saved_path = _save_model(model, tokenizer, output_dir, spec) @@ -248,7 +263,23 @@ def train_sft( } -def _resolve_local_data_files(path: str) -> str | list[str]: +def _create_progress_callback() -> TrainingProgressCallback: + """Build a Jobs-service progress callback from platform env vars.""" + from nmp.unsloth.app.jobs.context import NMPJobContext + from nmp.unsloth.tasks.training.progress import JobsServiceProgressReporter + + return TrainingProgressCallback(JobsServiceProgressReporter(NMPJobContext.from_env())) + + +TRAIN_FILE = "train.jsonl" +VAL_FILE = "validation.jsonl" + + +def _resolve_local_data_files( + path: str, + *, + split: Literal["train", "validation"] | None = None, +) -> str | list[str]: """Resolve a local dataset path to the JSON/JSONL file(s) to load. A file path is returned unchanged. A directory (the file_io download @@ -256,6 +287,10 @@ def _resolve_local_data_files(path: str) -> str | list[str]: it, falling back to ``.json``. Both ``.json``/``.jsonl`` and nested layouts are handled via a recursive glob. + When both ``train.jsonl`` and ``validation.jsonl`` live in the same + directory, pass ``split`` so training and eval each load the right + file instead of concatenating every JSONL in the folder. + Raises: FileNotFoundError: if ``path`` is a directory with no JSON/JSONL files under it. @@ -266,6 +301,15 @@ def _resolve_local_data_files(path: str) -> str | list[str]: if not p.is_dir(): return str(p) + if split == "train": + train_file = p / TRAIN_FILE + if train_file.is_file(): + return str(train_file) + elif split == "validation": + val_file = p / VAL_FILE + if val_file.is_file(): + return str(val_file) + for pattern in ("*.jsonl", "*.json"): files = sorted(str(f) for f in p.rglob(pattern)) if files: @@ -285,6 +329,7 @@ def _load_training_dataset( tokenizer: Any, load_dataset: Any, Dataset: Any, + split: Literal["train", "validation"] | None = None, ) -> Any: """Load a JSONL or HF dataset; optionally apply the chat template. @@ -301,7 +346,7 @@ def _load_training_dataset( in-process plugin run hands us a concrete file). """ is_local = path.endswith((".jsonl", ".json")) or path.startswith(("/", "./", "~")) - raw = Dataset.from_json(_resolve_local_data_files(path)) if is_local else load_dataset(path) + raw = Dataset.from_json(_resolve_local_data_files(path, split=split)) if is_local else load_dataset(path) if not apply_chat_template: return raw diff --git a/services/unsloth/src/nmp/unsloth/tasks/training/progress.py b/services/unsloth/src/nmp/unsloth/tasks/training/progress.py new file mode 100644 index 0000000000..0a4f596ef1 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/tasks/training/progress.py @@ -0,0 +1,106 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Progress reporting for Unsloth training tasks. + +Mirrors :mod:`nmp.automodel.tasks.training.progress` so Unsloth jobs +expose the same ``status_details`` shape as Automodel (``train_loss``, +``metrics``, ``step``, ``epoch``, etc.). +""" + +import logging +import os +from typing import Any + +from nmp.common.sdk_factory import get_task_sdk +from nmp.unsloth.app.constants import SERVICE_NAME +from nmp.unsloth.app.jobs.context import NMPJobContext + +logger = logging.getLogger(__name__) + + +class JobsServiceProgressReporter: + """Reports high-level progress to the Jobs service.""" + + def __init__(self, job_ctx: NMPJobContext): + self._job_ctx = job_ctx + self._sdk = get_task_sdk(SERVICE_NAME) + self._is_main_rank = int(os.environ.get("RANK", "0")) == 0 + self._max_steps = 0 + self._num_epochs = 0 + + self._enabled = self._is_main_rank and all( + [self._job_ctx.job_id, self._job_ctx.step, self._job_ctx.normalized_task], + ) + + def configure_progress_tracking(self, max_steps: int, num_epochs: int) -> None: + """Configure progress tracking at the start of training.""" + self._max_steps = max_steps + self._num_epochs = num_epochs + + def _calculate_percentage_done(self, step: int | None) -> int: + if step is None or self._max_steps <= 0: + return 0 + return int((step / self._max_steps) * 100) + + def update_task( + self, + status: str = "active", + status_details: dict[str, Any] | None = None, + error_details: dict[str, Any] | None = None, + ) -> None: + if not self._enabled: + return + + if not self._is_main_rank: + return + + try: + self._sdk.jobs.tasks.create_or_update( + name=self._job_ctx.normalized_task, + workspace=self._job_ctx.workspace, + job=self._job_ctx.job_id, + step=self._job_ctx.step, + status=status, + status_details=status_details or {}, + error_details=error_details or {}, + ) + except Exception as e: + logger.warning(f"Failed to update task progress: {e}") + + def fetch_current_metrics(self) -> dict[str, list[dict[str, float | int]]]: + if not self._enabled: + return {"train_loss": [], "val_loss": []} + + try: + task = self._sdk.jobs.tasks.retrieve( + name=self._job_ctx.normalized_task, + workspace=self._job_ctx.workspace, + job=self._job_ctx.job_id, + step=self._job_ctx.step, + ) + metrics = (task.status_details or {}).get("metrics", {}) + return { + "train_loss": metrics.get("train_loss", []), + "val_loss": metrics.get("val_loss", []), + } + except Exception as e: + logger.info(f"No prior metrics to seed (expected on first run): {e}") + return {"train_loss": [], "val_loss": []} + + def report_running(self, phase: str, **details: Any) -> None: + if "step" in details and "percentage_done" not in details and self._max_steps > 0: + details["percentage_done"] = self._calculate_percentage_done(details["step"]) + + status_details = {"phase": phase, **details} + self.update_task(status="active", status_details=status_details) + + def report_completed(self, message: str = "Completed") -> None: + self.update_task(status="completed", status_details={"message": message, "phase": "completed"}) + + def report_error(self, error: str | dict[str, Any]) -> None: + error_details = {"message": error} if isinstance(error, str) else error + self.update_task(status="error", error_details=error_details) + + def close(self) -> None: + self._sdk.close() diff --git a/services/unsloth/tests/test_callbacks.py b/services/unsloth/tests/test_callbacks.py new file mode 100644 index 0000000000..bdd09282c7 --- /dev/null +++ b/services/unsloth/tests/test_callbacks.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for TrainingProgressCallback metric accumulation.""" + +from unittest.mock import MagicMock + +from nmp.unsloth.tasks.training.backends.callbacks import TrainingProgressCallback + + +class TestTrainingProgressCallback: + def _make_callback(self, prior_metrics: dict | None = None) -> tuple[TrainingProgressCallback, MagicMock]: + mock_reporter = MagicMock() + mock_reporter.fetch_current_metrics.return_value = prior_metrics or { + "train_loss": [], + "val_loss": [], + } + callback = TrainingProgressCallback(mock_reporter) + return callback, mock_reporter + + def _last_report_kwargs(self, mock_reporter: MagicMock) -> dict: + return mock_reporter.report_running.call_args.kwargs + + def test_train_step_accumulates_metrics(self): + callback, reporter = self._make_callback() + + callback.report_train_step(step=1, epoch=1, loss=3.21) + callback.report_train_step(step=2, epoch=1, loss=2.89) + callback.report_train_step(step=3, epoch=1, loss=2.56) + + kwargs = self._last_report_kwargs(reporter) + assert kwargs["metrics"]["train_loss"] == [ + {"step": 1, "epoch": 1, "value": 3.21}, + {"step": 2, "epoch": 1, "value": 2.89}, + {"step": 3, "epoch": 1, "value": 2.56}, + ] + + def test_train_step_uses_train_loss_flat_field(self): + callback, reporter = self._make_callback() + + callback.report_train_step(step=1, epoch=1, loss=3.21) + + kwargs = self._last_report_kwargs(reporter) + assert kwargs["train_loss"] == 3.21 + assert kwargs["backend"] == "unsloth" + assert "loss" not in kwargs + + def test_train_step_passes_optional_fields(self): + callback, reporter = self._make_callback() + + callback.report_train_step(step=1, epoch=1, loss=3.21, lr=0.0002, grad_norm=1.5) + + kwargs = self._last_report_kwargs(reporter) + assert kwargs["lr"] == 0.0002 + assert kwargs["grad_norm"] == 1.5 + + def test_report_training_start_delegates(self): + callback, reporter = self._make_callback() + + callback.report_training_start(max_steps=500, num_epochs=2) + + reporter.configure_progress_tracking.assert_called_once_with(500, 2) + reporter.report_running.assert_called_once_with( + phase="training", + step=0, + max_steps=500, + num_epochs=2, + backend="unsloth", + ) + + def test_close_delegates(self): + callback, reporter = self._make_callback() + callback.close() + reporter.close.assert_called_once() diff --git a/services/unsloth/tests/test_compile.py b/services/unsloth/tests/test_compile.py index 461c1767de..560f01c0d8 100644 --- a/services/unsloth/tests/test_compile.py +++ b/services/unsloth/tests/test_compile.py @@ -31,7 +31,7 @@ def _canonical_spec() -> UnslothJobOutput: model=ModelLoadSpec(name="default/base"), dataset=DatasetSpec(path="default/training"), training=TrainingSpec(lora=LoRAParams()), - schedule={"max_steps": 1}, # type: ignore[arg-type] + schedule={"max_steps": 1}, output=OutputResponse( name="r", type="adapter", @@ -52,7 +52,7 @@ async def test_compile_delegates_to_app_jobs_compiler() -> None: result = await platform_job_config_compiler( workspace="default", spec=spec, - sdk=sdk, # type: ignore[arg-type] + sdk=sdk, job_name="job-x", profile="gpu-large", ) diff --git a/services/unsloth/tests/test_compiler_validation_path.py b/services/unsloth/tests/test_compiler_validation_path.py new file mode 100644 index 0000000000..f48c002d27 --- /dev/null +++ b/services/unsloth/tests/test_compiler_validation_path.py @@ -0,0 +1,99 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Regression: validation fileset refs must resolve to on-disk paths in the training step.""" + +from __future__ import annotations + +import types +from unittest.mock import AsyncMock, MagicMock + +import pytest +from nmp.unsloth.app.constants import DEFAULT_DATASET_PATH, DEFAULT_VALIDATION_DATASET_PATH +from nmp.unsloth.app.jobs.compiler import platform_job_config_compiler +from nmp.unsloth.schemas import ( + DatasetSpec, + LoRAParams, + ModelLoadSpec, + OutputResponse, + ScheduleSpec, + TrainingSpec, + UnslothJobOutput, +) + + +def _spec(*, validation_path: str | None) -> UnslothJobOutput: + return UnslothJobOutput( + model=ModelLoadSpec(name="default/qwen3-1.7b"), + dataset=DatasetSpec( + path="default/commonsense_qa", + validation_path=validation_path, + apply_chat_template=True, + ), + training=TrainingSpec(lora=LoRAParams()), + schedule=ScheduleSpec(epochs=1), + output=OutputResponse( + name="out", + type="adapter", + save_method="lora", + fileset="out", + ), + ) + + +@pytest.mark.asyncio +async def test_training_step_gets_local_validation_path_for_same_fileset() -> None: + from nmp.unsloth.app.jobs import compiler as compiler_mod + + original_fetch = compiler_mod.fetch_model_entity + compiler_mod.fetch_model_entity = AsyncMock( + return_value=types.SimpleNamespace( + workspace="default", + name="qwen3-1.7b", + fileset="default/qwen3-1.7b", + trust_remote_code=False, + ), + ) + try: + job = await platform_job_config_compiler( + workspace="default", + job_spec=_spec(validation_path="default/commonsense_qa"), + sdk=MagicMock(), + ) + finally: + compiler_mod.fetch_model_entity = original_fetch + + training = next(s for s in job["steps"] if s["name"] == "training") + assert training["config"]["validation_path"] == DEFAULT_DATASET_PATH + + download = next(s for s in job["steps"] if s["name"] == "model-and-dataset-download") + assert len(download["config"]["download"]) == 2 + + +@pytest.mark.asyncio +async def test_training_step_gets_separate_validation_path_for_different_fileset() -> None: + from nmp.unsloth.app.jobs import compiler as compiler_mod + + original_fetch = compiler_mod.fetch_model_entity + compiler_mod.fetch_model_entity = AsyncMock( + return_value=types.SimpleNamespace( + workspace="default", + name="qwen3-1.7b", + fileset="default/qwen3-1.7b", + trust_remote_code=False, + ), + ) + try: + job = await platform_job_config_compiler( + workspace="default", + job_spec=_spec(validation_path="default/commonsense_qa_val"), + sdk=MagicMock(), + ) + finally: + compiler_mod.fetch_model_entity = original_fetch + + training = next(s for s in job["steps"] if s["name"] == "training") + assert training["config"]["validation_path"] == DEFAULT_VALIDATION_DATASET_PATH + + download = next(s for s in job["steps"] if s["name"] == "model-and-dataset-download") + assert len(download["config"]["download"]) == 3 diff --git a/services/unsloth/tests/test_dataset_loading.py b/services/unsloth/tests/test_dataset_loading.py index 1b5b2eb1fb..42fb8cd391 100644 --- a/services/unsloth/tests/test_dataset_loading.py +++ b/services/unsloth/tests/test_dataset_loading.py @@ -52,3 +52,17 @@ def test_directory_returns_sorted_files(tmp_path: Path) -> None: def test_empty_directory_raises(tmp_path: Path) -> None: with pytest.raises(FileNotFoundError, match="No .jsonl/.json files"): _resolve_local_data_files(str(tmp_path)) + + +def test_split_train_picks_train_jsonl_only(tmp_path: Path) -> None: + (tmp_path / "train.jsonl").write_text('{"text": "train"}\n') + (tmp_path / "validation.jsonl").write_text('{"text": "val"}\n') + assert _resolve_local_data_files(str(tmp_path), split="train") == str(tmp_path / "train.jsonl") + + +def test_split_validation_picks_validation_jsonl_only(tmp_path: Path) -> None: + (tmp_path / "train.jsonl").write_text('{"text": "train"}\n') + (tmp_path / "validation.jsonl").write_text('{"text": "val"}\n') + assert _resolve_local_data_files(str(tmp_path), split="validation") == str( + tmp_path / "validation.jsonl", + ) diff --git a/services/unsloth/tests/test_hf_trainer_callback.py b/services/unsloth/tests/test_hf_trainer_callback.py new file mode 100644 index 0000000000..800fbbe186 --- /dev/null +++ b/services/unsloth/tests/test_hf_trainer_callback.py @@ -0,0 +1,81 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for HuggingFace Trainer → Jobs progress bridge.""" + +from unittest.mock import MagicMock + +import pytest +from nmp.unsloth.tasks.training.backends.callbacks import TrainingProgressCallback +from nmp.unsloth.tasks.training.backends.hf_trainer_callback import ( + _epoch_from_value, + create_hf_trainer_progress_callback, +) + + +class TestEpochFromValue: + def test_fractional_epoch_maps_to_one(self): + assert _epoch_from_value(0.01314, 1) == 1 + + def test_completed_epoch_capped_at_num_epochs(self): + assert _epoch_from_value(1.0, 1) == 1 + assert _epoch_from_value(2.0, 2) == 2 + + +class TestHfTrainerProgressCallback: + @pytest.fixture + def progress(self) -> tuple[TrainingProgressCallback, MagicMock]: + reporter = MagicMock() + reporter.fetch_current_metrics.return_value = {"train_loss": [], "val_loss": []} + return TrainingProgressCallback(reporter), reporter + + def test_on_log_reports_train_step(self, progress: tuple[TrainingProgressCallback, MagicMock]) -> None: + callback, reporter = progress + hf_callback = create_hf_trainer_progress_callback(callback) + + args = MagicMock(num_train_epochs=1) + state = MagicMock(max_steps=77, global_step=8, epoch=0.1) + + hf_callback.on_train_begin(args, state, MagicMock()) + hf_callback.on_log( + args, state, MagicMock(), logs={"loss": 2.89, "learning_rate": 5e-5, "grad_norm": 10.6, "epoch": 0.1} + ) + + reporter.report_running.assert_called() + kwargs = reporter.report_running.call_args.kwargs + assert kwargs["phase"] == "training" + assert kwargs["step"] == 8 + assert kwargs["train_loss"] == 2.89 + assert kwargs["lr"] == 5e-5 + assert kwargs["grad_norm"] == 10.6 + assert kwargs["backend"] == "unsloth" + assert kwargs["metrics"]["train_loss"][-1]["value"] == 2.89 + + def test_on_log_skips_non_train_logs(self, progress: tuple[TrainingProgressCallback, MagicMock]) -> None: + callback, reporter = progress + hf_callback = create_hf_trainer_progress_callback(callback) + + args = MagicMock(num_train_epochs=1) + state = MagicMock(max_steps=77, global_step=8, epoch=0.1) + hf_callback.on_train_begin(args, state, MagicMock()) + + reporter.report_running.reset_mock() + hf_callback.on_log(args, state, MagicMock(), logs={"eval_loss": 1.2}) + + reporter.report_running.assert_not_called() + + def test_on_evaluate_reports_validation(self, progress: tuple[TrainingProgressCallback, MagicMock]) -> None: + callback, reporter = progress + hf_callback = create_hf_trainer_progress_callback(callback) + + args = MagicMock(num_train_epochs=1) + state = MagicMock(max_steps=77, global_step=40, epoch=0.5) + hf_callback.on_train_begin(args, state, MagicMock()) + + reporter.report_running.reset_mock() + hf_callback.on_evaluate(args, state, MagicMock(), metrics={"eval_loss": 1.75, "epoch": 0.5}) + + kwargs = reporter.report_running.call_args.kwargs + assert kwargs["phase"] == "validation" + assert kwargs["val_loss"] == 1.75 + assert kwargs["metrics"]["val_loss"][-1]["value"] == 1.75 diff --git a/services/unsloth/tests/test_main.py b/services/unsloth/tests/test_main.py index 4a0dd20474..2b901acc97 100644 --- a/services/unsloth/tests/test_main.py +++ b/services/unsloth/tests/test_main.py @@ -39,7 +39,7 @@ def _step_config() -> TrainingStepConfig: model=ModelLoadSpec(name="default/base"), dataset=DatasetSpec(path="default/training"), training=TrainingSpec(lora=LoRAParams()), - schedule={"max_steps": 1}, # type: ignore[arg-type] + schedule={"max_steps": 1}, output=OutputResponse(name="r", type="adapter", save_method="lora", fileset="r"), ) return TrainingStepConfig( diff --git a/services/unsloth/tests/test_model_entity.py b/services/unsloth/tests/test_model_entity.py index e522fc1f5f..75acc1cbbd 100644 --- a/services/unsloth/tests/test_model_entity.py +++ b/services/unsloth/tests/test_model_entity.py @@ -150,7 +150,7 @@ def test_conflict_falls_back_to_update(self) -> None: peft=None, ) - _result, _deploy = runner.create_model_entity(config) + _, _ = runner.create_model_entity(config) sdk.models.update.assert_called_once() update_call = sdk.models.update.call_args @@ -420,7 +420,7 @@ async def test_inline_params_pass_through_to_model_entity_step(self) -> None: from nmp.unsloth.app.jobs import compiler as compiler_mod original_fetch = compiler_mod.fetch_model_entity - compiler_mod.fetch_model_entity = AsyncMock( # type: ignore[assignment] + compiler_mod.fetch_model_entity = AsyncMock( return_value=types.SimpleNamespace( workspace="default", name="base", @@ -435,7 +435,7 @@ async def test_inline_params_pass_through_to_model_entity_step(self) -> None: sdk=MagicMock(), ) finally: - compiler_mod.fetch_model_entity = original_fetch # type: ignore[assignment] + compiler_mod.fetch_model_entity = original_fetch # PlatformJobSpec is a TypedDict, so we index it instead of using attributes. me_step = next(s for s in job_spec["steps"] if s["name"] == "model-entity-creation") @@ -472,7 +472,7 @@ async def test_string_ref_passes_through_unchanged(self) -> None: from nmp.unsloth.app.jobs import compiler as compiler_mod original_fetch = compiler_mod.fetch_model_entity - compiler_mod.fetch_model_entity = AsyncMock( # type: ignore[assignment] + compiler_mod.fetch_model_entity = AsyncMock( return_value=types.SimpleNamespace( workspace="default", name="base", @@ -487,7 +487,7 @@ async def test_string_ref_passes_through_unchanged(self) -> None: sdk=MagicMock(), ) finally: - compiler_mod.fetch_model_entity = original_fetch # type: ignore[assignment] + compiler_mod.fetch_model_entity = original_fetch me_step = next(s for s in job_spec["steps"] if s["name"] == "model-entity-creation") assert me_step["config"]["deployment_config"] == "my-config" diff --git a/services/unsloth/tests/test_progress.py b/services/unsloth/tests/test_progress.py new file mode 100644 index 0000000000..b239db9165 --- /dev/null +++ b/services/unsloth/tests/test_progress.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from pathlib import Path +from unittest.mock import MagicMock, patch + +from nmp.unsloth.app.jobs.context import NMPJobContext +from nmp.unsloth.tasks.training.progress import JobsServiceProgressReporter + + +def test_progress_reporter_calls_sdk_create_or_update() -> None: + ctx = NMPJobContext( + workspace="ws-a", + job_id="job-1", + attempt_id="attempt-0", + step="training", + task="train-model", + jobs_url="http://jobs.example.com", + files_url=None, + storage_path=Path("/tmp/job"), + config_path=Path("/tmp/job/config.json"), + ) + mock_sdk = MagicMock() + + with patch("nmp.unsloth.tasks.training.progress.get_task_sdk", return_value=mock_sdk): + reporter = JobsServiceProgressReporter(ctx) + reporter.report_running(phase="training", step=1, train_loss=2.5, backend="unsloth") + + mock_sdk.jobs.tasks.create_or_update.assert_called_once() + call_kwargs = mock_sdk.jobs.tasks.create_or_update.call_args.kwargs + assert call_kwargs["name"] == ctx.normalized_task + assert call_kwargs["workspace"] == ctx.workspace + assert call_kwargs["job"] == ctx.job_id + assert call_kwargs["step"] == ctx.step + assert call_kwargs["status_details"]["train_loss"] == 2.5 + assert call_kwargs["status_details"]["backend"] == "unsloth" diff --git a/tests/agentic-use/customizer-lora-job-cli/README.md b/tests/agentic-use/customizer-lora-job-cli/README.md index 8609c1c3b7..6d8e4bb726 100644 --- a/tests/agentic-use/customizer-lora-job-cli/README.md +++ b/tests/agentic-use/customizer-lora-job-cli/README.md @@ -1,19 +1,34 @@ # LoRA Customization Job (CLI, GPU) -Tests the agent's ability to set up and submit a real LoRA fine-tuning job through the NeMo Platform Customizer service using the CLI. +Tests the agent's ability to set up and submit a real LoRA fine-tuning job through the **nemo-customizer** plugin with the **nmp-automodel** backend. ## What This Tests - Creating workspaces and filesets via NeMo Platform CLI - Preparing and uploading SFT training data in JSONL format -- Submitting a LoRA customization job with correct hyperparameters -- Monitoring job progress +- Submitting a LoRA job with `nemo customization automodel submit` +- Monitoring job progress via the jobs API -## GPU Requirements +## Prerequisites + +Build and push platform/automodel images before running this eval. From the repo root: + +```bash +export BAKE_TAG=$(git rev-parse --short HEAD) +export BASE_TAG_AUTOMODEL=$BAKE_TAG +export NMP_IMAGE_TAG=$BAKE_TAG +export NMP_IMAGE_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev +echo "$NMP_IMAGE_TAG" -This eval requires **1 GPU** allocated to the Harbor container. The customization job performs actual LoRA fine-tuning on the GPU. +# Bake/push nmp-automodel images (and platform task images as needed) +docker buildx bake -f docker-bake.hcl --push +``` + +The eval container sources `environment/image-env.sh`, which defaults to the same registry and derives `NMP_IMAGE_TAG` from `git rev-parse --short HEAD` when unset. + +## GPU Requirements -If the NeMo Platform job executor is not configured to run inside the Harbor container, the eval still validates correct job submission. To enable end-to-end training, ensure the job dispatcher backend is configured for local GPU execution. +This eval requires **1 GPU** allocated to the Harbor container (via `environment/docker-compose.yaml`). The customization job performs actual LoRA fine-tuning on the GPU using `nmp-automodel-training`. ## Flow Reference diff --git a/tests/agentic-use/customizer-lora-job-cli/environment/Dockerfile b/tests/agentic-use/customizer-lora-job-cli/environment/Dockerfile index fb9705f6db..a540ad8c85 100644 --- a/tests/agentic-use/customizer-lora-job-cli/environment/Dockerfile +++ b/tests/agentic-use/customizer-lora-job-cli/environment/Dockerfile @@ -8,9 +8,10 @@ RUN rm -f /app/.mcp.json RUN apt-get update && apt-get install -y --no-install-recommends docker.io && rm -rf /var/lib/apt/lists/* # Copy environment scripts +COPY image-env.sh /app/image-env.sh COPY setup-env.sh /app/setup-env.sh COPY entrypoint.sh /app/entrypoint.sh -RUN chmod +x /app/entrypoint.sh /app/setup-env.sh +RUN chmod +x /app/image-env.sh /app/entrypoint.sh /app/setup-env.sh ENTRYPOINT ["/app/entrypoint.sh"] CMD ["sleep", "infinity"] diff --git a/tests/agentic-use/customizer-lora-job-cli/environment/docker-compose.yaml b/tests/agentic-use/customizer-lora-job-cli/environment/docker-compose.yaml index 8bd1d68ae4..c2d8d29b7a 100644 --- a/tests/agentic-use/customizer-lora-job-cli/environment/docker-compose.yaml +++ b/tests/agentic-use/customizer-lora-job-cli/environment/docker-compose.yaml @@ -4,7 +4,15 @@ services: - /var/run/docker.sock:/var/run/docker.sock environment: - DOCKER_HOST=unix:///var/run/docker.sock - - NMP_CUSTOMIZER_TRAINING_AUTOMODEL_IMAGE=gpu-finetune-test:latest + - NMP_IMAGE_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev + # BAKE_TAG / NMP_IMAGE_TAG default to `git rev-parse --short HEAD` inside the container + # when unset. Set both on the host before `docker compose up` to pin a baked tag: + # export BAKE_TAG=$(git rev-parse --short HEAD) + # export BASE_TAG_AUTOMODEL=$BAKE_TAG + # export NMP_IMAGE_TAG=$BAKE_TAG + - NMP_IMAGE_TAG=${NMP_IMAGE_TAG:-} + - BASE_TAG_AUTOMODEL=${BASE_TAG_AUTOMODEL:-} + - BAKE_TAG=${BAKE_TAG:-} deploy: resources: reservations: diff --git a/tests/agentic-use/customizer-lora-job-cli/environment/entrypoint.sh b/tests/agentic-use/customizer-lora-job-cli/environment/entrypoint.sh index 54ba4592d4..c0c21a54cc 100644 --- a/tests/agentic-use/customizer-lora-job-cli/environment/entrypoint.sh +++ b/tests/agentic-use/customizer-lora-job-cli/environment/entrypoint.sh @@ -9,8 +9,10 @@ else echo 'WARNING: Docker socket not found - job execution will not work' fi -# Point the customizer at our training image (gpu-finetune-test has torch + nmp training module) -export NMP_CUSTOMIZER_TRAINING_AUTOMODEL_IMAGE=gpu-finetune-test:latest +# Image registry/tag for platform CPU tasks and nmp-automodel GPU job steps. +# Must be set before `nemo services run` so job compilation resolves the right images. +source /app/image-env.sh +echo "Using NMP_IMAGE_REGISTRY=${NMP_IMAGE_REGISTRY} NMP_IMAGE_TAG=${NMP_IMAGE_TAG}" cd /app && /app/.venv/bin/nemo services run > /tmp/nmp-api.log 2>&1 & API_PID=$! @@ -39,7 +41,7 @@ if [ "$CONSECUTIVE_SUCCESS" -lt 3 ]; then fi echo '=== Pre-configuring CLI auth ===' -/app/.venv/bin/nmp auth login --base-url http://localhost:8080 --unsigned-token --email agent@harbor.local --no-exp +/app/.venv/bin/nemo auth login --base-url http://localhost:8080 --unsigned-token --email agent@harbor.local --no-exp bash /app/setup-env.sh diff --git a/tests/agentic-use/customizer-lora-job-cli/environment/image-env.sh b/tests/agentic-use/customizer-lora-job-cli/environment/image-env.sh new file mode 100644 index 0000000000..80f32e03ce --- /dev/null +++ b/tests/agentic-use/customizer-lora-job-cli/environment/image-env.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Platform and nmp-automodel image registry/tag defaults for agentic GPU evals. +# +# Host operators typically bake images with: +# export BAKE_TAG=$(git rev-parse --short HEAD) +# export BASE_TAG_AUTOMODEL=$BAKE_TAG +# export NMP_IMAGE_TAG=$BAKE_TAG +# export NMP_IMAGE_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev +# +# Override any variable before starting the eval container when testing a +# different tag or registry. + +if [ -z "${BAKE_TAG:-}" ]; then + if command -v git >/dev/null 2>&1 && git -C /app rev-parse --short HEAD >/dev/null 2>&1; then + BAKE_TAG="$(git -C /app rev-parse --short HEAD)" + else + BAKE_TAG="${NMP_IMAGE_TAG:-local}" + fi +fi + +export BAKE_TAG +export BASE_TAG_AUTOMODEL="${BASE_TAG_AUTOMODEL:-$BAKE_TAG}" +export NMP_IMAGE_TAG="${NMP_IMAGE_TAG:-$BAKE_TAG}" +export NMP_IMAGE_REGISTRY="${NMP_IMAGE_REGISTRY:-nvcr.io/0921617854601259/nemo-platform-dev}" +export NMP_AUTOMODEL_IMAGE_REGISTRY="${NMP_AUTOMODEL_IMAGE_REGISTRY:-$NMP_IMAGE_REGISTRY}" diff --git a/tests/agentic-use/customizer-lora-job-cli/environment/setup-env.sh b/tests/agentic-use/customizer-lora-job-cli/environment/setup-env.sh index 275bf16893..a77b73f944 100644 --- a/tests/agentic-use/customizer-lora-job-cli/environment/setup-env.sh +++ b/tests/agentic-use/customizer-lora-job-cli/environment/setup-env.sh @@ -1,31 +1,38 @@ #!/bin/bash set -e -echo '=== Pre-pulling training image ===' +source /app/image-env.sh + +echo '=== Pre-pulling nmp-automodel job images ===' if command -v docker &> /dev/null && [ -S /var/run/docker.sock ]; then - # The customizer dispatches training jobs using this image - # gpu-finetune-test:latest must be pre-built on the host - docker image inspect gpu-finetune-test:latest > /dev/null 2>&1 && \ - echo 'gpu-finetune-test:latest found' || \ - echo 'WARNING: gpu-finetune-test:latest not found - build it with: docker build -f tests/agentic-use/gpu-direct-lora-finetune-cli/environment/Dockerfile -t gpu-finetune-test:latest .' + TRAINING_IMAGE="${NMP_IMAGE_REGISTRY}/nmp-automodel-training:${NMP_IMAGE_TAG}" + TASKS_IMAGE="${NMP_IMAGE_REGISTRY}/nmp-automodel-tasks:${NMP_IMAGE_TAG}" + + docker pull "$TRAINING_IMAGE" && echo "Pulled ${TRAINING_IMAGE}" || \ + echo "WARNING: Failed to pull ${TRAINING_IMAGE} — bake and push with BASE_TAG_AUTOMODEL=${BASE_TAG_AUTOMODEL}" + docker pull "$TASKS_IMAGE" && echo "Pulled ${TASKS_IMAGE}" || \ + echo "WARNING: Failed to pull ${TASKS_IMAGE} — bake and push with BASE_TAG_AUTOMODEL=${BASE_TAG_AUTOMODEL}" else - echo 'WARNING: Docker not available' + echo 'WARNING: Docker not available for image pre-pull' fi echo '=== Creating workspace ===' -/app/.venv/bin/nmp workspaces create --name lora-training-workspace || echo 'Workspace may already exist' +/app/.venv/bin/nemo workspaces create --name lora-training-workspace || echo 'Workspace may already exist' + +echo '=== Registering model weights fileset and entity ===' +/app/.venv/bin/nemo files filesets create smollm-135m-weights \ + --workspace lora-training-workspace \ + --purpose model \ + --exist-ok \ + --storage '{"type":"huggingface","repo_id":"HuggingFaceTB/SmolLM-135M","repo_type":"model","revision":"main"}' \ + 2>&1 || echo 'Weights fileset may already exist' -echo '=== Registering model entity ===' -# Register a model entity that the customizer can reference. -# The model name is what users pass to the customizer API. -/app/.venv/bin/nmp models create --workspace lora-training-workspace \ +/app/.venv/bin/nemo models create smollm-135m \ + --workspace lora-training-workspace \ + --exist-ok \ --input-data '{ "name": "smollm-135m", - "spec": { - "num_parameters": 135000000, - "is_chat": false, - "family": "smollm" - }, + "fileset": "lora-training-workspace/smollm-135m-weights", "custom_fields": { "hf_model_id": "HuggingFaceTB/SmolLM-135M" } diff --git a/tests/agentic-use/customizer-lora-job-cli/instruction.md b/tests/agentic-use/customizer-lora-job-cli/instruction.md index bcaebfb806..117e21cfd6 100644 --- a/tests/agentic-use/customizer-lora-job-cli/instruction.md +++ b/tests/agentic-use/customizer-lora-job-cli/instruction.md @@ -1,66 +1,77 @@ -# LoRA Customization Job via NeMo Platform Customizer API (GPU) +# LoRA Customization Job via NeMo Platform Customizer (GPU) -This task tests submitting and running a real LoRA fine-tuning job through the NeMo Platform Customizer service. The job is dispatched through the NeMo Platform jobs pipeline to a GPU container. +This task tests submitting and running a real LoRA fine-tuning job through the **nemo-customizer** plugin with the **nmp-automodel** backend. Training is dispatched through the NeMo Platform jobs pipeline to GPU containers built from the dev registry. -You have access to the `nmp` CLI for NeMo Platform operations. Note: MCP tools are not available in this environment - you must use the CLI. +You have access to the `nemo` and `nmp` CLIs for NeMo Platform operations. Note: MCP tools are not available in this environment — you must use the CLI. -The `nmp` CLI is available at `/app/.venv/bin/nmp`. The CLI connects to the local NeMo Platform API server at http://localhost:8080 by default. CLI auth is pre-configured. +The CLIs are available at `/app/.venv/bin/nemo` and `/app/.venv/bin/nmp`. The platform API runs at http://localhost:8080. CLI auth is pre-configured. ## Context -- The NeMo Platform API server is running with the jobs controller enabled -- The Docker backend is configured for GPU job execution -- The Docker socket is mounted for real container execution +- The NeMo Platform API server is running with the jobs controller and customization plugin enabled +- Platform image registry/tag are configured for `nvcr.io/0921617854601259/nemo-platform-dev` (see environment setup) +- The Docker backend is configured for GPU job execution; the Docker socket is mounted - A workspace `lora-training-workspace` has been pre-created -- A model entity `smollm-135m` has been registered in the workspace +- A model entity `smollm-135m` (HF weights fileset `smollm-135m-weights`) has been registered in the workspace ## Task ### Step 1: Prepare and upload a training dataset 1. Create a JSONL file with at least 20 prompt/completion training examples for a simple task (e.g., customer service responses). Example format: + ```jsonl {"prompt": "Customer complaint: My order arrived damaged.\nResponse:", "completion": "I apologize for the inconvenience. I will arrange a replacement immediately."} ``` -2. Upload this dataset to NeMo Platform as a fileset named `sft-training-data` in the `lora-training-workspace` workspace. +2. Create a dataset fileset and upload the data: + + ```bash + nemo files filesets create sft-training-data --workspace lora-training-workspace --purpose dataset --exist-ok + nemo files upload /path/to/train.jsonl sft-training-data --workspace lora-training-workspace --remote-path train.jsonl + ``` -### Step 2: Submit a LoRA customization job +### Step 2: Submit a LoRA customization job (automodel backend) -Create a customization job using the NeMo Platform Customizer API: +Write `/tmp/job.json` using the **AutomodelJobInput** schema, then submit: ```bash -nmp customization jobs create --workspace lora-training-workspace --input-data '{ - "spec": { - "model": "lora-training-workspace/smollm-135m", - "dataset": "fileset://lora-training-workspace/sft-training-data", - "training": { - "type": "sft", - "peft": { - "type": "lora", - "rank": 8, - "alpha": 16 - }, - "epochs": 2, - "learning_rate": 0.0001, - "batch_size": 4 - }, - "output": { - "name": "lora-email-model" - } - } -}' +cat > /tmp/job.json <<'EOF' +{ + "model": "lora-training-workspace/smollm-135m", + "dataset": { + "training": "lora-training-workspace/sft-training-data" + }, + "training": { + "training_type": "sft", + "finetuning_type": "lora", + "lora": { "rank": 8, "alpha": 16 }, + "max_seq_length": 2048, + "execution_profile": "default" + }, + "schedule": { "epochs": 2 }, + "batch": { "global_batch_size": 4, "micro_batch_size": 1 }, + "optimizer": { "learning_rate": 0.0001 }, + "parallelism": { "num_nodes": 1, "num_gpus_per_node": 1, "tensor_parallel_size": 1 }, + "output": { "name": "lora-email-model" } +} +EOF + +nemo customization automodel submit /tmp/job.json --workspace lora-training-workspace ``` +Use `nemo customization automodel explain` if you need the live schema. List GPU execution profiles with `nemo jobs list-execution-profiles -f json` when choosing `training.execution_profile`. + ### Step 3: Monitor the job -1. Poll the job status using `nmp customization jobs list --workspace lora-training-workspace` or `nmp customization jobs get --workspace lora-training-workspace`. +1. Poll job status with `nemo jobs get-status ` (job names are typically prefixed `automodel-`). 2. Check status periodically until it reaches a terminal state. -3. If it fails, investigate using `nmp customization jobs get ` and check for error details. +3. If it fails, investigate with `nemo jobs get-status ` and check platform logs for missing-image errors. ### Step 4: Verify results Once the job completes (or if it fails), document: + - The job status - Any error messages if it failed - The output model/adapter if it completed @@ -68,15 +79,15 @@ Once the job completes (or if it fails), document: ## Success Criteria The task is complete when: + - A fileset `sft-training-data` exists with training data uploaded -- A customization job was submitted via the NeMo Platform Customizer API +- A customization job was submitted via `nemo customization automodel submit` - The agent polled for job status and reported the outcome - The job progressed beyond "created" status (indicating the jobs controller dispatched it) ## Notes -- The customizer dispatches training jobs through the NeMo Platform jobs pipeline to GPU containers -- The model reference format is `workspace/model-name` (e.g., `lora-training-workspace/smollm-135m`) -- The dataset reference format is `fileset://workspace/fileset-name` -- Jobs may take a few minutes to complete depending on dataset size -- If the job encounters errors, investigating and reporting the error is a valid outcome +- LoRA/SFT training uses the **automodel** contributor (`nmp-automodel-training` / `nmp-automodel-tasks` images), not the legacy customizer automodel path +- Model reference format: `workspace/model-entity-name` (e.g., `lora-training-workspace/smollm-135m`) +- Dataset reference format in job JSON: `workspace/fileset-name` inside `dataset.training` +- Jobs may take a few minutes depending on dataset size and GPU availability diff --git a/tests/agentic-use/customizer-lora-job-cli/tests/test_outputs.py b/tests/agentic-use/customizer-lora-job-cli/tests/test_outputs.py index 70c7bd696a..68b858f335 100644 --- a/tests/agentic-use/customizer-lora-job-cli/tests/test_outputs.py +++ b/tests/agentic-use/customizer-lora-job-cli/tests/test_outputs.py @@ -1,15 +1,16 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Verify that the agent submitted a LoRA customization job via the NeMo Platform customizer API. +"""Verify that the agent submitted a LoRA job via the automodel customization plugin. -Tests workspace/fileset creation, dataset upload, and customization job submission -through the real NeMo Platform pipeline. +Tests workspace/fileset creation, dataset upload, and automodel job submission +through the NeMo Platform customization + jobs pipeline. """ import base64 import json import os +from typing import Any import pytest from nemo_platform import NeMoPlatform @@ -41,6 +42,16 @@ def client() -> NeMoPlatform: ) +def _list_automodel_jobs(client: NeMoPlatform) -> list[dict[str, Any]]: + """List automodel customization jobs in the eval workspace.""" + url = f"{str(client.base_url).rstrip('/')}/apis/customization/v2/workspaces/{WORKSPACE}/automodel/jobs" + response = client._client.get(url) + response.raise_for_status() + payload = response.json() + data = payload.get("data", payload if isinstance(payload, list) else []) + return data if isinstance(data, list) else [] + + def test_workspace_exists(client: NeMoPlatform): """Verify the lora-training-workspace exists.""" response = client.workspaces.list() @@ -62,17 +73,17 @@ def test_fileset_has_data(client: NeMoPlatform): def test_customization_job_created(client: NeMoPlatform): - """Verify that a customization job was submitted via the NeMo Platform customizer API.""" - jobs = client.customization.jobs.list(workspace=WORKSPACE) - assert len(jobs.data) > 0, "No customization jobs found in workspace" + """Verify that an automodel customization job was submitted.""" + jobs = _list_automodel_jobs(client) + assert len(jobs) > 0, "No automodel customization jobs found in workspace" def test_customization_job_has_spec(client: NeMoPlatform): - """Verify the customization job has a valid training spec.""" - jobs = client.customization.jobs.list(workspace=WORKSPACE) - assert len(jobs.data) > 0, "No customization jobs found" - job = jobs.data[0] - assert job.spec is not None, "Customization job has no spec" + """Verify the automodel job has a valid training spec.""" + jobs = _list_automodel_jobs(client) + assert len(jobs) > 0, "No automodel customization jobs found" + job = jobs[0] + assert job.get("spec") is not None, "Automodel job has no spec" def test_customization_job_dispatched(client: NeMoPlatform): @@ -81,19 +92,15 @@ def test_customization_job_dispatched(client: NeMoPlatform): With the Docker socket mounted and GPU available, the jobs controller should schedule the training container. The job should reach at least 'pending' status. """ - jobs = client.customization.jobs.list(workspace=WORKSPACE) - assert len(jobs.data) > 0, "No customization jobs found" - job = jobs.data[0] + jobs = _list_automodel_jobs(client) + assert len(jobs) > 0, "No automodel customization jobs found" + job = jobs[0] - # Give the jobs controller a moment to process - status = getattr(job, "status", "unknown") + status = job.get("status", "unknown") if hasattr(status, "lower"): status = status.lower() - # Any status beyond 'created' means the jobs controller picked it up dispatched_statuses = {"pending", "running", "completed", "error", "cancelled", "paused"} - # 'created' is also acceptable - it means the job was submitted correctly - # even if the controller hasn't picked it up yet valid_statuses = dispatched_statuses | {"created", "unknown"} assert status in valid_statuses, f"Job in unexpected status: '{status}'. Expected one of: {valid_statuses}" diff --git a/tests/smoke_gpu/conftest.py b/tests/smoke_gpu/conftest.py index 6e0c602ba3..08fe792f4c 100644 --- a/tests/smoke_gpu/conftest.py +++ b/tests/smoke_gpu/conftest.py @@ -5,9 +5,6 @@ def pytest_configure(config): config.addinivalue_line("markers", "smoke_gpu_tasks: Import smoke tests for the nmp-gpu-tasks image") config.addinivalue_line("markers", "smoke_customizer_tasks: Import smoke tests for the customizer-tasks image") - config.addinivalue_line( - "markers", "smoke_customizer_automodel: Import smoke tests for the customizer-automodel image" - ) config.addinivalue_line("markers", "smoke_customizer_rl: Import smoke tests for the customizer-rl image") config.addinivalue_line( "markers", "smoke_nmp_automodel_tasks: Import smoke tests for the nmp/automodel-tasks image" diff --git a/tests/smoke_gpu/test_customizer_automodel.py b/tests/smoke_gpu/test_customizer_automodel.py index 58eb93d69c..e72783e8b0 100644 --- a/tests/smoke_gpu/test_customizer_automodel.py +++ b/tests/smoke_gpu/test_customizer_automodel.py @@ -1,9 +1,9 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Customizer automodel image import smoke tests. +"""nmp-automodel image import smoke tests. -Built as part of the docker-customizer bake group (smoke-test stage) and run +Built as part of the docker-bake.hcl bake group (smoke-test stage) and run on a CPU runner — no GPU hardware required. Two failure classes are caught at .so load time, before any GPU device is touched: @@ -18,28 +18,45 @@ import pytest -pytestmark = pytest.mark.smoke_customizer_automodel - +@pytest.mark.smoke_nmp_automodel_tasks +@pytest.mark.smoke_nmp_automodel_training def test_torch_importable(): import torch # noqa: F401 +@pytest.mark.smoke_nmp_automodel_tasks +@pytest.mark.smoke_nmp_automodel_training def test_transformers_importable(): import transformers # noqa: F401 +@pytest.mark.smoke_nmp_automodel_tasks +@pytest.mark.smoke_nmp_automodel_training def test_mamba_ssm_importable(): import mamba_ssm # noqa: F401 +@pytest.mark.smoke_nmp_automodel_tasks +@pytest.mark.smoke_nmp_automodel_training def test_causal_conv1d_importable(): import causal_conv1d # noqa: F401 +@pytest.mark.smoke_nmp_automodel_tasks +@pytest.mark.smoke_nmp_automodel_training def test_bitsandbytes_importable(): import bitsandbytes # noqa: F401 -def test_nmp_customizer_importable(): - import nmp.customizer # noqa: F401 +@pytest.mark.smoke_nmp_automodel_tasks +def test_nmp_automodel_tasks_importable(): + from nmp.automodel.tasks import file_io # noqa: F401 + from nmp.automodel.tasks.model_entity import __main__ as model_entity_main # noqa: F401 + from nmp.core.models.tasks.model_spec import __main__ as model_spec_main # noqa: F401 + + +@pytest.mark.smoke_nmp_automodel_training +def test_nmp_automodel_training_importable(): + import nemo_automodel # noqa: F401 + from nmp.automodel.tasks.training import __main__ as training_main # noqa: F401 diff --git a/tests/smoke_gpu/test_customizer_entry_points.py b/tests/smoke_gpu/test_customizer_entry_points.py index 6b1fa8f1ba..1911bb8398 100644 --- a/tests/smoke_gpu/test_customizer_entry_points.py +++ b/tests/smoke_gpu/test_customizer_entry_points.py @@ -18,7 +18,6 @@ # --------------------------------------------------------------------------- -@pytest.mark.smoke_customizer_automodel @pytest.mark.smoke_customizer_rl @pytest.mark.smoke_customizer_tasks @pytest.mark.smoke_gpu_tasks diff --git a/tests/smoke_gpu/test_nemo_automodel.py b/tests/smoke_gpu/test_nemo_automodel.py deleted file mode 100644 index f9d8063874..0000000000 --- a/tests/smoke_gpu/test_nemo_automodel.py +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""NeMo Automodel image import smoke tests. - -Built as part of the nmp-automodel docker bake group (smoke-test stage) and run -on a CPU runner - no GPU hardware required. -""" - -import pytest - - -def test_torch_importable(): - import torch # noqa: F401 - - -def test_transformers_importable(): - import transformers # noqa: F401 - - -def test_mamba_ssm_importable(): - import mamba_ssm # noqa: F401 - - -def test_causal_conv1d_importable(): - import causal_conv1d # noqa: F401 - - -def test_bitsandbytes_importable(): - import bitsandbytes # noqa: F401 - - -@pytest.mark.smoke_nmp_automodel_tasks -def test_nmp_automodel_tasks_importable(): - from nmp.automodel.tasks import file_io # noqa: F401 - from nmp.automodel.tasks.model_entity import __main__ as model_entity_main # noqa: F401 - from nmp.core.models.tasks.model_spec import __main__ as model_spec_main # noqa: F401 - - -@pytest.mark.smoke_nmp_automodel_training -def test_nmp_automodel_training_importable(): - import nemo_automodel # noqa: F401 - from nmp.automodel.tasks.training import __main__ as training_main # noqa: F401 diff --git a/third_party/licenses.jsonl b/third_party/licenses.jsonl index bbb1612417..6dc2fc302e 100644 --- a/third_party/licenses.jsonl +++ b/third_party/licenses.jsonl @@ -40,6 +40,7 @@ {"name": "cloudpickle", "license": "BSD-3-CLAUSE", "compatible": true} {"name": "colorama", "license": "BSD-3-CLAUSE", "compatible": true} {"name": "colorlog", "license": "MIT", "compatible": true} +{"name": "crc32c", "license": "LGPL-2.1-OR-LATER", "compatible": true} {"name": "cryptography", "license": "APACHE-2.0", "compatible": true} {"name": "cyclopts", "license": "APACHE-2.0", "compatible": true} {"name": "data-designer", "license": "APACHE-2.0", "compatible": true} @@ -47,6 +48,7 @@ {"name": "data-designer-engine", "license": "APACHE-2.0", "compatible": true} {"name": "dataclasses-json", "license": "MIT", "compatible": true} {"name": "datasets", "license": "APACHE-2.0", "compatible": true} +{"name": "detect-installer", "license": "0BSD", "compatible": true} {"name": "diff-cover", "license": "APACHE-2.0", "compatible": true} {"name": "dill", "license": "BSD-3-CLAUSE", "compatible": true} {"name": "diskcache", "license": "APACHE-2.0", "compatible": true} @@ -54,7 +56,6 @@ {"name": "dnspython", "license": "ISC", "compatible": true} {"name": "docker", "license": "APACHE-2.0", "compatible": true} {"name": "docstring-parser", "license": "MIT", "compatible": true} -{"name": "docutils", "license": "BSD-3-CLAUSE", "compatible": true} {"name": "duckdb", "license": "MIT", "compatible": true} {"name": "durationpy", "license": "MIT", "compatible": true} {"name": "email-validator", "license": "UNLICENSE", "compatible": true} @@ -68,6 +69,7 @@ {"name": "fastar", "license": "MIT", "compatible": true} {"name": "fastembed", "license": "APACHE-2.0", "compatible": true} {"name": "fastmcp", "license": "APACHE-2.0", "compatible": true} +{"name": "fastmcp-slim", "license": "APACHE-2.0", "compatible": true} {"name": "fastuuid", "license": "BSD-3-CLAUSE", "compatible": true} {"name": "filelock", "license": "UNLICENSE", "compatible": true} {"name": "filetype", "license": "MIT", "compatible": true} @@ -78,6 +80,7 @@ {"name": "gitpython", "license": "BSD-3-CLAUSE", "compatible": true} {"name": "googleapis-common-protos", "license": "APACHE-2.0", "compatible": true} {"name": "greenlet", "license": "MIT", "compatible": true} +{"name": "griffelib", "license": "ISC", "compatible": true} {"name": "grpcio", "license": "APACHE-2.0", "compatible": true} {"name": "gunicorn", "license": "MIT", "compatible": true} {"name": "h11", "license": "MIT", "compatible": true} @@ -148,7 +151,6 @@ {"name": "mdurl", "license": "MIT", "compatible": true} {"name": "mmh3", "license": "MIT", "compatible": true} {"name": "more-itertools", "license": "MIT", "compatible": true} -{"name": "mpmath", "license": "BSD-3-CLAUSE", "compatible": true} {"name": "multidict", "license": "APACHE-2.0", "compatible": true} {"name": "multiprocess", "license": "BSD-3-CLAUSE", "compatible": true} {"name": "mypy-extensions", "license": "MIT", "compatible": true} @@ -277,7 +279,6 @@ {"name": "starlette", "license": "BSD-3-CLAUSE", "compatible": true} {"name": "streaming-form-data", "license": "MIT", "compatible": true} {"name": "structlog", "license": "APACHE-2.0", "compatible": true} -{"name": "sympy", "license": "BSD-3-CLAUSE", "compatible": true} {"name": "tabulate", "license": "MIT", "compatible": true} {"name": "tblib", "license": "BSD-2-CLAUSE", "compatible": true} {"name": "tenacity", "license": "APACHE-2.0", "compatible": true} diff --git a/third_party/osv-licenses.json b/third_party/osv-licenses.json index a67f72fb2f..e5133daf29 100644 --- a/third_party/osv-licenses.json +++ b/third_party/osv-licenses.json @@ -38,7 +38,7 @@ { "package": { "name": "aiofile", - "version": "3.9.0", + "version": "3.11.1", "ecosystem": "PyPI" }, "licenses": [ @@ -58,7 +58,7 @@ { "package": { "name": "aiohappyeyeballs", - "version": "2.6.1", + "version": "2.6.2", "ecosystem": "PyPI" }, "licenses": [ @@ -68,793 +68,9 @@ { "package": { "name": "aiohttp", - "version": "3.13.5", + "version": "3.14.1", "ecosystem": "PyPI" }, - "vulnerabilities": [ - { - "modified": "2026-06-04T15:29:16Z", - "published": "2026-06-03T21:34:38Z", - "schema_version": "1.7.5", - "id": "GHSA-hg6j-4rv6-33pg", - "aliases": [ - "CVE-2026-47265" - ], - "related": [ - "CGA-5f7q-7pmq-hq3r" - ], - "summary": "AIOHTTP is vulnerable to cross-origin redirect with per-request cookies", - "details": "### Summary\n\nCookies set with the `cookies` parameter on requests are sent after following a cross-origin redirect.\n\n### Impact\n\nIf a developer uses the `cookies` parameter on a per-request basis then sensitive data might be leaked to an attacker if they manage to control a redirect.\n\n### Workaround\n\nIf unable to upgrade, using a `Cookie` header in the `headers` parameter is not vulnerable.\n\n-----\n\nPatch: https://github.com/aio-libs/aiohttp/commit/f54c40851b0d6c4bbdab97ba518a223adda32478", - "severity": [ - { - "type": "CVSS_V4", - "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:U" - } - ], - "affected": [ - { - "package": { - "ecosystem": "PyPI", - "name": "aiohttp", - "purl": "pkg:pypi/aiohttp" - }, - "ranges": [ - { - "type": "ECOSYSTEM", - "events": [ - { - "introduced": "0" - }, - { - "fixed": "3.14.0" - } - ] - } - ], - "versions": [ - "0.1", - "0.10.0", - "0.10.1", - "0.10.2", - "0.11.0", - "0.12.0", - "0.13.0", - "0.13.1", - "0.14.0", - "0.14.1", - "0.14.2", - "0.14.3", - "0.14.4", - "0.15.0", - "0.15.1", - "0.15.2", - "0.15.3", - "0.16.0", - "0.16.1", - "0.16.2", - "0.16.3", - "0.16.4", - "0.16.5", - "0.16.6", - "0.17.0", - "0.17.1", - "0.17.2", - "0.17.3", - "0.17.4", - "0.18.0", - "0.18.1", - "0.18.2", - "0.18.3", - "0.18.4", - "0.19.0", - "0.2", - "0.20.0", - "0.20.1", - "0.20.2", - "0.21.0", - "0.21.1", - "0.21.2", - "0.21.4", - "0.21.5", - "0.21.6", - "0.22.0", - "0.22.0a0", - "0.22.0b0", - "0.22.0b1", - "0.22.0b2", - "0.22.0b3", - "0.22.0b4", - "0.22.0b5", - "0.22.0b6", - "0.22.1", - "0.22.2", - "0.22.3", - "0.22.4", - "0.22.5", - "0.3", - "0.4", - "0.4.1", - "0.4.2", - "0.4.3", - "0.4.4", - "0.5.0", - "0.6.0", - "0.6.1", - "0.6.2", - "0.6.3", - "0.6.4", - "0.6.5", - "0.7.0", - "0.7.1", - "0.7.2", - "0.7.3", - "0.8.0", - "0.8.1", - "0.8.2", - "0.8.3", - "0.8.4", - "0.9.0", - "0.9.1", - "0.9.2", - "0.9.3", - "1.0.0", - "1.0.1", - "1.0.2", - "1.0.3", - "1.0.5", - "1.1.0", - "1.1.1", - "1.1.2", - "1.1.3", - "1.1.4", - "1.1.5", - "1.1.6", - "1.2.0", - "1.3.0", - "1.3.1", - "1.3.2", - "1.3.3", - "1.3.4", - "1.3.5", - "2.0.0", - "2.0.0rc1", - "2.0.1", - "2.0.2", - "2.0.3", - "2.0.4", - "2.0.5", - "2.0.6", - "2.0.7", - "2.1.0", - "2.2.0", - "2.2.1", - "2.2.2", - "2.2.3", - "2.2.4", - "2.2.5", - "2.3.0", - "2.3.0a1", - "2.3.0a2", - "2.3.0a3", - "2.3.0a4", - "2.3.1", - "2.3.10", - "2.3.1a1", - "2.3.2", - "2.3.2b2", - "2.3.2b3", - "2.3.3", - "2.3.4", - "2.3.5", - "2.3.6", - "2.3.7", - "2.3.8", - "2.3.9", - "3.0.0", - "3.0.0b0", - "3.0.0b1", - "3.0.0b2", - "3.0.0b3", - "3.0.0b4", - "3.0.1", - "3.0.2", - "3.0.3", - "3.0.4", - "3.0.5", - "3.0.6", - "3.0.7", - "3.0.8", - "3.0.9", - "3.1.0", - "3.1.1", - "3.1.2", - "3.1.3", - "3.10.0", - "3.10.0b1", - "3.10.0rc0", - "3.10.1", - "3.10.10", - "3.10.11", - "3.10.11rc0", - "3.10.2", - "3.10.3", - "3.10.4", - "3.10.5", - "3.10.6", - "3.10.6rc0", - "3.10.6rc1", - "3.10.6rc2", - "3.10.7", - "3.10.8", - "3.10.9", - "3.11.0", - "3.11.0b0", - "3.11.0b1", - "3.11.0b2", - "3.11.0b3", - "3.11.0b4", - "3.11.0b5", - "3.11.0rc0", - "3.11.0rc1", - "3.11.0rc2", - "3.11.1", - "3.11.10", - "3.11.11", - "3.11.12", - "3.11.13", - "3.11.14", - "3.11.15", - "3.11.16", - "3.11.17", - "3.11.18", - "3.11.2", - "3.11.3", - "3.11.4", - "3.11.5", - "3.11.6", - "3.11.7", - "3.11.8", - "3.11.9", - "3.12.0", - "3.12.0b0", - "3.12.0b1", - "3.12.0b2", - "3.12.0b3", - "3.12.0rc0", - "3.12.0rc1", - "3.12.1", - "3.12.10", - "3.12.11", - "3.12.12", - "3.12.13", - "3.12.14", - "3.12.15", - "3.12.1rc0", - "3.12.2", - "3.12.3", - "3.12.4", - "3.12.6", - "3.12.7", - "3.12.7rc0", - "3.12.8", - "3.12.9", - "3.13.0", - "3.13.1", - "3.13.2", - "3.13.3", - "3.13.4", - "3.13.5", - "3.2.0", - "3.2.1", - "3.3.0", - "3.3.0a0", - "3.3.1", - "3.3.2", - "3.3.2a0", - "3.4.0", - "3.4.0a0", - "3.4.0a3", - "3.4.0b1", - "3.4.0b2", - "3.4.1", - "3.4.2", - "3.4.3", - "3.4.4", - "3.5.0", - "3.5.0a1", - "3.5.0b1", - "3.5.0b2", - "3.5.0b3", - "3.5.1", - "3.5.2", - "3.5.3", - "3.5.4", - "3.6.0", - "3.6.0a0", - "3.6.0a1", - "3.6.0a11", - "3.6.0a12", - "3.6.0a2", - "3.6.0a3", - "3.6.0a4", - "3.6.0a5", - "3.6.0a6", - "3.6.0a7", - "3.6.0a8", - "3.6.0a9", - "3.6.0b0", - "3.6.1", - "3.6.1b3", - "3.6.1b4", - "3.6.2", - "3.6.2a0", - "3.6.2a1", - "3.6.2a2", - "3.6.3", - "3.7.0", - "3.7.0b0", - "3.7.0b1", - "3.7.1", - "3.7.2", - "3.7.3", - "3.7.4", - "3.7.4.post0", - "3.8.0", - "3.8.0a7", - "3.8.0b0", - "3.8.1", - "3.8.2", - "3.8.3", - "3.8.4", - "3.8.5", - "3.8.6", - "3.9.0", - "3.9.0b0", - "3.9.0b1", - "3.9.0rc0", - "3.9.1", - "3.9.2", - "3.9.3", - "3.9.4", - "3.9.4rc0", - "3.9.5" - ], - "database_specific": { - "source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-hg6j-4rv6-33pg/GHSA-hg6j-4rv6-33pg.json" - } - } - ], - "references": [ - { - "type": "WEB", - "url": "https://github.com/aio-libs/aiohttp/security/advisories/GHSA-hg6j-4rv6-33pg" - }, - { - "type": "ADVISORY", - "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-47265" - }, - { - "type": "WEB", - "url": "https://github.com/aio-libs/aiohttp/commit/f54c40851b0d6c4bbdab97ba518a223adda32478" - }, - { - "type": "PACKAGE", - "url": "https://github.com/aio-libs/aiohttp" - } - ], - "database_specific": { - "cwe_ids": [ - "CWE-346" - ], - "github_reviewed": true, - "github_reviewed_at": "2026-06-03T21:34:38Z", - "nvd_published_at": "2026-06-02T20:16:37Z", - "severity": "MODERATE" - } - }, - { - "modified": "2026-06-04T15:29:17Z", - "published": "2026-06-03T20:56:54Z", - "schema_version": "1.7.5", - "id": "GHSA-jg22-mg44-37j8", - "aliases": [ - "CVE-2026-34993" - ], - "related": [ - "CGA-2r69-w36g-jxvr" - ], - "summary": "AIOHTTP is Vulnerable to Deserialization of Untrusted Data", - "details": "### Summary\n\nUsing ``CookieJar.load()`` with untrusted input may allow arbitrary code execution.\n\n### Impact\n\nMost applications using this function will be doing so with the user's own data, so this is unlikely to affect many applications.\n\n### Workaround\n\nIf an application does allow attacker controlled files to be loaded, a workaround on older releases would be to sanitise the files before loading.\n\n-----\n\nPatch: https://github.com/aio-libs/aiohttp/commit/dcf40f30637e8752c76781cf6703b5a236749a00", - "severity": [ - { - "type": "CVSS_V3", - "score": "CVSS:3.1/AV:L/AC:H/PR:H/UI:R/S:C/C:L/I:H/A:L" - } - ], - "affected": [ - { - "package": { - "ecosystem": "PyPI", - "name": "aiohttp", - "purl": "pkg:pypi/aiohttp" - }, - "ranges": [ - { - "type": "ECOSYSTEM", - "events": [ - { - "introduced": "0" - }, - { - "fixed": "3.14.0" - } - ] - } - ], - "versions": [ - "0.1", - "0.10.0", - "0.10.1", - "0.10.2", - "0.11.0", - "0.12.0", - "0.13.0", - "0.13.1", - "0.14.0", - "0.14.1", - "0.14.2", - "0.14.3", - "0.14.4", - "0.15.0", - "0.15.1", - "0.15.2", - "0.15.3", - "0.16.0", - "0.16.1", - "0.16.2", - "0.16.3", - "0.16.4", - "0.16.5", - "0.16.6", - "0.17.0", - "0.17.1", - "0.17.2", - "0.17.3", - "0.17.4", - "0.18.0", - "0.18.1", - "0.18.2", - "0.18.3", - "0.18.4", - "0.19.0", - "0.2", - "0.20.0", - "0.20.1", - "0.20.2", - "0.21.0", - "0.21.1", - "0.21.2", - "0.21.4", - "0.21.5", - "0.21.6", - "0.22.0", - "0.22.0a0", - "0.22.0b0", - "0.22.0b1", - "0.22.0b2", - "0.22.0b3", - "0.22.0b4", - "0.22.0b5", - "0.22.0b6", - "0.22.1", - "0.22.2", - "0.22.3", - "0.22.4", - "0.22.5", - "0.3", - "0.4", - "0.4.1", - "0.4.2", - "0.4.3", - "0.4.4", - "0.5.0", - "0.6.0", - "0.6.1", - "0.6.2", - "0.6.3", - "0.6.4", - "0.6.5", - "0.7.0", - "0.7.1", - "0.7.2", - "0.7.3", - "0.8.0", - "0.8.1", - "0.8.2", - "0.8.3", - "0.8.4", - "0.9.0", - "0.9.1", - "0.9.2", - "0.9.3", - "1.0.0", - "1.0.1", - "1.0.2", - "1.0.3", - "1.0.5", - "1.1.0", - "1.1.1", - "1.1.2", - "1.1.3", - "1.1.4", - "1.1.5", - "1.1.6", - "1.2.0", - "1.3.0", - "1.3.1", - "1.3.2", - "1.3.3", - "1.3.4", - "1.3.5", - "2.0.0", - "2.0.0rc1", - "2.0.1", - "2.0.2", - "2.0.3", - "2.0.4", - "2.0.5", - "2.0.6", - "2.0.7", - "2.1.0", - "2.2.0", - "2.2.1", - "2.2.2", - "2.2.3", - "2.2.4", - "2.2.5", - "2.3.0", - "2.3.0a1", - "2.3.0a2", - "2.3.0a3", - "2.3.0a4", - "2.3.1", - "2.3.10", - "2.3.1a1", - "2.3.2", - "2.3.2b2", - "2.3.2b3", - "2.3.3", - "2.3.4", - "2.3.5", - "2.3.6", - "2.3.7", - "2.3.8", - "2.3.9", - "3.0.0", - "3.0.0b0", - "3.0.0b1", - "3.0.0b2", - "3.0.0b3", - "3.0.0b4", - "3.0.1", - "3.0.2", - "3.0.3", - "3.0.4", - "3.0.5", - "3.0.6", - "3.0.7", - "3.0.8", - "3.0.9", - "3.1.0", - "3.1.1", - "3.1.2", - "3.1.3", - "3.10.0", - "3.10.0b1", - "3.10.0rc0", - "3.10.1", - "3.10.10", - "3.10.11", - "3.10.11rc0", - "3.10.2", - "3.10.3", - "3.10.4", - "3.10.5", - "3.10.6", - "3.10.6rc0", - "3.10.6rc1", - "3.10.6rc2", - "3.10.7", - "3.10.8", - "3.10.9", - "3.11.0", - "3.11.0b0", - "3.11.0b1", - "3.11.0b2", - "3.11.0b3", - "3.11.0b4", - "3.11.0b5", - "3.11.0rc0", - "3.11.0rc1", - "3.11.0rc2", - "3.11.1", - "3.11.10", - "3.11.11", - "3.11.12", - "3.11.13", - "3.11.14", - "3.11.15", - "3.11.16", - "3.11.17", - "3.11.18", - "3.11.2", - "3.11.3", - "3.11.4", - "3.11.5", - "3.11.6", - "3.11.7", - "3.11.8", - "3.11.9", - "3.12.0", - "3.12.0b0", - "3.12.0b1", - "3.12.0b2", - "3.12.0b3", - "3.12.0rc0", - "3.12.0rc1", - "3.12.1", - "3.12.10", - "3.12.11", - "3.12.12", - "3.12.13", - "3.12.14", - "3.12.15", - "3.12.1rc0", - "3.12.2", - "3.12.3", - "3.12.4", - "3.12.6", - "3.12.7", - "3.12.7rc0", - "3.12.8", - "3.12.9", - "3.13.0", - "3.13.1", - "3.13.2", - "3.13.3", - "3.13.4", - "3.13.5", - "3.2.0", - "3.2.1", - "3.3.0", - "3.3.0a0", - "3.3.1", - "3.3.2", - "3.3.2a0", - "3.4.0", - "3.4.0a0", - "3.4.0a3", - "3.4.0b1", - "3.4.0b2", - "3.4.1", - "3.4.2", - "3.4.3", - "3.4.4", - "3.5.0", - "3.5.0a1", - "3.5.0b1", - "3.5.0b2", - "3.5.0b3", - "3.5.1", - "3.5.2", - "3.5.3", - "3.5.4", - "3.6.0", - "3.6.0a0", - "3.6.0a1", - "3.6.0a11", - "3.6.0a12", - "3.6.0a2", - "3.6.0a3", - "3.6.0a4", - "3.6.0a5", - "3.6.0a6", - "3.6.0a7", - "3.6.0a8", - "3.6.0a9", - "3.6.0b0", - "3.6.1", - "3.6.1b3", - "3.6.1b4", - "3.6.2", - "3.6.2a0", - "3.6.2a1", - "3.6.2a2", - "3.6.3", - "3.7.0", - "3.7.0b0", - "3.7.0b1", - "3.7.1", - "3.7.2", - "3.7.3", - "3.7.4", - "3.7.4.post0", - "3.8.0", - "3.8.0a7", - "3.8.0b0", - "3.8.1", - "3.8.2", - "3.8.3", - "3.8.4", - "3.8.5", - "3.8.6", - "3.9.0", - "3.9.0b0", - "3.9.0b1", - "3.9.0rc0", - "3.9.1", - "3.9.2", - "3.9.3", - "3.9.4", - "3.9.4rc0", - "3.9.5" - ], - "database_specific": { - "source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-jg22-mg44-37j8/GHSA-jg22-mg44-37j8.json" - } - } - ], - "references": [ - { - "type": "WEB", - "url": "https://github.com/aio-libs/aiohttp/security/advisories/GHSA-jg22-mg44-37j8" - }, - { - "type": "ADVISORY", - "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-34993" - }, - { - "type": "WEB", - "url": "https://github.com/aio-libs/aiohttp/commit/dcf40f30637e8752c76781cf6703b5a236749a00" - }, - { - "type": "PACKAGE", - "url": "https://github.com/aio-libs/aiohttp" - } - ], - "database_specific": { - "cwe_ids": [ - "CWE-502" - ], - "github_reviewed": true, - "github_reviewed_at": "2026-06-03T20:56:54Z", - "nvd_published_at": "2026-06-02T20:16:34Z", - "severity": "MODERATE" - } - } - ], - "groups": [ - { - "ids": [ - "GHSA-hg6j-4rv6-33pg" - ], - "aliases": [ - "CVE-2026-47265", - "GHSA-hg6j-4rv6-33pg" - ], - "max_severity": "6.6" - }, - { - "ids": [ - "GHSA-jg22-mg44-37j8" - ], - "aliases": [ - "CVE-2026-34993", - "GHSA-jg22-mg44-37j8" - ], - "max_severity": "6.4" - } - ], "licenses": [ "Apache-2.0 AND MIT" ] @@ -942,7 +158,7 @@ { "package": { "name": "anthropic", - "version": "0.101.0", + "version": "0.107.1", "ecosystem": "PyPI" }, "licenses": [ @@ -1052,7 +268,7 @@ { "package": { "name": "beautifulsoup4", - "version": "4.14.3", + "version": "4.15.0", "ecosystem": "PyPI" }, "licenses": [ @@ -1082,7 +298,7 @@ { "package": { "name": "botocore-stubs", - "version": "1.42.41", + "version": "1.43.14", "ecosystem": "PyPI" }, "licenses": [ @@ -1092,7 +308,7 @@ { "package": { "name": "cachetools", - "version": "7.0.5", + "version": "7.1.4", "ecosystem": "PyPI" }, "licenses": [ @@ -1112,7 +328,7 @@ { "package": { "name": "certifi", - "version": "2026.2.25", + "version": "2026.5.20", "ecosystem": "PyPI" }, "licenses": [ @@ -1142,7 +358,7 @@ { "package": { "name": "charset-normalizer", - "version": "3.4.6", + "version": "3.4.7", "ecosystem": "PyPI" }, "licenses": [ @@ -1162,7 +378,7 @@ { "package": { "name": "click", - "version": "8.3.1", + "version": "8.4.1", "ecosystem": "PyPI" }, "licenses": [ @@ -1209,6 +425,16 @@ "MIT" ] }, + { + "package": { + "name": "crc32c", + "version": "2.7.1", + "ecosystem": "PyPI" + }, + "licenses": [ + "LGPL-2.1-or-later" + ] + }, { "package": { "name": "cryptography", @@ -1222,7 +448,7 @@ { "package": { "name": "cyclopts", - "version": "4.10.1", + "version": "4.17.0", "ecosystem": "PyPI" }, "licenses": [ @@ -1272,17 +498,27 @@ { "package": { "name": "datasets", - "version": "4.0.0", + "version": "4.3.0", "ecosystem": "PyPI" }, "licenses": [ "Apache-2.0" ] }, + { + "package": { + "name": "detect-installer", + "version": "0.1.0", + "ecosystem": "PyPI" + }, + "licenses": [ + "0BSD" + ] + }, { "package": { "name": "diff-cover", - "version": "10.2.0", + "version": "10.3.0", "ecosystem": "PyPI" }, "licenses": [ @@ -1292,7 +528,7 @@ { "package": { "name": "dill", - "version": "0.3.8", + "version": "0.4.0", "ecosystem": "PyPI" }, "licenses": [ @@ -1510,27 +746,17 @@ { "package": { "name": "docstring-parser", - "version": "0.17.0", + "version": "0.18.0", "ecosystem": "PyPI" }, "licenses": [ "MIT" ] }, - { - "package": { - "name": "docutils", - "version": "0.22.4", - "ecosystem": "PyPI" - }, - "licenses": [ - "non-standard" - ] - }, { "package": { "name": "duckdb", - "version": "1.5.1", + "version": "1.5.3", "ecosystem": "PyPI" }, "licenses": [ @@ -1620,7 +846,7 @@ { "package": { "name": "fastapi-cloud-cli", - "version": "0.15.1", + "version": "0.19.0", "ecosystem": "PyPI" }, "licenses": [ @@ -1630,7 +856,7 @@ { "package": { "name": "fastar", - "version": "0.9.0", + "version": "0.11.0", "ecosystem": "PyPI" }, "licenses": [ @@ -1650,7 +876,17 @@ { "package": { "name": "fastmcp", - "version": "3.2.0", + "version": "3.4.0", + "ecosystem": "PyPI" + }, + "licenses": [ + "Apache-2.0" + ] + }, + { + "package": { + "name": "fastmcp-slim", + "version": "3.4.0", "ecosystem": "PyPI" }, "licenses": [ @@ -1670,7 +906,7 @@ { "package": { "name": "filelock", - "version": "3.25.2", + "version": "3.29.1", "ecosystem": "PyPI" }, "licenses": [ @@ -1710,11 +946,11 @@ { "package": { "name": "fsspec", - "version": "2025.3.0", + "version": "2025.9.0", "ecosystem": "PyPI" }, "licenses": [ - "non-standard" + "BSD-3-Clause" ] }, { @@ -1740,7 +976,7 @@ { "package": { "name": "googleapis-common-protos", - "version": "1.73.1", + "version": "1.75.0", "ecosystem": "PyPI" }, "licenses": [ @@ -1750,17 +986,27 @@ { "package": { "name": "greenlet", - "version": "3.3.2", + "version": "3.5.1", "ecosystem": "PyPI" }, "licenses": [ "MIT AND PSF-2.0" ] }, + { + "package": { + "name": "griffelib", + "version": "2.0.2", + "ecosystem": "PyPI" + }, + "licenses": [ + "ISC" + ] + }, { "package": { "name": "grpcio", - "version": "1.80.0", + "version": "1.81.0", "ecosystem": "PyPI" }, "licenses": [ @@ -1770,7 +1016,7 @@ { "package": { "name": "gunicorn", - "version": "25.3.0", + "version": "26.0.0", "ecosystem": "PyPI" }, "licenses": [ @@ -1790,7 +1036,7 @@ { "package": { "name": "hf-xet", - "version": "1.4.3", + "version": "1.5.1", "ecosystem": "PyPI" }, "licenses": [ @@ -1810,7 +1056,7 @@ { "package": { "name": "httptools", - "version": "0.7.1", + "version": "0.8.0", "ecosystem": "PyPI" }, "licenses": [ @@ -1830,7 +1076,7 @@ { "package": { "name": "httpx-retries", - "version": "0.4.6", + "version": "0.5.0", "ecosystem": "PyPI" }, "licenses": [ @@ -1850,7 +1096,7 @@ { "package": { "name": "huggingface-hub", - "version": "1.15.0", + "version": "1.18.0", "ecosystem": "PyPI" }, "licenses": [ @@ -1870,7 +1116,7 @@ { "package": { "name": "idna", - "version": "3.15", + "version": "3.18", "ecosystem": "PyPI" }, "licenses": [ @@ -1880,7 +1126,7 @@ { "package": { "name": "importlib-metadata", - "version": "8.5.0", + "version": "8.9.0", "ecosystem": "PyPI" }, "licenses": [ @@ -1900,7 +1146,7 @@ { "package": { "name": "instructor", - "version": "1.11.3", + "version": "1.15.1", "ecosystem": "PyPI" }, "licenses": [ @@ -1940,7 +1186,7 @@ { "package": { "name": "jaraco-functools", - "version": "4.4.0", + "version": "4.5.0", "ecosystem": "PyPI" }, "licenses": [ @@ -1970,7 +1216,7 @@ { "package": { "name": "jiter", - "version": "0.10.0", + "version": "0.13.0", "ecosystem": "PyPI" }, "licenses": [ @@ -2000,7 +1246,7 @@ { "package": { "name": "joserfc", - "version": "1.6.5", + "version": "1.7.1", "ecosystem": "PyPI" }, "licenses": [ @@ -2070,7 +1316,7 @@ { "package": { "name": "jsonschema", - "version": "4.23.0", + "version": "4.26.0", "ecosystem": "PyPI" }, "licenses": [ @@ -2080,7 +1326,7 @@ { "package": { "name": "jsonschema-path", - "version": "0.3.4", + "version": "0.5.0", "ecosystem": "PyPI" }, "licenses": [ @@ -2110,7 +1356,7 @@ { "package": { "name": "kubernetes", - "version": "35.0.0", + "version": "36.0.2", "ecosystem": "PyPI" }, "licenses": [ @@ -2120,7 +1366,7 @@ { "package": { "name": "langchain", - "version": "1.2.14", + "version": "1.3.4", "ecosystem": "PyPI" }, "licenses": [ @@ -2150,7 +1396,7 @@ { "package": { "name": "langchain-community", - "version": "0.3.27", + "version": "0.3.31", "ecosystem": "PyPI" }, "licenses": [ @@ -2160,7 +1406,7 @@ { "package": { "name": "langchain-core", - "version": "1.3.3", + "version": "1.4.2", "ecosystem": "PyPI" }, "licenses": [ @@ -2190,7 +1436,7 @@ { "package": { "name": "langchain-litellm", - "version": "0.6.5", + "version": "0.6.6", "ecosystem": "PyPI" }, "licenses": [ @@ -2210,7 +1456,7 @@ { "package": { "name": "langchain-nvidia-ai-endpoints", - "version": "1.3.0", + "version": "1.4.1", "ecosystem": "PyPI" }, "licenses": [ @@ -2220,7 +1466,7 @@ { "package": { "name": "langchain-oci", - "version": "0.2.6", + "version": "0.2.7", "ecosystem": "PyPI" }, "licenses": [ @@ -2230,7 +1476,7 @@ { "package": { "name": "langchain-openai", - "version": "1.2.1", + "version": "1.2.2", "ecosystem": "PyPI" }, "licenses": [ @@ -2240,7 +1486,7 @@ { "package": { "name": "langchain-protocol", - "version": "0.0.15", + "version": "0.0.16", "ecosystem": "PyPI" }, "licenses": [ @@ -2270,7 +1516,7 @@ { "package": { "name": "langgraph", - "version": "1.1.4", + "version": "1.2.4", "ecosystem": "PyPI" }, "licenses": [ @@ -2280,7 +1526,7 @@ { "package": { "name": "langgraph-checkpoint", - "version": "4.0.1", + "version": "4.1.1", "ecosystem": "PyPI" }, "licenses": [ @@ -2290,7 +1536,7 @@ { "package": { "name": "langgraph-prebuilt", - "version": "1.0.8", + "version": "1.1.0", "ecosystem": "PyPI" }, "licenses": [ @@ -2300,7 +1546,7 @@ { "package": { "name": "langgraph-sdk", - "version": "0.3.12", + "version": "0.4.2", "ecosystem": "PyPI" }, "licenses": [ @@ -2330,7 +1576,7 @@ { "package": { "name": "litellm", - "version": "1.83.14", + "version": "1.88.1", "ecosystem": "PyPI" }, "licenses": [ @@ -2350,7 +1596,7 @@ { "package": { "name": "lxml", - "version": "6.1.0", + "version": "6.1.1", "ecosystem": "PyPI" }, "licenses": [ @@ -2380,7 +1626,7 @@ { "package": { "name": "markdown-it-py", - "version": "4.0.0", + "version": "4.2.0", "ecosystem": "PyPI" }, "licenses": [ @@ -2390,7 +1636,7 @@ { "package": { "name": "marko", - "version": "2.2.2", + "version": "2.2.3", "ecosystem": "PyPI" }, "licenses": [ @@ -2420,7 +1666,7 @@ { "package": { "name": "mcp", - "version": "1.26.0", + "version": "1.27.2", "ecosystem": "PyPI" }, "licenses": [ @@ -2450,23 +1696,13 @@ { "package": { "name": "more-itertools", - "version": "10.8.0", + "version": "11.1.0", "ecosystem": "PyPI" }, "licenses": [ "MIT" ] }, - { - "package": { - "name": "mpmath", - "version": "1.3.0", - "ecosystem": "PyPI" - }, - "licenses": [ - "non-standard" - ] - }, { "package": { "name": "multidict", @@ -2560,7 +1796,7 @@ { "package": { "name": "ngcsdk", - "version": "4.16.0", + "version": "4.19.1", "ecosystem": "PyPI" }, "licenses": [ @@ -2580,7 +1816,7 @@ { "package": { "name": "numpy", - "version": "2.4.4", + "version": "2.4.6", "ecosystem": "PyPI" }, "licenses": [ @@ -2590,7 +1826,7 @@ { "package": { "name": "nvidia-ml-py", - "version": "13.595.45", + "version": "13.610.43", "ecosystem": "PyPI" }, "licenses": [ @@ -2670,7 +1906,7 @@ { "package": { "name": "oci", - "version": "2.174.0", + "version": "2.178.0", "ecosystem": "PyPI" }, "licenses": [ @@ -2690,7 +1926,7 @@ { "package": { "name": "onnxruntime", - "version": "1.24.4", + "version": "1.26.0", "ecosystem": "PyPI" }, "licenses": [ @@ -2700,7 +1936,7 @@ { "package": { "name": "openai", - "version": "2.35.0", + "version": "2.41.0", "ecosystem": "PyPI" }, "licenses": [ @@ -2730,7 +1966,7 @@ { "package": { "name": "openinference-semantic-conventions", - "version": "0.1.29", + "version": "0.1.30", "ecosystem": "PyPI" }, "licenses": [ @@ -2740,7 +1976,7 @@ { "package": { "name": "opentelemetry-api", - "version": "1.40.0", + "version": "1.42.1", "ecosystem": "PyPI" }, "licenses": [ @@ -2750,7 +1986,7 @@ { "package": { "name": "opentelemetry-distro", - "version": "0.61b0", + "version": "0.63b1", "ecosystem": "PyPI" }, "licenses": [ @@ -2760,7 +1996,7 @@ { "package": { "name": "opentelemetry-exporter-otlp", - "version": "1.40.0", + "version": "1.42.1", "ecosystem": "PyPI" }, "licenses": [ @@ -2770,7 +2006,7 @@ { "package": { "name": "opentelemetry-exporter-otlp-proto-common", - "version": "1.40.0", + "version": "1.42.1", "ecosystem": "PyPI" }, "licenses": [ @@ -2780,7 +2016,7 @@ { "package": { "name": "opentelemetry-exporter-otlp-proto-grpc", - "version": "1.40.0", + "version": "1.42.1", "ecosystem": "PyPI" }, "licenses": [ @@ -2790,7 +2026,7 @@ { "package": { "name": "opentelemetry-exporter-otlp-proto-http", - "version": "1.40.0", + "version": "1.42.1", "ecosystem": "PyPI" }, "licenses": [ @@ -2800,7 +2036,7 @@ { "package": { "name": "opentelemetry-exporter-prometheus", - "version": "0.61b0", + "version": "0.63b1", "ecosystem": "PyPI" }, "licenses": [ @@ -2810,7 +2046,7 @@ { "package": { "name": "opentelemetry-instrumentation", - "version": "0.61b0", + "version": "0.63b1", "ecosystem": "PyPI" }, "licenses": [ @@ -2820,7 +2056,7 @@ { "package": { "name": "opentelemetry-instrumentation-asgi", - "version": "0.61b0", + "version": "0.63b1", "ecosystem": "PyPI" }, "licenses": [ @@ -2830,7 +2066,7 @@ { "package": { "name": "opentelemetry-instrumentation-fastapi", - "version": "0.61b0", + "version": "0.63b1", "ecosystem": "PyPI" }, "licenses": [ @@ -2840,7 +2076,7 @@ { "package": { "name": "opentelemetry-instrumentation-httpx", - "version": "0.61b0", + "version": "0.63b1", "ecosystem": "PyPI" }, "licenses": [ @@ -2850,7 +2086,7 @@ { "package": { "name": "opentelemetry-instrumentation-requests", - "version": "0.61b0", + "version": "0.63b1", "ecosystem": "PyPI" }, "licenses": [ @@ -2860,7 +2096,7 @@ { "package": { "name": "opentelemetry-instrumentation-sqlalchemy", - "version": "0.61b0", + "version": "0.63b1", "ecosystem": "PyPI" }, "licenses": [ @@ -2870,7 +2106,7 @@ { "package": { "name": "opentelemetry-instrumentation-system-metrics", - "version": "0.61b0", + "version": "0.63b1", "ecosystem": "PyPI" }, "licenses": [ @@ -2880,7 +2116,7 @@ { "package": { "name": "opentelemetry-processor-baggage", - "version": "0.61b0", + "version": "0.63b1", "ecosystem": "PyPI" }, "licenses": [ @@ -2890,7 +2126,7 @@ { "package": { "name": "opentelemetry-proto", - "version": "1.40.0", + "version": "1.42.1", "ecosystem": "PyPI" }, "licenses": [ @@ -2900,7 +2136,7 @@ { "package": { "name": "opentelemetry-sdk", - "version": "1.40.0", + "version": "1.42.1", "ecosystem": "PyPI" }, "licenses": [ @@ -2910,7 +2146,7 @@ { "package": { "name": "opentelemetry-semantic-conventions", - "version": "0.61b0", + "version": "0.63b1", "ecosystem": "PyPI" }, "licenses": [ @@ -2920,7 +2156,7 @@ { "package": { "name": "opentelemetry-util-http", - "version": "0.61b0", + "version": "0.63b1", "ecosystem": "PyPI" }, "licenses": [ @@ -2940,7 +2176,7 @@ { "package": { "name": "orjson", - "version": "3.11.8", + "version": "3.11.9", "ecosystem": "PyPI" }, "licenses": [ @@ -2960,7 +2196,7 @@ { "package": { "name": "packaging", - "version": "26.0", + "version": "26.2", "ecosystem": "PyPI" }, "licenses": [ @@ -2980,7 +2216,7 @@ { "package": { "name": "pathable", - "version": "0.4.4", + "version": "0.6.0", "ecosystem": "PyPI" }, "licenses": [ @@ -2990,7 +2226,7 @@ { "package": { "name": "pathspec", - "version": "1.0.4", + "version": "1.1.1", "ecosystem": "PyPI" }, "licenses": [ @@ -3008,238 +2244,11 @@ ] }, { - "package": { - "name": "pip", - "version": "26.1.1", - "ecosystem": "PyPI" - }, - "vulnerabilities": [ - { - "modified": "2026-06-05T12:45:14Z", - "published": "2026-06-01T17:17:35Z", - "schema_version": "1.7.5", - "id": "PYSEC-2026-196", - "aliases": [ - "CVE-2026-8643" - ], - "details": "pip would treat console_scripts and gui_scripts as paths instead of file names without sanitizing the resolved absolute path to the installation directory, leading to entry points being installed outside the installation directory.", - "severity": [ - { - "type": "CVSS_V3", - "score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N" - } - ], - "affected": [ - { - "package": { - "ecosystem": "PyPI", - "name": "pip", - "purl": "pkg:pypi/pip" - }, - "ranges": [ - { - "type": "ECOSYSTEM", - "events": [ - { - "introduced": "0" - }, - { - "fixed": "26.1.2" - } - ] - } - ], - "versions": [ - "0.2", - "0.2.1", - "0.3", - "0.3.1", - "0.4", - "0.5", - "0.5.1", - "0.6", - "0.6.1", - "0.6.2", - "0.6.3", - "0.7", - "0.7.1", - "0.7.2", - "0.8", - "0.8.1", - "0.8.2", - "0.8.3", - "1.0", - "1.0.1", - "1.0.2", - "1.1", - "1.2", - "1.2.1", - "1.3", - "1.3.1", - "1.4", - "1.4.1", - "1.5", - "1.5.1", - "1.5.2", - "1.5.3", - "1.5.4", - "1.5.5", - "1.5.6", - "10.0.0", - "10.0.0b1", - "10.0.0b2", - "10.0.1", - "18.0", - "18.1", - "19.0", - "19.0.1", - "19.0.2", - "19.0.3", - "19.1", - "19.1.1", - "19.2", - "19.2.1", - "19.2.2", - "19.2.3", - "19.3", - "19.3.1", - "20.0", - "20.0.1", - "20.0.2", - "20.1", - "20.1.1", - "20.1b1", - "20.2", - "20.2.1", - "20.2.2", - "20.2.3", - "20.2.4", - "20.2b1", - "20.3", - "20.3.1", - "20.3.2", - "20.3.3", - "20.3.4", - "20.3b1", - "21.0", - "21.0.1", - "21.1", - "21.1.1", - "21.1.2", - "21.1.3", - "21.2", - "21.2.1", - "21.2.2", - "21.2.3", - "21.2.4", - "21.3", - "21.3.1", - "22.0", - "22.0.1", - "22.0.2", - "22.0.3", - "22.0.4", - "22.1", - "22.1.1", - "22.1.2", - "22.1b1", - "22.2", - "22.2.1", - "22.2.2", - "22.3", - "22.3.1", - "23.0", - "23.0.1", - "23.1", - "23.1.1", - "23.1.2", - "23.2", - "23.2.1", - "23.3", - "23.3.1", - "23.3.2", - "24.0", - "24.1", - "24.1.1", - "24.1.2", - "24.1b1", - "24.1b2", - "24.2", - "24.3", - "24.3.1", - "25.0", - "25.0.1", - "25.1", - "25.1.1", - "25.2", - "25.3", - "26.0", - "26.0.1", - "26.1", - "26.1.1", - "6.0", - "6.0.1", - "6.0.2", - "6.0.3", - "6.0.4", - "6.0.5", - "6.0.6", - "6.0.7", - "6.0.8", - "6.1.0", - "6.1.1", - "7.0.0", - "7.0.1", - "7.0.2", - "7.0.3", - "7.1.0", - "7.1.1", - "7.1.2", - "8.0.0", - "8.0.1", - "8.0.2", - "8.0.3", - "8.1.0", - "8.1.1", - "8.1.2", - "9.0.0", - "9.0.1", - "9.0.2", - "9.0.3" - ], - "database_specific": { - "source": "https://github.com/pypa/advisory-database/blob/main/vulns/pip/PYSEC-2026-196.yaml" - } - } - ], - "references": [ - { - "type": "ADVISORY", - "url": "http://www.openwall.com/lists/oss-security/2026/06/01/5" - }, - { - "type": "ADVISORY", - "url": "https://mail.python.org/archives/list/security-announce@python.org/thread/YV63UET5D3OOJY7O4M5XCVYO2YM4NBYJ/" - }, - { - "type": "FIX", - "url": "https://github.com/pypa/pip/pull/14000" - } - ] - } - ], - "groups": [ - { - "ids": [ - "PYSEC-2026-196" - ], - "aliases": [ - "CVE-2026-8643", - "PYSEC-2026-196" - ], - "max_severity": "5.5" - } - ], + "package": { + "name": "pip", + "version": "26.1.2", + "ecosystem": "PyPI" + }, "licenses": [ "MIT" ] @@ -3267,7 +2276,7 @@ { "package": { "name": "platformdirs", - "version": "4.9.4", + "version": "4.10.0", "ecosystem": "PyPI" }, "licenses": [ @@ -3317,7 +2326,7 @@ { "package": { "name": "prometheus-client", - "version": "0.24.1", + "version": "0.25.0", "ecosystem": "PyPI" }, "licenses": [ @@ -3347,7 +2356,7 @@ { "package": { "name": "propcache", - "version": "0.4.1", + "version": "0.5.2", "ecosystem": "PyPI" }, "licenses": [ @@ -3377,7 +2386,7 @@ { "package": { "name": "psycopg2-binary", - "version": "2.9.11", + "version": "2.9.12", "ecosystem": "PyPI" }, "licenses": [ @@ -3387,7 +2396,7 @@ { "package": { "name": "py-key-value-aio", - "version": "0.4.4", + "version": "0.4.5", "ecosystem": "PyPI" }, "licenses": [ @@ -3395,690 +2404,273 @@ ] }, { - "package": { - "name": "py-rust-stemmers", - "version": "0.1.5", - "ecosystem": "PyPI" - }, - "licenses": [ - "UNKNOWN" - ] - }, - { - "package": { - "name": "pyarrow", - "version": "22.0.0", - "ecosystem": "PyPI" - }, - "vulnerabilities": [ - { - "modified": "2026-06-05T21:56:07Z", - "published": "2026-02-17T14:16:01Z", - "schema_version": "1.7.5", - "id": "PYSEC-2026-113", - "aliases": [ - "CVE-2026-25087", - "GHSA-rgxp-2hwp-jwgg" - ], - "details": "Use After Free vulnerability in Apache Arrow C++.\n\nThis issue affects Apache Arrow C++ from 15.0.0 through 23.0.0. It can be triggered when reading an Arrow IPC file (but not an IPC stream) with pre-buffering enabled, if the IPC file contains data with variadic buffers (such as Binary View and String View data). Depending on the number of variadic buffers in a record batch column and on the temporal sequence of multi-threaded IO, a write to a dangling pointer could occur. The value (a `std::shared_ptr` object)\u00a0that is written to the dangling pointer is not under direct control of the attacker.\n\nPre-buffering is disabled by default but can be enabled using a specific C++ API call (`RecordBatchFileReader::PreBufferMetadata`). The functionality is not exposed in language bindings (Python, Ruby, C GLib), so these bindings are not vulnerable.\n\nThe most likely consequence of this issue would be random crashes or memory corruption when reading specific kinds of IPC files. If the application allows ingesting IPC files from untrusted sources, this could plausibly be exploited for denial of service. Inducing more targeted kinds of misbehavior (such as confidential data extraction from the running process) depends on memory allocation and multi-threaded IO temporal patterns that are unlikely to be easily controlled by an attacker.\n\nAdvice for users of Arrow C++:\n\n1. check whether you enable pre-buffering on the IPC file reader (using\u00a0`RecordBatchFileReader::PreBufferMetadata`)\n\n2. if so, either disable pre-buffering (which may have adverse performance consequences), or switch to Arrow 23.0.1 which is not vulnerable", - "severity": [ - { - "type": "CVSS_V3", - "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:H" - } - ], - "affected": [ - { - "package": { - "ecosystem": "PyPI", - "name": "pyarrow", - "purl": "pkg:pypi/pyarrow" - }, - "ranges": [ - { - "type": "ECOSYSTEM", - "events": [ - { - "introduced": "15.0.0" - }, - { - "fixed": "23.0.1" - } - ] - } - ], - "versions": [ - "15.0.0", - "15.0.1", - "15.0.2", - "16.0.0", - "16.1.0", - "17.0.0", - "18.0.0", - "18.1.0", - "19.0.0", - "19.0.1", - "20.0.0", - "21.0.0", - "22.0.0", - "23.0.0" - ], - "database_specific": { - "source": "https://github.com/pypa/advisory-database/blob/main/vulns/pyarrow/PYSEC-2026-113.yaml" - } - } - ], - "references": [ - { - "type": "ADVISORY", - "url": "http://www.openwall.com/lists/oss-security/2026/02/17/4" - }, - { - "type": "ADVISORY", - "url": "https://lists.apache.org/thread/mpm4ld1qony30tchfpjtk5b11tcyvmwh" - }, - { - "type": "FIX", - "url": "https://github.com/apache/arrow/pull/48925" - } - ] - }, - { - "modified": "2026-06-05T21:56:07Z", - "published": "2026-02-17T15:31:35Z", - "schema_version": "1.7.5", - "id": "GHSA-rgxp-2hwp-jwgg", - "aliases": [ - "CVE-2026-25087", - "PYSEC-2026-113" - ], - "summary": "Apache Arrow: Potential use-after-free when reading IPC file with pre-buffering", - "details": "Use After Free vulnerability in Apache Arrow C++.\n\nThis issue affects Apache Arrow C++ from 15.0.0 through 23.0.0. It can be triggered when reading an Arrow IPC file (but not an IPC stream) with pre-buffering enabled, if the IPC file contains data with variadic buffers (such as Binary View and String View data). Depending on the number of variadic buffers in a record batch column and on the temporal sequence of multi-threaded IO, a write to a dangling pointer could occur. The value (a `std::shared_ptr` object)\u00a0that is written to the dangling pointer is not under direct control of the attacker.\n\nPre-buffering is disabled by default but can be enabled using a specific C++ API call (`RecordBatchFileReader::PreBufferMetadata`). The functionality is not exposed in language bindings (Python, Ruby, C GLib), so these bindings are not vulnerable.\n\nThe most likely consequence of this issue would be random crashes or memory corruption when reading specific kinds of IPC files. If the application allows ingesting IPC files from untrusted sources, this could plausibly be exploited for denial of service. Inducing more targeted kinds of misbehavior (such as confidential data extraction from the running process) depends on memory allocation and multi-threaded IO temporal patterns that are unlikely to be easily controlled by an attacker.\n\nAdvice for users of Arrow C++:\n\n1. check whether you enable pre-buffering on the IPC file reader (using\u00a0`RecordBatchFileReader::PreBufferMetadata`)\n\n2. if so, either disable pre-buffering (which may have adverse performance consequences), or switch to Arrow 23.0.1 which is not vulnerable", - "severity": [ - { - "type": "CVSS_V3", - "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:H" - } - ], - "affected": [ - { - "package": { - "ecosystem": "PyPI", - "name": "pyarrow", - "purl": "pkg:pypi/pyarrow" - }, - "ranges": [ - { - "type": "ECOSYSTEM", - "events": [ - { - "introduced": "15.0.0" - }, - { - "fixed": "23.0.1" - } - ] - } - ], - "versions": [ - "15.0.0", - "15.0.1", - "15.0.2", - "16.0.0", - "16.1.0", - "17.0.0", - "18.0.0", - "18.1.0", - "19.0.0", - "19.0.1", - "20.0.0", - "21.0.0", - "22.0.0", - "23.0.0" - ], - "database_specific": { - "source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/02/GHSA-rgxp-2hwp-jwgg/GHSA-rgxp-2hwp-jwgg.json" - } - } - ], - "references": [ - { - "type": "ADVISORY", - "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-25087" - }, - { - "type": "WEB", - "url": "https://github.com/apache/arrow/pull/48925" - }, - { - "type": "PACKAGE", - "url": "https://github.com/apache/arrow" - }, - { - "type": "WEB", - "url": "https://github.com/pypa/advisory-database/tree/main/vulns/pyarrow/PYSEC-2026-113.yaml" - }, - { - "type": "WEB", - "url": "https://lists.apache.org/thread/mpm4ld1qony30tchfpjtk5b11tcyvmwh" - }, - { - "type": "WEB", - "url": "http://www.openwall.com/lists/oss-security/2026/02/17/4" - } - ], - "database_specific": { - "cwe_ids": [ - "CWE-416" - ], - "github_reviewed": true, - "github_reviewed_at": "2026-06-05T21:39:35Z", - "nvd_published_at": "2026-02-17T14:16:01Z", - "severity": "HIGH" - } - } - ], - "groups": [ - { - "ids": [ - "PYSEC-2026-113", - "GHSA-rgxp-2hwp-jwgg" - ], - "aliases": [ - "CVE-2026-25087", - "GHSA-rgxp-2hwp-jwgg", - "PYSEC-2026-113" - ], - "max_severity": "7.0" - } - ], - "licenses": [ - "non-standard" - ] - }, - { - "package": { - "name": "pycparser", - "version": "3.0", - "ecosystem": "PyPI" - }, - "licenses": [ - "BSD-3-Clause" - ] - }, - { - "package": { - "name": "pydantic", - "version": "2.12.5", - "ecosystem": "PyPI" - }, - "licenses": [ - "MIT" - ] - }, - { - "package": { - "name": "pydantic-core", - "version": "2.41.5", - "ecosystem": "PyPI" - }, - "licenses": [ - "MIT" - ] - }, - { - "package": { - "name": "pydantic-extra-types", - "version": "2.11.1", - "ecosystem": "PyPI" - }, - "licenses": [ - "MIT" - ] - }, - { - "package": { - "name": "pydantic-settings", - "version": "2.8.1", - "ecosystem": "PyPI" - }, - "licenses": [ - "MIT" - ] - }, - { - "package": { - "name": "pygments", - "version": "2.20.0", + "package": { + "name": "py-rust-stemmers", + "version": "0.1.8", "ecosystem": "PyPI" }, "licenses": [ - "BSD-2-Clause" + "UNKNOWN" ] }, { "package": { - "name": "pyjwt", - "version": "2.12.1", + "name": "pyarrow", + "version": "22.0.0", "ecosystem": "PyPI" }, "vulnerabilities": [ { - "modified": "2026-06-02T12:15:09Z", - "published": "2026-05-28T16:16:29Z", - "schema_version": "1.7.5", - "id": "PYSEC-2026-175", - "aliases": [ - "CVE-2026-48522", - "GHSA-993g-76c3-p5m4" - ], - "details": "PyJWT is a JSON Web Token implementation in Python. Prior to 2.13.0, PyJWKClient passes its uri argument directly to urllib.request.urlopen() which uses Python stdlib's default OpenerDirector registering HTTPHandler, HTTPSHandler, FTPHandler, FileHandler, and DataHandler. There is currently no documented option to restrict which schemes PyJWKClient will fetch. If an application's jku URL ingestion path accepts attacker-influenced URLs (e.g., from JWT header, configuration file, OAuth flow parameter), the attacker can cause PyJWKClient to read arbitrary local files via file:// (SSRF on local filesystem), cause PyJWKClient to attempt FTP / data-URI fetches (broader SSRF surface), or forge tokens that PyJWT verifies as valid. The library does not directly return non-HTTP(S) URI contents to the attacker; the chained \"plant a JWKS to forge tokens\" scenario described in the original report requires additional application-layer flaws (attacker write access to a filesystem path, untrusted jku derivation) that this fix does not address. This vulnerability is fixed in 2.13.0.", - "severity": [ - { - "type": "CVSS_V3", - "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N" - } - ], - "affected": [ - { - "package": { - "ecosystem": "PyPI", - "name": "pyjwt", - "purl": "pkg:pypi/pyjwt" - }, - "ranges": [ - { - "type": "ECOSYSTEM", - "events": [ - { - "introduced": "0" - }, - { - "fixed": "2.13.0" - } - ] - } - ], - "versions": [ - "0.1.1", - "0.1.2", - "0.1.3", - "0.1.4", - "0.1.5", - "0.1.6", - "0.1.7", - "0.1.8", - "0.1.9", - "0.2.0", - "0.2.1", - "0.2.3", - "0.3.0", - "0.3.1", - "0.3.2", - "0.4.0", - "0.4.1", - "0.4.2", - "0.4.3", - "1.0.0", - "1.0.1", - "1.1.0", - "1.3.0", - "1.4.0", - "1.4.1", - "1.4.2", - "1.5.0", - "1.5.1", - "1.5.2", - "1.5.3", - "1.6.0", - "1.6.1", - "1.6.3", - "1.6.4", - "1.7.0", - "1.7.1", - "2.0.0", - "2.0.0a1", - "2.0.0a2", - "2.0.1", - "2.1.0", - "2.10.0", - "2.10.1", - "2.11.0", - "2.12.0", - "2.12.1", - "2.2.0", - "2.3.0", - "2.4.0", - "2.5.0", - "2.6.0", - "2.7.0", - "2.8.0", - "2.9.0" - ], - "database_specific": { - "source": "https://github.com/pypa/advisory-database/blob/main/vulns/pyjwt/PYSEC-2026-175.yaml" - } - } - ], - "references": [ - { - "type": "EVIDENCE", - "url": "https://github.com/jpadilla/pyjwt/security/advisories/GHSA-993g-76c3-p5m4" - } - ] - }, - { - "modified": "2026-06-02T12:15:10Z", - "published": "2026-05-28T16:16:29Z", + "modified": "2026-06-05T21:56:07Z", + "published": "2026-02-17T14:16:01Z", "schema_version": "1.7.5", - "id": "PYSEC-2026-177", + "id": "PYSEC-2026-113", "aliases": [ - "CVE-2026-48524", - "GHSA-fhv5-28vv-h8m8" + "CVE-2026-25087", + "GHSA-rgxp-2hwp-jwgg" ], - "details": "PyJWT is a JSON Web Token implementation in Python. Prior to 2.13.0, PyJWKClient.get_signing_key() forces a fresh HTTP request to the JWKS endpoint for every JWT with an unknown kid value, with no rate limiting. Since kid comes from the unverified token header, an attacker can trigger unlimited outbound requests. The vulnerability surfaces only when a JWKS fetch fails; an attacker can attempt to provoke that with sustained unknown-kid traffic, but the outcome depends on upstream JWKS-endpoint behavior (rate limiting, transient errors) which is beyond the attacker's control. This vulnerability is fixed in 2.13.0.", + "details": "Use After Free vulnerability in Apache Arrow C++.\n\nThis issue affects Apache Arrow C++ from 15.0.0 through 23.0.0. It can be triggered when reading an Arrow IPC file (but not an IPC stream) with pre-buffering enabled, if the IPC file contains data with variadic buffers (such as Binary View and String View data). Depending on the number of variadic buffers in a record batch column and on the temporal sequence of multi-threaded IO, a write to a dangling pointer could occur. The value (a `std::shared_ptr` object)\u00a0that is written to the dangling pointer is not under direct control of the attacker.\n\nPre-buffering is disabled by default but can be enabled using a specific C++ API call (`RecordBatchFileReader::PreBufferMetadata`). The functionality is not exposed in language bindings (Python, Ruby, C GLib), so these bindings are not vulnerable.\n\nThe most likely consequence of this issue would be random crashes or memory corruption when reading specific kinds of IPC files. If the application allows ingesting IPC files from untrusted sources, this could plausibly be exploited for denial of service. Inducing more targeted kinds of misbehavior (such as confidential data extraction from the running process) depends on memory allocation and multi-threaded IO temporal patterns that are unlikely to be easily controlled by an attacker.\n\nAdvice for users of Arrow C++:\n\n1. check whether you enable pre-buffering on the IPC file reader (using\u00a0`RecordBatchFileReader::PreBufferMetadata`)\n\n2. if so, either disable pre-buffering (which may have adverse performance consequences), or switch to Arrow 23.0.1 which is not vulnerable", "severity": [ { "type": "CVSS_V3", - "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L" + "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:H" } ], "affected": [ { "package": { "ecosystem": "PyPI", - "name": "pyjwt", - "purl": "pkg:pypi/pyjwt" + "name": "pyarrow", + "purl": "pkg:pypi/pyarrow" }, "ranges": [ { "type": "ECOSYSTEM", "events": [ { - "introduced": "0" + "introduced": "15.0.0" }, { - "fixed": "2.13.0" + "fixed": "23.0.1" } ] } ], "versions": [ - "0.1.1", - "0.1.2", - "0.1.3", - "0.1.4", - "0.1.5", - "0.1.6", - "0.1.7", - "0.1.8", - "0.1.9", - "0.2.0", - "0.2.1", - "0.2.3", - "0.3.0", - "0.3.1", - "0.3.2", - "0.4.0", - "0.4.1", - "0.4.2", - "0.4.3", - "1.0.0", - "1.0.1", - "1.1.0", - "1.3.0", - "1.4.0", - "1.4.1", - "1.4.2", - "1.5.0", - "1.5.1", - "1.5.2", - "1.5.3", - "1.6.0", - "1.6.1", - "1.6.3", - "1.6.4", - "1.7.0", - "1.7.1", - "2.0.0", - "2.0.0a1", - "2.0.0a2", - "2.0.1", - "2.1.0", - "2.10.0", - "2.10.1", - "2.11.0", - "2.12.0", - "2.12.1", - "2.2.0", - "2.3.0", - "2.4.0", - "2.5.0", - "2.6.0", - "2.7.0", - "2.8.0", - "2.9.0" + "15.0.0", + "15.0.1", + "15.0.2", + "16.0.0", + "16.1.0", + "17.0.0", + "18.0.0", + "18.1.0", + "19.0.0", + "19.0.1", + "20.0.0", + "21.0.0", + "22.0.0", + "23.0.0" ], "database_specific": { - "source": "https://github.com/pypa/advisory-database/blob/main/vulns/pyjwt/PYSEC-2026-177.yaml" + "source": "https://github.com/pypa/advisory-database/blob/main/vulns/pyarrow/PYSEC-2026-113.yaml" } } ], "references": [ { "type": "ADVISORY", - "url": "https://github.com/jpadilla/pyjwt/security/advisories/GHSA-fhv5-28vv-h8m8" - } - ] - }, - { - "modified": "2026-06-02T12:15:10Z", - "published": "2026-05-28T16:16:29Z", - "schema_version": "1.7.5", - "id": "PYSEC-2026-178", - "aliases": [ - "CVE-2026-48525", - "GHSA-w7vc-732c-9m39" - ], - "details": "PyJWT is a JSON Web Token implementation in Python. From 2.8.0 to 2.12.1, when verifying detached JWS tokens using the unencoded-payload option (\"b64\": false, RFC 7797), PyJWT performs Base64URL decoding of the compact-serialization payload segment before enforcing the detached-payload rules. For b64=false, PyJWT later discards that decoded payload and replaces it with the caller-provided detached_payload. In practice, this turns the middle segment into an attacker-controlled \u201cwork amplifier\u201d: a remote client can supply an arbitrarily large Base64URL payload segment that forces CPU work + memory allocations even if the signature is invalid. This creates an unauthenticated DoS vector against any endpoint that verifies detached JWS using PyJWT. This vulnerability is fixed in 2.13.0.", - "severity": [ - { - "type": "CVSS_V3", - "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L" - } - ], - "affected": [ + "url": "http://www.openwall.com/lists/oss-security/2026/02/17/4" + }, { - "package": { - "ecosystem": "PyPI", - "name": "pyjwt", - "purl": "pkg:pypi/pyjwt" - }, - "ranges": [ - { - "type": "ECOSYSTEM", - "events": [ - { - "introduced": "2.8.0" - }, - { - "fixed": "2.13.0" - } - ] - } - ], - "versions": [ - "2.10.0", - "2.10.1", - "2.11.0", - "2.12.0", - "2.12.1", - "2.8.0", - "2.9.0" - ], - "database_specific": { - "source": "https://github.com/pypa/advisory-database/blob/main/vulns/pyjwt/PYSEC-2026-178.yaml" - } - } - ], - "references": [ + "type": "ADVISORY", + "url": "https://lists.apache.org/thread/mpm4ld1qony30tchfpjtk5b11tcyvmwh" + }, { - "type": "EVIDENCE", - "url": "https://github.com/jpadilla/pyjwt/security/advisories/GHSA-w7vc-732c-9m39" + "type": "FIX", + "url": "https://github.com/apache/arrow/pull/48925" } ] }, { - "modified": "2026-06-02T12:15:10Z", - "published": "2026-05-28T16:16:29Z", + "modified": "2026-06-05T21:56:07Z", + "published": "2026-02-17T15:31:35Z", "schema_version": "1.7.5", - "id": "PYSEC-2026-179", + "id": "GHSA-rgxp-2hwp-jwgg", "aliases": [ - "CVE-2026-48526", - "GHSA-xgmm-8j9v-c9wx" + "CVE-2026-25087", + "PYSEC-2026-113" ], - "details": "PyJWT is a JSON Web Token implementation in Python. Prior to 2.13.0, when the verifier is decoding JSON Web Tokens, while supporting both asymmetric and HMAC algorithms, the library does not validate use of JSON Web Keys in HMAC algorithm, allowing attacker to use the issuer public key as the secret key for HMAC algorithm. This vulnerability is fixed in 2.13.0.", + "summary": "Apache Arrow: Potential use-after-free when reading IPC file with pre-buffering", + "details": "Use After Free vulnerability in Apache Arrow C++.\n\nThis issue affects Apache Arrow C++ from 15.0.0 through 23.0.0. It can be triggered when reading an Arrow IPC file (but not an IPC stream) with pre-buffering enabled, if the IPC file contains data with variadic buffers (such as Binary View and String View data). Depending on the number of variadic buffers in a record batch column and on the temporal sequence of multi-threaded IO, a write to a dangling pointer could occur. The value (a `std::shared_ptr` object)\u00a0that is written to the dangling pointer is not under direct control of the attacker.\n\nPre-buffering is disabled by default but can be enabled using a specific C++ API call (`RecordBatchFileReader::PreBufferMetadata`). The functionality is not exposed in language bindings (Python, Ruby, C GLib), so these bindings are not vulnerable.\n\nThe most likely consequence of this issue would be random crashes or memory corruption when reading specific kinds of IPC files. If the application allows ingesting IPC files from untrusted sources, this could plausibly be exploited for denial of service. Inducing more targeted kinds of misbehavior (such as confidential data extraction from the running process) depends on memory allocation and multi-threaded IO temporal patterns that are unlikely to be easily controlled by an attacker.\n\nAdvice for users of Arrow C++:\n\n1. check whether you enable pre-buffering on the IPC file reader (using\u00a0`RecordBatchFileReader::PreBufferMetadata`)\n\n2. if so, either disable pre-buffering (which may have adverse performance consequences), or switch to Arrow 23.0.1 which is not vulnerable", "severity": [ { "type": "CVSS_V3", - "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N" + "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:H" } ], "affected": [ { "package": { "ecosystem": "PyPI", - "name": "pyjwt", - "purl": "pkg:pypi/pyjwt" + "name": "pyarrow", + "purl": "pkg:pypi/pyarrow" }, "ranges": [ { "type": "ECOSYSTEM", "events": [ { - "introduced": "0" + "introduced": "15.0.0" }, { - "fixed": "2.13.0" + "fixed": "23.0.1" } ] } ], "versions": [ - "0.1.1", - "0.1.2", - "0.1.3", - "0.1.4", - "0.1.5", - "0.1.6", - "0.1.7", - "0.1.8", - "0.1.9", - "0.2.0", - "0.2.1", - "0.2.3", - "0.3.0", - "0.3.1", - "0.3.2", - "0.4.0", - "0.4.1", - "0.4.2", - "0.4.3", - "1.0.0", - "1.0.1", - "1.1.0", - "1.3.0", - "1.4.0", - "1.4.1", - "1.4.2", - "1.5.0", - "1.5.1", - "1.5.2", - "1.5.3", - "1.6.0", - "1.6.1", - "1.6.3", - "1.6.4", - "1.7.0", - "1.7.1", - "2.0.0", - "2.0.0a1", - "2.0.0a2", - "2.0.1", - "2.1.0", - "2.10.0", - "2.10.1", - "2.11.0", - "2.12.0", - "2.12.1", - "2.2.0", - "2.3.0", - "2.4.0", - "2.5.0", - "2.6.0", - "2.7.0", - "2.8.0", - "2.9.0" + "15.0.0", + "15.0.1", + "15.0.2", + "16.0.0", + "16.1.0", + "17.0.0", + "18.0.0", + "18.1.0", + "19.0.0", + "19.0.1", + "20.0.0", + "21.0.0", + "22.0.0", + "23.0.0" ], "database_specific": { - "source": "https://github.com/pypa/advisory-database/blob/main/vulns/pyjwt/PYSEC-2026-179.yaml" + "source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/02/GHSA-rgxp-2hwp-jwgg/GHSA-rgxp-2hwp-jwgg.json" } } ], "references": [ { - "type": "EVIDENCE", - "url": "https://github.com/jpadilla/pyjwt/security/advisories/GHSA-xgmm-8j9v-c9wx" + "type": "ADVISORY", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-25087" + }, + { + "type": "WEB", + "url": "https://github.com/apache/arrow/pull/48925" + }, + { + "type": "PACKAGE", + "url": "https://github.com/apache/arrow" + }, + { + "type": "WEB", + "url": "https://github.com/pypa/advisory-database/tree/main/vulns/pyarrow/PYSEC-2026-113.yaml" + }, + { + "type": "WEB", + "url": "https://lists.apache.org/thread/mpm4ld1qony30tchfpjtk5b11tcyvmwh" + }, + { + "type": "WEB", + "url": "http://www.openwall.com/lists/oss-security/2026/02/17/4" } - ] + ], + "database_specific": { + "cwe_ids": [ + "CWE-416" + ], + "github_reviewed": true, + "github_reviewed_at": "2026-06-05T21:39:35Z", + "nvd_published_at": "2026-02-17T14:16:01Z", + "severity": "HIGH" + } } ], "groups": [ { "ids": [ - "PYSEC-2026-175" - ], - "aliases": [ - "CVE-2026-48522", - "GHSA-993g-76c3-p5m4", - "PYSEC-2026-175" - ], - "max_severity": "4.2" - }, - { - "ids": [ - "PYSEC-2026-177" - ], - "aliases": [ - "CVE-2026-48524", - "GHSA-fhv5-28vv-h8m8", - "PYSEC-2026-177" - ], - "max_severity": "3.7" - }, - { - "ids": [ - "PYSEC-2026-178" - ], - "aliases": [ - "CVE-2026-48525", - "GHSA-w7vc-732c-9m39", - "PYSEC-2026-178" - ], - "max_severity": "5.3" - }, - { - "ids": [ - "PYSEC-2026-179" + "PYSEC-2026-113", + "GHSA-rgxp-2hwp-jwgg" ], "aliases": [ - "CVE-2026-48526", - "GHSA-xgmm-8j9v-c9wx", - "PYSEC-2026-179" + "CVE-2026-25087", + "GHSA-rgxp-2hwp-jwgg", + "PYSEC-2026-113" ], - "max_severity": "7.4" + "max_severity": "7.0" } ], + "licenses": [ + "non-standard" + ] + }, + { + "package": { + "name": "pycparser", + "version": "3.0", + "ecosystem": "PyPI" + }, + "licenses": [ + "BSD-3-Clause" + ] + }, + { + "package": { + "name": "pydantic", + "version": "2.13.4", + "ecosystem": "PyPI" + }, + "licenses": [ + "MIT" + ] + }, + { + "package": { + "name": "pydantic-core", + "version": "2.46.4", + "ecosystem": "PyPI" + }, + "licenses": [ + "MIT" + ] + }, + { + "package": { + "name": "pydantic-extra-types", + "version": "2.11.1", + "ecosystem": "PyPI" + }, + "licenses": [ + "MIT" + ] + }, + { + "package": { + "name": "pydantic-settings", + "version": "2.14.1", + "ecosystem": "PyPI" + }, + "licenses": [ + "MIT" + ] + }, + { + "package": { + "name": "pygments", + "version": "2.20.0", + "ecosystem": "PyPI" + }, + "licenses": [ + "BSD-2-Clause" + ] + }, + { + "package": { + "name": "pyjwt", + "version": "2.13.0", + "ecosystem": "PyPI" + }, "licenses": [ "MIT" ] @@ -4186,7 +2778,7 @@ { "package": { "name": "pytz", - "version": "2026.1.post1", + "version": "2026.2", "ecosystem": "PyPI" }, "licenses": [ @@ -4347,7 +2939,7 @@ { "package": { "name": "referencing", - "version": "0.36.2", + "version": "0.37.0", "ecosystem": "PyPI" }, "licenses": [ @@ -4367,7 +2959,7 @@ { "package": { "name": "requests", - "version": "2.33.1", + "version": "2.34.2", "ecosystem": "PyPI" }, "licenses": [ @@ -4397,7 +2989,7 @@ { "package": { "name": "rich", - "version": "14.3.3", + "version": "14.3.4", "ecosystem": "PyPI" }, "licenses": [ @@ -4407,7 +2999,7 @@ { "package": { "name": "rich-argparse", - "version": "1.7.2", + "version": "1.8.0", "ecosystem": "PyPI" }, "licenses": [ @@ -4417,7 +3009,7 @@ { "package": { "name": "rich-rst", - "version": "1.3.2", + "version": "2.0.1", "ecosystem": "PyPI" }, "licenses": [ @@ -4427,7 +3019,7 @@ { "package": { "name": "rich-toolkit", - "version": "0.19.7", + "version": "0.20.1", "ecosystem": "PyPI" }, "licenses": [ @@ -4457,7 +3049,7 @@ { "package": { "name": "rpds-py", - "version": "0.30.0", + "version": "2026.5.1", "ecosystem": "PyPI" }, "licenses": [ @@ -4497,7 +3089,7 @@ { "package": { "name": "safetensors", - "version": "0.8.0rc1", + "version": "0.8.0", "ecosystem": "PyPI" }, "licenses": [ @@ -4537,7 +3129,7 @@ { "package": { "name": "sentry-sdk", - "version": "2.57.0", + "version": "2.62.0", "ecosystem": "PyPI" }, "licenses": [ @@ -4597,7 +3189,7 @@ { "package": { "name": "smart-open", - "version": "7.0.5", + "version": "7.6.1", "ecosystem": "PyPI" }, "licenses": [ @@ -4627,7 +3219,7 @@ { "package": { "name": "soupsieve", - "version": "2.8.3", + "version": "2.8.4", "ecosystem": "PyPI" }, "licenses": [ @@ -4637,7 +3229,7 @@ { "package": { "name": "sqlalchemy", - "version": "2.0.48", + "version": "2.0.50", "ecosystem": "PyPI" }, "licenses": [ @@ -4887,7 +3479,7 @@ { "package": { "name": "sqlmodel", - "version": "0.0.37", + "version": "0.0.38", "ecosystem": "PyPI" }, "licenses": [ @@ -4897,7 +3489,7 @@ { "package": { "name": "sse-starlette", - "version": "3.3.4", + "version": "3.4.4", "ecosystem": "PyPI" }, "licenses": [ @@ -5494,23 +4086,13 @@ { "package": { "name": "structlog", - "version": "25.5.0", + "version": "26.1.0", "ecosystem": "PyPI" }, "licenses": [ "Apache-2.0 OR MIT" ] }, - { - "package": { - "name": "sympy", - "version": "1.14.0", - "ecosystem": "PyPI" - }, - "licenses": [ - "non-standard" - ] - }, { "package": { "name": "tabulate", @@ -5544,7 +4126,7 @@ { "package": { "name": "tiktoken", - "version": "0.12.0", + "version": "0.13.0", "ecosystem": "PyPI" }, "licenses": [ @@ -5564,7 +4146,7 @@ { "package": { "name": "tomlkit", - "version": "0.14.0", + "version": "0.15.0", "ecosystem": "PyPI" }, "licenses": [ @@ -5574,7 +4156,7 @@ { "package": { "name": "tornado", - "version": "6.5.5", + "version": "6.5.7", "ecosystem": "PyPI" }, "licenses": [ @@ -5584,7 +4166,7 @@ { "package": { "name": "tqdm", - "version": "4.67.3", + "version": "4.68.2", "ecosystem": "PyPI" }, "licenses": [ @@ -5594,7 +4176,7 @@ { "package": { "name": "transformers", - "version": "5.5.0", + "version": "5.10.2", "ecosystem": "PyPI" }, "licenses": [ @@ -5604,7 +4186,7 @@ { "package": { "name": "typer", - "version": "0.24.1", + "version": "0.25.1", "ecosystem": "PyPI" }, "licenses": [ @@ -5624,7 +4206,7 @@ { "package": { "name": "types-aiobotocore", - "version": "3.3.0", + "version": "3.7.0", "ecosystem": "PyPI" }, "licenses": [ @@ -5644,7 +4226,7 @@ { "package": { "name": "types-awscrt", - "version": "0.31.3", + "version": "0.34.1", "ecosystem": "PyPI" }, "licenses": [ @@ -5694,7 +4276,7 @@ { "package": { "name": "tzdata", - "version": "2025.3", + "version": "2026.2", "ecosystem": "PyPI" }, "licenses": [ @@ -5714,7 +4296,7 @@ { "package": { "name": "uncalled-for", - "version": "0.2.0", + "version": "0.3.2", "ecosystem": "PyPI" }, "licenses": [ @@ -5734,7 +4316,7 @@ { "package": { "name": "uuid-utils", - "version": "0.14.1", + "version": "0.16.0", "ecosystem": "PyPI" }, "licenses": [ @@ -5744,7 +4326,7 @@ { "package": { "name": "uvicorn", - "version": "0.42.0", + "version": "0.49.0", "ecosystem": "PyPI" }, "licenses": [ @@ -5774,163 +4356,9 @@ { "package": { "name": "wasmtime", - "version": "43.0.0", + "version": "45.0.0", "ecosystem": "PyPI" }, - "vulnerabilities": [ - { - "modified": "2026-05-21T15:00:24Z", - "published": "2026-04-09T19:16:24Z", - "schema_version": "1.7.5", - "id": "PYSEC-2026-151", - "aliases": [ - "CVE-2026-34983", - "GHSA-hfr4-7c6c-48w2", - "RUSTSEC-2026-0090" - ], - "details": "Wasmtime is a runtime for WebAssembly. In 43.0.0, cloning a wasmtime::Linker is unsound and can result in use-after-free bugs. This bug is not controllable by guest Wasm programs. It can only be triggered by a specific sequence of embedder API calls made by the host. Specifically, the following steps must occur to trigger the bug clone a wasmtime::Linker, drop the original linker instance, use the new, cloned linker instance, resulting in a use-after-free. This vulnerability is fixed in 43.0.1.", - "severity": [ - { - "type": "CVSS_V3", - "score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H" - } - ], - "affected": [ - { - "package": { - "ecosystem": "PyPI", - "name": "wasmtime", - "purl": "pkg:pypi/wasmtime" - }, - "ranges": [ - { - "type": "ECOSYSTEM", - "events": [ - { - "introduced": "0" - }, - { - "last_affected": "43.0.0" - } - ] - } - ], - "versions": [ - "0.0.1", - "0.0.2", - "0.11.0", - "0.12.0", - "0.15.0", - "0.15.1", - "0.16.0", - "0.16.1", - "0.17.0", - "0.18.0", - "0.18.1", - "0.18.2", - "0.19.0", - "0.20.0", - "0.21.0", - "0.22.0", - "0.23.0", - "0.24.0", - "0.25.0", - "0.26.0", - "0.27.0", - "0.28.0", - "0.28.1", - "0.29.0", - "0.30.0", - "0.31.0", - "0.32.0", - "0.33.0", - "0.34.0", - "0.35.0", - "0.36.0", - "0.37.0", - "0.38.0", - "0.39.1", - "0.40.0", - "0.9.0", - "1.0.0", - "1.0.1", - "10.0.0", - "10.0.1", - "11.0.0", - "12.0.0", - "13.0.0", - "13.0.1", - "13.0.2", - "14.0.0", - "15.0.0", - "16.0.0", - "17.0.0", - "17.0.1", - "18.0.0", - "18.0.2", - "19.0.0", - "2.0.0", - "20.0.0", - "21.0.0", - "22.0.0", - "23.0.0", - "24.0.0", - "25.0.0", - "27.0.0", - "27.0.1", - "27.0.2", - "28.0.0", - "29.0.0", - "3.0.0", - "30.0.0", - "31.0.0", - "32.0.0", - "33.0.0", - "34.0.0", - "35.0.0", - "36.0.0", - "37.0.0", - "38.0.0", - "39.0.0", - "4.0.0", - "40.0.0", - "41.0.0", - "42.0.0", - "43.0.0", - "5.0.0", - "6.0.0", - "7.0.0", - "8.0.0", - "8.0.1", - "9.0.0" - ], - "database_specific": { - "source": "https://github.com/pypa/advisory-database/blob/main/vulns/wasmtime/PYSEC-2026-151.yaml" - } - } - ], - "references": [ - { - "type": "ADVISORY", - "url": "https://github.com/bytecodealliance/wasmtime/security/advisories/GHSA-hfr4-7c6c-48w2" - } - ] - } - ], - "groups": [ - { - "ids": [ - "PYSEC-2026-151" - ], - "aliases": [ - "CVE-2026-34983", - "GHSA-hfr4-7c6c-48w2", - "PYSEC-2026-151", - "RUSTSEC-2026-0090" - ], - "max_severity": "5.0" - } - ], "licenses": [ "Apache-2.0 WITH LLVM-exception" ] @@ -5948,7 +4376,7 @@ { "package": { "name": "watchfiles", - "version": "1.1.1", + "version": "1.2.0", "ecosystem": "PyPI" }, "licenses": [ @@ -5958,7 +4386,7 @@ { "package": { "name": "wcwidth", - "version": "0.6.0", + "version": "0.8.1", "ecosystem": "PyPI" }, "licenses": [ @@ -5978,7 +4406,7 @@ { "package": { "name": "websockets", - "version": "16.0", + "version": "15.0.1", "ecosystem": "PyPI" }, "licenses": [ @@ -6018,7 +4446,7 @@ { "package": { "name": "xxhash", - "version": "3.6.0", + "version": "3.7.0", "ecosystem": "PyPI" }, "licenses": [ @@ -6038,7 +4466,7 @@ { "package": { "name": "yarl", - "version": "1.23.0", + "version": "1.24.2", "ecosystem": "PyPI" }, "licenses": [ @@ -6048,7 +4476,7 @@ { "package": { "name": "zipp", - "version": "3.23.0", + "version": "4.1.0", "ecosystem": "PyPI" }, "licenses": [ @@ -6081,19 +4509,19 @@ }, { "name": "Apache-2.0", - "count": 81 + "count": 82 }, { "name": "non-standard", - "count": 43 + "count": 39 }, { "name": "BSD-3-Clause", - "count": 32 + "count": 33 }, { "name": "ISC", - "count": 5 + "count": 6 }, { "name": "Apache-2.0 OR MIT", @@ -6115,6 +4543,10 @@ "name": "UPL-1.0", "count": 2 }, + { + "name": "0BSD", + "count": 1 + }, { "name": "0BSD AND BSD-3-Clause AND CC0-1.0 AND MIT AND Zlib", "count": 1 @@ -6143,6 +4575,10 @@ "name": "Apache-2.0 WITH LLVM-exception", "count": 1 }, + { + "name": "LGPL-2.1-or-later", + "count": 1 + }, { "name": "MIT AND MPL-2.0", "count": 1 diff --git a/third_party/requirements-main.txt b/third_party/requirements-main.txt index 049452c2ac..80b85ad1c6 100644 --- a/third_party/requirements-main.txt +++ b/third_party/requirements-main.txt @@ -179,9 +179,9 @@ aiobotocore==2.25.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') --hash=sha256:ea9be739bfd7ece8864f072ec99bb9ed5c7e78ebb2b0b15f29781fbe02daedbc \ --hash=sha256:eb6daebe3cbef5b39a0bb2a97cffbe9c7cb46b2fcc399ad141f369f3c2134b1f # via aioboto3 -aiofile==3.9.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa \ - --hash=sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b +aiofile==3.11.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:1f91912c6643d2a4e49ca4ae3514f0bf3867ce948a36d99a6411b8f4755f4cf9 \ + --hash=sha256:ce77d14ac07f77bc2b757834a5c129321f3f705c474593deed5ab209079a52c9 # via py-key-value-aio aiofiles==25.1.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2 \ @@ -195,36 +195,64 @@ aiofiles==25.1.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # nmp-evaluator # nvidia-nat-core # streaming-form-data -aiohappyeyeballs==2.6.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ - --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 +aiohappyeyeballs==2.6.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4 \ + --hash=sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64 # via aiohttp -aiohttp==3.13.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9 \ - --hash=sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c \ - --hash=sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9 \ - --hash=sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc \ - --hash=sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665 \ - --hash=sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090 \ - --hash=sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49 \ - --hash=sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3 \ - --hash=sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6 \ - --hash=sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb \ - --hash=sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14 \ - --hash=sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1 \ - --hash=sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb \ - --hash=sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61 \ - --hash=sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4 \ - --hash=sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9 \ - --hash=sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2 \ - --hash=sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1 \ - --hash=sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c +aiohttp==3.14.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4 \ + --hash=sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a \ + --hash=sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee \ + --hash=sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09 \ + --hash=sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2 \ + --hash=sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264 \ + --hash=sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf \ + --hash=sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035 \ + --hash=sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6 \ + --hash=sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4 \ + --hash=sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c \ + --hash=sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621 \ + --hash=sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080 \ + --hash=sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397 \ + --hash=sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8 \ + --hash=sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345 \ + --hash=sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2 \ + --hash=sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95 \ + --hash=sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3 \ + --hash=sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6 \ + --hash=sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573 \ + --hash=sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af \ + --hash=sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe \ + --hash=sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876 \ + --hash=sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817 \ + --hash=sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd \ + --hash=sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f \ + --hash=sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca \ + --hash=sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa \ + --hash=sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2 \ + --hash=sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3 \ + --hash=sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730 \ + --hash=sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842 \ + --hash=sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96 \ + --hash=sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85 \ + --hash=sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199 \ + --hash=sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588 \ + --hash=sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480 \ + --hash=sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04 \ + --hash=sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8 \ + --hash=sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087 \ + --hash=sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296 \ + --hash=sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c \ + --hash=sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a \ + --hash=sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7 \ + --hash=sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451 # via # aiobotocore # aiohttp-retry # fsspec # garak-api # instructor + # kubernetes # langchain-community # langchain-nvidia-ai-endpoints # langchain-oci @@ -272,9 +300,9 @@ annotated-types==0.7.0 ; (platform_machine == 'arm64' and sys_platform == 'darwi annoy==1.17.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:9cbfebefe0a5f843eba29c6be4c84d601f4f41ad4ded0486f1b88c3b07739c15 # via nemoguardrails -anthropic==0.101.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:1116a6a87c55757e0fbe3e1ba40804fbd04de7963601a6dd6b539a889f18de3e \ - --hash=sha256:cc3cc6576989471e2aa9132258034ad0ff0d8fe500b04ac499e4e46ed68c5ed0 +anthropic==0.107.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:8e7169a6ab57fb806b778d9af018c867bad688144efec8969cdb4c5ccecd6670 \ + --hash=sha256:b74338d08000ba105dfc8adae29af3713ece845a4bffec9986a20697e087c7b3 # via # nemo-agents-plugin # nemo-platform-plugin @@ -332,12 +360,13 @@ attrs==26.1.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p # aiohttp # cyclopts # jsonschema + # jsonschema-path # referencing authlib==1.7.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231 \ --hash=sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f # via - # fastmcp + # fastmcp-slim # nvidia-nat-core backports-tarfile==1.2.0 ; (python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ @@ -356,9 +385,9 @@ beartype==0.22.9 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or --hash=sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f \ --hash=sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2 # via py-key-value-aio -beautifulsoup4==4.14.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb \ - --hash=sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86 +beautifulsoup4==4.15.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7 \ + --hash=sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9 # via wikipedia boto3==1.40.61 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:6b9c57b2a922b5d8c17766e29ed792586a818098efe84def27c8f582b33f898c \ @@ -377,15 +406,15 @@ botocore==1.40.61 ; (platform_machine == 'arm64' and sys_platform == 'darwin') o # nemo-agents-plugin # ngcsdk # s3transfer -botocore-stubs==1.42.41 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:9423110fb0e391834bd2ed44ae5f879d8cb370a444703d966d30842ce2bcb5f0 \ - --hash=sha256:dbeac2f744df6b814ce83ec3f3777b299a015cbea57a2efc41c33b8c38265825 +botocore-stubs==1.43.14 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:9e3bc1fdd51da7473f0df726c82747a1b0ae913449d629659765c247fecc2039 \ + --hash=sha256:fb98f1475c92fd718644e786b5c543a20f1b1f610e89e0a7191c3f1f429c75aa # via # types-aioboto3 # types-aiobotocore -cachetools==7.0.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990 \ - --hash=sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114 +cachetools==7.1.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54 \ + --hash=sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6 # via # py-key-value-aio # pymilvus @@ -405,9 +434,9 @@ caio==0.9.25 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (pl --hash=sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044 \ --hash=sha256:fc220b8533dcf0f238a6b1a4a937f92024c71e7b10b5a2dfc1c73604a25709bc # via aiofile -certifi==2026.2.25 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \ - --hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7 +certifi==2026.5.20 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \ + --hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d # via # clickhouse-connect # httpcore @@ -418,6 +447,7 @@ certifi==2026.2.25 ; (platform_machine == 'arm64' and sys_platform == 'darwin') # requests # sentry-sdk cffi==2.0.0 ; (platform_machine == 'arm64' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') \ + --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ @@ -425,15 +455,20 @@ cffi==2.0.0 ; (platform_machine == 'arm64' and platform_python_implementation != --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ + --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ + --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ + --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ - --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c + --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ + --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ + --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 # via cryptography chardet==5.2.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \ @@ -442,33 +477,58 @@ chardet==5.2.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or ( # data-designer-engine # diff-cover # sqlfluff -charset-normalizer==3.4.6 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5 \ - --hash=sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f \ - --hash=sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6 \ - --hash=sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab \ - --hash=sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f \ - --hash=sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843 \ - --hash=sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9 \ - --hash=sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd \ - --hash=sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9 \ - --hash=sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2 \ - --hash=sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e \ - --hash=sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69 \ - --hash=sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73 \ - --hash=sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923 \ - --hash=sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21 \ - --hash=sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2 \ - --hash=sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021 +charset-normalizer==3.4.7 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4 \ + --hash=sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c \ + --hash=sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5 \ + --hash=sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b \ + --hash=sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c \ + --hash=sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7 \ + --hash=sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb \ + --hash=sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1 \ + --hash=sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df \ + --hash=sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e \ + --hash=sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38 \ + --hash=sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18 \ + --hash=sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d \ + --hash=sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48 \ + --hash=sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5 \ + --hash=sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d \ + --hash=sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c \ + --hash=sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116 \ + --hash=sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2 \ + --hash=sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a \ + --hash=sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265 \ + --hash=sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15 \ + --hash=sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7 \ + --hash=sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8 \ + --hash=sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66 \ + --hash=sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d \ + --hash=sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5 \ + --hash=sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7 \ + --hash=sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49 \ + --hash=sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c \ + --hash=sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd \ + --hash=sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e \ + --hash=sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b \ + --hash=sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859 \ + --hash=sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46 \ + --hash=sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a \ + --hash=sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215 \ + --hash=sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063 \ + --hash=sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832 \ + --hash=sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6 \ + --hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464 # via requests circuitbreaker==2.1.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:1a4baee510f7bea3c91b194dcce7c07805fe96c4423ed5594b75af438531d084 \ --hash=sha256:87ba6a3ed03fdc7032bc175561c2b04d52ade9d5faf94ca2b035fbdc5e6b1dd1 # via oci -click==8.3.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a \ - --hash=sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 +click==8.4.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2 \ + --hash=sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96 # via + # huggingface-hub # litellm # nltk # nvidia-nat-core @@ -510,10 +570,38 @@ colorlog==6.10.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or --hash=sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c \ --hash=sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321 # via optuna +crc32c==2.7.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:03a92551a343702629af91f78d205801219692b6909f8fa126b830e332bfb0e0 \ + --hash=sha256:19e03a50545a3ef400bd41667d5525f71030488629c57d819e2dd45064f16192 \ + --hash=sha256:24949bffb06fc411cc18188d33357923cb935273642164d0bb37a5f375654169 \ + --hash=sha256:55a77e29a265418fa34bef15bd0f2c60afae5348988aaf35ed163b4bbf93cf37 \ + --hash=sha256:56ef661b34e9f25991fface7f9ad85e81bbc1b3fe3b916fd58c893eabe2fa0b8 \ + --hash=sha256:57a20dfc27995f568f64775eea2bbb58ae269f1a1144561df5e4a4955f79db32 \ + --hash=sha256:588587772e55624dd9c7a906ec9e8773ae0b6ac5e270fc0bc84ee2758eba90d5 \ + --hash=sha256:5c056ef043393085523e149276a7ce0cb534b872e04f3e20d74d9a94a75c0ad7 \ + --hash=sha256:724d5ff4d29ff093a983ae656be3307093706d850ea2a233bf29fcacc335d945 \ + --hash=sha256:80ebbf144a1a56a532b353e81fa0f3edca4f4baa1bf92b1dde2c663a32bb6a15 \ + --hash=sha256:88732070f6175530db04e0bb36880ac45c33d49f8ac43fa0e50cfb1830049d23 \ + --hash=sha256:96b794fd11945298fdd5eb1290a812efb497c14bc42592c5c992ca077458eeba \ + --hash=sha256:99d17637c4867672cb8adeea007294e3c3df9d43964369516cfe2c1f47ce500a \ + --hash=sha256:a1738259802978cdf428f74156175da6a5fdfb7256f647fdc0c9de1bc6cd7173 \ + --hash=sha256:afd778fc8ac0ed2ffbfb122a9aa6a0e409a8019b894a1799cda12c01534493e0 \ + --hash=sha256:b2416c4d88696ac322632555c0f81ab35e15f154bc96055da6cf110d642dbc10 \ + --hash=sha256:ba110df60c64c8e2d77a9425b982a520ccdb7abe42f06604f4d98a45bb1fff62 \ + --hash=sha256:c02a3bd67dea95cdb25844aaf44ca2e1b0c1fd70b287ad08c874a95ef4bb38db \ + --hash=sha256:c277f9d16a3283e064d54854af0976b72abaa89824955579b2b3f37444f89aae \ + --hash=sha256:d698eec444b18e296a104d0b9bb6c596c38bdcb79d24eba49604636e9d747305 \ + --hash=sha256:db9ac92294284b22521356715784b91cc9094eee42a5282ab281b872510d1831 \ + --hash=sha256:e07cf10ef852d219d179333fd706d1c415626f1f05e60bd75acf0143a4d8b225 \ + --hash=sha256:edefc0e46f3c37372183f70338e5bdee42f6789b62fcd36ec53aa933e9dfbeaf \ + --hash=sha256:f7d1c4e761fe42bf856130daf8b2658df33fe0ced3c43dadafdfeaa42b57b950 \ + --hash=sha256:f91b144a21eef834d64178e01982bb9179c354b3e9e5f4c803b0e5096384968c + # via oci cryptography==46.0.7 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65 \ --hash=sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832 \ --hash=sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067 \ + --hash=sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de \ --hash=sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0 \ --hash=sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968 \ --hash=sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef \ @@ -527,9 +615,14 @@ cryptography==46.0.7 ; (platform_machine == 'arm64' and sys_platform == 'darwin' --hash=sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7 \ --hash=sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83 \ --hash=sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85 \ + --hash=sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006 \ + --hash=sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb \ --hash=sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e \ --hash=sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba \ --hash=sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325 \ + --hash=sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1 \ + --hash=sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2 \ + --hash=sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0 \ --hash=sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455 \ --hash=sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15 \ --hash=sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5 \ @@ -547,11 +640,11 @@ cryptography==46.0.7 ; (platform_machine == 'arm64' and sys_platform == 'darwin' # pyjwt # pyopenssl # secretstorage -cyclopts==4.10.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:35f37257139380a386d9fe4475e1e7c87ca7795765ef4f31abba579fcfcb6ecd \ - --hash=sha256:ad4e4bb90576412d32276b14a76f55d43353753d16217f2c3cd5bdceba7f15a0 +cyclopts==4.17.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:6b3231f18b404879e978214ef26fa174e8b505bd0f2117290b4135560666004b \ + --hash=sha256:6ee947c9f3bbe9679b9fa9cea1bb327298db80b302df62d7f1d1bd82726508e0 # via - # fastmcp + # fastmcp-slim # nemo-anonymizer data-designer==0.6.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:257e58e1fb860c59c9d0cc83969c52f313f55f113f112301672382d04be78a05 \ @@ -578,29 +671,31 @@ dataclasses-json==0.6.7 ; (platform_machine == 'arm64' and sys_platform == 'darw # langchain-community # nemo-guardrails-plugin # nmp-guardrails -datasets==4.0.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:7ef95e62025fd122882dbce6cb904c8cd3fbc829de6669a5eb939c77d50e203d \ - --hash=sha256:9657e7140a9050db13443ba21cb5de185af8af944479b00e7ff1e00a61c8dbf1 +datasets==4.3.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:0ea157e72138b3ca6c7d2415f19a164ecf7d4c4fa72da2a570da286882e96903 \ + --hash=sha256:bc9118ed9afd92346c5be7ed3aaa00177eb907c25467f9d072a0d22777efbd2b # via # nemo-customizer-plugin # nmp-evaluator # ragas -diff-cover==10.2.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:59c328595e0b8948617cc5269af9e484c86462e2844bfcafa3fb37f8fca0af87 \ - --hash=sha256:61bf83025f10510c76ef6a5820680cf61b9b974e8f81de70c57ac926fa63872a +detect-installer==0.1.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:00ad7ba0a36e3cf7d08a40d3643011746dbc112597c7d475cc91c416710ca4e7 \ + --hash=sha256:034fb20fd665c36e6ba52b8821525ea07fb4f7f938cac459df889fb33801528a + # via fastapi-cloud-cli +diff-cover==10.3.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:2e47d5ab3868d1e92131c11f364f3f4a8583c97123d3bbc6b6cc8ce0a4cc2202 \ + --hash=sha256:474dbc63e815fbb7567d7b7ca5b104123e96129f25426ebdbc9a1bdbb935b2c6 # via sqlfluff -dill==0.3.8 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca \ - --hash=sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7 +dill==0.4.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0 \ + --hash=sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049 # via # datasets # multiprocess diskcache==5.6.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc \ --hash=sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19 - # via - # instructor - # ragas + # via ragas distro==1.9.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \ --hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2 @@ -621,31 +716,27 @@ docker==7.1.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p # ngcsdk # nmp-jobs # nmp-models -docstring-parser==0.17.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912 \ - --hash=sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708 +docstring-parser==0.18.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015 \ + --hash=sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b # via # anthropic # cyclopts # instructor -docutils==0.22.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968 \ - --hash=sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de - # via rich-rst -duckdb==1.5.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:26e56b5f0c96189e3288d83cf7b476e23615987902f801e5788dee15ee9f24a9 \ - --hash=sha256:40c5220ec93790b18ec6278da9c6ac2608d997ee6d6f7cd44c5c3992764e8e71 \ - --hash=sha256:482f8a13f2600f527e427f73c42b5aa75536f9892868068f0aaf573055a0135f \ - --hash=sha256:553c273a6a8f140adaa6da6a6135c7f95bdc8c2e5f95252fcdf9832d758e2141 \ - --hash=sha256:5d4147422d91ccdc2d2abf6ed24196025e020259d1d267970ae20c13c2ce84b1 \ - --hash=sha256:6af347debc8b721aa72e48671166282da979d5e5ae52dbc660ab417282b48e23 \ - --hash=sha256:6f7361d66cc801d9eb4df734b139cd7b0e3c257a16f3573ebd550ddb255549e6 \ - --hash=sha256:8150c569b2aa4573b51ba8475e814aa41fd53a3d510c1ffb96f1139f46faf611 \ - --hash=sha256:b370d1620a34a4538ef66524fcee9de8171fa263c701036a92bc0b4c1f2f9c6d \ - --hash=sha256:b8b0808dba0c63b7633bdaefb34e08fe0612622224f9feb0e7518904b1615101 \ - --hash=sha256:bc7ca6a1a40e7e4c933017e6c09ef18032add793df4e42624c6c0c87e0bebdad \ - --hash=sha256:da137802688190835b4c863cafa77fd7e29dff662ee6d905a9ffc14f00299c91 \ - --hash=sha256:ed6d23a3f806898e69c77430ebd8da0c79c219f97b9acbc9a29a653e09740c59 +duckdb==1.5.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:0ce80aed7a538422129a57eaca9141e3afb51f8bf562b1908b1576c9725b5b22 \ + --hash=sha256:10960400ed60cdf0fe05bab2086fa8eb733889cb0ceca18d07ff9a00c0e0be7b \ + --hash=sha256:3248b49cd835ea322574bc6aac0ae7a83be85547f49d4f5f5777cb380ee6627f \ + --hash=sha256:341a2672e2551ba51c95c1898f0ade983e76675e79038ccb16342c3d6cfb82d7 \ + --hash=sha256:3d5db8c0b55e072cf437948ebb5d7e23d7b9d03d905fa5f9145583e65aa447f7 \ + --hash=sha256:70a18f932cf6d87bd0e554613657a515c1443a1724aacfc7ec5137dd28698b03 \ + --hash=sha256:787df63824f07bf18022dbc3b8ca4b2bfab0ebe616464f55c6e8cd0f59ea762e \ + --hash=sha256:9fb7516255a8764545e30f7efacea408cc847764a3027b3b0b3e7d1a7bebbc5c \ + --hash=sha256:c5f18e7561403054433706c187589e86629a7af09a7efc23a06a8b308e6acc68 \ + --hash=sha256:df39428eb130faa35ae96fd35245bdeae6ecf43936250b116b5fead568eb9f16 \ + --hash=sha256:e75a6122c12579a99848517f6f00a4e342aebda3590c30fe9b5cc5f39d5e6afc \ + --hash=sha256:e80eb4d0fb59869cb2c7d7ef494c07fb92014fe8e77d96c170cd1ebc1488a708 \ + --hash=sha256:ff11a457258148337ef9a392148a8cdbd1069b6c27c21958816c7b67fe6c542d # via # data-designer-engine # data-designer-nemo @@ -669,7 +760,7 @@ exceptiongroup==1.3.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \ --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 # via - # fastmcp + # fastmcp-slim # pyleak expandvars==1.1.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:6c5822b7b756a99a356b915dd1267f52ab8a4efaa135963bd7f4bd5d368f71d7 \ @@ -712,43 +803,67 @@ fastapi-cli==0.0.24 ; (platform_machine == 'arm64' and sys_platform == 'darwin') --hash=sha256:1afc9c9e21d7ebc8a3ca5e31790cd8d837742be7e4f8b9236e99cb3451f0de00 \ --hash=sha256:4a1f78ed798f106b4fee85ca93b85d8fe33c0a3570f775964d37edb80b8f0edc # via fastapi -fastapi-cloud-cli==0.15.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:71a46f8a1d9fea295544113d6b79f620dc5768b24012887887306d151165745d \ - --hash=sha256:b1e8b3b26dc314e180fc0ab67dfd39d7d9fe160d3951081d09184eafaacf5649 +fastapi-cloud-cli==0.19.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:a2dfc4074c321e63ec88589cc1f90573d4b5bf980ddc44a7033e6f3cd8e96628 \ + --hash=sha256:f97b31c2ad6af3832eb4065870bdca3365b6e827a0ccf6eeb15e477bc1662b13 # via fastapi-cli -fastar==0.9.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:108bb46c080ca152bb331f1e0576177d36e9badba51b1d5724d2823542e0dd1f \ - --hash=sha256:17e2c3b46408193ea13c1e1177275ca7951e88bd3dce16baccb8de4f5e0dc2e8 \ - --hash=sha256:2394980cc126a3263e115600bc4ff9e7320cddde83c99fc334ab530be5b7166e \ - --hash=sha256:59bc500d7b6bdaf2ffb2b632bc6b0f97ddfb3bb7d31b54d61ceb00b5698d6484 \ - --hash=sha256:5a67b061b1099cf3b8b6234dd3605fa16f5078ab6b51c8d77ad7a5d11c3cf834 \ - --hash=sha256:5c03fad1ad9ac57cf03a4db9e18c7109c37416ff4eb9ebfca98fcd2b233a26c4 \ - --hash=sha256:76be31936cabce31cbb6381128f851cf0a6da2d5c25357615cd1504b26dc31cf \ - --hash=sha256:87006c8770dfc558aefe927590bbcdaf9648ca4472a9ee6d10dfb7c0bda4ce5b \ - --hash=sha256:9ec841a69fea73361c6df6d9183915c09e9ce3bd96493763fa46019e79918400 \ - --hash=sha256:acb62e2369834fb23d26327157f0a2dbec40b230c709fa85b1ce96cf010e6fbf \ - --hash=sha256:b665c33afcd1d581b82235b690d999c5446ccc2c4d80c4a95f30df3b43d22494 \ - --hash=sha256:c75e779f72d845037d4bf6692d01ac66f014eaef965c9231d41d5cc1276b89fc \ - --hash=sha256:c8ac3e8aaee57dfc822b04f570f0a963c2381a9dc8990fe0c6e965efd23fd451 \ - --hash=sha256:c9bd8879ebf05aa247e60e454bb7568cbdd44f016b8c58e31e5398039403e61d \ - --hash=sha256:d49114d5f0b76c5cc242875d90fa4706de45e0456ddedf416608ecd0787fb410 \ - --hash=sha256:d62a4fd86eda3bea7cc32efd64d43b6d0fcdbbec009558b750fc362f20142789 \ - --hash=sha256:d9ac410d32cbb514e966c45f0fedd0f9447b0dea9e734af714648da503603df6 \ - --hash=sha256:de264da9e8ef6407aa0b23c7c47ed4e34fde867e7c1f6e3cb98945a93e5f89f2 \ - --hash=sha256:ec7852de506d022ad36ad56f4aefb10c259dd59e485bf87af827954d404ba9d5 \ - --hash=sha256:f07c6bdeedfeb30ef459f21fa9ab06e2b6727f7e7653176d3abb7a85f447c400 \ - --hash=sha256:fad70e257daefb42bab68dcd68beaf2e2a99da056d65f2c9f988449a4e869306 +fastar==0.11.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:0324ed1d1ef0186e1bbd843b17807d6d837d0906899d4c99378b02c5d86bdd9c \ + --hash=sha256:03a112395a8b0bff251423bd1564c012f0cc058ad8b6bd8fba96f3d7fc117e44 \ + --hash=sha256:0d9d6b052baf5380baea866675dab6ccd04ec2460d12b1c46f10ce3f4ee6a820 \ + --hash=sha256:0fafb95ecbe70f666a5e9b35dd63974ccdc9bb3d99ccdbd4014a823ec3e659b5 \ + --hash=sha256:136cf342735464091c39dc3708168f9fdeb9ebea40b1ead937c61afaf46143d9 \ + --hash=sha256:27eed386fd0558e6daa29211111bbd7b740f7c7e881197f8a00ac7c0f3cdb1d7 \ + --hash=sha256:28095bb8f821e85fc2764e1a55f03e5e2876dee2abe7cd0ee9420d929905d643 \ + --hash=sha256:298a827ec04ade43733f6ca960d0faec38706aa1494175869ea7ea17f5bad5d3 \ + --hash=sha256:29c9c386dc0d5dda78845a8e6b1480d26ab861c1e0b68f42ae5735cb70ca07f1 \ + --hash=sha256:2e160919b1c47ddb8538e7e8eb4cd527281b40f0bf75110a75993838ef61f286 \ + --hash=sha256:38ef77fe940bbc9b37a98bd838727f844b11731cd39358a2640ff864fb385086 \ + --hash=sha256:483532442cdb08fbff0169510224eae0836f2f672cea6aacb52847d90fefdc46 \ + --hash=sha256:4bb4dc0fc8f7a6807febcebce8a2f3626ba4955a9263d81ecc630aad83be84c0 \ + --hash=sha256:59af8dbb683b24b90fb5b506de080faeab0a17a908e6c2a5d93a97260ed75d7b \ + --hash=sha256:5b83c1f61f7017d6e1498568038f8745440cfc16ca2f697ec81bac83050108f6 \ + --hash=sha256:625827d52eb4e8fec942e0233f125ff8010fcf6a67c0a974a8e5f4666b771e3c \ + --hash=sha256:6a1c56957ac82408be37a3f63594bc83e0919e8760492a4475e542f9f1828778 \ + --hash=sha256:7496def0a2befd82d429cb004ef7ca831585cc887947bd6b9abb68a5ef852b0b \ + --hash=sha256:74cd96163f39b8638ab4e8d49708ca887959672a22871d8170d01f067319533b \ + --hash=sha256:863b7929845c9fec92ef6c8d59579cf46af5136655e5342f8df5cebe46cab06c \ + --hash=sha256:878eaf15463eb572e3538af7ca3a8534e5e279cf8196db902d24e5725c4af86e \ + --hash=sha256:891f72ce42a5e28a74fbd4d5fbf1a3ac1a1163d13cbc200cbd005fb0fabc54bd \ + --hash=sha256:8955e61b32d6aff82c983217abf80933fd823b0e727586fc72f08043d996fd59 \ + --hash=sha256:8c15af91b8cd87ddf23ea55355ae513c1de3ab67178f26dad017c9e9c0af6096 \ + --hash=sha256:91c1c792447e4a642745f347ff9847c52af39633071c57ee67ed53c157fc3506 \ + --hash=sha256:96b4a57df12bf3211662627a3ea29d62ecb314a2434a0d0843f9fc23e47536e5 \ + --hash=sha256:9f3df73a3c4292cfe15696cdf59cdb6c309ab59d30b34c733be13c6e32d9a264 \ + --hash=sha256:a8c7bc8ac74cb359bb546b199288c83236372d094b402e557c197e85527495cd \ + --hash=sha256:aa3762cbb16e41a76b61f4a6914937a71aab3a7b6c2d82ca233bc686ebaf756b \ + --hash=sha256:aa7f100f7313c03fdb20f1385927ba95671071ba308ad0c1763fef295e1895ce \ + --hash=sha256:af48fed039b94016629dcdad1c95c90c486326dd068de2b0a4df419ee09b6821 \ + --hash=sha256:b8e545918441910a779659d4759ad0eef349e935fbdb4668a666d3681567eb05 \ + --hash=sha256:bdf9bd863205590beaf8ef6e66f315310196632180dceaf674985d01a876cac3 \ + --hash=sha256:c1e6e74aba1ae77ca4aedcaf1697cd413319f4c88a5ccbe5b42c709517c5097e \ + --hash=sha256:ceef1c2c4df7b7b8ebd3f5d718bbf457b9bbdf25ce0bd07870211ec4fbd9aff4 \ + --hash=sha256:d7f5fd8fa21ec0a88296a38dc5d7fc35efd3b26d46a17b8b7c73c5563925ca15 \ + --hash=sha256:db73a9b765a516e73983b25341e7b5e0189733878279e278b2295131b0e3a21e \ + --hash=sha256:dfe39d91fc28e37e06162d94afe01050220edb7df554acb5b702b5503e564816 \ + --hash=sha256:e45e598af5afe8412197d4786efd6cf29be02e7d3d4f6a3461149eae5d7e94f1 \ + --hash=sha256:ef5a6071121e05d8287fc75bccb054bcbac8bb0501200a0c0a8feeace5303ea4 \ + --hash=sha256:f2994bb8f5f8c11eb12beae1e6e77a907173c9819236b8a4c8f0573652ceccce # via fastapi-cloud-cli fastembed==0.8.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:40bee672657574a1009e35ec50030a55f2b426842cb011845379817641bbbbd0 \ --hash=sha256:75966edfa8b006ee78514c726bd7f6a50721dadc89305279052be9db72fd53e8 # via nemoguardrails -fastmcp==3.2.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:d4830b8ffc3592d3d9c76dc0f398904cf41f04910e41a0de38cc1004e0903bef \ - --hash=sha256:e71aba3df16f86f546a4a9e513261d3233bcc92bef0dfa647bac3fa33623f681 +fastmcp==3.4.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:29055fb6816f4862c615aabaf0112ae8feb8b469740db13403a0ce5b799ec1dc \ + --hash=sha256:34523083d6149400a0655a8aa769eb34f85b1ce6dac6d66efb07503ebbe5f44b # via # nmp-core-mcp # nmp-entities +fastmcp-slim==3.4.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:17cd0a1535972d3748d8c2416f0826dfc86c18df7a6cbc38602373277d44baa6 \ + --hash=sha256:faa0ccf16e85ec4b9f79c006fed3546b866d7e6dba3f60cd32cd98e84753a496 + # via fastmcp fastuuid==0.14.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:05a8dde1f395e0c9b4be515b7a521403d1e8349443e7641761af07c7ad1624b1 \ --hash=sha256:09378a05020e3e4883dfdab438926f31fea15fd17604908f3d39cbeb22a0b4dc \ @@ -770,9 +885,9 @@ fastuuid==0.14.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or --hash=sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022 \ --hash=sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070 # via litellm -filelock==3.25.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694 \ - --hash=sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70 +filelock==3.29.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:85199dfd706869641b72b2e8955d5416a4b2b7dc4b0e8e6d97b4cc1299a6983b \ + --hash=sha256:d97e6b1b9757569626c58caa07dc4beb1613f4a2938b1e8cc81afca398906c9e # via # datasets # huggingface-hub @@ -784,38 +899,62 @@ flatbuffers==25.12.19 ; (platform_machine == 'arm64' and sys_platform == 'darwin --hash=sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4 # via onnxruntime frozenlist==1.8.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0 \ + --hash=sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121 \ --hash=sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84 \ --hash=sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d \ --hash=sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967 \ + --hash=sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd \ --hash=sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed \ --hash=sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f \ + --hash=sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143 \ + --hash=sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e \ + --hash=sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8 \ --hash=sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad \ + --hash=sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82 \ --hash=sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29 \ --hash=sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383 \ --hash=sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52 \ + --hash=sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d \ + --hash=sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1 \ + --hash=sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714 \ --hash=sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65 \ --hash=sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506 \ --hash=sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41 \ + --hash=sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608 \ --hash=sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa \ --hash=sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1 \ + --hash=sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52 \ --hash=sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a \ + --hash=sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3 \ --hash=sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695 \ --hash=sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4 \ --hash=sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51 \ + --hash=sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8 \ --hash=sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b \ + --hash=sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b \ + --hash=sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d \ + --hash=sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567 \ + --hash=sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e \ + --hash=sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52 \ + --hash=sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1 \ --hash=sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94 \ --hash=sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822 \ --hash=sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11 \ + --hash=sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581 \ + --hash=sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51 \ --hash=sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40 \ + --hash=sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92 \ + --hash=sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5 \ --hash=sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4 \ --hash=sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93 \ --hash=sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027 # via # aiohttp # aiosignal -fsspec==2025.3.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:a935fd1ea872591f2b5148907d103488fc523295e6c64b835cfad8c3eca44972 \ - --hash=sha256:efb87af3efa9103f94ca91a7f8cb7a4df91af9f74fc106c9c7ea0efd7277c1b3 +fsspec==2025.9.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:19fd429483d25d28b65ec68f9f4adc16c17ea2c7c7bf54ec61360d478fb19c19 \ + --hash=sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7 # via # data-designer-engine # datasets @@ -832,55 +971,71 @@ gitpython==3.1.50 ; (platform_machine == 'arm64' and sys_platform == 'darwin') o --hash=sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc \ --hash=sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9 # via ragas -googleapis-common-protos==1.73.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:13114f0e9d2391756a0194c3a8131974ed7bffb06086569ba193364af59163b6 \ - --hash=sha256:e51f09eb0a43a8602f5a915870972e6b4a394088415c79d79605a46d8e826ee8 +googleapis-common-protos==1.75.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd \ + --hash=sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed # via # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http -greenlet==3.3.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b \ - --hash=sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f \ - --hash=sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2 \ - --hash=sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd \ - --hash=sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070 \ - --hash=sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99 \ - --hash=sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be \ - --hash=sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79 \ - --hash=sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a \ - --hash=sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395 \ - --hash=sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358 \ - --hash=sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4 \ - --hash=sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986 \ - --hash=sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd \ - --hash=sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab \ - --hash=sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86 +greenlet==3.5.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:017a544f0385d441e88714160d089d6900ef46c9eff9d99b6715a5ef2d127747 \ + --hash=sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1 \ + --hash=sha256:1072b4f9edcc1e192d9283a66a3e68d6b84c561de33a83d7858beb9ba1effe10 \ + --hash=sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a \ + --hash=sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b \ + --hash=sha256:5a56aeb7d5d9cc4b3a735efb5095bd4b4f6f0e4f93e5ca876d0e2315137b7829 \ + --hash=sha256:67821bb03e4e98664490edb787ff6af501194c29bbee0f5c1dfdcf1dc3d9d436 \ + --hash=sha256:6ebeb75c81211f5c702576cf81f315e77e23cfdb2c7c6fcb9dd143e6de35c360 \ + --hash=sha256:73f78f9b9f0a5c06e5c946ba1e8e36f5114923b6be109ee618c54f079c3ea14f \ + --hash=sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283 \ + --hash=sha256:89101bfd5011e069be974903cb3a4e4523845e4ece2d62dcd8d358933c0ef249 \ + --hash=sha256:8a271fcd66c74615cda6a964fda3f304267a12e50a084472218a39bb0376f563 \ + --hash=sha256:a0cbed8bb44e23c5b199f888f4e4ce096b45ad9f25ff74a7ad0213875e936bb2 \ + --hash=sha256:a203a8bd0acb0701653d3bbb26e404854a68674139ed5cbb778830f42b09bb33 \ + --hash=sha256:a5ea42a752d47a145eae922b605cd1634665ac3d5ec1e72402d5048e8d60d207 \ + --hash=sha256:add5217d68b31130f0beca584d7fef4878327d2e31642b66618a14eef312b63b \ + --hash=sha256:c5551170cf4f5ff5623e9af81323751979fee2c731e2287b61f73cd27257b823 \ + --hash=sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd \ + --hash=sha256:d0932b81d72f552ded9d810d00021b64d89f2195a91ce115b893f943b7a4ab3c \ + --hash=sha256:d40a890035c0058cadbdc4af7569800fd28a0e527a0fdbb7b5f9418f176846ce \ + --hash=sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135 \ + --hash=sha256:ded7b068c7c31c1a8657d4fd42d886b3e051ae29f88b80c5ff9d502257b0f071 \ + --hash=sha256:e6cd99ea59dd5d89f0c956606571d79bfe6f68c9eb7f4a4083a41a7f1587edee \ + --hash=sha256:fa4f98af3a528f0c3fd592a26df7f376f93329c8f4d987f6bb979057af8bf5e2 \ + --hash=sha256:ffea73584b216150eab159b6d12348fb253e68757974de1e2c40d8a318ac89ed # via # nmp-entities # sqlalchemy -grpcio==1.80.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1 \ - --hash=sha256:09e5e478b3d14afd23f12e49e8b44c8684ac3c5f08561c43a5b9691c54d136ab \ - --hash=sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257 \ - --hash=sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d \ - --hash=sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd \ - --hash=sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411 \ - --hash=sha256:5c07e82e822e1161354e32da2662f741a4944ea955f9f580ec8fb409dd6f6060 \ - --hash=sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140 \ - --hash=sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f \ - --hash=sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7 \ - --hash=sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0 \ - --hash=sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f \ - --hash=sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2 \ - --hash=sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6 \ - --hash=sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de \ - --hash=sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2 +griffelib==2.0.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e \ + --hash=sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1 + # via fastmcp-slim +grpcio==1.81.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:0fba53cb96004b2b7fb758b46b2288cb49d0b658316a4e73f3ef67230616ee65 \ + --hash=sha256:19f201da7b4e5c0559198abe5a97157e726f3abe6e8f5e832d4a50740f6dcc22 \ + --hash=sha256:275144b0115353339dbb8a6f28a9cf8997b5bf40e37f8f66ac0b0ea57e95b43f \ + --hash=sha256:43c121e135ae44d1559b430db2b2dfad7421cbbe40e1deba506c7dc62b439719 \ + --hash=sha256:57b3b0e73a518fa286959b40c3eddd02703504ca186e8b7b2945954519bd8b2c \ + --hash=sha256:62bbe463c9f0f2ff24e31bd25f8dd8b4bae78900e315915a3195a0ef1471a855 \ + --hash=sha256:638ccc1b86f7540170a169cb900799b9296a1381e47879ce60b0de9d3db73d33 \ + --hash=sha256:77eb4e9fe61486bd1198cc7236ebb0f70e66234e63c0348f40bc2553ed16a88b \ + --hash=sha256:794e6aa648e8df47d8f908dc8c3b42347d04ec58438f1dcd4e445f09b4f6b0ce \ + --hash=sha256:8bb1789c94322a13336a2b6c58d9c14d68f8628b6e24205a799c69f5bf8516ce \ + --hash=sha256:a524cd530900bd24511fcb7f2ed144da4ea37711c4b094475d0bceca7a93a170 \ + --hash=sha256:a5acd7efd3b1fe9b4eb0bcaaa1507eed68a0ad0678b654c3f7b464df9ba9dca5 \ + --hash=sha256:c36f5d5e97944cbda2d4096b4ae262e6e68506246b61582acf1b8591607f3ccc \ + --hash=sha256:c6ff087cb1f563f47b504b4e29e684129fc5ae4863faf3ebca08a327764ee6cb \ + --hash=sha256:cd78145b7f7784661c524624f3526c9c6f891b30a4b54cb93a40806d0d0d61e9 \ + --hash=sha256:dbdb99986548a7e87f8343805ef315fd4eb50ffaabf4fb1206e42f2542bb805d \ + --hash=sha256:e4d053900a0d24b75d7521139a3872150301b3d6bde3bed5e12318fb25791e4d \ + --hash=sha256:e7746ba3e6efc9e2b748eff59470a2b8684d5a9ec607c6580bcaa5be175820bc \ + --hash=sha256:f345de40ef2e65f63645d53d251824e6070e07804827c5b00ec2e44555f9f901 # via # opentelemetry-exporter-otlp-proto-grpc # pymilvus -gunicorn==25.3.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:cacea387dab08cd6776501621c295a904fe8e3b7aae9a1a3cbb26f4e7ed54660 \ - --hash=sha256:f74e1b2f9f76f6cd1ca01198968bd2dd65830edc24b6e8e4d78de8320e2fe889 +gunicorn==26.0.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc \ + --hash=sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf # via # nemo-safe-synthesizer-plugin # nmp-guardrails @@ -891,18 +1046,18 @@ h11==0.16.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (pla # httpcore # nemoplatform # uvicorn -hf-xet==1.4.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:21644b404bb0100fe3857892f752c4d09642586fd988e61501c95bbf44b393a3 \ - --hash=sha256:2815a49a7a59f3e2edf0cf113ae88e8cb2ca2a221bf353fb60c609584f4884d4 \ - --hash=sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f \ - --hash=sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac \ - --hash=sha256:60cf7fc43a99da0a853345cf86d23738c03983ee5249613a6305d3e57a5dca74 \ - --hash=sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba \ - --hash=sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113 \ - --hash=sha256:987f09cfe418237812896a6736b81b1af02a3a6dcb4b4944425c4c4fca7a7cf8 \ - --hash=sha256:bee693ada985e7045997f05f081d0e12c4c08bd7626dc397f8a7c487e6c04f7f \ - --hash=sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583 \ - --hash=sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08 +hf-xet==1.5.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:0c97106032ef70467b4f6bc2d0ccc266d7613ee076afc56516c502f87ce1c4a6 \ + --hash=sha256:51ef4500dab3764b41135ee1381a4b62ce56fc54d4c92b719b59e597d6df5bf6 \ + --hash=sha256:6208adb15d192b90e4c2ad2a27ed864359b2cb0f2494eb6d7c7f3699ac02e2bf \ + --hash=sha256:6f7a04a8ad962422e225bc49fbbac99dc1806764b1f3e54dbd154bffa7593947 \ + --hash=sha256:892e3a3a3aecc12aded8b93cf4f9cd059282c7de0732f7d55026f3abdf474350 \ + --hash=sha256:94e761bbd266bf4c03cee73753916062665ce8365aa40ed321f45afcb934b41e \ + --hash=sha256:97f212a88d14bbf573619a74b7fecb238de77d08fc702e54dec6f78276ca3283 \ + --hash=sha256:a93df2039190502835b1db8cd7e178b0b7b889fe9ab51299d5ced26e0dd879a4 \ + --hash=sha256:d48199c2bf4f8df0adc55d31d1368b6ec0e4d4f45bc86b08038089c23db0bed8 \ + --hash=sha256:e78e4e5192ad2b674c2e1160b651cb9134db974f8ae1835bdfbfb0166b894a43 \ + --hash=sha256:f61e3665892a6c8c5e765395838b8ddf36185da835253d4bc4509a81e49fb342 # via huggingface-hub httpcore==1.0.9 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ @@ -910,26 +1065,26 @@ httpcore==1.0.9 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # via # exa-py # httpx -httptools==0.7.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c \ - --hash=sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03 \ - --hash=sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df \ - --hash=sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5 \ - --hash=sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346 \ - --hash=sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650 \ - --hash=sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657 \ - --hash=sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca \ - --hash=sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66 \ - --hash=sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3 \ - --hash=sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2 \ - --hash=sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70 \ - --hash=sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9 \ - --hash=sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e \ - --hash=sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c \ - --hash=sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274 \ - --hash=sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5 \ - --hash=sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec \ - --hash=sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362 +httptools==0.8.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683 \ + --hash=sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124 \ + --hash=sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c \ + --hash=sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09 \ + --hash=sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085 \ + --hash=sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376 \ + --hash=sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5 \ + --hash=sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8 \ + --hash=sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681 \ + --hash=sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999 \ + --hash=sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d \ + --hash=sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d \ + --hash=sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d \ + --hash=sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745 \ + --hash=sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2 \ + --hash=sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d \ + --hash=sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7 \ + --hash=sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168 \ + --hash=sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a # via uvicorn httpx==0.28.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ @@ -937,10 +1092,11 @@ httpx==0.28.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p # via # anthropic # data-designer-engine + # datasets # exa-py # fastapi # fastapi-cloud-cli - # fastmcp + # fastmcp-slim # garak-api # httpx-retries # huggingface-hub @@ -966,9 +1122,9 @@ httpx==0.28.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p # oci-openai # openai # switchyard -httpx-retries==0.4.6 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:a076d8a5ede5d5794e9c241da17b15b393b482129ddd2fdf1fa56a3fa1f28a7f \ - --hash=sha256:d66d912173b844e065ffb109345a453b922f4c2cd9c9e11139304cb33e7a1ee1 +httpx-retries==0.5.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:d3124592979a9dc6197e666d1f02e9ab996a0c58fce59fad8db6201a6a87304e \ + --hash=sha256:d8c8e1e0852d84be3837aba0bcf78aeb89a4b77db95e8cc988c8c058830b3044 # via data-designer-engine httpx-sse==0.4.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \ @@ -976,9 +1132,9 @@ httpx-sse==0.4.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # via # langchain-community # mcp -huggingface-hub==1.15.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:28abfdddda3927fd4de6a63cf26ab012498a2c24dae52baf150c5c6edf98a1d5 \ - --hash=sha256:a4a59af04cbc41a3fe3fec429b171ef994ef8c971eda10136746f408dd4e3744 +huggingface-hub==1.18.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:729be4a976fb706dcc02d176bcda8a3f32bdf21a294e8f4b3dda6fbcbc9c1ab1 \ + --hash=sha256:f0c5ecd1ef8c6a60f86f61ee278f2c1570ba9e279c9f54de9094210723b3613b # via # data-designer-engine # datasets @@ -996,29 +1152,28 @@ hvac==2.4.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (pla # nemoplatform # nmp-common # nmp-jobs -idna==3.15 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ - --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc +idna==3.18 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via # anyio # email-validator # httpx # requests # yarl -importlib-metadata==8.5.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b \ - --hash=sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7 +importlib-metadata==8.9.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:58850626cef4bd2df100378b0f2aea9724a7b92f10770d547725b047078f99ee \ + --hash=sha256:e0f761b6ea91ced3b0844c14c9d955224d538105921f8e6754c00f6ca79fba7f # via # keyring # litellm - # opentelemetry-api iniconfig==2.3.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 # via pytest -instructor==1.11.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:6f58fea6fadfa228c411ecdedad4662230c456718f4a770a97a806dcb36b3287 \ - --hash=sha256:9ecd7a3780a045506165debad2ddcc4a30e1057f06997973185f356b0a42c6e3 +instructor==1.15.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:be81d17ba2b154a04ab4720808f24f9d6b598f80992f82eaf9cc79006099cf6c \ + --hash=sha256:c72406469d9025b742e83cf0c13e914b317db2089d08d889944e74fcd659ef94 # via ragas isodate==0.7.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15 \ @@ -1032,9 +1187,9 @@ jaraco-context==6.1.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin --hash=sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535 \ --hash=sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3 # via keyring -jaraco-functools==4.4.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176 \ - --hash=sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb +jaraco-functools==4.5.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:3bb5665ea4a020cf78a7040e89154c77edadb3ca74f366479669c5999aa70b03 \ + --hash=sha256:79ce39246eddbde4b3a03b77ea5f0f7878dc669b166a66cf3fa8e266aa3fa2f4 # via keyring jeepney==0.9.0 ; (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683 \ @@ -1056,25 +1211,41 @@ jinja2==3.1.6 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p # nemoplatform # nvidia-nat-core # sqlfluff -jiter==0.10.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500 \ - --hash=sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605 \ - --hash=sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070 \ - --hash=sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2 \ - --hash=sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca \ - --hash=sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041 \ - --hash=sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a \ - --hash=sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5 \ - --hash=sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d \ - --hash=sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6 \ - --hash=sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b \ - --hash=sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc \ - --hash=sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea \ - --hash=sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744 \ - --hash=sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5 \ - --hash=sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4 \ - --hash=sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a \ - --hash=sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e +jiter==0.13.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726 \ + --hash=sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5 \ + --hash=sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228 \ + --hash=sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018 \ + --hash=sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820 \ + --hash=sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2 \ + --hash=sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089 \ + --hash=sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434 \ + --hash=sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4 \ + --hash=sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d \ + --hash=sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0 \ + --hash=sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911 \ + --hash=sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19 \ + --hash=sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663 \ + --hash=sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6 \ + --hash=sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f \ + --hash=sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59 \ + --hash=sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef \ + --hash=sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68 \ + --hash=sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93 \ + --hash=sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152 \ + --hash=sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701 \ + --hash=sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3 \ + --hash=sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2 \ + --hash=sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2 \ + --hash=sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c \ + --hash=sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159 \ + --hash=sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165 \ + --hash=sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4 \ + --hash=sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a \ + --hash=sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb \ + --hash=sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505 \ + --hash=sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10 \ + --hash=sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f # via # anthropic # instructor @@ -1090,10 +1261,12 @@ joblib==1.5.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p --hash=sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713 \ --hash=sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3 # via nltk -joserfc==1.6.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:1482a7db78fb4602e44ed89e51b599d052e091288c7c532c5b694e20149dec48 \ - --hash=sha256:e9878a0f8243fe7b95e11fdda81374ca9f7a689e302751579d3dfdeec559675e - # via authlib +joserfc==1.7.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:77d0b76514879c68c6f433bc5b7357a4ab72008ff1e33d8379fd11d72bd8ca81 \ + --hash=sha256:b3e3d655612e2e1ef67b2600f2f420e12e537b020208fab1761fad647319c164 + # via + # authlib + # fastmcp-slim json-repair==0.58.7 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:2ea5c841cc1d91dbbe79e9ed4b2b77a5cc04d20892d62a15261b70ce999ec758 \ --hash=sha256:af0c7ff6a2ddd80bb1e7e6121c1a86d8bb57a539aa211acbfb25c64087b7116f @@ -1111,23 +1284,39 @@ jsonpath-ng==1.8.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') # nvidia-nat-core jsonpath-rust-bindings==1.1.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:0017af7054fb6bce55863a7065ae465a9c47fd93fb94f002ca98bb8adf15101a \ + --hash=sha256:02373d581a093d0640e60858884d67ec93259e7b6d6bd8e5874400ad99558e00 \ --hash=sha256:0ca169ac219bc141775fb19df8165d4d0162e6ed77102e1ab19a74a80c1f9051 \ --hash=sha256:13446ad021abe05d622a01eaa648c238ef3b98e9fc0bd837a589bafb246ca3bc \ + --hash=sha256:146b69ce20cb9869e05a6d369f4a10b52f98e1f8575f1ac5b49e285fa2032380 \ + --hash=sha256:1ff4cd052f733d5f270329c552a04e08a1520053355d35f0be886714dff46955 \ --hash=sha256:26955685acf0208b6061419cab4bd79fe869ebce57f3cec1e9b20f0e0af56b35 \ + --hash=sha256:330f457556d06abc1ea36b6738eb172288afff6bd251350eaba42bed2f459fd3 \ + --hash=sha256:366cba544c080c08530cef0cc19922f0380f0caab6e7e5a0ddfb70de288d5abc \ + --hash=sha256:36a40ed04d2db70897cde2ac92f6c9aae2ed1b426aa4c97a47f3e2be911ea4ba \ --hash=sha256:3c220c2d27ab6a0791e3af10e2a7c53ccd1dc2dfc8681999fed4458392aa0372 \ --hash=sha256:40c23781d28a8b126c8a2b337e4fe275cc8f35a149bda769e3ec2760dfb58b91 \ --hash=sha256:44de7464ad227028c36e8d713653b4bfe5eb7524ac1a4b0a71e8bcb3bd4f4f3a \ + --hash=sha256:4eacb98f80fff7d43956503ca7b42e491f7084c7b9bd8b5b6bad3f50d08480df \ + --hash=sha256:50f16c3dd6eb572dda74731508d2fca1abbb927ab4f6511fb65eeba6e59fd041 \ + --hash=sha256:6716caa0855dbf9d021509a3caa00a9fa7cc241930f40830c24e85d0e17a6246 \ --hash=sha256:734eee89754c829a0fb55a30467c8a33081976375b763c907f71f7018682c26c \ + --hash=sha256:7bf30e27a81d07c79cc58c86600687e5adfe0f7b1aaf8069a737085bebfaea71 \ --hash=sha256:7f2a526c87a245f708dc1d8d4988c471384c369a5909b8b730e63b6a7f0c2d60 \ --hash=sha256:8c390c33582cd268d35b86eb0f550229e0cf26f03bb06c470db4712d6fa4dc0f \ + --hash=sha256:9212d3746a57015fc3722488f61c4afc465d993f68371d864be8fa5b0c58d635 \ --hash=sha256:9d656507b5913f9515ff136797c5850df907c5040fa1368baa428f7e829e33f0 \ --hash=sha256:a239166bd1418897de327c952a9d9ff912d1fabc9da82e688204ccfcd7b22584 \ --hash=sha256:a43107f6efc4e66ee046c338741429a268fd972e887721b01bf0f32e47387e30 \ + --hash=sha256:aa7e9d25b00c227c51e7a916a13fbf22cf483df622699dbc3ef051861ec1de85 \ --hash=sha256:b06b24668085b2791acbfefdfe2f2824d36be539c7647c00aee33242b4d3385d \ + --hash=sha256:b9583e965fe5f8f21cd0d047244db9716a119e0e82a06f2336e6b14c9a9637af \ --hash=sha256:ce1c6804706012c3c7a194903ef20befafa3cc913a4ef553696bc837ac738a66 \ --hash=sha256:ce7039a2f497674785a423076e803a1fa547c2f9cf568b25e2ac83ff5890b98f \ --hash=sha256:d21101114514d34b21ab216eef1d7bb41155311fa61284e8f2dbdb93bde41c78 \ + --hash=sha256:dc0c3488f04dbd318fa876fb880e8cb7d1e53abcf8b0d9e697e10a0a15ac3158 \ --hash=sha256:ddbf025592bf88fc5395d9d023d7bcc8fab977898c406e0a5722925c3b887c71 \ + --hash=sha256:e423363b47080830bbb4d8257c0f26bda8ee655a18c4f934952bfe4c46e8d510 \ + --hash=sha256:ebb9a05a2b80195ac47aec0ce98d861c102459d16225fefb0f7e0158196c4a58 \ --hash=sha256:f55ee1e7fdb6bb2363c40a6d6ce0285e53bd52b4ecae7bef3909eeb11a9b4cd2 \ --hash=sha256:fbfeb05c7a6854104e97a0e3234f312004b3f4e678d14b68180a6a4f33f4d7c3 \ --hash=sha256:fe44737c6c72079ef30c85f975c19fa0114c13039fe538d8c5b259007a35a0ff @@ -1139,10 +1328,10 @@ jsonpointer==3.1.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') jsonref==1.1.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552 \ --hash=sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9 - # via fastmcp -jsonschema==4.23.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4 \ - --hash=sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566 + # via fastmcp-slim +jsonschema==4.26.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ + --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce # via # data-designer-engine # litellm @@ -1150,10 +1339,10 @@ jsonschema==4.23.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') # nemo-evaluator-sdk # nemo-safe-synthesizer # nmp-automodel -jsonschema-path==0.3.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001 \ - --hash=sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8 - # via fastmcp +jsonschema-path==0.5.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:2790a070bc7abb08ea3dbe4d340ece4efadf639223001f020c7503229ba068e2 \ + --hash=sha256:493b156ba895c97602655b620a8456caa2ce08c1aa389f5a7addec065e6e855c + # via fastmcp-slim jsonschema-specifications==2025.9.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d @@ -1162,17 +1351,17 @@ keyring==25.7.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or --hash=sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f \ --hash=sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b # via py-key-value-aio -kubernetes==35.0.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:39e2b33b46e5834ef6c3985ebfe2047ab39135d41de51ce7641a7ca5b372a13d \ - --hash=sha256:3d00d344944239821458b9efd484d6df9f011da367ecb155dadf9513f05f09ee +kubernetes==36.0.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:03551fcb49cae1f708f63624041e37403545b7aaed10cbf54e2b01a37a5438e3 \ + --hash=sha256:faf9b5241b58de0c4a5069f2a0ffc8ac06fece7215156cd3d3ba081a78a858b6 # via # nmp-common # nmp-evaluator # nmp-jobs # nmp-models -langchain==1.2.14 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:96da6d7338d5a6fc41eb4ec0db83f7ef5d03bb5efd17bb269f34ba4378ebdb4d \ - --hash=sha256:fc5511e8f8af7efee9e5a144da4392d700d627b301d240470db97272940ad317 +langchain==1.3.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:d6e0654c22848925534f5c0a706f9be481bb09a619ec60a738fbd1e5502e457a \ + --hash=sha256:e51b05ab23d056bc6bf2d97d8c694fb92d6d5765126fef74565d007c27581672 # via # langchain-community # langchain-oci @@ -1191,17 +1380,17 @@ langchain-classic==1.0.7 ; (platform_machine == 'arm64' and sys_platform == 'dar --hash=sha256:d9d9be38f7aa534ed0259c2410432e34a1f80b1d491e686749bb55af56479be3 \ --hash=sha256:debbec8065e69b95108d2652e8d5c44f4516e19aa8d716c02ed2211c3aee099d # via nvidia-nat-langchain -langchain-community==0.3.27 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:581f97b795f9633da738ea95da9cb78f8879b538090c9b7a68c0aed49c828f0d \ - --hash=sha256:e1037c3b9da0c6d10bf06e838b034eb741e016515c79ef8f3f16e53ead33d882 +langchain-community==0.3.31 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:1c727e3ebbacd4d891b07bd440647668001cea3e39cbe732499ad655ec5cb569 \ + --hash=sha256:250e4c1041539130f6d6ac6f9386cb018354eafccd917b01a4cff1950b80fd81 # via # nemoguardrails # nmp-evaluator # nvidia-nat-langchain # ragas -langchain-core==1.3.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:18aae8506f37da7f74398492279a7d6efcee4f8e23c4c41c7af080eeb7ef7bd1 \ - --hash=sha256:fa510a5db8efdc0c6ff41c0939fb5c00a0183c11f6b84233e892e3227ff69182 +langchain-core==1.4.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:242abe763db71de05fe0d7ecff03f9cc6022fbceba8be15902fb89e35b7292f9 \ + --hash=sha256:a2906d339514e02a46d6c0888021dd2651ed5acc661a1f546fe33e1453adfcb9 # via # langchain # langchain-aws @@ -1219,6 +1408,7 @@ langchain-core==1.3.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin # langgraph # langgraph-checkpoint # langgraph-prebuilt + # langgraph-sdk # nemoguardrails # nvidia-nat-langchain # ragas @@ -1230,28 +1420,28 @@ langchain-huggingface==1.2.2 ; (platform_machine == 'arm64' and sys_platform == --hash=sha256:1dd91ec415190d2704e93ec149618e3145075863ba37e74afc9080d685dc2743 \ --hash=sha256:f94944b0c0d5afc687568d426c87ed5236907464c41e72108ed76eee1a690f6d # via nvidia-nat-langchain -langchain-litellm==0.6.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:30741fda59803336d0d39788be441f6ccd2b4e41d7747ff0d2b002950a07453b \ - --hash=sha256:dce2ebfddddd0dfd6b1ed473399ccc095dd2f5cb6adfe1336d7bbe489ef32b4b +langchain-litellm==0.6.6 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:d49d0353254e10e38c351d7eae3d7b34128a0d31a3a22928ac289a8987c21a30 \ + --hash=sha256:fb4399ae4c239b5bb85c19574a5bb4c17988433d48ec716e62144f0dad4a63af # via nvidia-nat-langchain langchain-milvus==0.3.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:406c2d88da133741f5cc3e2fea4b36386182b35500205c70d003382ded210e41 \ --hash=sha256:6e12f15453372dd48836978faa4a149de79c721df3322229ad732a5e628e8e97 # via nvidia-nat-langchain -langchain-nvidia-ai-endpoints==1.3.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:5223aa7988ee5044f38715ae757faa0af4ba64f2ed0c82851a99c052592eaa09 \ - --hash=sha256:cc2b356e96e86ffb92dcfe83980aa73227e1fad8f3a4cbdd76cdcf980c42e7cc +langchain-nvidia-ai-endpoints==1.4.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:3edd1678a3e2c55789128e53ba32aab3dffe94cb201c70e6cea521fab7c261ff \ + --hash=sha256:8835f7e56d559b370b87164f937c1eb048ab837f25de91598f00555a705c2d16 # via # nmp-evaluator # nmp-guardrails # nvidia-nat-langchain -langchain-oci==0.2.6 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:3451385da788926d5cffd19de8afb912e15bdb28fb76f3844d3d88a5683142b0 \ - --hash=sha256:92538d3ee45e3323290fcc672e3f6618b13878b464abd8692ade9b7441b5863b +langchain-oci==0.2.7 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:1fb7ef305008ebbb1fb53e32af4823daa754d256947b3462b2f85e147638dc55 \ + --hash=sha256:cf793058d3b76334b57e1c80203bb2ba42941a41ce236b5825d5dbc4fe188151 # via nvidia-nat-langchain -langchain-openai==1.2.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:a80732185030d4f453dda6c25feef46f645f665423fdffe38ae3edf1ac3c6c4d \ - --hash=sha256:ee4480b787706361b7125fad46930589a624df87aa158c6986ef1fad10d10675 +langchain-openai==1.2.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:7da39a3c70cbafa93853456199e39a264dc70651be79b12ac49b4f6a448bce2d \ + --hash=sha256:8698ffcee9a086e91ab6d207f0026181a03effcbf86bf9aee1808ee35af69dcc # via # langchain-oci # nemoguardrails @@ -1259,10 +1449,12 @@ langchain-openai==1.2.1 ; (platform_machine == 'arm64' and sys_platform == 'darw # nvidia-nat-langchain # openevals # ragas -langchain-protocol==0.0.15 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:461eb794358f83d5e42635a5797799ffec7b4702314e34edf73ac21e75d3ef79 \ - --hash=sha256:9ab2d11ee73944754f10e037e717098d3a6796f0e58afa9cadda6154e7655ade - # via langchain-core +langchain-protocol==0.0.16 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:3658c142c5d0fb3a023a4be442ce4c15c6d626aab6135eb79a76dc64ad19c3c3 \ + --hash=sha256:806c7cdd951b1c4f692fa40fce60821ff0f221d4360e27673ddf2c2b99c2b7ff + # via + # langchain-core + # langgraph-sdk langchain-tavily==0.2.18 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:cd7859ae1a6ce79236580ef67072ff5fc43c7ded94e7eac38ff04209ca85a320 \ --hash=sha256:dccf3ad1c50e2cb2a89bec11727555805c9df8abd42c1f3ad42ccad86e28aa44 @@ -1271,26 +1463,26 @@ langchain-text-splitters==1.1.2 ; (platform_machine == 'arm64' and sys_platform --hash=sha256:782a723db0a4746ac91e251c7c1d57fd23636e4f38ed733074e28d7a86f41627 \ --hash=sha256:a2de0d799ff31886429fd6e2e0032df275b60ec817c19059a7b46181cc1c2f10 # via langchain-classic -langgraph==1.1.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:77ebe7ed44a2699f13696bf41f1dabe7b5fa8e6ad51e3597f2f175492e8f3656 \ - --hash=sha256:c951a859f68a021c69a27500db4eafc1900fc7ac32a54f7fc31d277165d04bed +langgraph==1.2.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:5df076973a2d23efb13eceb279d1e5b46feebcbbeded0a86a2ef669abd9e4399 \ + --hash=sha256:ffe3e1e31dce28907640f82525858470f293506d2b272d07ea3b3ce97974b067 # via # langchain # langchain-oci # nvidia-nat-langchain -langgraph-checkpoint==4.0.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:b433123735df11ade28829e40ce25b9be614930cd50245ff2af60629234befd9 \ - --hash=sha256:e3adcd7a0e0166f3b48b8cf508ce0ea366e7420b5a73aa81289888727769b034 +langgraph-checkpoint==4.1.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e \ + --hash=sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25 # via # langgraph # langgraph-prebuilt -langgraph-prebuilt==1.0.8 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:0cd3cf5473ced8a6cd687cc5294e08d3de57529d8dd14fdc6ae4899549efcf69 \ - --hash=sha256:d16a731e591ba4470f3e313a319c7eee7dbc40895bcf15c821f985a3522a7ce0 +langgraph-prebuilt==1.1.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528 \ + --hash=sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9 # via langgraph -langgraph-sdk==0.3.12 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:44323804965d6ec2a07127b3cf08a0428ea6deaeb172c2d478d5cd25540e3327 \ - --hash=sha256:c9c9ec22b3c0fcd352e2b8f32a815164f69446b8648ca22606329f4ff4c59a71 +langgraph-sdk==0.4.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:75fa5096c1177ce39c847096a8fe3745ffd480ddb412995f836e9f5f884c43dd \ + --hash=sha256:b88f0f5f6328ac0680d6790614a905b2bcfa257f2276dba4e38f0e86db0aa738 # via langgraph langsmith==0.8.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:4ff80d7dc1b273315401b681aef9b1fc92f4fa8a6d9d49eb65535520f8264fd4 \ @@ -1308,41 +1500,57 @@ lark==1.3.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (pla # nemoguardrails # nmp-common # nmp-entities -litellm==1.83.14 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:24aef9b47cdc424c833e32f3727f411741c690832cd1fe4405e0077144fe09c9 \ - --hash=sha256:92b11ba2a32cf80707ddf388d18526696c7999a21b418c5e3b6eda1243d2cfdb +litellm==1.88.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:369b84e57d9426582ddc35e731956ddb6618cda97cc44e4e4d2dfa75982a6e3a \ + --hash=sha256:89c6b74cc7912d6365793006ff951c0450fe847625008dfe49de8a7dc4529aa5 # via langchain-litellm loguru==0.7.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6 \ --hash=sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c # via fastembed -lxml==6.1.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:07f98f5496f96bf724b1e3c933c107f0cbf2745db18c03d2e13a291c3afd2635 \ - --hash=sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d \ - --hash=sha256:363e47283bde87051b821826e71dde47f107e08614e1aa312ba0c5711e77738c \ - --hash=sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491 \ - --hash=sha256:3ae5d8d5427f3cc317e7950f2da7ad276df0cfa37b8de2f5658959e618ea8512 \ - --hash=sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2 \ - --hash=sha256:4642e04449a1e164b5ff71ffd901ddb772dfabf5c9adf1b7be5dffe1212bc037 \ - --hash=sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105 \ - --hash=sha256:5cfa1a34df366d9dc0d5eaf420f4cf2bb1e1bebe1066d1c2fc28c179f8a4004c \ - --hash=sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d \ - --hash=sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33 \ - --hash=sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a \ - --hash=sha256:942454ff253da14218f972b23dc72fa4edf6c943f37edd19cd697618b626fac5 \ - --hash=sha256:976a6b39b1b13e8c354ad8d3f261f3a4ac6609518af91bdb5094760a08f132c4 \ - --hash=sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45 \ - --hash=sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc \ - --hash=sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814 \ - --hash=sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13 \ - --hash=sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181 \ - --hash=sha256:cec05be8c876f92a5aa07b01d60bbb4d11cfbdd654cad0561c0d7b5c043a61b9 \ - --hash=sha256:d036ee7b99d5148072ac7c9b847193decdfeac633db350363f7bce4fff108f0e \ - --hash=sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d \ - --hash=sha256:db88156fcf544cdbf0d95588051515cfdfd4c876fc66444eb98bceb5d6db76de \ - --hash=sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2 \ - --hash=sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d \ - --hash=sha256:fcf3da95e93349e0647d48d4b36a12783105bcc74cb0c416952f9988410846a3 +lxml==6.1.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7 \ + --hash=sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83 \ + --hash=sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8 \ + --hash=sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085 \ + --hash=sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f \ + --hash=sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004 \ + --hash=sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d \ + --hash=sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947 \ + --hash=sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2 \ + --hash=sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc \ + --hash=sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955 \ + --hash=sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c \ + --hash=sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736 \ + --hash=sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6 \ + --hash=sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2 \ + --hash=sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b \ + --hash=sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca \ + --hash=sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635 \ + --hash=sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08 \ + --hash=sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f \ + --hash=sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383 \ + --hash=sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c \ + --hash=sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099 \ + --hash=sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510 \ + --hash=sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a \ + --hash=sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b \ + --hash=sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5 \ + --hash=sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf \ + --hash=sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40 \ + --hash=sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e \ + --hash=sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2 \ + --hash=sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc \ + --hash=sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785 \ + --hash=sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8 \ + --hash=sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a \ + --hash=sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6 \ + --hash=sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6 \ + --hash=sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354 \ + --hash=sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818 \ + --hash=sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2 \ + --hash=sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf \ + --hash=sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc # via # data-designer-engine # sacrebleu @@ -1365,32 +1573,40 @@ mako==1.3.12 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (pl --hash=sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9 \ --hash=sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a # via alembic -markdown-it-py==4.0.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +markdown-it-py==4.2.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via rich -marko==2.2.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:6940308e655f63733ca518c47a68ec9510279dbb916c83616e4c4b5829f052e8 \ - --hash=sha256:f064ae8c10416285ad1d96048dc11e98ef04e662d3342ae416f662b70aa7959e +marko==2.2.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:8e1d7a0387281e59dfbc52a381b58c570156970e36b2bbe047f8a3a2f368cacc \ + --hash=sha256:e31ec2875383bc62f9093d16babed5a2c2cde601c00d834ea935a2222120ec19 # via data-designer-engine markupsafe==3.0.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ + --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ + --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ + --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ + --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ + --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ + --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ + --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ + --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ @@ -1402,12 +1618,12 @@ marshmallow==3.26.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') --hash=sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73 \ --hash=sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57 # via dataclasses-json -mcp==1.26.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca \ - --hash=sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66 +mcp==1.27.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:8e02db104096d1c25b28e64bde29a5c32b31bc241710213e12fd4d84985bdfef \ + --hash=sha256:d6ff5160c6ca65d93013626efb3fc249de683c30b2d8570755ceddd490344de5 # via # data-designer-engine - # fastmcp + # fastmcp-slim mdurl==0.1.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba @@ -1419,59 +1635,91 @@ mmh3==5.2.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (pla --hash=sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8 \ --hash=sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e \ --hash=sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825 \ + --hash=sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4 \ + --hash=sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f \ --hash=sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593 \ + --hash=sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a \ --hash=sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5 \ + --hash=sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1 \ + --hash=sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b \ + --hash=sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000 \ --hash=sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5 \ --hash=sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15 \ --hash=sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006 \ + --hash=sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211 \ + --hash=sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d \ --hash=sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38 \ --hash=sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f \ --hash=sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166 \ --hash=sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad \ --hash=sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03 \ + --hash=sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2 \ + --hash=sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4 \ --hash=sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6 \ + --hash=sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1 \ --hash=sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450 \ --hash=sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d \ --hash=sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6 \ --hash=sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7 \ + --hash=sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2 \ --hash=sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a # via fastembed -more-itertools==10.8.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b \ - --hash=sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd +more-itertools==11.1.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d \ + --hash=sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192 # via # jaraco-classes # jaraco-functools -mpmath==1.3.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ - --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c - # via sympy multidict==6.7.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9 \ + --hash=sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43 \ + --hash=sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c \ --hash=sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa \ --hash=sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6 \ + --hash=sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd \ + --hash=sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d \ --hash=sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3 \ + --hash=sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0 \ --hash=sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292 \ + --hash=sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed \ --hash=sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23 \ --hash=sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e \ --hash=sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582 \ + --hash=sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0 \ + --hash=sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e \ + --hash=sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a \ + --hash=sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d \ --hash=sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108 \ --hash=sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144 \ + --hash=sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060 \ --hash=sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56 \ + --hash=sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84 \ + --hash=sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71 \ --hash=sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7 \ + --hash=sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8 \ --hash=sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49 \ --hash=sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d \ --hash=sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445 \ + --hash=sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a \ --hash=sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33 \ + --hash=sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca \ + --hash=sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733 \ --hash=sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429 \ + --hash=sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6 \ --hash=sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172 \ + --hash=sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52 \ --hash=sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7 \ --hash=sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961 \ + --hash=sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b \ --hash=sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1 \ + --hash=sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c \ + --hash=sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a \ --hash=sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23 \ --hash=sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34 \ --hash=sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75 \ --hash=sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d \ --hash=sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855 \ + --hash=sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4 \ --hash=sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba # via # aiobotocore @@ -1519,8 +1767,8 @@ networkx==3.6.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # data-designer-engine # nvidia-nat-core # ragas -ngcsdk==4.16.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:3fe7267fab02b5e4c63521ade365a708238113a1b19802e53fa699b548e13fce +ngcsdk==4.19.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:945455d06f9a215772660472d2ff4432c67a53986f86a384a98fbee62b01e434 # via # nemo-platform-ext # nemo-platform-sdk @@ -1529,36 +1777,36 @@ nltk==3.9.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (pla --hash=sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0 \ --hash=sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f # via rouge-score -numpy==2.4.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959 \ - --hash=sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd \ - --hash=sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7 \ - --hash=sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e \ - --hash=sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0 \ - --hash=sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103 \ - --hash=sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af \ - --hash=sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5 \ - --hash=sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7 \ - --hash=sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392 \ - --hash=sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c \ - --hash=sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40 \ - --hash=sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44 \ - --hash=sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5 \ - --hash=sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0 \ - --hash=sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e \ - --hash=sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015 \ - --hash=sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d \ - --hash=sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842 \ - --hash=sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed \ - --hash=sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f \ - --hash=sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e \ - --hash=sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83 \ - --hash=sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502 \ - --hash=sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115 \ - --hash=sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e \ - --hash=sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e \ - --hash=sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121 \ - --hash=sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d +numpy==2.4.6 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f \ + --hash=sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47 \ + --hash=sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d \ + --hash=sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1 \ + --hash=sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41 \ + --hash=sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8 \ + --hash=sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0 \ + --hash=sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f \ + --hash=sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f \ + --hash=sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67 \ + --hash=sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b \ + --hash=sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93 \ + --hash=sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02 \ + --hash=sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853 \ + --hash=sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd \ + --hash=sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089 \ + --hash=sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb \ + --hash=sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a \ + --hash=sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8 \ + --hash=sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7 \ + --hash=sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605 \ + --hash=sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2 \ + --hash=sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe \ + --hash=sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb \ + --hash=sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d \ + --hash=sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a \ + --hash=sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda \ + --hash=sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6 \ + --hash=sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20 # via # data-designer-config # data-designer-engine @@ -1566,6 +1814,7 @@ numpy==2.4.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (pl # fastembed # langchain-aws # langchain-community + # langchain-oci # nvidia-nat-config-optimizer # nvidia-nat-core # onnxruntime @@ -1577,9 +1826,9 @@ numpy==2.4.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (pl # scikit-network # scipy # transformers -nvidia-ml-py==13.595.45 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:b65a7977f503d56154b14d683710125ef93594adb63fbf7e559336e3318f1376 \ - --hash=sha256:c9f34897fe0441ff35bc8f35baf80f830a20b0f4e6ce71e0a325bc0e66acf079 +nvidia-ml-py==13.610.43 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:65437eb73d68d0c62c931ca4d45038472faff03bd0b8729abba4b899f70d60f2 \ + --hash=sha256:f13c72698edef492f985cc225f14faafe68ae065a2e407f45bdf6f4b9b43fde8 # via # nemo-platform-ext # nemo-platform-sdk @@ -1615,9 +1864,9 @@ oauthlib==3.3.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or --hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \ --hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1 # via requests-oauthlib -oci==2.174.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:36c377fb59452b607686d73c1ae1604f2c19e3cabd7d12abe43a4404b10a17c5 \ - --hash=sha256:f960e413a7f0e59ca5523b57349165f992812bd2738abc34bd9fecbce4722733 +oci==2.178.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:830cb97cbcac818f8eb8d05d4abbc00192f4bcef10260b14d0978f649799a26e \ + --hash=sha256:d3a19859d80aa5c4988905e1a30b46dcc2af146c76f3d8c813129d71247d1a94 # via # langchain-oci # oci-openai @@ -1625,24 +1874,24 @@ oci-openai==1.1.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') o --hash=sha256:1819ef7d17c1fdbe05c5c0653301fdca0d2fa99f6f8b1b7bd7667da9704d62a1 \ --hash=sha256:a028ee3e1a1b1ad4e0495b10ef70b81b5e6cd50e7f13cf485a112762641a9160 # via langchain-oci -onnxruntime==1.24.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:046ff290045a387676941a02a8ae5c3ebec6b4f551ae228711968c4a69d8f6b7 \ - --hash=sha256:0bdfce8e9a6497cec584aab407b71bf697dac5e1b7b7974adc50bf7533bdb3a2 \ - --hash=sha256:0d640eb9f3782689b55cfa715094474cd5662f2f137be6a6f847a594b6e9705c \ - --hash=sha256:1700f559c8086d06b2a4d5de51e62cb4ff5e2631822f71a36db8c72383db71ee \ - --hash=sha256:1a5c5a544b22f90859c88617ecb30e161ee3349fcc73878854f43d77f00558b5 \ - --hash=sha256:4c74e268dc808e61e63784d43f9ddcdaf50a776c2819e8bd1d1b11ef64bf7e36 \ - --hash=sha256:cad1c2b3f455c55678ab2a8caa51fb420c25e6e3cf10f4c23653cdabedc8de78 \ - --hash=sha256:dc4aaed1e5e1aaacf2343c838a30a7c3ade78f13eeb16817411f929d04040a13 \ - --hash=sha256:e30c972bc02e072911aabb6891453ec73795386c0af2b761b65444b8a4c4745f \ - --hash=sha256:e54ad52e61d2d4618dcff8fa1480ac66b24ee2eab73331322db1049f11ccf330 \ - --hash=sha256:e99a48078baaefa2b50fe5836c319499f71f13f76ed32d0211f39109147a49e0 +onnxruntime==1.26.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:05b028781b322ad74b57ce5b50aa5280bb1fe96ceec334628ade681e0b24c1ac \ + --hash=sha256:11a8df4dcfe9ad5ff0bd71a7571dbed019fabc7594676c89fe8b86ea029c246f \ + --hash=sha256:35c7c7b0ac2e02001d28fab6c9fc24e9abc5e6faa35e6e19c63cecf1406ba89f \ + --hash=sha256:5e016edc15d3c19f36807e1c6b10be5b27807688c32720f91b5ae480a95215d0 \ + --hash=sha256:91f2bb870a4b9224eba0a6728c1fa7a9e552b8e59e1083c51fbbc3d013f2b5c0 \ + --hash=sha256:9b6dd70599005bd1bf29779f04a91978b92b5e719c11a20068a8f8e535f725b6 \ + --hash=sha256:bdbed8cf3b672b66acb032f33a253bc27f42bce6ece48ae3fab4fa483a5e96e0 \ + --hash=sha256:c07af6fc6d5557835f2b6ee7a96d8b3235d0c57a8e230efdedaee106a8a3cbc6 \ + --hash=sha256:ccce19c5f771b8268902f77d9fed9e88f9499465d6780808faa6611a789d33f0 \ + --hash=sha256:ee1109ef4ef27cad90e823399e61e03b3c6c7bfe0fb820b4baf3678c15be8b3c \ + --hash=sha256:f5fc48a91a046a6a5c9b147f83fb41d65d24d24923373b222cdd248f0f4f4aac # via # fastembed # nemoguardrails -openai==2.35.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:164fd0477d001e784369f7cd81ccadb8db3c22f16b33973d8f95e3095c7f71d8 \ - --hash=sha256:607f62257d6be167240c6b82db052fabf940e3c4d9ad3e8629364e837a601395 +openai==2.41.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:20cc7952e8501c7e5773dd2ef7be437bae9cb549044902e1041a83a54516e375 \ + --hash=sha256:db5c362acd6604b84f076abbefa66826ea4b46ecba2954ed866e6a149a1352c0 # via # exa-py # instructor @@ -1662,20 +1911,20 @@ openai==2.35.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or ( openapi-pydantic==0.5.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146 \ --hash=sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d - # via fastmcp + # via fastmcp-slim openevals==0.2.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:2bce5964be9d162e3d38c2dfd026739156e1ac521536ade6b8e2f0a89b632f2c \ --hash=sha256:7e95fa64625be53eaa8c657d7f69b842a52bda10bdf3bb91781c7d09a385b069 # via nvidia-nat-langchain -openinference-semantic-conventions==0.1.29 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:bbeb6472777a45a574169894bb9c4d80c6832a8befd32ab238cb875438ce1044 \ - --hash=sha256:f45e0b1cf79fe407af4722bcf391a01565f0878c95be3ebcc9382245d0367cc5 +openinference-semantic-conventions==0.1.30 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:36d946d3f95f699b7c4b12324ae9c1f02d6c7750df11eece56aa159cff430b3d \ + --hash=sha256:81fece76e09c83789e35c393b8b30523481eeabf1008745b955631a53e3221d9 # via nvidia-nat-opentelemetry -opentelemetry-api==1.40.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f \ - --hash=sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9 +opentelemetry-api==1.42.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714 \ + --hash=sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716 # via - # fastmcp + # fastmcp-slim # nemoguardrails # nvidia-nat-opentelemetry # opentelemetry-distro @@ -1692,44 +1941,44 @@ opentelemetry-api==1.40.0 ; (platform_machine == 'arm64' and sys_platform == 'da # opentelemetry-processor-baggage # opentelemetry-sdk # opentelemetry-semantic-conventions -opentelemetry-distro==0.61b0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:975b845f50181ad53753becf4fd4b123b54fa04df5a9d78812264436d6518981 \ - --hash=sha256:f21d1ac0627549795d75e332006dd068877f00e461b1b2e8fe4568d6eb7b9590 +opentelemetry-distro==0.63b1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:b405b04ad70e430390265eb38e82e067a84ca1f49a21429eaadb930c13330d66 \ + --hash=sha256:f435098abc7953f58226e8bf79e4c90bc6b32e50aa75d6fa074201db8243b577 # via # nmp-evaluator # nmp-guardrails -opentelemetry-exporter-otlp==1.40.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:48c87e539ec9afb30dc443775a1334cc5487de2f72a770a4c00b1610bf6c697d \ - --hash=sha256:7caa0870b95e2fcb59d64e16e2b639ecffb07771b6cd0000b5d12e5e4fef765a +opentelemetry-exporter-otlp==1.42.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:2d9ebaed714377a67d224d46795ddcc11d2c877fa5de35fda70b6f3b010729a9 \ + --hash=sha256:aedd54545bb0587cd45210abdc8be545af9c01413f3307786e276df1e3c83bee # via # nmp-evaluator # nmp-guardrails # nvidia-nat-opentelemetry -opentelemetry-exporter-otlp-proto-common==1.40.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:1cbee86a4064790b362a86601ee7934f368b81cd4cc2f2e163902a6e7818a0fa \ - --hash=sha256:7081ff453835a82417bf38dccf122c827c3cbc94f2079b03bba02a3165f25149 +opentelemetry-exporter-otlp-proto-common==1.42.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee \ + --hash=sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140 # via # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http -opentelemetry-exporter-otlp-proto-grpc==1.40.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:2aa0ca53483fe0cf6405087a7491472b70335bc5c7944378a0a8e72e86995c52 \ - --hash=sha256:bd4015183e40b635b3dab8da528b27161ba83bf4ef545776b196f0fb4ec47740 +opentelemetry-exporter-otlp-proto-grpc==1.42.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:0ae1177e2038b18a929b3098215243631ef91136cba26b7e2b12790ceb7e87cc \ + --hash=sha256:975c4461f167dd8ed8857d68d3b6b25f3d272eab896f6a9470d0f5b90e2faf15 # via # nmp-common # opentelemetry-exporter-otlp -opentelemetry-exporter-otlp-proto-http==1.40.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:a8d1dab28f504c5d96577d6509f80a8150e44e8f45f82cdbe0e34c99ab040069 \ - --hash=sha256:db48f5e0f33217588bbc00274a31517ba830da576e59503507c839b38fa0869c +opentelemetry-exporter-otlp-proto-http==1.42.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d \ + --hash=sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d # via # nmp-common # opentelemetry-exporter-otlp -opentelemetry-exporter-prometheus==0.61b0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:3013b41f4370143d48d219a2351473761423e5882fa4c213811eaefacba39cb7 \ - --hash=sha256:7c4919bd8e79abd62b610767e80f42c9c3a06c5183f4dd9141eedeb57aea284b +opentelemetry-exporter-prometheus==0.63b1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:0efd00aa6b1939345ddcc6de141b83ebffa2b4401a37a68f880e54217602701d \ + --hash=sha256:31902e22c89431058a95b6dcdb644f9309f226aa4872cc755f0a780d2895e97f # via nmp-common -opentelemetry-instrumentation==0.61b0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:92a93a280e69788e8f88391247cc530fd81f16f2b011979d4d6398f805cfbc63 \ - --hash=sha256:cb21b48db738c9de196eba6b805b4ff9de3b7f187e4bbf9a466fa170514f1fc7 +opentelemetry-instrumentation==0.63b1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541 \ + --hash=sha256:f1986716d52cc316ea5f60189098726a9071d8ecc0eee96c9ed110be08bade9c # via # opentelemetry-distro # opentelemetry-instrumentation-asgi @@ -1738,50 +1987,50 @@ opentelemetry-instrumentation==0.61b0 ; (platform_machine == 'arm64' and sys_pla # opentelemetry-instrumentation-requests # opentelemetry-instrumentation-sqlalchemy # opentelemetry-instrumentation-system-metrics -opentelemetry-instrumentation-asgi==0.61b0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:9d08e127244361dc33976d39dd4ca8f128b5aa5a7ae425208400a80a095019b5 \ - --hash=sha256:e4b3ce6b66074e525e717efff20745434e5efd5d9df6557710856fba356da7a4 +opentelemetry-instrumentation-asgi==0.63b1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:1a22453dfa965f14799b10a674b8acbcb897a8a75c79136060af54214cc7886e \ + --hash=sha256:267b422416d768f3c7f4054883b41d9c3a7c943d86d20032b738c99a3dbb5862 # via opentelemetry-instrumentation-fastapi -opentelemetry-instrumentation-fastapi==0.61b0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:3a24f35b07c557ae1bbc483bf8412221f25d79a405f8b047de8b670722e2fa9f \ - --hash=sha256:a1a844d846540d687d377516b2ff698b51d87c781b59f47c214359c4a241047c +opentelemetry-instrumentation-fastapi==0.63b1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:52ee2cde9a2ac094bdd45d79f85860e03a972928a2553006071fe61d94cf7281 \ + --hash=sha256:cc42dff56c96d0a2921510c4abab2a4c2e27fe64b26dc1254727fb550df100ba # via # nmp-common # nmp-guardrails -opentelemetry-instrumentation-httpx==0.61b0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:6569ec097946c5551c2a4252f74c98666addd1bf047c1dde6b4ef426719ff8dd \ - --hash=sha256:dee05c93a6593a5dc3ae5d9d5c01df8b4e2c5d02e49275e5558534ee46343d5e +opentelemetry-instrumentation-httpx==0.63b1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:14df6e99d81be9a8cd238f6639b6fa52404c4d3ce219058fcb5dc8c0f2211f86 \ + --hash=sha256:f41ec82f25c3abcdada621052db3e5fd648e3b43d55eec4b9c0c5d3ecb7b4ff4 # via # nmp-common # nmp-guardrails -opentelemetry-instrumentation-requests==0.61b0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:15f879ce8fb206bd7e6fdc61663ea63481040a845218c0cf42902ce70bd7e9d9 \ - --hash=sha256:cce19b379949fe637eb73ba39b02c57d2d0805447ca6d86534aa33fcb141f683 +opentelemetry-instrumentation-requests==0.63b1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:513fcaa3d93debbdb359c00ce1a137a34a89ee908c51ac43beb7e8c18ac2b3cd \ + --hash=sha256:935c980a11e33bfd7ed969c741e4bd7c84077045651469f10e163534368d87f7 # via nmp-guardrails -opentelemetry-instrumentation-sqlalchemy==0.61b0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:13a3a159a2043a52f0180b3757fbaa26741b0e08abb50deddce4394c118956e6 \ - --hash=sha256:f115e0be54116ba4c327b8d7b68db4045ee18d44439d888ab8130a549c50d1c1 +opentelemetry-instrumentation-sqlalchemy==0.63b1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:621f9eb800ea24a98b4eda968373e3909bfede0ff47f77b96f8b8a18bc2a2a1a \ + --hash=sha256:d417414f6517963e9c1ee91ec971b94938b46904499114d035a43937bd62b6a1 # via nmp-common -opentelemetry-instrumentation-system-metrics==0.61b0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:3eb55f9a058797cf915946cbb7445e00b31316ac3e55050475792edf3367c321 \ - --hash=sha256:7d4fe3e0ce14e0e6eb18f5826100d6cc1af662e5a8ebc74e9b91fe23f192f3e8 +opentelemetry-instrumentation-system-metrics==0.63b1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:995051f47876d79461aed8b7aa205d4584d90794ef864342cc748929c389bb42 \ + --hash=sha256:d6d4d7a1a854be4165143cf6420ee5894188762eb367d7bf9da5be4a83a4b632 # via nmp-common -opentelemetry-processor-baggage==0.61b0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:4d1d2a624e3aa9a8b6c6d1f560ba2951f97acf875f57502a274c5078043a69d5 \ - --hash=sha256:f6b5937e93bda8f380d8f5f667355c7d127e9296b38dfacf39fd328ab410262c +opentelemetry-processor-baggage==0.63b1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:334b77963ea5807efd6f05664a6064aa92fc6c03571edbf1f749b9dee370d567 \ + --hash=sha256:b205c343720ce4d5e420204e09862a043917ee433b2304d87bb6f388084f3c15 # via nmp-common -opentelemetry-proto==1.40.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:03f639ca129ba513f5819810f5b1f42bcb371391405d99c168fe6937c62febcd \ - --hash=sha256:266c4385d88923a23d63e353e9761af0f47a6ed0d486979777fe4de59dc9b25f +opentelemetry-proto==1.42.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6 \ + --hash=sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c # via # nmp-files # nmp-intake # opentelemetry-exporter-otlp-proto-common # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http -opentelemetry-sdk==1.40.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2 \ - --hash=sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1 +opentelemetry-sdk==1.42.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d \ + --hash=sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7 # via # nmp-common # nmp-guardrails @@ -1791,9 +2040,9 @@ opentelemetry-sdk==1.40.0 ; (platform_machine == 'arm64' and sys_platform == 'da # opentelemetry-exporter-otlp-proto-http # opentelemetry-exporter-prometheus # opentelemetry-processor-baggage -opentelemetry-semantic-conventions==0.61b0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a \ - --hash=sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2 +opentelemetry-semantic-conventions==0.63b1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9 \ + --hash=sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682 # via # opentelemetry-instrumentation # opentelemetry-instrumentation-asgi @@ -1801,10 +2050,11 @@ opentelemetry-semantic-conventions==0.61b0 ; (platform_machine == 'arm64' and sy # opentelemetry-instrumentation-httpx # opentelemetry-instrumentation-requests # opentelemetry-instrumentation-sqlalchemy + # opentelemetry-instrumentation-system-metrics # opentelemetry-sdk -opentelemetry-util-http==0.61b0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:1039cb891334ad2731affdf034d8fb8b48c239af9b6dd295e5fabd07f1c95572 \ - --hash=sha256:8e715e848233e9527ea47e275659ea60a57a75edf5206a3b937e236a6da5fc33 +opentelemetry-util-http==0.63b1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:6284194028c59cd439f8acfe388145069a6127f11dc077e1344a2094adacc3f8 \ + --hash=sha256:ba1268f00922ee522dba2ae38458060f99486e7385a8056985901ca9685adfff # via # opentelemetry-instrumentation-asgi # opentelemetry-instrumentation-fastapi @@ -1814,26 +2064,38 @@ optuna==4.4.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p --hash=sha256:a9029f6a92a1d6c8494a94e45abd8057823b535c2570819072dbcdc06f1c1da4 \ --hash=sha256:fad8d9c5d5af993ae1280d6ce140aecc031c514a44c3b639d8c8658a8b7920ea # via nvidia-nat-config-optimizer -orjson==3.11.8 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:0022bb50f90da04b009ce32c512dc1885910daa7cb10b7b0cba4505b16db82a8 \ - --hash=sha256:003646067cc48b7fcab2ae0c562491c9b5d2cbd43f1e5f16d98fd118c5522d34 \ - --hash=sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25 \ - --hash=sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f \ - --hash=sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc \ - --hash=sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f \ - --hash=sha256:5f8952d6d2505c003e8f0224ff7858d341fa4e33fef82b91c4ff0ef070f2393c \ - --hash=sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6 \ - --hash=sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53 \ - --hash=sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b \ - --hash=sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8 \ - --hash=sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e \ - --hash=sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e \ - --hash=sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623 \ - --hash=sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e \ - --hash=sha256:a5c370674ebabe16c6ccac33ff80c62bf8a6e59439f5e9d40c1f5ab8fd2215b7 \ - --hash=sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559 \ - --hash=sha256:ed193ce51d77a3830cad399a529cd4ef029968761f43ddc549e1bc62b40d88f8 \ - --hash=sha256:f30491bc4f862aa15744b9738517454f1e46e56c972a2be87d70d727d5b2a8f8 +orjson==3.11.9 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4 \ + --hash=sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62 \ + --hash=sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a \ + --hash=sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1 \ + --hash=sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09 \ + --hash=sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877 \ + --hash=sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd \ + --hash=sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe \ + --hash=sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97 \ + --hash=sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c \ + --hash=sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5 \ + --hash=sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021 \ + --hash=sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362 \ + --hash=sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206 \ + --hash=sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218 \ + --hash=sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9 \ + --hash=sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f \ + --hash=sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2 \ + --hash=sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa \ + --hash=sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c \ + --hash=sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677 \ + --hash=sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49 \ + --hash=sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0 \ + --hash=sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c \ + --hash=sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2 \ + --hash=sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61 \ + --hash=sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff \ + --hash=sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff \ + --hash=sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291 \ + --hash=sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f \ + --hash=sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81 # via # langgraph-sdk # langsmith @@ -1841,29 +2103,35 @@ orjson==3.11.8 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or ( # pymilvus ormsgpack==1.12.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d \ + --hash=sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172 \ --hash=sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a \ + --hash=sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5 \ --hash=sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d \ --hash=sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181 \ --hash=sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7 \ --hash=sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc \ + --hash=sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685 \ --hash=sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355 \ --hash=sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7 \ --hash=sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b \ --hash=sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e \ --hash=sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33 \ + --hash=sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e \ --hash=sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9 \ --hash=sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a \ --hash=sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258 \ + --hash=sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92 \ + --hash=sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6 \ --hash=sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1 \ --hash=sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd # via langgraph-checkpoint -packaging==26.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ - --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 +packaging==26.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via # data-designer # datasets - # fastmcp + # fastmcp-slim # gunicorn # huggingface-hub # langchain-core @@ -1911,13 +2179,13 @@ pandas==2.3.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p # nvidia-nat-config-optimizer # nvidia-nat-core # pymilvus -pathable==0.4.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2 \ - --hash=sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2 +pathable==0.6.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:6404b8b82aef5ff0fd478934137128b99b12212ba35afdde5525ca4f8388ea58 \ + --hash=sha256:82c4ca6c98c502ad12e0d4e9779b6210afee93c38990988c8c5d1b49bdcdf566 # via jsonschema-path -pathspec==1.0.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645 \ - --hash=sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723 +pathspec==1.1.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 # via sqlfluff pillow==12.2.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9 \ @@ -1961,9 +2229,9 @@ pillow==12.2.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or ( # data-designer-config # fastembed # ragas -pip==26.1.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb \ - --hash=sha256:d36762751d156a4ee895de8af39aa0abeeeb577f93a2eca6ab62467bbf0f8a78 +pip==26.1.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \ + --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605 # via nvidia-nat-core pkce==1.0.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:55927e24c7d403b2491ebe182b95d9dcb1807643243d47e3879fbda5aad4471d \ @@ -1973,11 +2241,11 @@ pkginfo==1.12.1.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') o --hash=sha256:5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b \ --hash=sha256:c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343 # via nvidia-nat-core -platformdirs==4.9.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934 \ - --hash=sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868 +platformdirs==4.10.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7 \ + --hash=sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a # via - # fastmcp + # fastmcp-slim # nvidia-nat-core # sqlfluff pluggy==1.6.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ @@ -1998,9 +2266,9 @@ prettytable==3.17.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') --hash=sha256:59f2590776527f3c9e8cf9fe7b66dd215837cca96a9c39567414cbc632e8ddb0 \ --hash=sha256:aad69b294ddbe3e1f95ef8886a060ed1666a0b83018bbf56295f6f226c43d287 # via ngcsdk -prometheus-client==0.24.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055 \ - --hash=sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9 +prometheus-client==0.25.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28 \ + --hash=sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1 # via # nmp-common # opentelemetry-exporter-prometheus @@ -2017,33 +2285,61 @@ prompt-toolkit==3.0.52 ; (platform_machine == 'arm64' and sys_platform == 'darwi # nemo-platform-ext # nemo-platform-sdk # nemoguardrails -propcache==0.4.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3 \ - --hash=sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf \ - --hash=sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe \ - --hash=sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75 \ - --hash=sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566 \ - --hash=sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf \ - --hash=sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1 \ - --hash=sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af \ - --hash=sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf \ - --hash=sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e \ - --hash=sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1 \ - --hash=sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b \ - --hash=sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f \ - --hash=sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66 \ - --hash=sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0 \ - --hash=sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237 \ - --hash=sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835 \ - --hash=sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 \ - --hash=sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f \ - --hash=sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2 \ - --hash=sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72 \ - --hash=sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207 \ - --hash=sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d \ - --hash=sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e \ - --hash=sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570 \ - --hash=sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48 +propcache==0.5.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427 \ + --hash=sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa \ + --hash=sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a \ + --hash=sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660 \ + --hash=sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94 \ + --hash=sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42 \ + --hash=sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33 \ + --hash=sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a \ + --hash=sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511 \ + --hash=sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0 \ + --hash=sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66 \ + --hash=sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb \ + --hash=sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853 \ + --hash=sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55 \ + --hash=sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6 \ + --hash=sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704 \ + --hash=sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f \ + --hash=sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64 \ + --hash=sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d \ + --hash=sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f \ + --hash=sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba \ + --hash=sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b \ + --hash=sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144 \ + --hash=sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e \ + --hash=sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67 \ + --hash=sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476 \ + --hash=sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78 \ + --hash=sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a \ + --hash=sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba \ + --hash=sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c \ + --hash=sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9 \ + --hash=sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913 \ + --hash=sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f \ + --hash=sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d \ + --hash=sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4 \ + --hash=sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f \ + --hash=sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836 \ + --hash=sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe \ + --hash=sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1 \ + --hash=sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a \ + --hash=sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7 \ + --hash=sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a \ + --hash=sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164 \ + --hash=sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568 \ + --hash=sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4 \ + --hash=sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2 \ + --hash=sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27 \ + --hash=sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf \ + --hash=sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f \ + --hash=sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc \ + --hash=sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c \ + --hash=sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7 \ + --hash=sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098 \ + --hash=sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751 # via # aiohttp # yarl @@ -2051,6 +2347,7 @@ protobuf==6.33.6 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or --hash=sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901 \ --hash=sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a \ --hash=sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135 \ + --hash=sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3 \ --hash=sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2 \ --hash=sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593 # via @@ -2075,50 +2372,71 @@ psutil==7.2.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p # nemo-platform-sdk # ngcsdk # opentelemetry-instrumentation-system-metrics -psycopg2-binary==2.9.11 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1 \ - --hash=sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee \ - --hash=sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4 \ - --hash=sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee \ - --hash=sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3 \ - --hash=sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d \ - --hash=sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a \ - --hash=sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0 \ - --hash=sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a \ - --hash=sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c \ - --hash=sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4 \ - --hash=sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908 \ - --hash=sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f \ - --hash=sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3 \ - --hash=sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc \ - --hash=sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db +psycopg2-binary==2.9.12 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f \ + --hash=sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c \ + --hash=sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c \ + --hash=sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be \ + --hash=sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6 \ + --hash=sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c \ + --hash=sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d \ + --hash=sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019 \ + --hash=sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7 \ + --hash=sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3 \ + --hash=sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777 \ + --hash=sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39 \ + --hash=sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c \ + --hash=sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d \ + --hash=sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290 \ + --hash=sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b \ + --hash=sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9 \ + --hash=sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9 \ + --hash=sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be \ + --hash=sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580 \ + --hash=sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f \ + --hash=sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326 \ + --hash=sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e \ + --hash=sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5 \ + --hash=sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06 \ + --hash=sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6 \ + --hash=sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8 \ + --hash=sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a # via # nmp-entities # nmp-evaluator -py-key-value-aio==0.4.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d \ - --hash=sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55 - # via fastmcp -py-rust-stemmers==0.1.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:191ea8bf922c984631ffa20bf02ef0ad7eec0465baeaed3852779e8f97c7e7a3 \ - --hash=sha256:1c3593d895453fa06bf70a7b76d6f00d06def0f91fc253fe4260920650c5e078 \ - --hash=sha256:1f9efc4da5e734bdd00612e7506de3d0c9b7abc4b89d192742a0569d0d1fe749 \ - --hash=sha256:31ff4fb9417cec35907c18a6463e3d5a4941a5aa8401f77fbb4156b3ada69e3f \ - --hash=sha256:4d62410ada44a01e02974b85d45d82f4b4c511aae9121e5f3c1ba1d0bea9126b \ - --hash=sha256:4e308fc7687901f0c73603203869908f3156fa9c17c4ba010a7fcc98a7a1c5f2 \ - --hash=sha256:5845709d48afc8b29e248f42f92431155a3d8df9ba30418301c49c6072b181b0 \ - --hash=sha256:804944eeb5c5559443d81f30c34d6e83c6292d72423f299e42f9d71b9d240941 \ - --hash=sha256:85944262c248ea30444155638c9e148a3adc61fe51cf9a3705b4055b564ec95d \ - --hash=sha256:910d87d39ba75da1fe3d65df88b926b4b454ada8d73893cbd36e258a8a648158 \ - --hash=sha256:96ccc7fd042ffc3f7f082f2223bb7082ed1423aa6b43d5d89ab23e321936c045 \ - --hash=sha256:a231dc6f0b2a5f12a080dfc7abd9e6a4ea0909290b10fd0a4620e5a0f52c3d17 \ - --hash=sha256:b28ef729a4c83c7d9418be3c23c0372493fcccc67e86783ff04596ef8a208cdf \ - --hash=sha256:c52c5c326de78c70cfc71813fa56818d1bd4894264820d037d2be0e805b477bd \ - --hash=sha256:d8f374c0f26ef35fb87212686add8dff394bcd9a1364f14ce40fe11504e25e30 \ - --hash=sha256:e48bfd5e3ce9d223bfb9e634dc1425cf93ee57eef6f56aa9a7120ada3990d4be \ - --hash=sha256:e9c310cfb5c2470d7c7c8a0484725965e7cab8b1237e106a0863d5741da3e1f7 \ - --hash=sha256:ef18cfced2c9c676e0d7d172ba61c3fab2aa6969db64cc8f5ca33a7759efbefe \ - --hash=sha256:ffd946a36e9ac17ca96821963663012e04bc0ee94d21e8b5ae034721070b436c +py-key-value-aio==0.4.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:ab862adbcb8c72547d1c57821f22cbbb71ab86509039c96f36e914e0336c8dd7 \ + --hash=sha256:c6563a2c6abe5da5e20f4f9e875c2a9b425a2244a54fadbf46cf140a9eea45d7 + # via fastmcp-slim +py-rust-stemmers==0.1.8 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:08c258deab6d994551a92e9468ce88e58f97e636e73d9c5763978a57d7675a13 \ + --hash=sha256:0a68745d4b3c7f5abc778ca967e8711df6154873abcfe4e62a6631fa2363cc32 \ + --hash=sha256:1686fc009869ff8bcc1d5a305f071eeb8c3b3612a9827bcadd4e61fdb5727179 \ + --hash=sha256:21ed8055cec1f78d666afad8ffd7a51775ba419d2c615b8a1df7b32ca7f33e2b \ + --hash=sha256:234fdcb58f4d907877ed03c9358668a149b5a66d096abcf43c324a4f5697d36d \ + --hash=sha256:2e86ad68fe297a6652f0f0390625ea81858b6f27862fd4c5ee1214bf5af29b9d \ + --hash=sha256:3007ad4ec51e0c352ae410234a24a9ac75fab0c1e06c585fbac9fcced69385f8 \ + --hash=sha256:35570098da02eb439afcd7270a12bf850bbe874b85cb912e0fb2d87a6e703920 \ + --hash=sha256:40c86be90cee4a709ad84fde4db7f11ca44d65630a56b77ec86fe84c23adfc09 \ + --hash=sha256:4a1e11d22a240318dc917266eb3c85919455b6ea834445b95997712d9ede6b93 \ + --hash=sha256:4b90fc81411943b114e8eb4988a876ba3b12bd2d20741559803eddc4131575dc \ + --hash=sha256:515884bcfb47b10335146648f276930d0c1201ae5e8b7b400fb46d8ea05c0ec2 \ + --hash=sha256:51d0042d2a92ef0f7048bfc06b6c2a02306af31ea47f09d24b34e4b7e63c4e80 \ + --hash=sha256:56cc2c2df742fa6529285b7d204720f34b7da789ed78eb578442f93c6de97d89 \ + --hash=sha256:5cc8fab9d0f1b274a26935a632362b8278f03e81b65e8b8644d5ca3f62a5a1a4 \ + --hash=sha256:6b0f6f48bc54d607aed802de872fcd5a71bae969a6760976dc78ce55e8eaf3da \ + --hash=sha256:6c92733b020534470ca5a0d7fe8b85c85622ff383d4f37fec75a1c677aa84921 \ + --hash=sha256:769f37882905da2311cb720681b112eb70a4e6bd56fb424d473427b5379c8396 \ + --hash=sha256:7cc0cc0b8eb45d2158c28ea43e2f338c110aad63052ad3bd00bc7446a595e12f \ + --hash=sha256:870afb2d1d4731bd2d74b715b34439b29734e4dc94c55342096f07669f7f9fa0 \ + --hash=sha256:89d3d34094b9b6078a8ea6fe1c7044e5fd32f14e76c94818c5008f49ae075f08 \ + --hash=sha256:9ab605a86c950ba7e8ab1392cf91296c0bec3084babb897a4aecf90a10c82395 \ + --hash=sha256:ae773e1d01e9aa328d175f461475d0cd7074a82bfcc71de6dc5765e51f1cc9f7 \ + --hash=sha256:bfc185b599e646a0e39d11df3f5e6d15edefb110496601556385d33b55fed5de \ + --hash=sha256:dca0ae40715238582d6f1824b61d09ea3982359a061b69798ab5732b3ba0d4c5 \ + --hash=sha256:eee4af7ada2ce9cb3ec59ffe8458148c3933a86507d816bf954ee506a0e45b61 \ + --hash=sha256:f16deb1557b8253d8c11693047bec4ed67d6b09ae0f84c8b896ea03ac2fc8925 \ + --hash=sha256:fa42f5f8feb694aaaa869eedf477fcaf66f67a192cd64d94302d06920c33864a # via fastembed pyarrow==22.0.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016 \ @@ -2150,9 +2468,9 @@ pycparser==3.0 ; (implementation_name != 'PyPy' and platform_machine == 'arm64' --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi -pydantic==2.12.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49 \ - --hash=sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d +pydantic==2.13.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ + --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 # via # anthropic # data-designer-config @@ -2160,7 +2478,7 @@ pydantic==2.12.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # exa-py # fastapi # fastapi-cloud-cli - # fastmcp + # fastmcp-slim # instructor # langchain # langchain-aws @@ -2207,33 +2525,49 @@ pydantic==2.12.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # ragas # sqlmodel # switchyard -pydantic-core==2.41.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740 \ - --hash=sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84 \ - --hash=sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0 \ - --hash=sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e \ - --hash=sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0 \ - --hash=sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34 \ - --hash=sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808 \ - --hash=sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a \ - --hash=sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284 \ - --hash=sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586 \ - --hash=sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc \ - --hash=sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c \ - --hash=sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b \ - --hash=sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b \ - --hash=sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858 \ - --hash=sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2 \ - --hash=sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc \ - --hash=sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1 \ - --hash=sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56 \ - --hash=sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c \ - --hash=sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e \ - --hash=sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69 \ - --hash=sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c \ - --hash=sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f \ - --hash=sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad \ - --hash=sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b +pydantic-core==2.46.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \ + --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ + --hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \ + --hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \ + --hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \ + --hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \ + --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ + --hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \ + --hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \ + --hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \ + --hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \ + --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ + --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ + --hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \ + --hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \ + --hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \ + --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ + --hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \ + --hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \ + --hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \ + --hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \ + --hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \ + --hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \ + --hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \ + --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ + --hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \ + --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ + --hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \ + --hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \ + --hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \ + --hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \ + --hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \ + --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ + --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ + --hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \ + --hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \ + --hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \ + --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ + --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ + --hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \ + --hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \ + --hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e # via # instructor # pydantic @@ -2241,11 +2575,12 @@ pydantic-extra-types==2.11.1 ; (platform_machine == 'arm64' and sys_platform == --hash=sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1 \ --hash=sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049 # via fastapi -pydantic-settings==2.8.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c \ - --hash=sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585 +pydantic-settings==2.14.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de \ + --hash=sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa # via # fastapi + # fastmcp-slim # langchain-community # mcp # nemo-automodel-plugin @@ -2275,9 +2610,10 @@ pygments==2.20.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # nemo-anonymizer # pytest # rich -pyjwt==2.12.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c \ - --hash=sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b + # rich-rst +pyjwt==2.13.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 # via # mcp # nmp-common @@ -2304,7 +2640,7 @@ pyopenssl==26.2.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') o pyperclip==1.11.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6 \ --hash=sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273 - # via fastmcp + # via fastmcp-slim pytest==9.0.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9 \ --hash=sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c @@ -2335,7 +2671,7 @@ python-dotenv==1.2.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin' --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 # via # exa-py - # fastmcp + # fastmcp-slim # litellm # nvidia-nat-core # pydantic-settings @@ -2351,13 +2687,14 @@ python-multipart==0.0.32 ; (platform_machine == 'arm64' and sys_platform == 'dar # via # data-designer-engine # fastapi + # fastmcp-slim # mcp # nemo-safe-synthesizer-plugin # nmp-guardrails # nvidia-nat-core -pytz==2026.1.post1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1 \ - --hash=sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a +pytz==2026.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126 \ + --hash=sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a # via # clickhouse-connect # oci @@ -2369,9 +2706,12 @@ pyyaml==6.0.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ @@ -2382,7 +2722,7 @@ pyyaml==6.0.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (p # via # data-designer-config # datasets - # fastmcp + # fastmcp-slim # garak-api # huggingface-hub # jsonschema-path @@ -2415,36 +2755,60 @@ ragas==0.3.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (pl --hash=sha256:164d5c0a96048d9c9373aa3e9123f0096649abbd2b58e747c2f0a454da6c2d6b \ --hash=sha256:3e917b12dc90ef692776263f66d220df40ff0573d2a96c8868198629f8b35206 # via nmp-evaluator -referencing==0.36.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa \ - --hash=sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0 +referencing==0.37.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ + --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 # via # jsonschema # jsonschema-path # jsonschema-specifications regex==2026.5.9 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611 \ --hash=sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3 \ + --hash=sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf \ --hash=sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c \ + --hash=sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0 \ --hash=sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc \ --hash=sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c \ --hash=sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21 \ + --hash=sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d \ + --hash=sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c \ --hash=sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9 \ --hash=sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026 \ + --hash=sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2 \ + --hash=sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020 \ --hash=sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06 \ --hash=sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0 \ --hash=sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e \ + --hash=sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2 \ + --hash=sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178 \ + --hash=sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e \ --hash=sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88 \ + --hash=sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107 \ + --hash=sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309 \ + --hash=sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2 \ --hash=sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919 \ --hash=sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270 \ --hash=sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c \ --hash=sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed \ --hash=sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2 \ --hash=sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff \ + --hash=sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100 \ + --hash=sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451 \ --hash=sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48 \ + --hash=sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621 \ + --hash=sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f \ --hash=sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb \ + --hash=sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6 \ + --hash=sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66 \ --hash=sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8 \ + --hash=sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041 \ + --hash=sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8 \ --hash=sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081 \ + --hash=sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04 \ + --hash=sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962 \ --hash=sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555 \ + --hash=sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d \ --hash=sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225 \ --hash=sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce \ --hash=sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b @@ -2454,9 +2818,9 @@ regex==2026.5.9 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # sqlfluff # tiktoken # transformers -requests==2.33.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517 \ - --hash=sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a +requests==2.34.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via # data-designer-config # datasets @@ -2466,7 +2830,6 @@ requests==2.33.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # garak-api # hvac # instructor - # jsonschema-path # kubernetes # langchain-classic # langchain-community @@ -2496,13 +2859,13 @@ requests-toolbelt==1.0.0 ; (platform_machine == 'arm64' and sys_platform == 'dar # via # langsmith # ngcsdk -rich==14.3.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d \ - --hash=sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b +rich==14.3.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952 \ + --hash=sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9 # via # cyclopts # data-designer-config - # fastmcp + # fastmcp-slim # instructor # nemo-agents-plugin # nemo-platform-ext @@ -2519,17 +2882,17 @@ rich==14.3.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (pl # rich-rst # rich-toolkit # typer -rich-argparse==1.7.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:0559b1f47a19bbeb82bf15f95a057f99bcbbc98385532f57937f9fc57acc501a \ - --hash=sha256:64fd2e948fc96e8a1a06e0e72c111c2ce7f3af74126d75c0f5f63926e7289cd1 +rich-argparse==1.8.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:679df3d832fa94ad6e4bdb07ded088cd7ea2dddc58ae9b2b46346a40b06cbc0c \ + --hash=sha256:d2a3ce7854654e2253c578763ab0a32f05016f23a55fadba7b9a91b6c0e92142 # via nmp-platform -rich-rst==1.3.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4 \ - --hash=sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a +rich-rst==2.0.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:7ee15f345ce25fa02b582c272a6cdbaf0c21243e38061cea273cff659bf3ef61 \ + --hash=sha256:cbe236ed0901d1ec8427cc6a50bf0a34353ba28ad014dc24def68bfe7f3b9e68 # via cyclopts -rich-toolkit==0.19.7 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:0288e9203728c47c5a4eb60fd2f0692d9df7455a65901ab6f898437a2ba5989d \ - --hash=sha256:133c0915872da91d4c25d85342d5ec1dfacc69b63448af1a08a0d4b4f23ef46e +rich-toolkit==0.20.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:2a6d5f8e15759b9eba5a9ee63da10b275359ead20e5a0fc92bd5b4dbae8ce4bf \ + --hash=sha256:c7336ae281f435c785acecaedc4b71d4b663dc73d9c8079fea96372527e822a4 # via # fastapi-cli # fastapi-cloud-cli @@ -2545,57 +2908,99 @@ rignore==0.7.6 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or ( --hash=sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7 \ --hash=sha256:392dcabfecbe176c9ebbcb40d85a5e86a5989559c4f988c2741da7daf1b5be25 \ --hash=sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696 \ + --hash=sha256:53fb28882d2538cb2d231972146c4927a9d9455e62b209f85d634408c4103538 \ + --hash=sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5 \ + --hash=sha256:5991e46ab9b4868334c9e372ab0892b0150f3f586ff2b1e314272caeb38aaedb \ + --hash=sha256:62020dbb89a1dd4b84ab3d60547b3b2eb2723641d5fb198463643f71eaaed57d \ + --hash=sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e \ + --hash=sha256:684014e42e4341ab3ea23a203551857fcc03a7f8ae96ca3aefb824663f55db32 \ --hash=sha256:6e01cad2b0b92f6b1993f29fc01f23f2d78caf4bf93b11096d28e9d578eb08ce \ --hash=sha256:77356ebb01ba13f8a425c3d30fcad40e57719c0e37670d022d560884a30e4767 \ + --hash=sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b \ + --hash=sha256:87409f7eeb1103d6b77f3472a3a0d9a5953e3ae804a55080bdcb0120ee43995b \ --hash=sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010 \ + --hash=sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a \ --hash=sha256:aaf938530dcc0b47c4cfa52807aa2e5bfd5ca6d57a621125fe293098692f6345 \ + --hash=sha256:b34acd532769d5a6f153a52a98dcb81615c949ab11697ce26b2eb776af2e174d \ + --hash=sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e \ --hash=sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961 \ + --hash=sha256:ba5524f5178deca4d7695e936604ebc742acb8958f9395776e1fcb8133f8257a \ --hash=sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a \ + --hash=sha256:c081f17290d8a2b96052b79207622aa635686ea39d502b976836384ede3d303c \ --hash=sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c \ + --hash=sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2 \ --hash=sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd \ + --hash=sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0 \ --hash=sha256:d8955b57e42f2a5434670d5aa7b75eaf6e74602ccd8955dddf7045379cd762fb \ - --hash=sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360 + --hash=sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360 \ + --hash=sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116 # via fastapi-cloud-cli rouge-score==0.1.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:c7d4da2683e68c9abf0135ef915d63a46643666f848e558a1b9f7ead17ff0f04 # via nemo-evaluator-sdk -rpds-py==0.30.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4 \ - --hash=sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89 \ - --hash=sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85 \ - --hash=sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb \ - --hash=sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4 \ - --hash=sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23 \ - --hash=sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27 \ - --hash=sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083 \ - --hash=sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738 \ - --hash=sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7 \ - --hash=sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05 \ - --hash=sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5 \ - --hash=sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394 \ - --hash=sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6 \ - --hash=sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e \ - --hash=sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95 \ - --hash=sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e \ - --hash=sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94 \ - --hash=sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28 \ - --hash=sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000 \ - --hash=sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7 \ - --hash=sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d \ - --hash=sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84 \ - --hash=sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a \ - --hash=sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8 \ - --hash=sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a +rpds-py==2026.5.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:01d17b29c0c23d82b1f4751147ec49cf451f1fc2554eb9ef5f957e55d2656ead \ + --hash=sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4 \ + --hash=sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256 \ + --hash=sha256:0b35217adefe87f2fe4db7e9766cabe84744bfe9616d9667be18988928c7f2dc \ + --hash=sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08 \ + --hash=sha256:141c9498daf2ace9eda35d2b0e376f9ea8b058d84f2aef4f96fccfd449a2f251 \ + --hash=sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b \ + --hash=sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db \ + --hash=sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d \ + --hash=sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0 \ + --hash=sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b \ + --hash=sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5 \ + --hash=sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89 \ + --hash=sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732 \ + --hash=sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef \ + --hash=sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c \ + --hash=sha256:613fc4ee9eaef26dc5840666214dd6fbcebcf32f46e76f4abc473059f4e13dda \ + --hash=sha256:6142dbd80c4df62a5d899f0d616d417f84e0bc8d32526c8e5589019d75d028a7 \ + --hash=sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02 \ + --hash=sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1 \ + --hash=sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00 \ + --hash=sha256:6f249f8b860a200ad35193af961183ebe9132710484e6f6ce0cf89fd83c63a9a \ + --hash=sha256:7559f72b94ae52659086c595dfa017cde03155f7832071d30959049052cb3ece \ + --hash=sha256:85264a90ff4c05c1568dd65f5921c837614b67c60358fb4c17df3b7f2e90690a \ + --hash=sha256:8895840ac4809e5f60c88fd07617cd71326e73d6e5a8aa783c5c0f7c24985de2 \ + --hash=sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf \ + --hash=sha256:8c43a8a973270fd173bf48cdf80bbe66312421cba68d40845034f174f2389049 \ + --hash=sha256:99ab6ba7bfa2cb0f96a04e3652355bf04e3f51aceb1e943b8541dab7ba4828cc \ + --hash=sha256:9e25b7088f9ccbfc0dfcaa52bf969300ca229e10ecf758974ebcbb080a4b37bb \ + --hash=sha256:a04df86b3f0fade39ec8fd0e0aab089b1da9fbd2b48df778a57ef96f5e7d38df \ + --hash=sha256:a05fa4f41f37ec97c9c260441a940450a192f78d774d2b097eee1379f1e1246a \ + --hash=sha256:ad3773236e95f7f33991eb125224b7da66f206504d032a253a02da7e134519fb \ + --hash=sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e \ + --hash=sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559 \ + --hash=sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644 \ + --hash=sha256:b95d5e11fc712b752081183a55a244c03cd00570489edd7014d8899f8ceb8162 \ + --hash=sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83 \ + --hash=sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3 \ + --hash=sha256:c74005a7bb87752acf351c93897ec63ad77a07a0da7ecad9c050e32e7286ba34 \ + --hash=sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb \ + --hash=sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa \ + --hash=sha256:d0efbe45632665e53e3db8fe1e5692db58fc5cb9bab4459d570b83efefe11164 \ + --hash=sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97 \ + --hash=sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4 \ + --hash=sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3 \ + --hash=sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd # via # jsonschema # referencing ruff==0.15.7 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac \ --hash=sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580 \ + --hash=sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12 \ + --hash=sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036 \ --hash=sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c \ --hash=sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf \ --hash=sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e \ - --hash=sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5 + --hash=sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85 \ + --hash=sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e \ + --hash=sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4 \ + --hash=sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5 \ + --hash=sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912 # via data-designer-engine s3transfer==0.14.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456 \ @@ -2607,18 +3012,18 @@ sacrebleu==2.6.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # via # nemo-evaluator-sdk # nemoplatform -safetensors==0.8.0rc1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:3a72817e309ed17a6b805168bca500af711c8bf50cccf7bf790ad247788c676c \ - --hash=sha256:53f59971f435fb5c23bb7bfa3c00e6cdbb26486d41f3aaf04c6d9e2d91bc8e4c \ - --hash=sha256:6ea00be4f7066ba834064fd7b104ba4b10d2e42cd915c9ece31f0e2b42e6b6d2 \ - --hash=sha256:87aad206a0bb02fa3ddaeb2feb1c6f7f39a87bea1976750ba28a857fbfcd6b31 \ - --hash=sha256:a4bacbcd2ab9efe4eb5f1ea44afc9ac5f3b40e103ebde146e370e885ba46f2fc \ - --hash=sha256:b070ba9428e6b2b3b820152b1e1583931cfbd692cda595442d5fbc8b5a46e824 \ - --hash=sha256:ba0397739b71400eab5d1f492d68484595cf8b5d13dbfef274e3bcf518348bd8 \ - --hash=sha256:ba66ebb7eaa5914ff41cd2b2cd7ccd22c844854be2a5179289e748c70ffa90a4 \ - --hash=sha256:c945f1ec6fc5a04abc174e79bce69c4613ae536c5276a3812a2a89b62ae009d7 \ - --hash=sha256:cf949f6a37287572de1c47294b36131bf49528436724eec2f96015f75a3d0bc8 \ - --hash=sha256:f61b7d2c1babc6271d778138788c57c8a95898be6ad3b559971fce20ec1c9c23 +safetensors==0.8.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358 \ + --hash=sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0 \ + --hash=sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc \ + --hash=sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235 \ + --hash=sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98 \ + --hash=sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4 \ + --hash=sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846 \ + --hash=sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25 \ + --hash=sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d \ + --hash=sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78 \ + --hash=sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774 # via transformers scikit-network==0.33.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:2408d3f4c81256a3193d536aad4a6ffcfbb05d096abe6a9cc0b6b5e275df876d \ @@ -2665,9 +3070,9 @@ secretstorage==3.5.0 ; (platform_machine == 'aarch64' and sys_platform == 'linux --hash=sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137 \ --hash=sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be # via keyring -sentry-sdk==2.57.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:4be8d1e71c32fb27f79c577a337ac8912137bba4bcbc64a4ec1da4d6d8dc5199 \ - --hash=sha256:812c8bf5ff3d2f0e89c82f5ce80ab3a6423e102729c4706af7413fd1eb480585 +sentry-sdk==2.62.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:27f61d13a86c3c1648dec666dd5a64f79772dd6a84b446f11866601ecab24f6f \ + --hash=sha256:3c870b9f50d9fd15b58c817dbde1c7cfaa9fe3f05df0a4c6edd5571cb82f5491 # via fastapi-cloud-cli setuptools==82.0.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9 \ @@ -2694,9 +3099,9 @@ six==1.17.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (pla # kubernetes # python-dateutil # rouge-score -smart-open==7.0.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:8523ed805c12dff3eaa50e9c903a6cb0ae78800626631c5fe7ea073439847b89 \ - --hash=sha256:d3672003b1dbc85e2013e4983b88eb9a5ccfd389b0d4e5015f39a9ee5620ec18 +smart-open==7.6.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:4347996e7ba21db7cd1e059632e0b30395407e4f6c660d2ddffc8f2a9ae5f990 \ + --hash=sha256:b4de6aebef023aca91cc9fb372052e1343ba3f152de215bd22391a663e3ddd21 # via streaming-form-data smmap==5.0.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c \ @@ -2710,32 +3115,28 @@ sniffio==1.3.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or ( # nemo-platform-sdk # openai # pyleak -soupsieve==2.8.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349 \ - --hash=sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95 +soupsieve==2.8.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e \ + --hash=sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65 # via beautifulsoup4 -sqlalchemy==2.0.48 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e \ - --hash=sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc \ - --hash=sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b \ - --hash=sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f \ - --hash=sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894 \ - --hash=sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b \ - --hash=sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8 \ - --hash=sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb \ - --hash=sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9 \ - --hash=sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658 \ - --hash=sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7 \ - --hash=sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae \ - --hash=sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7 \ - --hash=sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d \ - --hash=sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096 \ - --hash=sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed \ - --hash=sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4 \ - --hash=sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571 \ - --hash=sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c \ - --hash=sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121 \ - --hash=sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb +sqlalchemy==2.0.50 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:03f4323c980ad0e918cc9e5369b015f759f4e534db5bbaf4dc36832c10d05064 \ + --hash=sha256:06a9210bdc5f4298cff0781087e2ff45683922252dacc452846373a58761f093 \ + --hash=sha256:0f5e4ac70e9e757f6b3e87c0491ff034442ecd8dfd36d041a50564c322dafc0e \ + --hash=sha256:0fec460e18cdbb4c7773531122ce9a27e96c6ca17af3933941d94da475ad2c86 \ + --hash=sha256:110fdac56ace278949f00de805edacbd6141e382d992f9ba28238b3a0827a600 \ + --hash=sha256:1aa6e403663a9c43c8fef7ce4bdb4cf48bcd8d352e91deda2a99f963270bd508 \ + --hash=sha256:23ae23d8b9d344d30d0a92f06d45825024a5790f1c1dd4cf452636a50d3e58cb \ + --hash=sha256:2b9dcc43afef8ac157cd92fce96985d6b8b0cfbd3df4d666f66b4d55a75d202f \ + --hash=sha256:2dab927761d9108550f0cf8e66ff21af56f907a0ce0a689793db615e2b55f62c \ + --hash=sha256:31648fa14460537e768a7303b078e4344d208e0d23e06867c1f376a227ed82db \ + --hash=sha256:47b71b933e7b4ebad407c8fdfd70d2c4f08b78b3238bb30eebdd6eb32ca51b89 \ + --hash=sha256:51b637a84f9fa35ae1f9017e786cb142974a25305085e1b378b3647a67f65ad3 \ + --hash=sha256:545eae198d37bcf837a10ede3684e2af32458d6f35c597c35c2de7502dc38fc4 \ + --hash=sha256:724f3dcbe53dd0151e3cb5e7ec4ba4c620bede579caacd16275dc35ce06e8615 \ + --hash=sha256:8b53784972ade4f8174b9aa661f31a06f8a936d2cfdd602913ff3c6dd40ae873 \ + --hash=sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9 \ + --hash=sha256:af5607d11ef90fd6a5c0549fe0045dce1663d427426bcfb506dcb5346a85a3b9 # via # alembic # langchain-classic @@ -2748,16 +3149,16 @@ sqlfluff==4.1.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or --hash=sha256:83dd4c081afb48c0af861833015a18b13d52726bfe52a286246dbd7a64b7d111 \ --hash=sha256:ae11123ca4a697abadbd2783f85f04e58c36e7dd26ae8024f400efccc6a44631 # via data-designer-engine -sqlmodel==0.0.37 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:2137a4045ef3fd66a917a7717ada959a1ceb3630d95e1f6aaab39dd2c0aef278 \ - --hash=sha256:d2c19327175794faf50b1ee31cc966764f55b1dedefc046450bc5741a3d68352 +sqlmodel==0.0.38 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:84e3fa990a77395461ded72a6c73173438ce8449d5c1c4d97fbff1b1df692649 \ + --hash=sha256:d583ec237b14103809f74e8630032bc40ab68cd6b754a610f0813c56911a547b # via # nmp-evaluator # nmp-jobs # nmp-models -sse-starlette==3.3.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1 \ - --hash=sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1 +sse-starlette==3.4.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0 \ + --hash=sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973 # via mcp starlette==0.52.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74 \ @@ -2782,16 +3183,12 @@ streaming-form-data==2.0.0 ; (platform_machine == 'arm64' and sys_platform == 'd --hash=sha256:cebbcdf31e38bb3569d5cafc2f8cbcf9b8da5298eaff2464b5ba224abc915a29 \ --hash=sha256:e7bdbf78a5c44b2d1816300f5b461138c33efde774560e849c36302077dfe9a3 # via nmp-files -structlog==25.5.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:098522a3bebed9153d4570c6d0288abf80a031dfdb2048d59a49e9dc2190fc98 \ - --hash=sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f +structlog==26.1.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:e081a26d6c373e6d201eca24eede26d8ffab07f88f477822e679183428d3d91e \ + --hash=sha256:f63a716cbd1b1291cf7661de7794b455acfa4c43c5bcf1630e6ad5ddc1adb3b7 # via # nemo-safe-synthesizer # nmp-common -sympy==1.14.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ - --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 - # via onnxruntime tabulate==0.10.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 @@ -2812,28 +3209,28 @@ tenacity==9.1.4 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # nmp-automodel # nmp-models # nmp-unsloth -tiktoken==0.12.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa \ - --hash=sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e \ - --hash=sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb \ - --hash=sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25 \ - --hash=sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b \ - --hash=sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5 \ - --hash=sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded \ - --hash=sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be \ - --hash=sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd \ - --hash=sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37 \ - --hash=sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3 \ - --hash=sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a \ - --hash=sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3 \ - --hash=sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160 \ - --hash=sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967 \ - --hash=sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931 \ - --hash=sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a \ - --hash=sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa \ - --hash=sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad \ - --hash=sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc \ - --hash=sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27 +tiktoken==0.13.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:059c8ecf554eb5b41e6e054ba467b871b03277d267dee7244380aca4359747d4 \ + --hash=sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2 \ + --hash=sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f \ + --hash=sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b \ + --hash=sha256:303f7d91b4fce3baddbcde05c139091d4caa5026ac7214c1dc7ff7a71ee429ff \ + --hash=sha256:36217497eaffc158607a3b26f065300db2aefd43b115263f3b9688ce38146173 \ + --hash=sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7 \ + --hash=sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a \ + --hash=sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b \ + --hash=sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce \ + --hash=sha256:5d48843bee149630eb735a99e1f4a85b47308d21868ea63163f6e87768d3cfed \ + --hash=sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf \ + --hash=sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615 \ + --hash=sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d \ + --hash=sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545 \ + --hash=sha256:91c180fe255bd5a86d8316210d2833a1d4d33d026cd86a67812f4773743c8d26 \ + --hash=sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91 \ + --hash=sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486 \ + --hash=sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649 \ + --hash=sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1 \ + --hash=sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2 # via # data-designer-engine # langchain-openai @@ -2841,32 +3238,36 @@ tiktoken==0.12.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or # nemo-anonymizer # ragas tokenizers==0.22.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e \ --hash=sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001 \ --hash=sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7 \ + --hash=sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd \ --hash=sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4 \ --hash=sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67 \ + --hash=sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a \ --hash=sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5 \ - --hash=sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917 + --hash=sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917 \ + --hash=sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b # via # fastembed # langchain-huggingface # litellm # transformers -tomlkit==0.14.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680 \ - --hash=sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064 +tomlkit==0.15.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738 \ + --hash=sha256:7d1a9ecba3086638211b13814ea79c90dd54dd11993564376f3aa92271f5c7a3 # via nemoplatform -tornado==6.5.5 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9 \ - --hash=sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca \ - --hash=sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e \ - --hash=sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07 \ - --hash=sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa \ - --hash=sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5 +tornado==6.5.7 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163 \ + --hash=sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2 \ + --hash=sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92 \ + --hash=sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b \ + --hash=sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972 \ + --hash=sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5 # via nemoplatform -tqdm==4.67.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb \ - --hash=sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf +tqdm==4.68.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:89c230e8dbc67c7615c142487111222f878c77427ea09549960f62389e258add \ + --hash=sha256:d4240441fb5353290b87d6a85968c9decc131a99b8c7faa28269d829de669ede # via # datasets # fastembed @@ -2878,13 +3279,13 @@ tqdm==4.67.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (pl # ragas # sqlfluff # transformers -transformers==5.5.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:821a9ff0961abbb29eb1eb686d78df1c85929fdf213a3fe49dc6bd94f9efa944 \ - --hash=sha256:c8db656cf51c600cd8c75f06b20ef85c72e8b8ff9abc880c5d3e8bc70e0ddcbd +transformers==5.10.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:8a669db546f82c7c3618cb46ceb0f0afd89292bc70f319c058f8332ec63e268d \ + --hash=sha256:f9a44b9c8ca9ab1156b467f574d832ea066284299c2fd0ed84641ccb592751fc # via nemo-customizer-plugin -typer==0.24.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e \ - --hash=sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45 +typer==0.25.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89 \ + --hash=sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc # via # data-designer # fastapi-cli @@ -2907,17 +3308,17 @@ types-aioboto3==15.5.0 ; (platform_machine == 'arm64' and sys_platform == 'darwi --hash=sha256:5769a1c3df7ca1abedf3656ddf0b970c9b0436d0f88cf4686040b55cd2a02925 \ --hash=sha256:8aed7c9b6fe9b59e6ce74f7a6db7b8a9912a34c8f80ed639fac1fa59d6b20aa1 # via nmp-files -types-aiobotocore==3.3.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:017e9666d5cba2c26134256ad5e4efb320a68352358b9f3257b4e2aae3fb4c18 \ - --hash=sha256:c754c2888631d56c370cab4d2108da2bfd3afe80049303fb7132004ead3b21d6 +types-aiobotocore==3.7.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:fe35de52c12e5fdb89ca60b3989766e7fe827e3d2e95fcf4583e91581945205c \ + --hash=sha256:ff4139b3eae22d242b6b39ba56048344b2b86f67daeeca4680da1a6e191681fd # via types-aioboto3 types-aiobotocore-s3==2.25.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:151301e84bb2f1cbf30f0d1ef791bb75c141cfbfe47b93fd317b7f1ba3eb35e4 \ --hash=sha256:678aa425491af19bd6d011d59ecdbbb7ae7e95800efddcf4fd559ab72c94e194 # via types-aioboto3 -types-awscrt==0.31.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:09d3eaf00231e0f47e101bd9867e430873bc57040050e2a3bd8305cb4fc30865 \ - --hash=sha256:e5ce65a00a2ab4f35eacc1e3d700d792338d56e4823ee7b4dbe017f94cfc4458 +types-awscrt==0.34.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:20c752b6031544d8f694803c35174aee129f1be5ddf886ae46d22f7ffd9b7d75 \ + --hash=sha256:559aa04250f6a419a617dfb788f3e10903aaf74700ef23e521b64a411b83b803 # via botocore-stubs types-s3transfer==0.16.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:1c0cd111ecf6e21437cb410f5cddb631bfb2263b77ad973e79b9c6d0cb24e0ef \ @@ -2927,6 +3328,7 @@ typing-extensions==4.15.0 ; (platform_machine == 'arm64' and sys_platform == 'da --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via + # aiohttp # aiosignal # alembic # anthropic @@ -2935,6 +3337,7 @@ typing-extensions==4.15.0 ; (platform_machine == 'arm64' and sys_platform == 'da # exa-py # exceptiongroup # fastapi + # fastmcp-slim # grpcio # huggingface-hub # langchain-core @@ -2955,6 +3358,7 @@ typing-extensions==4.15.0 ; (platform_machine == 'arm64' and sys_platform == 'da # referencing # rich-toolkit # sqlalchemy + # sqlmodel # starlette # types-aioboto3 # types-aiobotocore @@ -2972,18 +3376,19 @@ typing-inspection==0.4.2 ; (platform_machine == 'arm64' and sys_platform == 'dar # fastapi # mcp # pydantic -tzdata==2025.3 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1 \ - --hash=sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7 + # pydantic-settings +tzdata==2026.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10 \ + --hash=sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7 # via pandas tzlocal==5.3.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd \ --hash=sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d # via nvidia-nat-core -uncalled-for==0.2.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:2c0bd338faff5f930918f79e7eb9ff48290df2cb05fcc0b40a7f334e55d4d85f \ - --hash=sha256:b4f8fdbcec328c5a113807d653e041c5094473dd4afa7c34599ace69ccb7e69f - # via fastmcp +uncalled-for==0.3.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:0ff60b142c7d1f8070bde9d42afaa70aedc77dcc10998c227687e9c15713418e \ + --hash=sha256:89f5dbcd71e2b8f47c030b1fa302e6cce2ec795d1ac565eeb6525c5fe55cb8a2 + # via fastmcp-slim urllib3==2.7.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 @@ -2999,27 +3404,57 @@ urllib3==2.7.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or ( # oci # requests # sentry-sdk -uuid-utils==0.14.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:0b5d2ad28063d422ccc2c28d46471d47b61a58de885d35113a8f18cb547e25bf \ - --hash=sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0 \ - --hash=sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69 \ - --hash=sha256:b197cd5424cf89fb019ca7f53641d05bfe34b1879614bed111c9c313b5574cd8 \ - --hash=sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1 \ - --hash=sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43 \ - --hash=sha256:c1dbe718765f70f5b7f9b7f66b6a937802941b1cc56bcf642ce0274169741e01 \ - --hash=sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297 \ - --hash=sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862 +uuid-utils==0.16.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:04af9966ecd82b78eeba5725e29aa1e86fb8eb84b5443dd6a9935f9fadb6678e \ + --hash=sha256:06fc7db470c37e5c1ab3fd2cd159697d6f8b279d7d23b5b96bd418b115f8caa9 \ + --hash=sha256:130f7452c1b87b7c16d0bdc1f32a1de531ae4cc4220ed4e691402bbcfc39e0a9 \ + --hash=sha256:13a797e5e8f0dadc18351a5aa013815ddac25dce6864072a539d510910c95f71 \ + --hash=sha256:1c3c5afaaa68b1d6393d653e9fc93a2fde9da1681da01f74b4593f41d31fb5f1 \ + --hash=sha256:24e6fa0d0ade7a9ad60a3c296022474983243df5b4e863babb4828a85ef2e52c \ + --hash=sha256:26fe23ab60f05de4ad70aaa5b6a4c2a7bbd43055e3dd6f6b31efba0532ac9c71 \ + --hash=sha256:27a071a899ba46a551d6524dbbc5a98b88be176d0f55ddf72cf71c005326ac10 \ + --hash=sha256:2e2f369dd734050fe96ae4905c58779b09276d47d5e9a0e5cd33ec7982784341 \ + --hash=sha256:39453f1ebf4398fbeb71607f3437e2ac469c9e38b5921755c1e17ad0158a8907 \ + --hash=sha256:3e1a1f57fe3631e164dad27b24aa81267810e20575f705af3b0fa734f3a21247 \ + --hash=sha256:3ee392fe59808a731b7b6bf4d453fb6e833774921331cceae5f254d1e9c5b97d \ + --hash=sha256:41985e342a30e76366a8becc60bbdb07d72cd1b86ec657b1f31654e9fb1baada \ + --hash=sha256:41a67e546d9adf11c4e4cb5c8e81f000f8b1f000c17912ced089b499855719a5 \ + --hash=sha256:420aa3ca403cedb73490b6ea3aeefeea7e0455f5ce60bbf856390ee872ae3306 \ + --hash=sha256:4e35e9a986e86806a61288fac3afbb51317f2580929feefd1661891ffd7b8c24 \ + --hash=sha256:50361aca5c2a770728a6343df85109fe57f89ac026827f34fe0153563cdc9ce7 \ + --hash=sha256:57c3583b1f1c00a94f59726a5e2b988fa209221143919a1af5c2fc24e318fc98 \ + --hash=sha256:57d85f48535dc541060f6b82f277cbcd12b78c04008ccc1039546cfcec027327 \ + --hash=sha256:727fae3f0682191ec9c8ce1cd0f71e81b471a2e26b7c5fd66712fc0f11640aa0 \ + --hash=sha256:73486b6aa3f755a6c97000f5ea67e7ac78d6df89bf22980789a1e943e24b74f0 \ + --hash=sha256:9152bff801ec2ccf630df06d67389090a2c612dea87fbf9a887ab4b222929f6f \ + --hash=sha256:91db59bad97ed2b9d2c6ed25082fe9762b2c422e694fe06786b28cf4e776ac4c \ + --hash=sha256:9346ce6eb1fbd8b03a6b331d66016afcb4edcdff6eac708e21391600529a016a \ + --hash=sha256:9a250e111903c4368745fce5ac2aa607bd477c62d3307e45347338fdb64b38e0 \ + --hash=sha256:a0fc6eb3fd821466fbab69cf356c6ec2b7327266bbbc740a2eb57c77c4bef965 \ + --hash=sha256:a4fd5c7936a876ba2606ba124603b559a5c2cea458c59b9c31677e6acc3c53cc \ + --hash=sha256:a750d8aeb8ae880aa9a2529606bde0e994bcc7448730c953107f357a28e6102e \ + --hash=sha256:b617a334bb01ef2ff8c22900f5a14125eb9063f602131494cc9dc59519beaa5b \ + --hash=sha256:b8a9a7b1065a12d40f2cc25b7d705ab34954cc57095034367bca39ebcf4a876b \ + --hash=sha256:bbb92feb4db08cd76e27b4d3b1a82bfde708447317150c614eb9f761a43b387e \ + --hash=sha256:c8083284488b84ad178e74add64cfd1e74e8be5e30821e5acbc5019281c658b0 \ + --hash=sha256:c9f504efeb20ffd9571621658f7c8093c646d33150406d5742e49ff7cd861615 \ + --hash=sha256:ceef237cf8467fddbf6d8466cc1f6e2c04605ec919046ef5eba10a895b559fcf \ + --hash=sha256:d34cf9681e8892fad2a63e393068e544505408748cd8bf0c3517d753a01528d4 \ + --hash=sha256:d5ee0bbbd4ca3968422cd8308f0072520bc73dc760cb26c6fa75ca1aca14d210 \ + --hash=sha256:d6902d4375dfba4c9902c736bb82d3c040417b67f7d0fa48910ddfdb1ac95de7 \ + --hash=sha256:dc0824a31898ef46a9d84d748c3abe27cdb615ac3773c53cc1f84fc8e66dc7c4 \ + --hash=sha256:ed45fb8732d216426227096b55accbb87cba57febc86a044d90780b090eb99d0 # via # langchain-core # langsmith -uvicorn==0.42.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359 \ - --hash=sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775 +uvicorn==0.49.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f \ + --hash=sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3 # via # fastapi # fastapi-cli # fastapi-cloud-cli - # fastmcp + # fastmcp-slim # mcp # nemo-safe-synthesizer-plugin # nemoguardrails @@ -3062,57 +3497,77 @@ validators==0.35.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') --hash=sha256:992d6c48a4e77c81f1b4daba10d16c3a9bb0dbb79b3a19ea847ff0928e70497a \ --hash=sha256:e8c947097eae7892cb3d26868d637f79f47b4a0554bc6b80065dfe5aac3705dd # via ngcsdk -wasmtime==43.0.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:30b042fd4a05d0f8a320baed53fcb971aff8a3789ed6967f4521f87931ace717 \ - --hash=sha256:341542e87caf1f2ef7ff648a78827fcef5751e3e9be2ee07a1fcf3a04413c213 \ - --hash=sha256:34ff18384ad62625cb1438fd0266f6c74b4a72ddcb8ba30c60a66be3632db44b \ - --hash=sha256:5a03c7aa03519df58fed5115ad8093d6deac46386115add715e725448e89ab25 \ - --hash=sha256:9441349d9346230420ed24d357d6f8330fe7251ac5938bb892147728bbe731d7 \ - --hash=sha256:c7025d477d807df30dad07c9318ea747c6cfc99764c7cb2a8e44e75b8c43e3be \ - --hash=sha256:eb98b8e2bc35d03dd69c9dd095a388044323622526fc94a9406b8efc48ddc259 +wasmtime==45.0.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:2ad4bf7ca286ceea35c1e420d10b368d7f83faf9a5ffde87b4ee334a9b7f55f3 \ + --hash=sha256:31d10f25c330cebcfb364e9a357123deeec96c41725ff2bba91b705587f38a93 \ + --hash=sha256:5d1416ec6da8cd87c29e2e9eb074358c91839c2fff971fe428c8921eaae68e73 \ + --hash=sha256:6251ee5074a8b8bfaa98e6e99cb5d49d6d0f2320b3265d5aa6c2ee5df5fb4519 \ + --hash=sha256:a0b6ca14b4628a5d1ffa91ccf2c0f2c58fa171f126ec085d564b09d5795395dd \ + --hash=sha256:a499f6ab0eebb70dca83d6a4904b743cd122f322af3abe86af08ad753533d946 \ + --hash=sha256:bef65282b7de744106a91da43e4d06ba19d2d587bc54abb83b3e757f0c4fc030 # via nmp-auth watchdog==6.0.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2 \ + --hash=sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f \ + --hash=sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c \ --hash=sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c \ --hash=sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c \ --hash=sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0 \ --hash=sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13 \ + --hash=sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379 \ --hash=sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282 \ --hash=sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b \ --hash=sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c \ - --hash=sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948 + --hash=sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948 \ + --hash=sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26 # via nemoguardrails -watchfiles==1.1.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219 \ - --hash=sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803 \ - --hash=sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94 \ - --hash=sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43 \ - --hash=sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10 \ - --hash=sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374 \ - --hash=sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051 \ - --hash=sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49 \ - --hash=sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77 \ - --hash=sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741 \ - --hash=sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a \ - --hash=sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701 \ - --hash=sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6 \ - --hash=sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef \ - --hash=sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af \ - --hash=sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336 \ - --hash=sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2 \ - --hash=sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606 \ - --hash=sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610 \ - --hash=sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b \ - --hash=sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d \ - --hash=sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24 \ - --hash=sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e \ - --hash=sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf - # via - # fastmcp +watchfiles==1.2.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9 \ + --hash=sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db \ + --hash=sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5 \ + --hash=sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427 \ + --hash=sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4 \ + --hash=sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa \ + --hash=sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906 \ + --hash=sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c \ + --hash=sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c \ + --hash=sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01 \ + --hash=sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9 \ + --hash=sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658 \ + --hash=sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5 \ + --hash=sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0 \ + --hash=sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5 \ + --hash=sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8 \ + --hash=sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1 \ + --hash=sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44 \ + --hash=sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5 \ + --hash=sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a \ + --hash=sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc \ + --hash=sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0 \ + --hash=sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e \ + --hash=sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0 \ + --hash=sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7 \ + --hash=sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55 \ + --hash=sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb \ + --hash=sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0 \ + --hash=sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3 \ + --hash=sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838 \ + --hash=sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71 \ + --hash=sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d \ + --hash=sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44 \ + --hash=sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2 \ + --hash=sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b \ + --hash=sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6 \ + --hash=sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165 \ + --hash=sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5 \ + --hash=sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72 \ + --hash=sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4 + # via + # fastmcp-slim # uvicorn -wcwidth==0.6.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad \ - --hash=sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159 +wcwidth==0.8.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:f453740b1e4a4f3291faa37944c555d71056c4da08d59809b307ef4feba695c8 \ + --hash=sha256:faf5b4a5366a72dc49cad48cdf21f52bdf63bdda995178e483ba247ff79089b9 # via # prettytable # prompt-toolkit @@ -3120,32 +3575,30 @@ websocket-client==1.9.0 ; (platform_machine == 'arm64' and sys_platform == 'darw --hash=sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98 \ --hash=sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef # via kubernetes -websockets==16.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c \ - --hash=sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe \ - --hash=sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec \ - --hash=sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64 \ - --hash=sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8 \ - --hash=sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2 \ - --hash=sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03 \ - --hash=sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8 \ - --hash=sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5 \ - --hash=sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f \ - --hash=sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00 \ - --hash=sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b \ - --hash=sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39 \ - --hash=sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9 \ - --hash=sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5 \ - --hash=sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c \ - --hash=sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1 \ - --hash=sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d \ - --hash=sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82 \ - --hash=sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5 \ - --hash=sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f \ - --hash=sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c \ - --hash=sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da - # via - # fastmcp +websockets==15.0.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \ + --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \ + --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \ + --hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \ + --hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \ + --hash=sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf \ + --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \ + --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \ + --hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \ + --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \ + --hash=sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431 \ + --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \ + --hash=sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413 \ + --hash=sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8 \ + --hash=sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905 \ + --hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \ + --hash=sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562 \ + --hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \ + --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \ + --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f + # via + # fastmcp-slim + # langgraph-sdk # uvicorn wikipedia==1.4.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:db0fad1829fdd441b1852306e9856398204dc0786d2996dd2e0c8bb8e26133b2 @@ -3182,30 +3635,66 @@ xdg-base-dirs==6.0.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin' --hash=sha256:3c01d1b758ed4ace150ac960ac0bd13ce4542b9e2cdf01312dcda5012cfebabe \ --hash=sha256:950504e14d27cf3c9cb37744680a43bf0ac42efefc4ef4acf98dc736cab2bced # via garak-api -xxhash==3.6.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8 \ - --hash=sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa \ - --hash=sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae \ - --hash=sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d \ - --hash=sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2 \ - --hash=sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3 \ - --hash=sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db \ - --hash=sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033 \ - --hash=sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f \ - --hash=sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd \ - --hash=sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1 \ - --hash=sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263 \ - --hash=sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13 \ - --hash=sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62 \ - --hash=sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2 \ - --hash=sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204 \ - --hash=sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9 \ - --hash=sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd \ - --hash=sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0 \ - --hash=sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6 \ - --hash=sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd \ - --hash=sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7 \ - --hash=sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee +xxhash==3.7.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:030c0fd688fce3569fbb49a2feefd4110cbb0b650186fb4610759ecfac677548 \ + --hash=sha256:03f8ff4474ee61c845758ce00711d7087a770d77efb36f7e74a6e867301000b8 \ + --hash=sha256:073c23900a9fbf3d26616c17c830db28af9803677cd5b33aea3224d824111514 \ + --hash=sha256:0c72fe9c7e3d6dfd7f1e21e224a877917fa09c465694ba4e06464b9511b65544 \ + --hash=sha256:0ff71596bd79816975b3de7130ab1ff4541410285a3c084584eeb1c8239996fd \ + --hash=sha256:13805f0461cba0a857924e70ff91ae6d52d2598f79a884e788db80532614a4a1 \ + --hash=sha256:14bf7a54e43825ec131ee7fe3c60e142e7c2c1e676ad0f93fc893432d15414af \ + --hash=sha256:151d7520838d4465461a0b7f4ae488b3b00de16183dd3214c1a6b14bf89d7fb6 \ + --hash=sha256:1910df4756a5ab58cfad8744fc2d0f23926e3efcc346ee76e87b974abab922f4 \ + --hash=sha256:1ad86695c19b1d46fe106925db3c7a37f16be37669dcf58dcc70a9dd6e324676 \ + --hash=sha256:1cc07c639e3a77ef1d32987464d3e408565b8a3be57b545d3542b191054d9923 \ + --hash=sha256:1d398f372496152f1c6933a33566373f8d1b37b98b8c9d608fa6edc0976f23b2 \ + --hash=sha256:2220af08163baf5fa36c2b8af079dc2cbe6e66ae061385267f9472362dfd53c6 \ + --hash=sha256:31ab1461c77a11461d703c88eb949e132a1c6515933cf675d97ec680f4bd18de \ + --hash=sha256:31e3516a0f829d06ded4a2c0f3c7c5561993256bfa1c493975fb9dc7bfa828a1 \ + --hash=sha256:347a93f2b4ce67ce61959665e32a7447c380f8347e55e100daa23766baacf0e5 \ + --hash=sha256:363c139bf15e1ac5f136b981d3c077eb551299b1effede7f12faa010b8590a60 \ + --hash=sha256:3b6b3d28228af044ebcded71c4a3dd86e1dbd7e2f4645bf40f7b5da65bb5fb5a \ + --hash=sha256:3bb5fd680c038fd5229e44e9c493782f90df9bef632fd0499d442374688ff70b \ + --hash=sha256:3e1860f1e43d40e9d904cf22d93e587ea42e010ebce4160877e46bcab4bc232a \ + --hash=sha256:43475925a766d01ca8cd9a857fd87f3d50406983c8506a4c07c4df12adcc867f \ + --hash=sha256:44fba4a5f1d179b7ddc7b3dc40f56f9209046421679b57025d4d8821b376fd8d \ + --hash=sha256:48b542c347c2089f43dc5a6db31d2a6f3cdb04ee33505ec6e9f653834dbb0bde \ + --hash=sha256:496736f86a9bedaf64b0dc70e3539d0766df01c71ea22032698e88f3f04a1ce9 \ + --hash=sha256:503722d52a615f2604f5e7611de7d43878df010dc0053094ef91cb9a9ac3d987 \ + --hash=sha256:54876a4e45101cec2bf8f31a973cda073a23e2e108538dad224ba07f85f22487 \ + --hash=sha256:5b1bde10324f4c31812ae0d0502e92d916ae8917cad7209353f122b8b8f610c3 \ + --hash=sha256:5de686e73690cdaf72b96d4fa083c230ec9020bcc2627ce6316138e2cf2fe2d1 \ + --hash=sha256:5e7ce913b61f35b0c1c839a49ac9c8e75dd8d860150688aed353b0ce1bf409d8 \ + --hash=sha256:693d02c6dc7d1aa0a45921d54cd8c1ff629e09dfdc2238471507af1f7a1c6f04 \ + --hash=sha256:6be4d70d9ab76c9f324ead9c01af6ff52c324745ea0c3731682a0cf99720f1fe \ + --hash=sha256:6cc4eefbb542a5d6ffd6d70ea9c502957c925e800f998c5630ecc809d6702bae \ + --hash=sha256:6e934bbae1e0ec74e27d5f0d7f37ef547ce5ff9f0a7e63fb39e559fc99526734 \ + --hash=sha256:7c4d596b7676f811172687ec567cbafb9e4dea2f9be1bbb4f622410cb7f40f40 \ + --hash=sha256:8f4608a06e4d61b7a3425665a46d00e0579122e1a2fae97a0c52953a3aad9aa3 \ + --hash=sha256:9122ad6f867c4a0f5e655f5c3bdf89103852009dbb442a3d23e688b9e699e800 \ + --hash=sha256:91c3b07cf3362086d8f126c6aecd8e5e9396ad8b2f2219ea7e49a8250c318acd \ + --hash=sha256:921c14e93817842dd0dd9f372890a0f0c72e534650b6ab13c5be5cd0db11d47e \ + --hash=sha256:970f9f8c50961d639cbd0d988c96f80ddf66006de93641719282c4fe7a87c5e6 \ + --hash=sha256:a169a036bed0995e090d1493b283cc2cc8a6f5046821086b843abefff80643bc \ + --hash=sha256:a6545e6b409e3d5cbafc850fb84c55a1ca26ed15a6b11e3bf07a0e0cd84517c8 \ + --hash=sha256:a778b25874cb0f862eaab5986bff4ca49ffb0def7c0a34c237b948b3c6c775b2 \ + --hash=sha256:abb65b4e947e958f7b3b0d71db3ce447d1bc5f37f5eab871ce7223bda8768a04 \ + --hash=sha256:acbb48679ddf3852c45280c10ff10d52ca2cd1da2e552fb81db1ff786c75d0e4 \ + --hash=sha256:ae3a39a4d96bdb6f8d154fd7f490c4ad06f0532fcd2bb656052a9a7762cf5d31 \ + --hash=sha256:b59ee2ac81de57771a09ecad09191e840a1d2fae1ef684208320591055768f83 \ + --hash=sha256:c50269d0055ac1faecfd559886d2cbe4b730de236585aba0e873f9d9dadbe585 \ + --hash=sha256:c72500a3b6d6c30ebfc135035bcace9eb5884f2dc220804efcaaba43e9f611dd \ + --hash=sha256:c9b31ab1f28b078a6a1ac1a54eb35e7d5390deddd56870d0be3a0a733d1c321c \ + --hash=sha256:d006faf3b491957efcb433489be3c149efe4787b7063d5cddb8ddaefdc60e0c1 \ + --hash=sha256:d610aa62cdb7d4d497740741772a24a794903bf3e79eaa51d2e800082abe11e5 \ + --hash=sha256:d7d9110d0c3fb02679972837a033251fd186c529aa62f19c132fc909c74052b8 \ + --hash=sha256:dc026e3b89d98e30a8288c95cb696e77d150b3f0fb7a51f73dcd49ee6b5577fa \ + --hash=sha256:e64a7c9d7dfca3e0fafcbc5e455519090706a3e36e95d655cec3e04e79f95aaa \ + --hash=sha256:ea6daa712f4e094a30830cf01e9b47d03b24d05cc9dab8609f0d9a9db8454712 \ + --hash=sha256:f14bb8b22a4a91325813e3d553b8963c10cf8c756cff65ee50c194431296c655 \ + --hash=sha256:f3e7b689c3bce16699efcf736066f5c6cc4472c3840fe4b22bd8279daf4abdac \ + --hash=sha256:fddbbb69a6fff4f421e7a0d1fa28f894b20112e9e3fab306af451e2dfd0e459b \ + --hash=sha256:fe14c356f8b23ad811dc026077a6d4abccdaa7bce5ca98579605550657b6fcfb # via # datasets # langgraph @@ -3223,61 +3712,91 @@ yara-python==4.5.1 ; (platform_machine == 'arm64' and sys_platform == 'darwin') --hash=sha256:bb65c17657b4cdbe5adee7a6e617ee05e214e8afdbc82b195885354a72a16476 \ --hash=sha256:f533848781f0e46e44eda77055eae4ec934cf56c1f473e787704f1a348e90094 # via nmp-guardrails -yarl==1.23.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25 \ - --hash=sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e \ - --hash=sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2 \ - --hash=sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860 \ - --hash=sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035 \ - --hash=sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4 \ - --hash=sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34 \ - --hash=sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512 \ - --hash=sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9 \ - --hash=sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5 \ - --hash=sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a \ - --hash=sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c \ - --hash=sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5 \ - --hash=sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b \ - --hash=sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7 \ - --hash=sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598 \ - --hash=sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8 \ - --hash=sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f \ - --hash=sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51 \ - --hash=sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa \ - --hash=sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a \ - --hash=sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99 \ - --hash=sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59 \ - --hash=sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432 \ - --hash=sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543 \ - --hash=sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24 +yarl==1.24.2 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:08d3a33218e0c64393e7610284e770409a9c31c429b078bcb24096ed0a783b8f \ + --hash=sha256:0a6377060e7927187a42b7eb202090cbe2b34933a4eeaf90e3bd9e33432e5cae \ + --hash=sha256:17076578bce0049a5ce57d14ad1bded391b68a3b213e9b81b0097b090244999a \ + --hash=sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44 \ + --hash=sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9 \ + --hash=sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db \ + --hash=sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b \ + --hash=sha256:34263e2fa8fb5bb63a0d97706cda38edbad62fddb58c7f12d6acbc092812aa50 \ + --hash=sha256:36348bebb147b83818b9d7e673ea4debc75970afc6ffdc7e3975ad05ce5a58c1 \ + --hash=sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488 \ + --hash=sha256:3f6d2c216318f8f32038ca3f72501ba08536f0fd18a36e858836b121b2deed9f \ + --hash=sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d \ + --hash=sha256:49016d82f032b1bd1e10b01078a7d29ae71bf468eeae0ea22df8bab691e60003 \ + --hash=sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536 \ + --hash=sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a \ + --hash=sha256:50713f1d4d6be6375bb178bb43d140ee1acb8abe589cd723320b7925a275be1e \ + --hash=sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035 \ + --hash=sha256:5d699376c4ca3cba49bbfae3a05b5b70ded572937171ce1e0b8d87118e2ba294 \ + --hash=sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7 \ + --hash=sha256:7b54b9c67c2b06bd7b9a77253d242124b9c95d2c02def5a1144001ee547dd9d5 \ + --hash=sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c \ + --hash=sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992 \ + --hash=sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf \ + --hash=sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986 \ + --hash=sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d \ + --hash=sha256:8d027d56f1035e339d1001ac33eceab5b2ec8e42e449787bb75e289fb9a5cd1d \ + --hash=sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617 \ + --hash=sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996 \ + --hash=sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8 \ + --hash=sha256:a1cab588b4fa14bea2e55ebea27478adfb05372f47573738e1acc4a36c0b05d2 \ + --hash=sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592 \ + --hash=sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b \ + --hash=sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92 \ + --hash=sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8 \ + --hash=sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576 \ + --hash=sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712 \ + --hash=sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1 \ + --hash=sha256:d1dd47a22843b212baa8d74f37796815d43bd046b42a0f41e9da433386c3136b \ + --hash=sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a \ + --hash=sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1 \ + --hash=sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c \ + --hash=sha256:ec87ccc31bd21db7ad009d8572c127c1000f268517618a4cc09adba3c2a7f21c \ + --hash=sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8 \ + --hash=sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056 # via aiohttp -zipp==3.23.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ - --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ - --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 +zipp==4.1.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ + --hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \ + --hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602 # via importlib-metadata zstandard==0.25.0 ; (platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') \ --hash=sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a \ + --hash=sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6 \ + --hash=sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250 \ --hash=sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f \ --hash=sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3 \ --hash=sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6 \ + --hash=sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa \ --hash=sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611 \ --hash=sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b \ + --hash=sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e \ --hash=sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa \ --hash=sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf \ --hash=sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902 \ + --hash=sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc \ + --hash=sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98 \ + --hash=sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a \ --hash=sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097 \ --hash=sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea \ + --hash=sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb \ --hash=sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7 \ --hash=sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b \ --hash=sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a \ --hash=sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00 \ --hash=sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072 \ + --hash=sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065 \ + --hash=sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512 \ --hash=sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1 \ --hash=sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f \ --hash=sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b \ --hash=sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea \ --hash=sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277 \ - --hash=sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313 + --hash=sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708 \ + --hash=sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313 \ + --hash=sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551 # via # clickhouse-connect # langsmith diff --git a/tools/nemo-platform-sdk-tools/src/nemo_platform_sdk_tools/license/overrides.yaml b/tools/nemo-platform-sdk-tools/src/nemo_platform_sdk_tools/license/overrides.yaml index 9138253dbd..2693160123 100644 --- a/tools/nemo-platform-sdk-tools/src/nemo_platform_sdk_tools/license/overrides.yaml +++ b/tools/nemo-platform-sdk-tools/src/nemo_platform_sdk_tools/license/overrides.yaml @@ -30,7 +30,7 @@ overrides: flashinfer-jit-cache: Apache-2.0 # https://github.com/flashinfer-ai/flashinfer/tree/main/flashinfer-jit-cache huggingface-hub: Apache-2.0 # https://github.com/huggingface/huggingface_hub peft: Apache-2.0 # HuggingFace Parameter-Efficient Fine-Tuning - safetensors: Apache-2.0 # HuggingFace safe tensor storage + safetensors: Apache-2.0 # HuggingFace safe tensor storage - https://github.com/safetensors/safetensors/blob/main/LICENSE — osv-scanner reports non-standard for 0.8.0rc1 tiktoken: MIT # OpenAI tokenizer tokenizers: Apache-2.0 # HuggingFace tokenizers trl: Apache-2.0 @@ -265,3 +265,5 @@ overrides: uncalled-for: MIT # https://pypi.org/project/uncalled-for/ torch-c-dlpack-ext: Apache-2.0 quack-kernels: Apache-2.0 # https://github.com/Dao-AILab/quack/blob/main/LICENSE + crc32c: LGPL-2.1-or-later # https://pypi.org/project/crc32c/ + detect-installer: 0BSD # https://pypi.org/project/detect-installer/ diff --git a/uv.lock b/uv.lock index 6875fd0acf..051a2daef1 100644 --- a/uv.lock +++ b/uv.lock @@ -76,12 +76,13 @@ constraints = [ { name = "diffusers", specifier = ">=0.38.0" }, { name = "filelock", specifier = ">=3.20.3" }, { name = "gitpython", specifier = ">=3.1.49" }, + { name = "json-repair", specifier = "==0.58.7" }, { name = "jupyter-server", specifier = ">=2.18.0" }, { name = "jupyterlab", specifier = ">=4.5.7" }, { name = "langchain-core", specifier = ">=1.3.3" }, { name = "langchain-openai", specifier = ">=1.1.14" }, { name = "langgraph", specifier = ">=1.0.10" }, - { name = "langsmith", specifier = ">=0.7.31" }, + { name = "langsmith", specifier = "==0.8.2" }, { name = "litellm", specifier = ">=1.83.10" }, { name = "lxml", specifier = ">=6.1.0" }, { name = "mako", specifier = ">=1.3.12" }, @@ -200,14 +201,14 @@ boto3 = [ [[package]] name = "aiofile" -version = "3.9.0" +version = "3.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "caio", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/41/2fea7e193e061ce54eacc3b7bc0e6a99e4fcff43c78cf0a76dd781ed8334/aiofile-3.11.1.tar.gz", hash = "sha256:1f91912c6643d2a4e49ca4ae3514f0bf3867ce948a36d99a6411b8f4755f4cf9", size = 19342, upload-time = "2026-05-16T08:18:33.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, + { url = "https://files.pythonhosted.org/packages/67/cd/0d76dfc5de72bde52f55f53e925c7d152d9c7906634ec1e0cbc7e8d4ad93/aiofile-3.11.1-py3-none-any.whl", hash = "sha256:ce77d14ac07f77bc2b757834a5c129321f3f705c474593deed5ab209079a52c9", size = 20446, upload-time = "2026-05-16T08:18:32.051Z" }, ] [[package]] @@ -221,16 +222,16 @@ wheels = [ [[package]] name = "aiohappyeyeballs" -version = "2.6.1" +version = "2.6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591, upload-time = "2026-05-20T15:12:24.631Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, + { url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062, upload-time = "2026-05-20T15:12:23.328Z" }, ] [[package]] name = "aiohttp" -version = "3.13.5" +version = "3.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs", 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')" }, @@ -239,28 +240,56 @@ dependencies = [ { name = "frozenlist", 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 = "multidict", 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 = "propcache", 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 = "typing-extensions", marker = "(python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "yarl", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" }, - { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" }, - { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" }, - { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" }, - { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, - { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, - { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, - { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, - { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, - { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, - { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, - { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, - { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/dd/bf526e6f0a1120dd6f2df2e97bacfe4d358f13d17a0ff5847301a1375a51/aiohttp-3.14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2", size = 765225, upload-time = "2026-06-07T21:06:07.957Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e7/c60c7b209e509cc787de3cea0550a518538cfc08003e1c1e14c1c63fff71/aiohttp-3.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8", size = 514139, upload-time = "2026-06-07T21:06:11.26Z" }, + { url = "https://files.pythonhosted.org/packages/5b/8d/614ace2f579702c9840ab1e1447fd8509e35b0b904f7196418fa2f57b25d/aiohttp-3.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04", size = 1784088, upload-time = "2026-06-07T21:06:12.887Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/726e90f99542bf292f81a96a12cc4847deb86f3ccf62c6f4014a201f4d33/aiohttp-3.14.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8", size = 1737835, upload-time = "2026-06-07T21:06:14.564Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4b/d176d5c4db9d33dacf0543102ea59503bc1d528af4cfd0b719949ca49389/aiohttp-3.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6", size = 1842801, upload-time = "2026-06-07T21:06:16.228Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d6/5a99b563690ea0cbed912ae94a2ce33993a5709a651a3a4fe761e7dd973a/aiohttp-3.14.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af", size = 1929992, upload-time = "2026-06-07T21:06:17.947Z" }, + { url = "https://files.pythonhosted.org/packages/76/7f/a987b14a3859094b3cea3f4825219c3e5536242564af6e3f9c2f6c994eb2/aiohttp-3.14.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730", size = 1786989, upload-time = "2026-06-07T21:06:19.677Z" }, + { url = "https://files.pythonhosted.org/packages/f1/1a/420e5c85a3e73349372ed22ce0b6af86bfa6ce16a4b20a64a2e94608c781/aiohttp-3.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621", size = 1640129, upload-time = "2026-06-07T21:06:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/a7/80/18a592ed3be0a402cc03670bd72ee1f8563ddbe1d8d5542dbf868f274136/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee", size = 1756576, upload-time = "2026-06-07T21:06:24.8Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0b/8b3d5713373858ff71a617daf6e3b0e81ad63e79d09a3cf2f6b6b983939c/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573", size = 1754668, upload-time = "2026-06-07T21:06:26.528Z" }, + { url = "https://files.pythonhosted.org/packages/9f/49/fd564575cf225821d7ba5a117cb8bc27213d8a7e1811162afb43ae077039/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7", size = 1817019, upload-time = "2026-06-07T21:06:28.297Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/e850c9ae6fc91356552ae668bb6c51e93fa29c8aef13398a10b56678557f/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf", size = 1631638, upload-time = "2026-06-07T21:06:30.242Z" }, + { url = "https://files.pythonhosted.org/packages/eb/94/3c337ba72451a89806ace6f75bddc92bafc5b8d53d90115a512858024b63/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85", size = 1835660, upload-time = "2026-06-07T21:06:31.943Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9c/9c18cf367a0498212d9ba7daf990b504a5e8ae064cda4b504e2647c89c03/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3", size = 1775698, upload-time = "2026-06-07T21:06:33.72Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" }, + { 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" }, ] [[package]] @@ -368,7 +397,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/07/38/e321b0e05d8cc068a [[package]] name = "anthropic" -version = "0.101.0" +version = "0.107.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", 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')" }, @@ -380,9 +409,9 @@ dependencies = [ { name = "sniffio", 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 = "typing-extensions", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/cb/9d0123243e749ac3a579972b2c398971bce1dc57bcc4efb08066df610360/anthropic-0.101.0.tar.gz", hash = "sha256:1116a6a87c55757e0fbe3e1ba40804fbd04de7963601a6dd6b539a889f18de3e", size = 758603, upload-time = "2026-05-11T15:46:33.944Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/f1/c6076a92e0bf6b0dfa126e213b3f9e8a510acd73567953210713aae6c256/anthropic-0.107.1.tar.gz", hash = "sha256:8e7169a6ab57fb806b778d9af018c867bad688144efec8969cdb4c5ccecd6670", size = 856312, upload-time = "2026-06-07T17:18:57.358Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/b2/74ff06762d005ecf1658929a292df0acb786d025f6a6c54fcb30e2dc7761/anthropic-0.101.0-py3-none-any.whl", hash = "sha256:cc3cc6576989471e2aa9132258034ad0ff0d8fe500b04ac499e4e46ed68c5ed0", size = 753594, upload-time = "2026-05-11T15:46:32.216Z" }, + { url = "https://files.pythonhosted.org/packages/86/0e/71432f0777a263701955a23ebcc6650485c2753be9afbce2a6a8d72526e3/anthropic-0.107.1-py3-none-any.whl", hash = "sha256:b74338d08000ba105dfc8adae29af3713ece845a4bffec9986a20697e087c7b3", size = 838729, upload-time = "2026-06-07T17:18:58.729Z" }, ] [[package]] @@ -595,15 +624,15 @@ wheels = [ [[package]] name = "beautifulsoup4" -version = "4.14.3" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve", 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 = "typing-extensions", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, ] [[package]] @@ -623,7 +652,7 @@ wheels = [ [[package]] name = "black" -version = "26.3.1" +version = "26.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", 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')" }, @@ -633,27 +662,27 @@ dependencies = [ { name = "platformdirs", 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 = "pytokens", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", size = 1689499, upload-time = "2026-03-12T03:40:07.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", size = 1754369, upload-time = "2026-03-12T03:40:09.279Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, - { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, - { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, - { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, + { url = "https://files.pythonhosted.org/packages/83/ea/5ad117b9ee3ecd933c712bcbae610006e5b7cc9f41c526cd7ed3b6c4124c/black-26.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e48b87e03bf109288e55cfceadcfa15ff5470aca2851a851950ed2926f450d7", size = 1792130, upload-time = "2026-05-18T17:05:12.983Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/7c448bc623fcdfa96672531beb5a616ea5e64f6975955254d7731ffb0ad9/black-26.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5119fa92ae61f786e8c3662fd60aece1d0a2dd5cca5d0c79417a95e7a4272a59", size = 1846134, upload-time = "2026-05-18T17:05:14.506Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c0/c5a3b1636dfd09c42534f2b3cf33506814f6d3e066fb0879ffa16c1ae860/black-26.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217", size = 1816016, upload-time = "2026-05-18T17:05:21.84Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0e/36044316b65ca471d3bb6d3703fd06fb50c6b727c3562f6a5a3153634f88/black-26.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d", size = 1884150, upload-time = "2026-05-18T17:05:23.546Z" }, + { url = "https://files.pythonhosted.org/packages/0b/df/9f31c5e0babbfed77d505fc5d120beb98b21b33feaeded3924ea941fe360/black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0", size = 1813335, upload-time = "2026-05-18T17:05:31.266Z" }, + { url = "https://files.pythonhosted.org/packages/fb/24/8e7b9a2fa61b0afd82209efe937557d180a1fa055bd7f6161eb9defc3719/black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", size = 1881614, upload-time = "2026-05-18T17:05:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" }, ] [[package]] name = "bleach" -version = "6.3.0" +version = "6.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "webencodings", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, + { url = "https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081", size = 165109, upload-time = "2026-06-05T13:01:12.504Z" }, ] [package.optional-dependencies] @@ -703,23 +732,23 @@ wheels = [ [[package]] name = "botocore-stubs" -version = "1.42.41" +version = "1.43.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-awscrt", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/a8/a26608ff39e3a5866c6c79eda10133490205cbddd45074190becece3ff2a/botocore_stubs-1.42.41.tar.gz", hash = "sha256:dbeac2f744df6b814ce83ec3f3777b299a015cbea57a2efc41c33b8c38265825", size = 42411, upload-time = "2026-02-03T20:46:14.479Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/81/79693e833291c00dc89ee610e5e915381b6f08233912e28df50106840780/botocore_stubs-1.43.14.tar.gz", hash = "sha256:9e3bc1fdd51da7473f0df726c82747a1b0ae913449d629659765c247fecc2039", size = 42738, upload-time = "2026-05-25T06:06:37.484Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/76/cab7af7f16c0b09347f2ebe7ffda7101132f786acb767666dce43055faab/botocore_stubs-1.42.41-py3-none-any.whl", hash = "sha256:9423110fb0e391834bd2ed44ae5f879d8cb370a444703d966d30842ce2bcb5f0", size = 66759, upload-time = "2026-02-03T20:46:13.02Z" }, + { url = "https://files.pythonhosted.org/packages/89/ca/f017727b11895908c5dedc829cf2ec35e0c4b2a26ba875db325fef2cefdf/botocore_stubs-1.43.14-py3-none-any.whl", hash = "sha256:fb98f1475c92fd718644e786b5c543a20f1b1f610e89e0a7191c3f1f429c75aa", size = 67093, upload-time = "2026-05-25T06:06:34.532Z" }, ] [[package]] name = "cachetools" -version = "7.0.5" +version = "7.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/dd/57fe3fdb6e65b25a5987fd2cdc7e22db0aef508b91634d2e57d22928d41b/cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", size = 37367, upload-time = "2026-03-09T20:51:29.451Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/8b/0d3945a13955303b81272f759a0331e54c5c793da455e6f5706b89d2639c/cachetools-7.1.4.tar.gz", hash = "sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6", size = 40085, upload-time = "2026-05-21T22:40:43.376Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114", size = 13918, upload-time = "2026-03-09T20:51:27.33Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7b/1fc1c09cc0756cf25861a3be10565915953876da48bb228fb9a672b20a42/cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54", size = 16761, upload-time = "2026-05-21T22:40:41.845Z" }, ] [[package]] @@ -754,11 +783,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.5.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, ] [[package]] @@ -772,16 +801,22 @@ sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8 wheels = [ { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, { url = "https://files.pythonhosted.org/packages/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" }, @@ -807,26 +842,50 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e", size = 293582, upload-time = "2026-03-15T18:50:25.454Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b7/b1a117e5385cbdb3205f6055403c2a2a220c5ea80b8716c324eaf75c5c95/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9", size = 197240, upload-time = "2026-03-15T18:50:27.196Z" }, - { url = "https://files.pythonhosted.org/packages/60/ac/3233d262a310c1b12633536a07cde5ddd16985e6e7e238e9f3f9423d8eb9/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73", size = 204697, upload-time = "2026-03-15T18:50:31.654Z" }, - { url = "https://files.pythonhosted.org/packages/b5/10/cf491fa1abd47c02f69687046b896c950b92b6cd7337a27e6548adbec8e4/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f", size = 200911, upload-time = "2026-03-15T18:50:36.819Z" }, - { url = "https://files.pythonhosted.org/packages/ae/98/7bc23513a33d8172365ed30ee3a3b3fe1ece14a395e5fc94129541fc6003/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021", size = 206951, upload-time = "2026-03-15T18:50:44.789Z" }, - { 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/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/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/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/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/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/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" }, +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] @@ -840,11 +899,11 @@ wheels = [ [[package]] name = "click" -version = "8.3.1" +version = "8.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, ] [[package]] @@ -889,18 +948,30 @@ sdist = { url = "https://files.pythonhosted.org/packages/46/9e/d8e40b29b6269a845 wheels = [ { url = "https://files.pythonhosted.org/packages/ec/e0/1ae285f4d5bb61bb62016deb38dc175a2b8cbe578dffdad5e1a5a02a176c/clickhouse_driver-0.2.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3572e74cd65828f72284bf607de259c059178b44b19a93cb67766b7e7458cf8e", size = 208477, upload-time = "2025-11-10T22:47:34.925Z" }, { url = "https://files.pythonhosted.org/packages/28/04/e2fb47a4aaf9653c9ed872e1505e997d42f834b0891351ef54169e100c5c/clickhouse_driver-0.2.10-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:767af2b2d2e02fb7abd8fc9619f8aa2de65be010d3024029d68bc9b1be564466", size = 1006131, upload-time = "2025-11-10T22:47:36.386Z" }, + { url = "https://files.pythonhosted.org/packages/e3/52/626cf3a908dde51638213a6c414e86bc66c47e384cb379c4a0533930a329/clickhouse_driver-0.2.10-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b00005a89f0e40ec0bc313f7e131d958aa17e6af5c3260dbb5c7daf5370c5443", size = 1059838, upload-time = "2025-11-10T22:47:38.266Z" }, + { url = "https://files.pythonhosted.org/packages/12/57/a5917930760e4032e98017916bd6770308e146d44c58e450da6fa87f2d4b/clickhouse_driver-0.2.10-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a62ad120dab6bcc68b6413b7ef0dbaef75ab5ca985d490a9e1ec13d93bf33dc3", size = 1069504, upload-time = "2025-11-10T22:47:40.681Z" }, { url = "https://files.pythonhosted.org/packages/f5/08/4419ce43b27b6349fd14af0d8f5d8594d270b9bb24cbaca575bacfec630e/clickhouse_driver-0.2.10-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9f65e71c99f0c8a64afa348977793967f897c4f731984ed54fed4eca8d375a0", size = 998284, upload-time = "2025-11-10T22:47:42.713Z" }, { url = "https://files.pythonhosted.org/packages/36/10/edbe55be3554e2cea7c68ed1761aaa2bea0153474d81a467a0ac862b3478/clickhouse_driver-0.2.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c120b182ea7e9713b119ba2b518dd75503c18f26cb001a6e436326765fddd123", size = 971989, upload-time = "2025-11-10T22:47:44.642Z" }, + { url = "https://files.pythonhosted.org/packages/2f/18/d0b883af04067c70e99c22dbab1f085062ae764a063f22d4e144e833048d/clickhouse_driver-0.2.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3da6f6d05f14780f3183f8b7b23ed9826d1e0f2f73c2471037d5335b474782e", size = 1022107, upload-time = "2025-11-10T22:47:46.148Z" }, + { url = "https://files.pythonhosted.org/packages/fe/40/11446c52c5330123354f2f88151c620e1b38ca6d5131b2cc71786ec3c067/clickhouse_driver-0.2.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9617c6ef154e58c6693be6e1169000957238f83d21fe20d57bb492412cc6128d", size = 1015750, upload-time = "2025-11-10T22:47:47.702Z" }, { url = "https://files.pythonhosted.org/packages/36/9b/32abe3c76fe8494ad1642febbe4c59dfd46477e27c401d2ac8cc8a0a6117/clickhouse_driver-0.2.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8c2dbee2083295c6d9789a10cb0c967c4e123e6315e3192e4ad935cd55967c3", size = 975901, upload-time = "2025-11-10T22:47:49.136Z" }, { 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" }, { url = "https://files.pythonhosted.org/packages/40/7d/9abdd95b0da0dcf6dc644336459f132575bfbdee1a4ba377195c2032c03a/clickhouse_driver-0.2.10-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68e96e04282d126486b3820391a8ebf1d7c32b61e5fcbd701aeeda79017349e5", size = 216933, upload-time = "2025-11-10T22:49:47.474Z" }, { url = "https://files.pythonhosted.org/packages/5a/a4/33d4b6f1650847280265756e4d54f94730cdac082ab3f9e6518ba97502bf/clickhouse_driver-0.2.10-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0fd9f72fcfe86e0fef0681cd124325714e92e96ad1fd675dfbeaccdfb7bd2f64", size = 219670, upload-time = "2025-11-10T22:49:49.401Z" }, @@ -944,31 +1015,47 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, - { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, - { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, - { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, - { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, - { 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/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/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/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/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/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/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" }, +version = "7.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/82/a5eb47257c50601bb7b9a9d2857c67b7a3a85ad74180eb2c98bb1fbe0ce5/coverage-7.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a24a81f9715ee42ef59a316cc11611c98fe23920f7c81861315c9f3ff4a230f4", size = 220354, upload-time = "2026-05-26T20:38:40.232Z" }, + { url = "https://files.pythonhosted.org/packages/77/63/e77aaacd491182210d639636b7a8bba23ffffa9b82aa3762da9431855fa9/coverage-7.14.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d452fd08b5c72c5167c93e6867b5c08500bd40f2a21e1e854a500550b6cc36f", size = 252683, upload-time = "2026-05-26T20:38:43.305Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/a022e3cfbec2ac241640003cb3a817e161d9c7f5aa9b49173756cdc03204/coverage-7.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23bf7fa51ac02e07fc7c96849b82946da47ae862dc8f86d183b2a4864fc38129", size = 254791, upload-time = "2026-05-26T20:38:45.361Z" }, + { url = "https://files.pythonhosted.org/packages/61/d6/967e408aca4c1ceb88cb0cc677169110ae7f5995fb5eaf5fb1f5a1bb8f5d/coverage-7.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcaa50684dcaadfa599ac48f81103c756d791cfd85c97203d2217c593d48b860", size = 256748, upload-time = "2026-05-26T20:38:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/b8/be/869188f7fe28638078ec479331ace6dc5f7b40b7153eb616f47ab79404d8/coverage-7.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ea1c034f95c9b056e856b794630b17f9fa3d57e4800ff1e503d3be0f9c9078c", size = 250907, upload-time = "2026-05-26T20:38:48.493Z" }, + { url = "https://files.pythonhosted.org/packages/07/aa/adb7d3b4278d690e68703abcd76ab1b948242e3668d921711551b78f9ddb/coverage-7.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e057326434e441306226fbeb5d1aaf14a2637efe97ba668306635835f32ad7", size = 252483, upload-time = "2026-05-26T20:38:50.074Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b6/c5dae3c104d89be04828f61810e6b3473825482e4c288cc4ed04553e08ae/coverage-7.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d75f892b3ab73ba11cab5442cce7b3e168fd64162b16f0e1e0d09c508edef", size = 254310, upload-time = "2026-05-26T20:38:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a1/2b9d5863e3b83c01ad8199e3c597802fbb3a9dc90b058885804c20296d31/coverage-7.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3a56abc20a472baf0304c455721bc601477440d28ecfde8a03dde79ede07e0df", size = 250266, upload-time = "2026-05-26T20:38:55.414Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5e/0e511fbdb269359be26fe678a1c3fa1f2aa2a01573cc3f54268c8d6d4797/coverage-7.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6a3cb83d1552c0cd1b4906655b6a33fd4a8473229633a901c6b73bf86914dee9", size = 251174, upload-time = "2026-05-26T20:38:57.141Z" }, + { url = "https://files.pythonhosted.org/packages/72/81/fdc0898a55c6219223291ec1a1fe89966ef212ce82276aa0899df84b5de0/coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c", size = 220379, upload-time = "2026-05-26T20:39:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/28/30/300c343f68beb9d4cbb64ec81e58c5b6b80b56927f72d2b38654ac26e013/coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6", size = 254624, upload-time = "2026-05-26T20:39:09.037Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ed/7b25642496e8170b6bac14adce00537c6e5fa2d586159401a4de3e8b49e6/coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37", size = 255739, upload-time = "2026-05-26T20:39:10.889Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a2/abd210b8c4e29c24e4624916db97bb519097a91034aaeb767f937e7da794/coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad", size = 257998, upload-time = "2026-05-26T20:39:12.722Z" }, + { url = "https://files.pythonhosted.org/packages/7f/24/7c50beed3792fe62f6ce0545c6686ce83379719e2c0276179333d97eae92/coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84", size = 252296, upload-time = "2026-05-26T20:39:14.259Z" }, + { url = "https://files.pythonhosted.org/packages/15/05/0f874628ebcbfc77ead559ff210281ef06a97db08481832e7dd39274a135/coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54", size = 253658, upload-time = "2026-05-26T20:39:15.923Z" }, + { url = "https://files.pythonhosted.org/packages/c0/30/b9b4d377cd9f40baf228068f5a81faf8450c6228503011bd499708483a50/coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9", size = 255873, upload-time = "2026-05-26T20:39:19.414Z" }, + { url = "https://files.pythonhosted.org/packages/3c/21/7c721a9e5e6bb88547d30a787aefb97512d3f54c1324c7488d9b3743f7f9/coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02", size = 251372, upload-time = "2026-05-26T20:39:21.169Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f8ae5a2200130e1503cd7661a6cd3b2b7bacef98277fbf3571fb13f8b766/coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a", size = 253245, upload-time = "2026-05-26T20:39:23.097Z" }, + { url = "https://files.pythonhosted.org/packages/75/92/e82aca356744cbbc0f77a0b623e38918c1872361963413a3bab5d0340393/coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d", size = 220412, upload-time = "2026-05-26T20:39:31.561Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/23faf6a2343a0d17f960a4bd56c43bc7eb4cf312f774dd6ceebd82c7d8fc/coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d", size = 254008, upload-time = "2026-05-26T20:39:35.009Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/36f4aa9ca8a815e6036156e80706a67828bb97bd826948244f6996dda957/coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b", size = 255241, upload-time = "2026-05-26T20:39:36.71Z" }, + { url = "https://files.pythonhosted.org/packages/ca/79/95266316352f90f6b1c6736bb413302edfde2453fb32422d3911642691b3/coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be", size = 257373, upload-time = "2026-05-26T20:39:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9c/58316d1f66c488b5fca8a0eb3e98348807813efa8a0d0833b9021be27488/coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43", size = 251635, upload-time = "2026-05-26T20:39:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5a/ca2398a568e16fed7bb713e84ba3603a7164fb65779abe645c565ec890d5/coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901", size = 253373, upload-time = "2026-05-26T20:39:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8f/a94f9221184c9cae1ee115820e3798e48b6b17777a9f19e46fb9a0c8dc74/coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4", size = 255497, upload-time = "2026-05-26T20:39:46.166Z" }, + { url = "https://files.pythonhosted.org/packages/71/69/505d70e47db1eaebcd002c39759707621ef184cd6b1ae084d9f41293f323/coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d", size = 251159, upload-time = "2026-05-26T20:39:48.03Z" }, + { url = "https://files.pythonhosted.org/packages/e0/aa/58681c383aa33a9d2ed40a02d7a22fbf780d1fa4d575396365777828198c/coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33", size = 252934, upload-time = "2026-05-26T20:39:49.872Z" }, + { url = "https://files.pythonhosted.org/packages/f7/67/2963cbdaf5cbadec44efa3a1e39eaa1f02df4079585f05387607a221e126/coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5", size = 221086, upload-time = "2026-05-26T20:39:59.019Z" }, + { url = "https://files.pythonhosted.org/packages/7c/28/7a64d73598263e0c5abd5084211a8474488d31b3c552ff531c719dfcff62/coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a", size = 264458, upload-time = "2026-05-26T20:40:02.506Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d8/4969179db9f7eb4df218e69540adf829d1c835f59452513d065d15446802/coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a", size = 266884, upload-time = "2026-05-26T20:40:04.421Z" }, + { url = "https://files.pythonhosted.org/packages/a6/78/a45d5794dbc9bafd97afc96a4377c86c7820d78b6cf51b89bc1d4e919275/coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2", size = 268022, upload-time = "2026-05-26T20:40:06.298Z" }, + { url = "https://files.pythonhosted.org/packages/21/cb/4f5e354e9e3e67af96bd4e57113e6db6b22298c7168b13eec408a549903d/coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e", size = 261631, upload-time = "2026-05-26T20:40:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/ec/49/eced49af4cb996d5d8b7e94e736175c513e4facd3398507b89892b4326d8/coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d", size = 264443, upload-time = "2026-05-26T20:40:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/f0/59/2ae3cb79da554a06c8619d6c88ea19dd1e4aed4b834b6a83bb1fa243bdc5/coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d", size = 265780, upload-time = "2026-05-26T20:40:13.858Z" }, + { url = "https://files.pythonhosted.org/packages/af/5f/b130c1dc999031f2648bd25317fbce505ad8d5562079b4ed81e736a84967/coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69", size = 260970, upload-time = "2026-05-26T20:40:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/87/d1/ec13ccddeb48ec963bdfa72a11224bac2584bd045ba13beca82f8113e9c7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54", size = 263157, upload-time = "2026-05-26T20:40:18.382Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" }, ] [package.optional-dependencies] @@ -976,6 +1063,38 @@ toml = [ { name = "tomli", marker = "(python_full_version <= '3.11' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version <= '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version <= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] +[[package]] +name = "crc32c" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/4c/4e40cc26347ac8254d3f25b9f94710b8e8df24ee4dddc1ba41907a88a94d/crc32c-2.7.1.tar.gz", hash = "sha256:f91b144a21eef834d64178e01982bb9179c354b3e9e5f4c803b0e5096384968c", size = 45712, upload-time = "2024-09-24T06:20:17.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/8e/2f37f46368bbfd50edfc11b96f0aa135699034b1b020966c70ebaff3463b/crc32c-2.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:19e03a50545a3ef400bd41667d5525f71030488629c57d819e2dd45064f16192", size = 49672, upload-time = "2024-09-24T06:18:18.032Z" }, + { url = "https://files.pythonhosted.org/packages/25/ee/0cfa82a68736697f3c7e435ba658c2ef8c997f42b89f6ab4545efe1b2649/crc32c-2.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ebbf144a1a56a532b353e81fa0f3edca4f4baa1bf92b1dde2c663a32bb6a15", size = 35372, upload-time = "2024-09-24T06:18:20.983Z" }, + { url = "https://files.pythonhosted.org/packages/aa/92/c878aaba81c431fcd93a059e9f6c90db397c585742793f0bf6e0c531cc67/crc32c-2.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96b794fd11945298fdd5eb1290a812efb497c14bc42592c5c992ca077458eeba", size = 54879, upload-time = "2024-09-24T06:18:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2b/9e29e9ac4c4213d60491db09487125db358cd9263490fbadbd55e48fbe03/crc32c-2.7.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d698eec444b18e296a104d0b9bb6c596c38bdcb79d24eba49604636e9d747305", size = 53674, upload-time = "2024-09-24T06:18:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/79/ed/df3c4c14bf1b29f5c9b52d51fb6793e39efcffd80b2941d994e8f7f5f688/crc32c-2.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e07cf10ef852d219d179333fd706d1c415626f1f05e60bd75acf0143a4d8b225", size = 54691, upload-time = "2024-09-24T06:18:26.578Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6f/26fc3dda5835cda8f6cd9d856afe62bdeae428de4c34fea200b0888e8835/crc32c-2.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1738259802978cdf428f74156175da6a5fdfb7256f647fdc0c9de1bc6cd7173", size = 53554, upload-time = "2024-09-24T06:18:29.104Z" }, + { url = "https://files.pythonhosted.org/packages/1d/02/998dc21333413ce63fe4c1ca70eafe61ca26afc7eb353f20cecdb77d614e/crc32c-2.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f7d1c4e761fe42bf856130daf8b2658df33fe0ced3c43dadafdfeaa42b57b950", size = 49568, upload-time = "2024-09-24T06:18:32.425Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7d/5ff9904046ad15a08772515db19df43107bf5e3901a89c36a577b5f40ba0/crc32c-2.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:afd778fc8ac0ed2ffbfb122a9aa6a0e409a8019b894a1799cda12c01534493e0", size = 35373, upload-time = "2024-09-24T06:18:35.02Z" }, + { url = "https://files.pythonhosted.org/packages/4d/41/4aedc961893f26858ab89fc772d0eaba91f9870f19eaa933999dcacb94ec/crc32c-2.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56ef661b34e9f25991fface7f9ad85e81bbc1b3fe3b916fd58c893eabe2fa0b8", size = 54675, upload-time = "2024-09-24T06:18:35.954Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/13576941bf7cf95026abae43d8427c812c0054408212bf8ed490eda846b0/crc32c-2.7.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c02a3bd67dea95cdb25844aaf44ca2e1b0c1fd70b287ad08c874a95ef4bb38db", size = 53495, upload-time = "2024-09-24T06:18:38.099Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b6/55ffb26d0517d2d6c6f430ce2ad36ae7647c995c5bfd7abce7f32bb2bad1/crc32c-2.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99d17637c4867672cb8adeea007294e3c3df9d43964369516cfe2c1f47ce500a", size = 54456, upload-time = "2024-09-24T06:18:39.051Z" }, + { url = "https://files.pythonhosted.org/packages/48/ec/ce4138eaf356cd9aae60bbe931755e5e0151b3eca5f491fce6c01b97fd59/crc32c-2.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:588587772e55624dd9c7a906ec9e8773ae0b6ac5e270fc0bc84ee2758eba90d5", size = 53332, upload-time = "2024-09-24T06:18:40.925Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/1a6d60d5b3b5edc8382777b64100343cb4aa6a7e172fae4a6cfcb8ebbbd9/crc32c-2.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:24949bffb06fc411cc18188d33357923cb935273642164d0bb37a5f375654169", size = 49567, upload-time = "2024-09-24T06:18:44.485Z" }, + { url = "https://files.pythonhosted.org/packages/47/02/2bd65fdef10139b6a802d83a7f966b7750fe5ffb1042f7cbe5dbb6403869/crc32c-2.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ba110df60c64c8e2d77a9425b982a520ccdb7abe42f06604f4d98a45bb1fff62", size = 35374, upload-time = "2024-09-24T06:18:46.304Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0d/3e797d1ed92d357a6a4c5b41cea15a538b27a8fdf18c7863747eb50b73ad/crc32c-2.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c277f9d16a3283e064d54854af0976b72abaa89824955579b2b3f37444f89aae", size = 54641, upload-time = "2024-09-24T06:18:47.207Z" }, + { url = "https://files.pythonhosted.org/packages/01/cf/32f019be5de9f6e180926a50ee5f08648e686c7d9a59f2c5d0806a77b1c7/crc32c-2.7.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:724d5ff4d29ff093a983ae656be3307093706d850ea2a233bf29fcacc335d945", size = 53447, upload-time = "2024-09-24T06:18:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/92f3f62f3bafe8f7ab4af7bfb7246dc683fd11ec0d6dfb73f91e09079f69/crc32c-2.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2416c4d88696ac322632555c0f81ab35e15f154bc96055da6cf110d642dbc10", size = 54484, upload-time = "2024-09-24T06:18:51.311Z" }, + { url = "https://files.pythonhosted.org/packages/b4/6c/309229e9acda8cf36a8ff4061d70b54d905f79b7037e16883ce6590a24ab/crc32c-2.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:edefc0e46f3c37372183f70338e5bdee42f6789b62fcd36ec53aa933e9dfbeaf", size = 53367, upload-time = "2024-09-24T06:18:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/1b/80/61dcae7568b33acfde70c9d651c7d891c0c578c39cc049107c1cf61f1367/crc32c-2.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db9ac92294284b22521356715784b91cc9094eee42a5282ab281b872510d1831", size = 49386, upload-time = "2024-09-24T06:18:56.813Z" }, + { url = "https://files.pythonhosted.org/packages/63/42/5fcfc71a3de493d920fd2590843762a2749981ea56b802b380e5df82309d/crc32c-2.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5c056ef043393085523e149276a7ce0cb534b872e04f3e20d74d9a94a75c0ad7", size = 35292, upload-time = "2024-09-24T06:18:58.676Z" }, + { url = "https://files.pythonhosted.org/packages/03/de/fef962e898a953558fe1c55141644553e84ef4190693a31244c59a0856c7/crc32c-2.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03a92551a343702629af91f78d205801219692b6909f8fa126b830e332bfb0e0", size = 54223, upload-time = "2024-09-24T06:18:59.675Z" }, + { url = "https://files.pythonhosted.org/packages/13/3b/13d40a7dfbf9ef05c84a0da45544ee72080dca4ce090679e5105689984bd/crc32c-2.7.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88732070f6175530db04e0bb36880ac45c33d49f8ac43fa0e50cfb1830049d23", size = 52678, upload-time = "2024-09-24T06:19:02.661Z" }, + { url = "https://files.pythonhosted.org/packages/36/09/65ffc4fb9fa60ff6714eeb50a92284a4525e5943f0b040b572c0c76368c1/crc32c-2.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:57a20dfc27995f568f64775eea2bbb58ae269f1a1144561df5e4a4955f79db32", size = 53847, upload-time = "2024-09-24T06:19:03.705Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d8/4526d5380189d6f2fa27256c204100f30214fe402f47cf6e9fb9a91ab890/crc32c-2.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:55a77e29a265418fa34bef15bd0f2c60afae5348988aaf35ed163b4bbf93cf37", size = 52508, upload-time = "2024-09-24T06:19:05.731Z" }, +] + [[package]] name = "cryptography" version = "46.0.7" @@ -989,8 +1108,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, @@ -998,8 +1120,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, @@ -1012,16 +1137,18 @@ wheels = [ [[package]] name = "cuda-bindings" -version = "12.9.4" +version = "13.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "cuda-pathfinder", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/45/e7/b47792cc2d01c7e1d37c32402182524774dadd2d26339bd224e0e913832e/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c912a3d9e6b6651853eed8eed96d6800d69c08e94052c292fec3f282c5a817c9", size = 12210593, upload-time = "2025-10-21T14:51:36.574Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, - { url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" }, - { url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/457ca12dad3ee9bfcc9a545cfd6b64b359ba49de40f776f6e028e678f262/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5879712accf6e14bb01aa5e67440eb84998b8d104b509cc7a6dc0b8f656a474", size = 6053539, upload-time = "2026-05-29T23:11:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/95/7a/c5e3c34a409b148f5c0f5a4ea374158f95d488862c1dffedf9aa5c639df9/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04436a9364059c84b8f9636f359eccda1cf814341f5b670c71d80d2f79dbc708", size = 6674166, upload-time = "2026-05-29T23:11:45.478Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965, upload-time = "2026-05-29T23:11:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/2394f8163360f8391f8f1b7e72d300a82724edb81a7b7084c799fbd4c91f/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9efb21c1ee64981e184b9e0ba5eb3179e5ba3d4b51665a6cb52b8ef3d01a7cbf", size = 5920504, upload-time = "2026-05-29T23:11:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2732904099e0a4d4db774a5fc6d91ee95fae065b4d2ecabb4968c5fe2406c9d7", size = 6476660, upload-time = "2026-05-29T23:11:59.188Z" }, ] [[package]] @@ -1032,14 +1159,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl", hash = "sha256:0228c023f95d1480f143ef5c8922d27a2ab052087a942e81dc289c9eb8f91689", size = 51671, upload-time = "2026-05-27T01:21:25.413Z" }, ] +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +curand = [ + { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + [[package]] name = "cut-cross-entropy" version = "25.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "torch", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "triton", version = "3.7.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, + { name = "torch", 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 = "triton", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7e/97/45ff09cfcda7b200389204daa0125168e6544fba257adbbcdf728501d4f9/cut_cross_entropy-25.1.1.tar.gz", hash = "sha256:5fe5924509248b1aea5c890f8887c6a7759f7c8b1ebc0490e42c247c4f7c1e34", size = 22972, upload-time = "2025-01-07T12:21:53.896Z" } wheels = [ @@ -1048,7 +1214,7 @@ wheels = [ [[package]] name = "cyclopts" -version = "4.10.1" +version = "4.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs", 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')" }, @@ -1056,9 +1222,9 @@ dependencies = [ { name = "rich", 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 = "rich-rst", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/c4/2ce2ca1451487dc7d59f09334c3fa1182c46cfcf0a2d5f19f9b26d53ac74/cyclopts-4.10.1.tar.gz", hash = "sha256:ad4e4bb90576412d32276b14a76f55d43353753d16217f2c3cd5bdceba7f15a0", size = 166623, upload-time = "2026-03-23T14:43:01.098Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/b7/2e64e26e7189c2f0f88cea467d068ec968f2fa24628b0ffb93132a0f2b14/cyclopts-4.17.0.tar.gz", hash = "sha256:6b3231f18b404879e978214ef26fa174e8b505bd0f2117290b4135560666004b", size = 181338, upload-time = "2026-06-09T13:41:26.801Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/2261922126b2e50c601fe22d7ff5194e0a4d50e654836260c0665e24d862/cyclopts-4.10.1-py3-none-any.whl", hash = "sha256:35f37257139380a386d9fe4475e1e7c87ca7795765ef4f31abba579fcfcb6ecd", size = 204331, upload-time = "2026-03-23T14:43:02.625Z" }, + { url = "https://files.pythonhosted.org/packages/ec/26/1614f15b8ea89ee201a2484ea5bede7319a2b07c796c321ffdadd705559e/cyclopts-4.17.0-py3-none-any.whl", hash = "sha256:6ee947c9f3bbe9679b9fa9cea1bb327298db80b302df62d7f1d1bd82726508e0", size = 219147, upload-time = "2026-06-09T13:41:24.97Z" }, ] [[package]] @@ -1166,16 +1332,16 @@ provides-extras = ["test"] [[package]] name = "databricks-sdk" -version = "0.102.0" +version = "0.115.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth", 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 = "requests", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/b3/41ff1c3afe092df9085e084e0dc81c45bca5ed65f7b60dc59df0ade43c76/databricks_sdk-0.102.0.tar.gz", hash = "sha256:8fa5f82317ee27cc46323c6e2543d2cfefb4468653f92ba558271043c6f72fb9", size = 887450, upload-time = "2026-03-19T08:15:54.428Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/49/1fd8121d4517849ea67fddbd827e535871b94037fab985235c011fdc60d1/databricks_sdk-0.115.0.tar.gz", hash = "sha256:a91f219313ea1afcde9575ea083825cbcb3dde2d00cb4c858c49d9dfd61b3129", size = 965481, upload-time = "2026-06-08T09:43:01.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/8c/d082bd5f72d7613524d5b35dfe1f71732b2246be2704fad68cd0e3fdd020/databricks_sdk-0.102.0-py3-none-any.whl", hash = "sha256:75d1253276ee8f3dd5e7b00d62594b7051838435e618f74a8570a6dbd723ec12", size = 838533, upload-time = "2026-03-19T08:15:52.248Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fd/80f87c4036b84a87102e95e87b1427b07c2b2de9e3101ff890bbf81ae2f4/databricks_sdk-0.115.0-py3-none-any.whl", hash = "sha256:4c6b32d7360442e99f4e662d8fe2638f217ce4dc3c1901a4d1ccc80e6c199f59", size = 912341, upload-time = "2026-06-08T09:42:59.327Z" }, ] [[package]] @@ -1193,7 +1359,7 @@ wheels = [ [[package]] name = "datamodel-code-generator" -version = "0.55.0" +version = "0.61.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete", 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')" }, @@ -1204,21 +1370,21 @@ dependencies = [ { name = "jinja2", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "pydantic", extra = ["email"], marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "pyyaml", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "tomli", marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/36/ec505ce62c143c0f045e82e2bb0360e2ede765c0cfe3a70bf32c5661b8a2/datamodel_code_generator-0.55.0.tar.gz", hash = "sha256:20ae7a4fbbb12be380f0bd02544db4abae96c5b644d4b3f2b9c3fc0bc9ee1184", size = 833828, upload-time = "2026-03-10T20:41:15.796Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/df/b2e7c15387d309bb7474dc4dbea96e624cfc42c1076e33abaed2d9164892/datamodel_code_generator-0.61.0.tar.gz", hash = "sha256:42d1b530bfa80d18a7bbfd2a24c21e53cb99a2dd4ef9d29f966a434d859c89c7", size = 1149717, upload-time = "2026-06-08T18:01:02.302Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/c6/2abc9d11adbbf689b6b4dfb7a136d57b9ccaa3b3f1ba83504462109e8dbb/datamodel_code_generator-0.55.0-py3-none-any.whl", hash = "sha256:efa5a925288ca2a135fdc3361c7d774ae5b24b4fd632868363e249d55ea2f137", size = 256860, upload-time = "2026-03-10T20:41:13.488Z" }, + { url = "https://files.pythonhosted.org/packages/eb/19/82582eae8ccd00ab76cb942d4a08f710c9044bbb1fe3dded804cc9a1208c/datamodel_code_generator-0.61.0-py3-none-any.whl", hash = "sha256:2c0ddcf5203c182e533c3567836ab5131d4b2ae4c8698419a59de3063e3cb447", size = 340657, upload-time = "2026-06-08T18:01:00.565Z" }, ] [[package]] name = "datasets" -version = "4.0.0" +version = "4.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dill", 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 = "filelock", 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 = "fsspec", extra = ["http"], 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 = "huggingface-hub", 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 = "multiprocess", 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 = "numpy", 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')" }, @@ -1230,33 +1396,33 @@ dependencies = [ { name = "tqdm", 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 = "xxhash", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e3/9d/348ed92110ba5f9b70b51ca1078d4809767a835aa2b7ce7e74ad2b98323d/datasets-4.0.0.tar.gz", hash = "sha256:9657e7140a9050db13443ba21cb5de185af8af944479b00e7ff1e00a61c8dbf1", size = 569566, upload-time = "2025-07-09T14:35:52.431Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/47/325206ac160f7699ed9f1798afa8f8f8d5189b03bf3815654859ac1d5cba/datasets-4.3.0.tar.gz", hash = "sha256:bc9118ed9afd92346c5be7ed3aaa00177eb907c25467f9d072a0d22777efbd2b", size = 582801, upload-time = "2025-10-23T16:31:51.547Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/62/eb8157afb21bd229c864521c1ab4fa8e9b4f1b06bafdd8c4668a7a31b5dd/datasets-4.0.0-py3-none-any.whl", hash = "sha256:7ef95e62025fd122882dbce6cb904c8cd3fbc829de6669a5eb939c77d50e203d", size = 494825, upload-time = "2025-07-09T14:35:50.658Z" }, + { url = "https://files.pythonhosted.org/packages/ca/51/409a8184ed35453d9cbb3d6b20d524b1115c2c2d117b85d5e9b06cd70b45/datasets-4.3.0-py3-none-any.whl", hash = "sha256:0ea157e72138b3ca6c7d2415f19a164ecf7d4c4fa72da2a570da286882e96903", size = 506846, upload-time = "2025-10-23T16:31:49.965Z" }, ] [[package]] name = "debugpy" -version = "1.8.20" +version = "1.8.21" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/aa/12037145b7a56eaa5b29b41872f7a21b538e807e13f32c4d3c46e59be084/debugpy-1.8.21.tar.gz", hash = "sha256:a3c53278e84c94e11bd87c53970ec391d1a67396c8b22609fcac576520e611a6", size = 1697577, upload-time = "2026-06-01T19:30:35.156Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" }, - { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" }, - { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, - { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, - { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, - { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, - { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/cbf306d6e07a313a91e7171a98669054502840931432c227cfd505ee367f/debugpy-1.8.21-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:da456226c7b4c69e35dbe35dcee6623d912000a77816db7856a41af1c72a0264", size = 2203120, upload-time = "2026-06-01T19:30:43.964Z" }, + { url = "https://files.pythonhosted.org/packages/aa/57/aa739bd4ad2cbf96aeb1b20b56918ddd5ae4c28b68709bfcd327f02123ee/debugpy-1.8.21-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:f68b891688e61bdc08b8d364d919ff0051e0b94657b39dcd027bc3173edb7cdc", size = 3059958, upload-time = "2026-06-01T19:30:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/a2/df/bf625547431a9cadc9f4cbfeda38866e2b17f6aed147b625377e87834449/debugpy-1.8.21-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:9f96713896f39c3dff0ee841f47320c3f2983d33c341e009361bb0ebc79adc4e", size = 2483609, upload-time = "2026-06-01T19:30:50.794Z" }, + { url = "https://files.pythonhosted.org/packages/bf/09/59324b903599031ff9faaec1758292409f6561a0ec2492fe4b703327705a/debugpy-1.8.21-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:c193d474f0a211191f2b4449d2d06157c689013035bd952f3b617e0ef422b176", size = 3968900, upload-time = "2026-06-01T19:30:52.341Z" }, + { url = "https://files.pythonhosted.org/packages/77/6b/d817e1f8cc77aa055d37fba092e0febfdff40fe652d8d53d4cd7a86ad98d/debugpy-1.8.21-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:13678151fc401e2d68c9880b91e28714f797d40422994572b24560ef80910a88", size = 2477398, upload-time = "2026-06-01T19:30:57.644Z" }, + { url = "https://files.pythonhosted.org/packages/48/57/412421516afc3055fa577516f00beec3d663f9b0ab330639547ae6c57720/debugpy-1.8.21-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:ecbd158386c31ffe71d46f72d44d56e66331ab9b16cad649156d514368f23ab2", size = 3962096, upload-time = "2026-06-01T19:30:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl", hash = "sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92", size = 5352888, upload-time = "2026-06-01T19:31:25.186Z" }, ] [[package]] name = "decorator" -version = "5.2.1" +version = "5.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, ] [[package]] @@ -1280,9 +1446,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/c7/d1ec24fb280caa5a79b6b950db565dab30210a66259d17d5bb2b3a9f878d/dependency_groups-1.3.1-py3-none-any.whl", hash = "sha256:51aeaa0dfad72430fcfb7bcdbefbd75f3792e5919563077f30bc0d73f4493030", size = 8664, upload-time = "2025-05-02T00:34:27.085Z" }, ] +[[package]] +name = "detect-installer" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/ce/6897d812825e9d4c53e3c7112726e800cc5231b013b2223bf64f653ff362/detect_installer-0.1.0.tar.gz", hash = "sha256:00ad7ba0a36e3cf7d08a40d3643011746dbc112597c7d475cc91c416710ca4e7", size = 3049, upload-time = "2026-02-23T10:40:22.567Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/34/8cc73273414405086c58852916e4031812a6a30fe04c057e37ad99397b7f/detect_installer-0.1.0-py3-none-any.whl", hash = "sha256:034fb20fd665c36e6ba52b8821525ea07fb4f7f938cac459df889fb33801528a", size = 4539, upload-time = "2026-02-23T10:40:23.807Z" }, +] + [[package]] name = "diff-cover" -version = "10.2.0" +version = "10.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "chardet", 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')" }, @@ -1290,9 +1465,9 @@ dependencies = [ { name = "pluggy", 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 = "pygments", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/b4/eee71d1e338bc1f9bd3539b46b70e303dac061324b759c9a80fa3c96d90d/diff_cover-10.2.0.tar.gz", hash = "sha256:61bf83025f10510c76ef6a5820680cf61b9b974e8f81de70c57ac926fa63872a", size = 102473, upload-time = "2026-01-09T01:59:07.605Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/21/057e816125c162662d2a2cc2ebcd72dd333e78e51678298d07dd3146011a/diff_cover-10.3.0.tar.gz", hash = "sha256:474dbc63e815fbb7567d7b7ca5b104123e96129f25426ebdbc9a1bdbb935b2c6", size = 106546, upload-time = "2026-05-30T14:17:14.32Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/2c/61eeb887055a37150db824b6bf830e821a736580769ac2fea4eadb0d613f/diff_cover-10.2.0-py3-none-any.whl", hash = "sha256:59c328595e0b8948617cc5269af9e484c86462e2844bfcafa3fb37f8fca0af87", size = 56748, upload-time = "2026-01-09T01:59:06.028Z" }, + { url = "https://files.pythonhosted.org/packages/83/0a/a96e57a7a3fca419cd5ceff0d13dee2166520fa67103fb82624ad64700fb/diff_cover-10.3.0-py3-none-any.whl", hash = "sha256:2e47d5ab3868d1e92131c11f364f3f4a8583c97123d3bbc6b6cc8ce0a4cc2202", size = 58989, upload-time = "2026-05-30T14:17:12.858Z" }, ] [[package]] @@ -1317,11 +1492,11 @@ wheels = [ [[package]] name = "dill" -version = "0.3.8" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/17/4d/ac7ffa80c69ea1df30a8aa11b3578692a5118e7cd1aa157e3ef73b092d15/dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca", size = 184847, upload-time = "2024-01-27T23:42:16.145Z" } +sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/7a/cef76fd8438a42f96db64ddaa85280485a9c395e7df3db8158cfec1eee34/dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7", size = 116252, upload-time = "2024-01-27T23:42:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, ] [[package]] @@ -1344,11 +1519,11 @@ wheels = [ [[package]] name = "distlib" -version = "0.4.0" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/8d/873e9252ea2c0e0c857884e0a2899ec43ade132345df1925ef24cbe64f18/distlib-0.4.2.tar.gz", hash = "sha256:baeb401c90f27acd15c4861ae0847d1e731c27ac3dbf4210643ba61fa1e813db", size = 614914, upload-time = "2026-06-08T16:24:15.439Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, + { url = "https://files.pythonhosted.org/packages/c1/60/aa891c893821d4d127292ed66c6940d1d715894bd5a0ce048056bc641773/distlib-0.4.2-py2.py3-none-any.whl", hash = "sha256:ca4cb11e5d746b5ec13c199cbf19ae27a241f89702b54e153a74332955446067", size = 470510, upload-time = "2026-06-08T16:24:13.208Z" }, ] [[package]] @@ -1384,11 +1559,11 @@ wheels = [ [[package]] name = "docstring-parser" -version = "0.17.0" +version = "0.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, ] [[package]] @@ -1402,22 +1577,22 @@ wheels = [ [[package]] name = "duckdb" -version = "1.5.1" +version = "1.5.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/62/590caabec6c41003f46a244b6fd707d35ca2e552e0c70cbf454e08bf6685/duckdb-1.5.1.tar.gz", hash = "sha256:b370d1620a34a4538ef66524fcee9de8171fa263c701036a92bc0b4c1f2f9c6d", size = 17995082, upload-time = "2026-03-23T12:12:15.894Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/00/d579dcb2a536b6ea3a2563cdad6844f77d81a9b2d4b22a858097f2468acf/duckdb-1.5.3.tar.gz", hash = "sha256:df39428eb130faa35ae96fd35245bdeae6ecf43936250b116b5fead568eb9f16", size = 18026640, upload-time = "2026-05-20T11:55:31.901Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/3e/827ffcf58f0abc6ad6dcf826c5d24ebfc65e03ad1a20d74cad9806f91c99/duckdb-1.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bc7ca6a1a40e7e4c933017e6c09ef18032add793df4e42624c6c0c87e0bebdad", size = 30067835, upload-time = "2026-03-23T12:10:34.026Z" }, - { url = "https://files.pythonhosted.org/packages/dd/da/ed804006cd09ba303389d573c8b15d74220667cbd1fd990c26e98d0e0a5b/duckdb-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b8b0808dba0c63b7633bdaefb34e08fe0612622224f9feb0e7518904b1615101", size = 14222994, upload-time = "2026-03-23T12:10:45.162Z" }, - { url = "https://files.pythonhosted.org/packages/b3/43/c904d81a61306edab81a9d74bb37bbe65679639abb7030d4c4fec9ed84f7/duckdb-1.5.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:553c273a6a8f140adaa6da6a6135c7f95bdc8c2e5f95252fcdf9832d758e2141", size = 19244880, upload-time = "2026-03-23T12:10:48.529Z" }, - { url = "https://files.pythonhosted.org/packages/50/db/358715d677bfe5e117d9e1f2d6cc2fc2b0bd621144d1f15335b8b59f95d7/duckdb-1.5.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40c5220ec93790b18ec6278da9c6ac2608d997ee6d6f7cd44c5c3992764e8e71", size = 21350874, upload-time = "2026-03-23T12:10:52.095Z" }, - { url = "https://files.pythonhosted.org/packages/3f/06/be4c62f812c6e23898733073ace0482eeb18dffabe0585d63a3bf38bca1e/duckdb-1.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6f7361d66cc801d9eb4df734b139cd7b0e3c257a16f3573ebd550ddb255549e6", size = 30113703, upload-time = "2026-03-23T12:11:02.536Z" }, - { url = "https://files.pythonhosted.org/packages/87/03/293bccd838a293d42ea26dec7f4eb4f58b57b6c9ffcfabc6518a5f20a24a/duckdb-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed6d23a3f806898e69c77430ebd8da0c79c219f97b9acbc9a29a653e09740c59", size = 14246803, upload-time = "2026-03-23T12:11:09.624Z" }, - { url = "https://files.pythonhosted.org/packages/15/2c/7b4f11879aa2924838168b4640da999dccda1b4a033d43cb998fd6dc33ea/duckdb-1.5.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6af347debc8b721aa72e48671166282da979d5e5ae52dbc660ab417282b48e23", size = 19271654, upload-time = "2026-03-23T12:11:13.354Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d6/8f9a6b1fbcc669108ec6a4d625a70be9e480b437ed9b70cd56b78cd577a6/duckdb-1.5.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8150c569b2aa4573b51ba8475e814aa41fd53a3d510c1ffb96f1139f46faf611", size = 21386100, upload-time = "2026-03-23T12:11:16.758Z" }, - { url = "https://files.pythonhosted.org/packages/a5/f2/af476945e3b97417945b0f660b5efa661863547c0ea104251bb6387342b1/duckdb-1.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:26e56b5f0c96189e3288d83cf7b476e23615987902f801e5788dee15ee9f24a9", size = 30113759, upload-time = "2026-03-23T12:11:26.5Z" }, - { url = "https://files.pythonhosted.org/packages/53/a5/b59cff67f5e0420b8f337ad86406801cffacae219deed83961dcceefda67/duckdb-1.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:482f8a13f2600f527e427f73c42b5aa75536f9892868068f0aaf573055a0135f", size = 14246482, upload-time = "2026-03-23T12:11:33.33Z" }, - { url = "https://files.pythonhosted.org/packages/e9/12/d72a82fe502aae82b97b481bf909be8e22db5a403290799ad054b4f90eb4/duckdb-1.5.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da137802688190835b4c863cafa77fd7e29dff662ee6d905a9ffc14f00299c91", size = 19270816, upload-time = "2026-03-23T12:11:36.79Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c3/ee49319b15f139e04c067378f0e763f78336fbab38ba54b0852467dd9da4/duckdb-1.5.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d4147422d91ccdc2d2abf6ed24196025e020259d1d267970ae20c13c2ce84b1", size = 21385695, upload-time = "2026-03-23T12:11:40.465Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fc/a8a89c6c73f31c2b58c6abbc2f543e0b736042dd5ef7cc1784c24ec31428/duckdb-1.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:341a2672e2551ba51c95c1898f0ade983e76675e79038ccb16342c3d6cfb82d7", size = 32583465, upload-time = "2026-05-20T11:54:13.132Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1a/7bf5ba1b7ea520557e6b2dbee1c85abab016bdac0c1779d9d0ef76c87300/duckdb-1.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70a18f932cf6d87bd0e554613657a515c1443a1724aacfc7ec5137dd28698b03", size = 15424794, upload-time = "2026-05-20T11:54:19.891Z" }, + { url = "https://files.pythonhosted.org/packages/ad/16/ce4b1e386e45fab0268edbf1b85bace20e9437589e9edb2bd5f9a226fa44/duckdb-1.5.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e80eb4d0fb59869cb2c7d7ef494c07fb92014fe8e77d96c170cd1ebc1488a708", size = 19306666, upload-time = "2026-05-20T11:54:22.77Z" }, + { url = "https://files.pythonhosted.org/packages/99/1f/651f8453f26931e8061b7e27b3090f868868185814ecb9216d0bd71ec8ef/duckdb-1.5.3-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3248b49cd835ea322574bc6aac0ae7a83be85547f49d4f5f5777cb380ee6627f", size = 21418306, upload-time = "2026-05-20T11:54:25.616Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/2e34929b16c8d544ef664fad8f7f3a2a9db05746aae1e7c8c4ee3a8b23e4/duckdb-1.5.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ff11a457258148337ef9a392148a8cdbd1069b6c27c21958816c7b67fe6c542d", size = 32626494, upload-time = "2026-05-20T11:54:33.738Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/c80af1eac2ab5d35fc2c372ef0a84668842e549fbbf7799277b3fccf3e39/duckdb-1.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10960400ed60cdf0fe05bab2086fa8eb733889cb0ceca18d07ff9a00c0e0be7b", size = 15449283, upload-time = "2026-05-20T11:54:39.777Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/c63af233c9f761bf5178a5210437e1bc6bcb30fa8a9073de6398cfb12c03/duckdb-1.5.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5f18e7561403054433706c187589e86629a7af09a7efc23a06a8b308e6acc68", size = 19332762, upload-time = "2026-05-20T11:54:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/21/cc/2d77af4fff86012f334ef82e6d54a995a86c8745e58074f1218ed7d25171/duckdb-1.5.3-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fb7516255a8764545e30f7efacea408cc847764a3027b3b0b3e7d1a7bebbc5c", size = 21453290, upload-time = "2026-05-20T11:54:45.272Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/a528eb09d8be51954c485864bd06753e616939a080cbc3dd4417e8c94a57/duckdb-1.5.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e75a6122c12579a99848517f6f00a4e342aebda3590c30fe9b5cc5f39d5e6afc", size = 32626254, upload-time = "2026-05-20T11:54:53.65Z" }, + { url = "https://files.pythonhosted.org/packages/23/fa/beafb91e6e152d2161c4a9cbc472334c87607eb61ad7104b5a7fa8d8d7b1/duckdb-1.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3d5db8c0b55e072cf437948ebb5d7e23d7b9d03d905fa5f9145583e65aa447f7", size = 15449411, upload-time = "2026-05-20T11:54:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/50/0a/49b6fe04e2fcd63729eb607dadd44818dde77342a4f5ce086c6c92f1dd4d/duckdb-1.5.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ce80aed7a538422129a57eaca9141e3afb51f8bf562b1908b1576c9725b5b22", size = 19333120, upload-time = "2026-05-20T11:55:01.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/4c/0907c3f76adb9dd90e67610b31e0304a35814e65c4c41a354a262c09b885/duckdb-1.5.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787df63824f07bf18022dbc3b8ca4b2bfab0ebe616464f55c6e8cd0f59ea762e", size = 21453266, upload-time = "2026-05-20T11:55:04.5Z" }, ] [[package]] @@ -1570,9 +1745,10 @@ standard = [ [[package]] name = "fastapi-cloud-cli" -version = "0.15.1" +version = "0.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "detect-installer", 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 = "fastar", 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 = "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')" }, @@ -1582,37 +1758,57 @@ dependencies = [ { name = "typer", 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", extra = ["standard"], 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/f2/fcd66ce245b7e3c3d84ca8717eda8896945fbc17c87a9b03f490ff06ace7/fastapi_cloud_cli-0.15.1.tar.gz", hash = "sha256:71a46f8a1d9fea295544113d6b79f620dc5768b24012887887306d151165745d", size = 43851, upload-time = "2026-03-26T10:23:12.932Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/7c/f194925af8fabdb0b7a886a1b89087c0b7f327f99e79497a882aa94c1e34/fastapi_cloud_cli-0.19.0.tar.gz", hash = "sha256:f97b31c2ad6af3832eb4065870bdca3365b6e827a0ccf6eeb15e477bc1662b13", size = 57476, upload-time = "2026-06-01T08:24:03.407Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/11/ecb0d5e1d114e8aaec1cdc8ee2d7b0f54292585067effe2756bde7e7a4b0/fastapi_cloud_cli-0.15.1-py3-none-any.whl", hash = "sha256:b1e8b3b26dc314e180fc0ab67dfd39d7d9fe160d3951081d09184eafaacf5649", size = 32284, upload-time = "2026-03-26T10:23:14.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e6/1a2ec890fc273b9da2b173ca45f692a2e24a369bdd39ea7812c1d8a799e5/fastapi_cloud_cli-0.19.0-py3-none-any.whl", hash = "sha256:a2dfc4074c321e63ec88589cc1f90573d4b5bf980ddc44a7033e6f3cd8e96628", size = 38239, upload-time = "2026-06-01T08:24:02.437Z" }, ] [[package]] name = "fastar" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dd/00/dab9ca274cf1fde19223fea7104631bea254751026e75bf99f2b6d0d1568/fastar-0.9.0.tar.gz", hash = "sha256:d49114d5f0b76c5cc242875d90fa4706de45e0456ddedf416608ecd0787fb410", size = 70124, upload-time = "2026-03-20T14:26:34.503Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/6a/085b3cae0e04da4d42306dc07e2cc4f95d9c8f27df4dfd1a25d0f80516cb/fastar-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c8ac3e8aaee57dfc822b04f570f0a963c2381a9dc8990fe0c6e965efd23fd451", size = 629764, upload-time = "2026-03-20T14:25:19.017Z" }, - { url = "https://files.pythonhosted.org/packages/30/d4/4a5a3c341d26197ea3ae6bed79fc9bb4ead8ddc74a93bdb74e4ee0bac18e/fastar-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17e2c3b46408193ea13c1e1177275ca7951e88bd3dce16baccb8de4f5e0dc2e8", size = 762096, upload-time = "2026-03-20T14:23:49.175Z" }, - { url = "https://files.pythonhosted.org/packages/b0/f8/521438041d69873bb68b144b09080ae4f1621cebb8238b1e54821057206b/fastar-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c75e779f72d845037d4bf6692d01ac66f014eaef965c9231d41d5cc1276b89fc", size = 822380, upload-time = "2026-03-20T14:25:06.825Z" }, - { url = "https://files.pythonhosted.org/packages/60/32/6e7cb45dce544f97b0199325084a0a5a895cb903e0539690619e78d8d7cf/fastar-0.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec7852de506d022ad36ad56f4aefb10c259dd59e485bf87af827954d404ba9d5", size = 969993, upload-time = "2026-03-20T14:25:44.222Z" }, - { url = "https://files.pythonhosted.org/packages/1f/44/a1c9f6afe93d1cc1abb68a7cda2bada509d756d24e22d5d949ca86b4f45e/fastar-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5c03fad1ad9ac57cf03a4db9e18c7109c37416ff4eb9ebfca98fcd2b233a26c4", size = 1029251, upload-time = "2026-03-20T14:26:23.215Z" }, - { 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/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/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/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/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/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/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" }, - { url = "https://files.pythonhosted.org/packages/d0/19/9f8fb5c0e803254c5d535c362102dd604d9bdb206d5a36150f4637cadf09/fastar-0.9.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76be31936cabce31cbb6381128f851cf0a6da2d5c25357615cd1504b26dc31cf", size = 633000, upload-time = "2026-03-20T14:25:28.496Z" }, - { url = "https://files.pythonhosted.org/packages/ef/04/366937320b1cca522570c527a45b1254bd68d057e68956baefc49eacae27/fastar-0.9.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b665c33afcd1d581b82235b690d999c5446ccc2c4d80c4a95f30df3b43d22494", size = 763872, upload-time = "2026-03-20T14:23:59.122Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e0/cec25d43df7ea4b4e3e875352c6d51c848c855792ba276c546732a7170af/fastar-0.9.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9ac410d32cbb514e966c45f0fedd0f9447b0dea9e734af714648da503603df6", size = 824024, upload-time = "2026-03-20T14:25:16.142Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ac/eb2a01ed94e79b72003840448d2b69644a54a47f615c7d693432a1337caa/fastar-0.9.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:d62a4fd86eda3bea7cc32efd64d43b6d0fcdbbec009558b750fc362f20142789", size = 972503, upload-time = "2026-03-20T14:25:54.207Z" }, - { url = "https://files.pythonhosted.org/packages/a4/45/1ea024be428ad9d89e9f738c9379507e97df9f9ed97e50e4a1d10ff90fef/fastar-0.9.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:fad70e257daefb42bab68dcd68beaf2e2a99da056d65f2c9f988449a4e869306", size = 1031304, upload-time = "2026-03-20T14:26:33.294Z" }, +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/0f/0aeb3fc50046617702acc0078b277b58367fd62eb727b9ec733ae0e8bbcc/fastar-0.11.0.tar.gz", hash = "sha256:aa7f100f7313c03fdb20f1385927ba95671071ba308ad0c1763fef295e1895ce", size = 70238, upload-time = "2026-04-13T17:11:17.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/ff/b87efb0dcfd081c62c7c7601d7681dabe63103cd51fc16f8d57a1ab45961/fastar-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27eed386fd0558e6daa29211111bbd7b740f7c7e881197f8a00ac7c0f3cdb1d7", size = 631668, upload-time = "2026-04-13T17:09:40.537Z" }, + { url = "https://files.pythonhosted.org/packages/58/ce/8b7fb3f23855accebaaf2d2637eac7f261a7a5d936f861a172079f1ef511/fastar-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f72ce42a5e28a74fbd4d5fbf1a3ac1a1163d13cbc200cbd005fb0fabc54bd", size = 762938, upload-time = "2026-04-13T17:07:54.51Z" }, + { url = "https://files.pythonhosted.org/packages/07/cc/5491e2b677bb841f768e3aba052d0344338a5c78aa5d4c18b443831a8e8d/fastar-0.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5b83c1f61f7017d6e1498568038f8745440cfc16ca2f697ec81bac83050108f6", size = 759232, upload-time = "2026-04-13T17:08:08.864Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/643630bdbd179e41e9fae31c03b4cf6061dbf4d6fbbae8425d16eb12545d/fastar-0.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db73a9b765a516e73983b25341e7b5e0189733878279e278b2295131b0e3a21e", size = 926271, upload-time = "2026-04-13T17:08:23.68Z" }, + { url = "https://files.pythonhosted.org/packages/09/5d/37ade50003b4540e0a53ef100f6692d7ab2ac1122d5acf39920cc09a3e8b/fastar-0.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:625827d52eb4e8fec942e0233f125ff8010fcf6a67c0a974a8e5f4666b771e3c", size = 818634, upload-time = "2026-04-13T17:08:54.268Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ff/135d177de32cc1e837c99019e4643e6e79352bde49544d4ece5b5eebf56b/fastar-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7f5fd8fa21ec0a88296a38dc5d7fc35efd3b26d46a17b8b7c73c5563925ca15", size = 822755, upload-time = "2026-04-13T17:09:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/27/cb/b835dbe76ceac7fa6105851468c259ffd06830eb9c029402e499d0ec153b/fastar-0.11.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8c15af91b8cd87ddf23ea55355ae513c1de3ab67178f26dad017c9e9c0af6096", size = 887101, upload-time = "2026-04-13T17:08:39.248Z" }, + { url = "https://files.pythonhosted.org/packages/9e/54/aa8289eb57fc550535470397cb051f5a58a7c89ca4de31d5502b916dd894/fastar-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a112395a8b0bff251423bd1564c012f0cc058ad8b6bd8fba96f3d7fc117e44", size = 973606, upload-time = "2026-04-13T17:10:10.98Z" }, + { url = "https://files.pythonhosted.org/packages/1f/fd/776d50a0897c01dc6bfd0926772ee913436fdae91b9affaf0a0cbd09f0a1/fastar-0.11.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f2994bb8f5f8c11eb12beae1e6e77a907173c9819236b8a4c8f0573652ceccce", size = 1036696, upload-time = "2026-04-13T17:10:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9e/21e4701aec4a1123d4dc4d31578dc18875582b5710e4725f7ceb752a248b/fastar-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29c9c386dc0d5dda78845a8e6b1480d26ab861c1e0b68f42ae5735cb70ca07f1", size = 1032336, upload-time = "2026-04-13T17:11:02.364Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a6/d5e2a4e48495616440a21eed07558219ca90243ad00b0502586f95bd4833/fastar-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0d9d6b052baf5380baea866675dab6ccd04ec2460d12b1c46f10ce3f4ee6a820", size = 628417, upload-time = "2026-04-13T17:09:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0d/f88daad53aff2e754b6b5ff2a7113f72447a34f6ef17cc23ca99988117b7/fastar-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e6e74aba1ae77ca4aedcaf1697cd413319f4c88a5ccbe5b42c709517c5097e", size = 760737, upload-time = "2026-04-13T17:07:55.958Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a6/82ef4ecd969d50d92ed3ed9dbd8fe77faa24be5e5736f716edc9f4ce8d62/fastar-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38ef77fe940bbc9b37a98bd838727f844b11731cd39358a2640ff864fb385086", size = 757603, upload-time = "2026-04-13T17:08:10.623Z" }, + { url = "https://files.pythonhosted.org/packages/03/35/50249f0d827251f8ac511495e2eacccebda80a00a0ad73e9615b8113b84f/fastar-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8955e61b32d6aff82c983217abf80933fd823b0e727586fc72f08043d996fd59", size = 923952, upload-time = "2026-04-13T17:08:25.526Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d8/faee41659e9c379d906d24eaee6d6833ac8cfef0a5df480e5c2a8d3efb33/fastar-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:483532442cdb08fbff0169510224eae0836f2f672cea6aacb52847d90fefdc46", size = 816574, upload-time = "2026-04-13T17:08:56.076Z" }, + { url = "https://files.pythonhosted.org/packages/22/47/0448ea7992b997dad2bf004bfd98eca74b5858630eae080b50c7b17d9ddc/fastar-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef5a6071121e05d8287fc75bccb054bcbac8bb0501200a0c0a8feeace5303ea4", size = 819382, upload-time = "2026-04-13T17:09:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/33/ef/0d63eb43586831b7a6f8b22c4d77125a7c594423af1f4f090fa9541b9b40/fastar-0.11.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e45e598af5afe8412197d4786efd6cf29be02e7d3d4f6a3461149eae5d7e94f1", size = 885254, upload-time = "2026-04-13T17:08:40.9Z" }, + { url = "https://files.pythonhosted.org/packages/01/25/edd584675d69e49a165052c3ee886df1c5d574f3e7d813c990306387c623/fastar-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e160919b1c47ddb8538e7e8eb4cd527281b40f0bf75110a75993838ef61f286", size = 971239, upload-time = "2026-04-13T17:10:12.997Z" }, + { url = "https://files.pythonhosted.org/packages/a5/37/e8bb24f506ba2b08fbaf36c5800e843bd4d542954e9331f00418e2d23349/fastar-0.11.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4bb4dc0fc8f7a6807febcebce8a2f3626ba4955a9263d81ecc630aad83be84c0", size = 1035185, upload-time = "2026-04-13T17:10:30.207Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cd/a81c1aaafb5a22ce57c98ae22f39c89413ed53e4ee6e1b1444b0bd666a6c/fastar-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:136cf342735464091c39dc3708168f9fdeb9ebea40b1ead937c61afaf46143d9", size = 1028054, upload-time = "2026-04-13T17:11:04.293Z" }, + { url = "https://files.pythonhosted.org/packages/e1/cd/7867aefb1784662554a335f2952c75a50f0c70585ed0d2210d6cc15e5627/fastar-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c1c792447e4a642745f347ff9847c52af39633071c57ee67ed53c157fc3506", size = 628460, upload-time = "2026-04-13T17:09:43.776Z" }, + { url = "https://files.pythonhosted.org/packages/25/39/d3f428b318fa940b1b6e785b8d54fc895dfb5d5b945ef8d5442ffa904fb2/fastar-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:863b7929845c9fec92ef6c8d59579cf46af5136655e5342f8df5cebe46cab06c", size = 760247, upload-time = "2026-04-13T17:07:57.396Z" }, + { url = "https://files.pythonhosted.org/packages/9e/04/03949aee82aabb8ede06ac5a4a5579ffaf98a8fe59ce958494508ff15513/fastar-0.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96b4a57df12bf3211662627a3ea29d62ecb314a2434a0d0843f9fc23e47536e5", size = 756512, upload-time = "2026-04-13T17:08:12.415Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0c/2ca1ae0a3828ca51047962d932b80daca2522db73e8cb9d040cb6ebe28d5/fastar-0.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceef1c2c4df7b7b8ebd3f5d718bbf457b9bbdf25ce0bd07870211ec4fbd9aff4", size = 922183, upload-time = "2026-04-13T17:08:27.187Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/7fe808b1f73a68e686f25434f538c6dc10ef4dfb3db0ace22cd861744bf8/fastar-0.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8e545918441910a779659d4759ad0eef349e935fbdb4668a666d3681567eb05", size = 816394, upload-time = "2026-04-13T17:08:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/1f/17/07d086080f8a83b8d7966955e29bcdbd6a060f5bd949dc9d5abd3658cead/fastar-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28095bb8f821e85fc2764e1a55f03e5e2876dee2abe7cd0ee9420d929905d643", size = 818983, upload-time = "2026-04-13T17:09:28.46Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/2c4edf0910af2e814ff6d65b77a91196d472ca8a9fb2033bd983f6856caa/fastar-0.11.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0fafb95ecbe70f666a5e9b35dd63974ccdc9bb3d99ccdbd4014a823ec3e659b5", size = 884689, upload-time = "2026-04-13T17:08:42.763Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/04fdcbd6558e60de4ced3b55230fac47675d181252582b2fcec3c74608e5/fastar-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af48fed039b94016629dcdad1c95c90c486326dd068de2b0a4df419ee09b6821", size = 970677, upload-time = "2026-04-13T17:10:15.124Z" }, + { url = "https://files.pythonhosted.org/packages/df/b3/2b860a9658550167dbd5824c85e88d0b4b912bf493e42a6322544d6e483d/fastar-0.11.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:74cd96163f39b8638ab4e8d49708ca887959672a22871d8170d01f067319533b", size = 1034026, upload-time = "2026-04-13T17:10:32.318Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/d2e501556dca9f1fbc9246111a31792fb49ad908fa4927f34938a97a3604/fastar-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfe39d91fc28e37e06162d94afe01050220edb7df554acb5b702b5503e564816", size = 1028377, upload-time = "2026-04-13T17:11:06.374Z" }, + { url = "https://files.pythonhosted.org/packages/7e/af/ae5cf39d4fb82d0c592705f5ec6db1b065be5265c151b108f86126ee8773/fastar-0.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:298a827ec04ade43733f6ca960d0faec38706aa1494175869ea7ea17f5bad5d3", size = 634371, upload-time = "2026-04-13T17:09:52.083Z" }, + { url = "https://files.pythonhosted.org/packages/bf/46/724dc796e1756d3977970f820d30d59bb8cab8e3671b285f1d82ab513aec/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7496def0a2befd82d429cb004ef7ca831585cc887947bd6b9abb68a5ef852b0b", size = 764469, upload-time = "2026-04-13T17:08:05.638Z" }, + { url = "https://files.pythonhosted.org/packages/99/e3/74d6859e632e8fb9339a14f652fb9f800c2bd6aa53071e311c0be3fbab8b/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:878eaf15463eb572e3538af7ca3a8534e5e279cf8196db902d24e5725c4af86e", size = 761375, upload-time = "2026-04-13T17:08:20.669Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e7/cc70e2be5ef8731a7525552b1c35c1448cf9eae6a62cb3a56f12c1bf27ea/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0324ed1d1ef0186e1bbd843b17807d6d837d0906899d4c99378b02c5d86bdd9c", size = 928189, upload-time = "2026-04-13T17:08:35.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/33/c9a969e78dca323547276a6fee5f4f9588f7cd5ab45acec3778c67399589/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bdf9bd863205590beaf8ef6e66f315310196632180dceaf674985d01a876cac3", size = 820864, upload-time = "2026-04-13T17:09:06.366Z" }, + { url = "https://files.pythonhosted.org/packages/84/bd/6b9434b541fe55c125b5f2e017a565596a2d215aa09207e4555e4585064f/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59af8dbb683b24b90fb5b506de080faeab0a17a908e6c2a5d93a97260ed75d7b", size = 824060, upload-time = "2026-04-13T17:09:37.377Z" }, + { url = "https://files.pythonhosted.org/packages/24/8d/871d5f8cf4c6f13987119fb0a9ae8be131e34f2756c2524e9974adf33824/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:9f3df73a3c4292cfe15696cdf59cdb6c309ab59d30b34c733be13c6e32d9a264", size = 889217, upload-time = "2026-04-13T17:08:50.884Z" }, + { url = "https://files.pythonhosted.org/packages/d0/26/cca0fd2704f3ed20165e5613ed911549aef3aaf3b0b5b02fee0e8e23e6cc/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa3762cbb16e41a76b61f4a6914937a71aab3a7b6c2d82ca233bc686ebaf756b", size = 975418, upload-time = "2026-04-13T17:10:24.307Z" }, + { url = "https://files.pythonhosted.org/packages/99/94/8bbb0b13f5b6cbe2492f0b7cbba5103e6163976a3331466d010e781fa189/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:a8c7bc8ac74cb359bb546b199288c83236372d094b402e557c197e85527495cd", size = 1038492, upload-time = "2026-04-13T17:10:41.939Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/56ef943ea524784598c035ccbd42e564e937da0438ae3f55f0e76cb95571/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6a1c56957ac82408be37a3f63594bc83e0919e8760492a4475e542f9f1828778", size = 1034886, upload-time = "2026-04-13T17:11:15.617Z" }, ] [[package]] @@ -1647,35 +1843,64 @@ wheels = [ [[package]] name = "fastmcp" -version = "3.2.0" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastmcp-slim", extra = ["client", "server"], 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')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/24/519739e98daf92ebc64580e9d3320649bf9a1612c029a913dd88c3474d73/fastmcp-3.4.0.tar.gz", hash = "sha256:29055fb6816f4862c615aabaf0112ae8feb8b469740db13403a0ce5b799ec1dc", size = 28754939, upload-time = "2026-06-03T02:32:40.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/72/9f9bbfc3a8d26870dbdbbd633cd1c6f42b8d3bec379426c760676d936e86/fastmcp-3.4.0-py3-none-any.whl", hash = "sha256:34523083d6149400a0655a8aa769eb34f85b1ce6dac6d66efb07503ebbe5f44b", size = 8017, upload-time = "2026-06-03T02:32:38.05Z" }, +] + +[[package]] +name = "fastmcp-slim" +version = "3.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "platformdirs", 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 = "pydantic-settings", 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-dotenv", 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 = "rich", 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 = "typing-extensions", 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')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/b0/4da6078c2d6aa0a38a8b1ae0271e1ed400f9e2cd1b3b46e6453fb1fe2b75/fastmcp_slim-3.4.0.tar.gz", hash = "sha256:faa0ccf16e85ec4b9f79c006fed3546b866d7e6dba3f60cd32cd98e84753a496", size = 575895, upload-time = "2026-06-03T02:32:18.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/66/cc283d4efd3faf325c26f51cfb43a118270ea732e70dda509f49d80ea625/fastmcp_slim-3.4.0-py3-none-any.whl", hash = "sha256:17cd0a1535972d3748d8c2416f0826dfc86c18df7a6cbc38602373277d44baa6", size = 748849, upload-time = "2026-06-03T02:32:17.435Z" }, +] + +[package.optional-dependencies] +client = [ + { name = "authlib", 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 = "exceptiongroup", 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 = "mcp", 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 = "opentelemetry-api", 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 = "py-key-value-aio", extra = ["filetree", "keyring", "memory"], 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')" }, +] +server = [ { name = "authlib", 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 = "cyclopts", 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 = "exceptiongroup", 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 = "griffelib", 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 = "joserfc", 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 = "jsonref", 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 = "jsonschema-path", 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 = "mcp", 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 = "openapi-pydantic", 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 = "opentelemetry-api", 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 = "packaging", 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 = "platformdirs", 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 = "py-key-value-aio", extra = ["filetree", "keyring", "memory"], 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 = "pyperclip", 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-dotenv", 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 = "rich", 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 = "uncalled-for", 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')" }, { name = "watchfiles", 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 = "websockets", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/32/4f1b2cfd7b50db89114949f90158b1dcc2c92a1917b9f57c0ff24e47a2f4/fastmcp-3.2.0.tar.gz", hash = "sha256:d4830b8ffc3592d3d9c76dc0f398904cf41f04910e41a0de38cc1004e0903bef", size = 26318581, upload-time = "2026-03-30T20:25:37.692Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/67/684fa2d2de1e7504549d4ca457b4f854ccec3cd3be03bd86b33b599fbf58/fastmcp-3.2.0-py3-none-any.whl", hash = "sha256:e71aba3df16f86f546a4a9e513261d3233bcc92bef0dfa647bac3fa33623f681", size = 705550, upload-time = "2026-03-30T20:25:35.499Z" }, -] [[package]] name = "fastuuid" @@ -1705,11 +1930,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.25.2" +version = "3.29.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/f9/f38573ed5844586db374d085911740a501ccfa373b455fc9413f09f85237/filelock-3.29.1.tar.gz", hash = "sha256:d97e6b1b9757569626c58caa07dc4beb1613f4a2938b1e8cc81afca398906c9e", size = 59335, upload-time = "2026-06-03T15:19:04.053Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a0/614c5fe402fd88951df45f4dda2fa3b4e17a99ecd92340771929169b3b95/filelock-3.29.1-py3-none-any.whl", hash = "sha256:85199dfd706869641b72b2e8955d5416a4b2b7dc4b0e8e6d97b4cc1299a6983b", size = 40750, upload-time = "2026-06-03T15:19:02.959Z" }, ] [[package]] @@ -1797,36 +2022,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, { 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" }, ] [[package]] name = "fsspec" -version = "2025.3.0" +version = "2025.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/f4/5721faf47b8c499e776bc34c6a8fc17efdf7fdef0b00f398128bc5dcb4ac/fsspec-2025.3.0.tar.gz", hash = "sha256:a935fd1ea872591f2b5148907d103488fc523295e6c64b835cfad8c3eca44972", size = 298491, upload-time = "2025-03-07T21:47:56.461Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/e0/bab50af11c2d75c9c4a2a26a5254573c0bd97cea152254401510950486fa/fsspec-2025.9.0.tar.gz", hash = "sha256:19fd429483d25d28b65ec68f9f4adc16c17ea2c7c7bf54ec61360d478fb19c19", size = 304847, upload-time = "2025-09-02T19:10:49.215Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/53/eb690efa8513166adef3e0669afd31e95ffde69fb3c52ec2ac7223ed6018/fsspec-2025.3.0-py3-none-any.whl", hash = "sha256:efb87af3efa9103f94ca91a7f8cb7a4df91af9f74fc106c9c7ea0efd7277c1b3", size = 193615, upload-time = "2025-03-07T21:47:54.809Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl", hash = "sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7", size = 199289, upload-time = "2025-09-02T19:10:47.708Z" }, ] [package.optional-dependencies] @@ -1903,88 +2152,109 @@ wheels = [ [[package]] name = "google-auth" -version = "2.49.1" +version = "2.53.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography", 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 = "pyasn1-modules", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/80/6a696a07d3d3b0a92488933532f03dbefa4a24ab80fb231395b9a2a1be77/google_auth-2.49.1.tar.gz", hash = "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", size = 333825, upload-time = "2026-03-12T19:30:58.135Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/ad/ff781329bbbdc0974a098d996e89c9e1f7024262f9e3eec442fbb9ad1ac6/google_auth-2.53.0.tar.gz", hash = "sha256:e7e6aa16f6bee7b2b264830fd04f08087a1d5a836df516251a5d15327b246c9c", size = 335844, upload-time = "2026-05-15T20:53:07.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/eb/c6c2478d8a8d633460be40e2a8a6f8f429171997a35a96f81d3b680dec83/google_auth-2.49.1-py3-none-any.whl", hash = "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7", size = 240737, upload-time = "2026-03-12T19:30:53.159Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c9/db44165ba7c581268c6d46017ef63339110378305062830104fc7fa144cb/google_auth-2.53.0-py3-none-any.whl", hash = "sha256:6e7449917c599b35126a99ec268ec6880301f2fea41dce198fe8fd83ff642b68", size = 246071, upload-time = "2026-05-15T20:53:05.609Z" }, ] [[package]] name = "googleapis-common-protos" -version = "1.73.1" +version = "1.75.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/c0/4a54c386282c13449eca8bbe2ddb518181dc113e78d240458a68856b4d69/googleapis_common_protos-1.73.1.tar.gz", hash = "sha256:13114f0e9d2391756a0194c3a8131974ed7bffb06086569ba193364af59163b6", size = 147506, upload-time = "2026-03-26T22:17:38.451Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/82/fcb6520612bec0c39b973a6c0954b6a0d948aadfe8f7e9487f60ceb8bfa6/googleapis_common_protos-1.73.1-py3-none-any.whl", hash = "sha256:e51f09eb0a43a8602f5a915870972e6b4a394088415c79d79605a46d8e826ee8", size = 297556, upload-time = "2026-03-26T22:15:58.455Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, ] [[package]] name = "greenlet" -version = "3.3.2" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/6e/802acd792aebb2256fbbee8cacf2727faaeb6f240ac11008f09eae4414bc/greenlet-3.5.1.tar.gz", hash = "sha256:5a56aeb7d5d9cc4b3a735efb5095bd4b4f6f0e4f93e5ca876d0e2315137b7829", size = 197356, upload-time = "2026-05-20T15:05:03.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/3c/ff890b466eaba2b0f5e6bdfff025f8c75f41b8ffdc3dbc3d24ad261e764a/greenlet-3.5.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:73f78f9b9f0a5c06e5c946ba1e8e36f5114923b6be109ee618c54f079c3ea14f", size = 284764, upload-time = "2026-05-20T13:09:10.204Z" }, + { url = "https://files.pythonhosted.org/packages/81/0e/5e5457be3d256918f6a4756f073548a3f0190836e2cc94aa6d0d617a940b/greenlet-3.5.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0cbed8bb44e23c5b199f888f4e4ce096b45ad9f25ff74a7ad0213875e936bb2", size = 603479, upload-time = "2026-05-20T14:00:04.757Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e1/f89a21d58d308298e6f275f13a1b472ed96c680b601a371b08be6a725989/greenlet-3.5.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a203a8bd0acb0701653d3bbb26e404854a68674139ed5cbb778830f42b09bb33", size = 615495, upload-time = "2026-05-20T14:05:40.87Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/8fd452fd81adb9ec79c8275c1375702ab0fd6bee4952da12eaa09b9508d8/greenlet-3.5.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ebeb75c81211f5c702576cf81f315e77e23cfdb2c7c6fcb9dd143e6de35c360", size = 623515, upload-time = "2026-05-20T14:09:07.853Z" }, + { url = "https://files.pythonhosted.org/packages/75/de/af6cef182862d2ccd6975440d21c9058a77c3f9b469abf94e322dfd2e0e3/greenlet-3.5.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a271fcd66c74615cda6a964fda3f304267a12e50a084472218a39bb0376f563", size = 614754, upload-time = "2026-05-20T13:14:24.947Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bc/c318aa9f3ffc77320fddcee3d892be957b42e2ff947198d9450b004f3a38/greenlet-3.5.1-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:017a544f0385d441e88714160d089d6900ef46c9eff9d99b6715a5ef2d127747", size = 418439, upload-time = "2026-05-20T14:01:38.446Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c6/50e520283a9f19388a7326b05f9e8637e566003475eacaadad04f558c68d/greenlet-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ded7b068c7c31c1a8657d4fd42d886b3e051ae29f88b80c5ff9d502257b0f071", size = 1574097, upload-time = "2026-05-20T14:02:24.003Z" }, + { url = "https://files.pythonhosted.org/packages/21/1c/13abd1f4860d987fa5e1170a01930d6e6cd40d328de487a3c9fdaff0ffd0/greenlet-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0932b81d72f552ded9d810d00021b64d89f2195a91ce115b893f943b7a4ab3c", size = 1641058, upload-time = "2026-05-20T13:14:31.83Z" }, + { url = "https://files.pythonhosted.org/packages/c4/37/4549f149c9797c21b32c2683c33522af22522099de128b2406672526d005/greenlet-3.5.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:fa4f98af3a528f0c3fd592a26df7f376f93329c8f4d987f6bb979057af8bf5e2", size = 286220, upload-time = "2026-05-20T13:07:28.463Z" }, + { url = "https://files.pythonhosted.org/packages/38/ff/a4f436709716965eaab9f36ea7b906c8a927fbe32fb1372a2071d964f6b1/greenlet-3.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffea73584b216150eab159b6d12348fb253e68757974de1e2c40d8a318ac89ed", size = 601585, upload-time = "2026-05-20T14:00:06.141Z" }, + { url = "https://files.pythonhosted.org/packages/65/ad/54bc3fcee3ad368a61b19b67d88117f7a8c29727bf71fffdeda81fbd946e/greenlet-3.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1072b4f9edcc1e192d9283a66a3e68d6b84c561de33a83d7858beb9ba1effe10", size = 614215, upload-time = "2026-05-20T14:05:42.675Z" }, + { url = "https://files.pythonhosted.org/packages/7c/6c/de5b1b388cd2d9fbdfeab324863daba37d54e6e233ddbefd70b385a8c591/greenlet-3.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89101bfd5011e069be974903cb3a4e4523845e4ece2d62dcd8d358933c0ef249", size = 620094, upload-time = "2026-05-20T14:09:09.18Z" }, + { url = "https://files.pythonhosted.org/packages/40/69/b91cda0647df839483201545913514c2827ebea5e5ccdf931842763bc127/greenlet-3.5.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:add5217d68b31130f0beca584d7fef4878327d2e31642b66618a14eef312b63b", size = 611358, upload-time = "2026-05-20T13:14:26.37Z" }, + { url = "https://files.pythonhosted.org/packages/4a/43/1204baffab8a6476464795a7ccf394a3248d4f22c9f87173a15b36b6d971/greenlet-3.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:e6cd99ea59dd5d89f0c956606571d79bfe6f68c9eb7f4a4083a41a7f1587edee", size = 422782, upload-time = "2026-05-20T14:01:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/59/90/3cf77e080350cd02fa307bb2abf05df48f4482c240275bbd2c203ba8bb1c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5ea42a752d47a145eae922b605cd1634665ac3d5ec1e72402d5048e8d60d207", size = 1570475, upload-time = "2026-05-20T14:02:25.29Z" }, + { url = "https://files.pythonhosted.org/packages/65/2c/18cece62045e74598c3c393f70dce4a63f56222015ba29a5d4eeb04f764c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5551170cf4f5ff5623e9af81323751979fee2c731e2287b61f73cd27257b823", size = 1635625, upload-time = "2026-05-20T13:14:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/27/69/7f7e5372d998b81001899b1c0823c957aa413ba0f2662e65821611cc31e4/greenlet-3.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b", size = 285060, upload-time = "2026-05-20T13:08:51.899Z" }, + { url = "https://files.pythonhosted.org/packages/b1/bf/387f9b6b865fd2ae0d0be09e0004827295a01b71be76ed350dd1e28a91a4/greenlet-3.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a", size = 604370, upload-time = "2026-05-20T14:00:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/32/f5/169ce3d4e4c67291bd18f8cbe0299c9f3e45102c7f1fb3c14780c93e4532/greenlet-3.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283", size = 616987, upload-time = "2026-05-20T14:05:44.237Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/c24110c55dffa55aa6e1d98b45310da33801aeba7686ff0190fe5d46fd32/greenlet-3.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d40a890035c0058cadbdc4af7569800fd28a0e527a0fdbb7b5f9418f176846ce", size = 622911, upload-time = "2026-05-20T14:09:10.598Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e5/7f2e41d5273be07e77560d61ea4e56485b4d6c316d2a84518c62d1364061/greenlet-3.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135", size = 613911, upload-time = "2026-05-20T13:14:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/ec/7b/d20db2e8a5ad6c038702f3179b136f93f0a3d1a21a0c0777f3e470cdf4b2/greenlet-3.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:67821bb03e4e98664490edb787ff6af501194c29bbee0f5c1dfdcf1dc3d9d436", size = 425228, upload-time = "2026-05-20T14:01:40.837Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a4/fbdc67579b73615a1f91615e814303cc71e06128f7baaba87be79b8fb90c/greenlet-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd", size = 1570689, upload-time = "2026-05-20T14:02:27.225Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b4/77abbe35078be39718a46cd49caf16bceb35662f97a34101dca28aa98e47/greenlet-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1", size = 1635602, upload-time = "2026-05-20T13:14:36.344Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, - { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, - { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, - { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, - { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, - { 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/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/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" }, + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, ] [[package]] name = "grpcio" -version = "1.80.0" +version = "1.81.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", 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')" }, ] -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/6e/18/c83f3cad64c5ca63bca7e91e5e46b0d026afc5af9d0a9972472ceba294b3/grpcio-1.80.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5c07e82e822e1161354e32da2662f741a4944ea955f9f580ec8fb409dd6f6060", size = 12035295, upload-time = "2026-03-30T08:46:49.099Z" }, - { url = "https://files.pythonhosted.org/packages/0f/8e/e14966b435be2dda99fbe89db9525ea436edc79780431a1c2875a3582644/grpcio-1.80.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2", size = 6610297, upload-time = "2026-03-30T08:46:52.123Z" }, - { url = "https://files.pythonhosted.org/packages/25/51/bd267c989f85a17a5b3eea65a6feb4ff672af41ca614e5a0279cc0ea381c/grpcio-1.80.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e5e478b3d14afd23f12e49e8b44c8684ac3c5f08561c43a5b9691c54d136ab", size = 6813442, upload-time = "2026-03-30T08:46:57.056Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d9/d80eef735b19e9169e30164bbf889b46f9df9127598a83d174eb13a48b26/grpcio-1.80.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", size = 7414743, upload-time = "2026-03-30T08:46:59.682Z" }, - { url = "https://files.pythonhosted.org/packages/62/29/73ef0141b4732ff5eacd68430ff2512a65c004696997f70476a83e548e7e/grpcio-1.80.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6", size = 7851641, upload-time = "2026-03-30T08:47:05.462Z" }, - { 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/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" }, - { url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" }, - { url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/15/f3/23f47b24f8d8c2028eba501db3acfbb2f592cbb5995eaa6e363a627b74d7/grpcio-1.81.0.tar.gz", hash = "sha256:a5acd7efd3b1fe9b4eb0bcaaa1507eed68a0ad0678b654c3f7b464df9ba9dca5", size = 13032272, upload-time = "2026-06-01T05:56:22.827Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/a8/9916ab10a0201f4c7afb6918125aa2f38a7626ee18ffbc066dd9cb04a74d/grpcio-1.81.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:794e6aa648e8df47d8f908dc8c3b42347d04ec58438f1dcd4e445f09b4f6b0ce", size = 6093557, upload-time = "2026-06-01T05:54:32.64Z" }, + { url = "https://files.pythonhosted.org/packages/a7/43/99e969a048904a65df3129ee53c5f523b7c4e43127786460cac4bee82470/grpcio-1.81.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:cd78145b7f7784661c524624f3526c9c6f891b30a4b54cb93a40806d0d0d61e9", size = 12075345, upload-time = "2026-06-01T05:54:35.77Z" }, + { url = "https://files.pythonhosted.org/packages/83/70/4c3a204e190333768d4f63f4ff56bd0bf405f05b9188f3a59a8bcf161f8b/grpcio-1.81.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:638ccc1b86f7540170a169cb900799b9296a1381e47879ce60b0de9d3db73d33", size = 6640664, upload-time = "2026-06-01T05:54:38.854Z" }, + { url = "https://files.pythonhosted.org/packages/f4/18/7c8e3d0dda2fb7a17076fcd6c9085209eabad3354696c64230f87b3a14eb/grpcio-1.81.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dbdb99986548a7e87f8343805ef315fd4eb50ffaabf4fb1206e42f2542bb805d", size = 6842564, upload-time = "2026-06-01T05:54:43.57Z" }, + { url = "https://files.pythonhosted.org/packages/f6/19/2f1726c2e03ad3f3fe241e6b41534532ad580d595de14a4054ad84999c80/grpcio-1.81.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c36f5d5e97944cbda2d4096b4ae262e6e68506246b61582acf1b8591607f3ccc", size = 7446236, upload-time = "2026-06-01T05:54:46.042Z" }, + { url = "https://files.pythonhosted.org/packages/e5/20/0e7ea7494955cf1beea3077b2fd2c04c84d4480c2ae85a1e1cfa150c62d7/grpcio-1.81.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:77eb4e9fe61486bd1198cc7236ebb0f70e66234e63c0348f40bc2553ed16a88b", size = 7873958, upload-time = "2026-06-01T05:54:52.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/d5/896a3aaf07068d707d88b282a04914b872db4d32d3c7e6d88e43a3b911fa/grpcio-1.81.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:57b3b0e73a518fa286959b40c3eddd02703504ca186e8b7b2945954519bd8b2c", size = 6053538, upload-time = "2026-06-01T05:54:58.965Z" }, + { url = "https://files.pythonhosted.org/packages/68/6a/7e3eafa4727cd405ff917605ed2949e2af162f233f5cbdd773723a5fea7d/grpcio-1.81.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:8bb1789c94322a13336a2b6c58d9c14d68f8628b6e24205a799c69f5bf8516ce", size = 12053447, upload-time = "2026-06-01T05:55:01.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/79/a4302aa82428de48a922421f522b027a1a727ab4d0926368454aa953d36d/grpcio-1.81.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e4d053900a0d24b75d7521139a3872150301b3d6bde3bed5e12318fb25791e4d", size = 6595872, upload-time = "2026-06-01T05:55:04.946Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/1f3896a9baae1f2aedf4e99c55291d6fa1f30ad9603d63bc18bda967b53e/grpcio-1.81.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19f201da7b4e5c0559198abe5a97157e726f3abe6e8f5e832d4a50740f6dcc22", size = 6809676, upload-time = "2026-06-01T05:55:09.513Z" }, + { url = "https://files.pythonhosted.org/packages/34/8b/3441983718095208c5d797fd3239882e97ea89a629f41c8df94b4eef4df9/grpcio-1.81.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:275144b0115353339dbb8a6f28a9cf8997b5bf40e37f8f66ac0b0ea57e95b43f", size = 7412654, upload-time = "2026-06-01T05:55:12.777Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/3860341e6a1f5347be6ab35c6c0e1e3a8eb59d010388207fd561dcf01a88/grpcio-1.81.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6ff087cb1f563f47b504b4e29e684129fc5ae4863faf3ebca08a327764ee6cb", size = 7849498, upload-time = "2026-06-01T05:55:18.078Z" }, + { url = "https://files.pythonhosted.org/packages/f3/29/779ee53c931d0fd55c1d459fde43e485172caa3ac87cbd43d003a13a0185/grpcio-1.81.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:62bbe463c9f0f2ff24e31bd25f8dd8b4bae78900e315915a3195a0ef1471a855", size = 6054973, upload-time = "2026-06-01T05:55:25.043Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b6/7211807926b5a17f8d9a5d47c739a163d6812fefe3e4714e81cf92945ed7/grpcio-1.81.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:43c121e135ae44d1559b430db2b2dfad7421cbbe40e1deba506c7dc62b439719", size = 12048662, upload-time = "2026-06-01T05:55:28.453Z" }, + { url = "https://files.pythonhosted.org/packages/64/89/b1b93ef6b34bd20bbaf707fa99133bc9cc302139d5ec6f77a165c7169796/grpcio-1.81.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f345de40ef2e65f63645d53d251824e6070e07804827c5b00ec2e44555f9f901", size = 6599116, upload-time = "2026-06-01T05:55:31.185Z" }, + { url = "https://files.pythonhosted.org/packages/65/4a/1df2a4cb4a1386e066ab7e4175e34bb884b35ccb60d3621c09c84af6aabb/grpcio-1.81.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a524cd530900bd24511fcb7f2ed144da4ea37711c4b094475d0bceca7a93a170", size = 6811797, upload-time = "2026-06-01T05:55:36.731Z" }, + { url = "https://files.pythonhosted.org/packages/8d/dc/fa189d20601a1be25b08850cfb733879bbb1047b62a8feec3a60e3e1a87b/grpcio-1.81.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e7746ba3e6efc9e2b748eff59470a2b8684d5a9ec607c6580bcaa5be175820bc", size = 7415131, upload-time = "2026-06-01T05:55:39.451Z" }, + { url = "https://files.pythonhosted.org/packages/75/34/0f8202c6809a46c2b4d69125ef3667c40b1c211f8e19930e5fa1f1197039/grpcio-1.81.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0fba53cb96004b2b7fb758b46b2288cb49d0b658316a4e73f3ef67230616ee65", size = 7844481, upload-time = "2026-06-01T05:55:44.849Z" }, ] [[package]] name = "gunicorn" -version = "25.3.0" +version = "26.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/f4/e78fa054248fab913e2eab0332c6c2cb07421fca1ce56d8fe43b6aef57a4/gunicorn-25.3.0.tar.gz", hash = "sha256:f74e1b2f9f76f6cd1ca01198968bd2dd65830edc24b6e8e4d78de8320e2fe889", size = 634883, upload-time = "2026-03-27T00:00:26.092Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/b7/a4a3f632f823e432ce6bc65f62961b7980c898c77f075a2f7118cb3846fe/gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf", size = 727286, upload-time = "2026-05-05T06:38:25.529Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/c8/8aaf447698c4d59aa853fd318eed300b5c9e44459f242ab8ead6c9c09792/gunicorn-25.3.0-py3-none-any.whl", hash = "sha256:cacea387dab08cd6776501621c295a904fe8e3b7aae9a1a3cbb26f4e7ed54660", size = 208403, upload-time = "2026-03-27T00:00:27.386Z" }, + { url = "https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", size = 212009, upload-time = "2026-05-05T06:38:23.007Z" }, ] [[package]] @@ -1998,7 +2268,7 @@ wheels = [ [[package]] name = "hatchling" -version = "1.29.0" +version = "1.30.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging", 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')" }, @@ -2006,9 +2276,9 @@ dependencies = [ { name = "pluggy", 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 = "trove-classifiers", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cf/9c/b4cfe330cd4f49cff17fd771154730555fa4123beb7f292cf0098b4e6c20/hatchling-1.29.0.tar.gz", hash = "sha256:793c31816d952cee405b83488ce001c719f325d9cda69f1fc4cd750527640ea6", size = 55656, upload-time = "2026-02-23T19:42:06.539Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/4c/8717ccb844b4fa5a5ba6352e97d743ed24e9a22cf90b7c109c17030a46a1/hatchling-1.30.1.tar.gz", hash = "sha256:eee4fd45357f72ebb3d7a42e5d72cfb5e29ed426d79e8836288926c4258d5f2e", size = 56929, upload-time = "2026-06-02T00:09:41.487Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/8a/44032265776062a89171285ede55a0bdaadc8ac00f27f0512a71a9e3e1c8/hatchling-1.29.0-py3-none-any.whl", hash = "sha256:50af9343281f34785fab12da82e445ed987a6efb34fd8c2fc0f6e6630dbcc1b0", size = 76356, upload-time = "2026-02-23T19:42:05.197Z" }, + { url = "https://files.pythonhosted.org/packages/56/49/2797ec0ef88008a653a8867bb8d1e5c223cd2df8e40390dd5c6a0279cbc5/hatchling-1.30.1-py3-none-any.whl", hash = "sha256:161eacafb3c6f91526e92116d21426369f2c36e98c36a864f11a96345ad4ee31", size = 77489, upload-time = "2026-06-02T00:09:40.139Z" }, ] [[package]] @@ -2037,20 +2307,20 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.4.3" +version = "1.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/92/ec9ad04d0b5728dca387a45af7bc98fbb0d73b2118759f5f6038b61a57e8/hf_xet-1.4.3.tar.gz", hash = "sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113", size = 670477, upload-time = "2026-03-31T22:40:07.874Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/2d/57fd21d84d93efb4bd0b962383790e19dd1bc053501b4264c97903b4e83e/hf_xet-1.5.1.tar.gz", hash = "sha256:51ef4500dab3764b41135ee1381a4b62ce56fc54d4c92b719b59e597d6df5bf6", size = 876636, upload-time = "2026-06-08T23:02:53.897Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/d2/8bee5996b699262edb87dbb54118d287c0e1b2fc78af7cdc41857ba5e3c4/hf_xet-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bee693ada985e7045997f05f081d0e12c4c08bd7626dc397f8a7c487e6c04f7f", size = 3558942, upload-time = "2026-03-31T22:39:47.938Z" }, - { url = "https://files.pythonhosted.org/packages/c3/a1/e993d09cbe251196fb60812b09a58901c468127b7259d2bf0f68bf6088eb/hf_xet-1.4.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21644b404bb0100fe3857892f752c4d09642586fd988e61501c95bbf44b393a3", size = 4207657, upload-time = "2026-03-31T22:39:39.69Z" }, - { url = "https://files.pythonhosted.org/packages/64/44/9eb6d21e5c34c63e5e399803a6932fa983cabdf47c0ecbcfe7ea97684b8c/hf_xet-1.4.3-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:987f09cfe418237812896a6736b81b1af02a3a6dcb4b4944425c4c4fca7a7cf8", size = 3986765, upload-time = "2026-03-31T22:39:37.936Z" }, - { url = "https://files.pythonhosted.org/packages/ea/7b/8ad6f16fdb82f5f7284a34b5ec48645bd575bdcd2f6f0d1644775909c486/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:60cf7fc43a99da0a853345cf86d23738c03983ee5249613a6305d3e57a5dca74", size = 4188162, upload-time = "2026-03-31T22:39:58.382Z" }, - { url = "https://files.pythonhosted.org/packages/1b/c4/39d6e136cbeea9ca5a23aad4b33024319222adbdc059ebcda5fc7d9d5ff4/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2815a49a7a59f3e2edf0cf113ae88e8cb2ca2a221bf353fb60c609584f4884d4", size = 4424525, upload-time = "2026-03-31T22:40:00.225Z" }, - { url = "https://files.pythonhosted.org/packages/0b/f8/7aacb8e5f4a7899d39c787b5984e912e6c18b11be136ef13947d7a66d265/hf_xet-1.4.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583", size = 3562178, upload-time = "2026-03-31T22:39:51.295Z" }, - { url = "https://files.pythonhosted.org/packages/df/9a/a24b26dc8a65f0ecc0fe5be981a19e61e7ca963b85e062c083f3a9100529/hf_xet-1.4.3-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08", size = 4212320, upload-time = "2026-03-31T22:39:42.922Z" }, - { url = "https://files.pythonhosted.org/packages/53/60/46d493db155d2ee2801b71fb1b0fd67696359047fdd8caee2c914cc50c79/hf_xet-1.4.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f", size = 3991546, upload-time = "2026-03-31T22:39:41.335Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f5/067363e1c96c6b17256910830d1b54099d06287e10f4ec6ec4e7e08371fc/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac", size = 4193200, upload-time = "2026-03-31T22:40:01.936Z" }, - { url = "https://files.pythonhosted.org/packages/42/4b/53951592882d9c23080c7644542fda34a3813104e9e11fa1a7d82d419cb8/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba", size = 4429392, upload-time = "2026-03-31T22:40:03.492Z" }, + { url = "https://files.pythonhosted.org/packages/b6/bc/9cae6cfeb4e03070874e73e5c97c66eb90369d3206b6a2b1ef5f96520888/hf_xet-1.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78e4e5192ad2b674c2e1160b651cb9134db974f8ae1835bdfbfb0166b894a43", size = 3838493, upload-time = "2026-06-08T23:02:15.282Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b4/d5c01e0eb6d9f2ca2dacd84d0d1b71e6cfbb2ef3208c968528e010e9b3d7/hf_xet-1.5.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6f7a04a8ad962422e225bc49fbbac99dc1806764b1f3e54dbd154bffa7593947", size = 4505658, upload-time = "2026-06-08T23:02:17.196Z" }, + { url = "https://files.pythonhosted.org/packages/76/c5/29a7598c0c6383c523dc22186d577f4e04267a626cd95ae60f67c00bfe66/hf_xet-1.5.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d48199c2bf4f8df0adc55d31d1368b6ec0e4d4f45bc86b08038089c23db0bed8", size = 4292822, upload-time = "2026-06-08T23:02:18.608Z" }, + { url = "https://files.pythonhosted.org/packages/04/9a/dceaf6ca69390126b86ea825fb354b93d01163199070b7bd849225de9468/hf_xet-1.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:97f212a88d14bbf573619a74b7fecb238de77d08fc702e54dec6f78276ca3283", size = 4491255, upload-time = "2026-06-08T23:02:20.124Z" }, + { url = "https://files.pythonhosted.org/packages/48/a7/e5a7afaacf6c1791fdbeeac42951fb81c3d2bc482992b115dedcc86d963e/hf_xet-1.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f61e3665892a6c8c5e765395838b8ddf36185da835253d4bc4509a81e49fb342", size = 4711062, upload-time = "2026-06-08T23:02:21.863Z" }, + { url = "https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:94e761bbd266bf4c03cee73753916062665ce8365aa40ed321f45afcb934b41e", size = 3845354, upload-time = "2026-06-08T23:02:42.702Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:892e3a3a3aecc12aded8b93cf4f9cd059282c7de0732f7d55026f3abdf474350", size = 4514864, upload-time = "2026-06-08T23:02:44.497Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6e/21f7e5a2381278bd3b7b7a5a4d90038518bb6308a0c1daf5d9f8268bb178/hf_xet-1.5.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a93df2039190502835b1db8cd7e178b0b7b889fe9ab51299d5ced26e0dd879a4", size = 4303784, upload-time = "2026-06-08T23:02:46.203Z" }, + { url = "https://files.pythonhosted.org/packages/35/0e/f992bb6927ac1cb30ef74e62268f551f338bc32b2191f7c96a44c6f7283e/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0c97106032ef70467b4f6bc2d0ccc266d7613ee076afc56516c502f87ce1c4a6", size = 4500703, upload-time = "2026-06-08T23:02:47.628Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d1/90a498d05447980b977b1669246eeeeae4cfb0ea3e7a286eaba627f91bf9/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6208adb15d192b90e4c2ad2a27ed864359b2cb0f2494eb6d7c7f3699ac02e2bf", size = 4719498, upload-time = "2026-06-08T23:02:49.268Z" }, ] [[package]] @@ -2068,28 +2338,28 @@ wheels = [ [[package]] name = "httptools" -version = "0.7.1" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, - { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, - { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, - { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, - { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, - { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, - { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, - { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, - { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, - { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, - { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, - { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, - { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/d2/c3eedaef57de65c3cc5f8dc244cf12d09c84ad258a479055aad6db23206c/httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168", size = 208428, upload-time = "2026-05-25T22:16:59.717Z" }, + { url = "https://files.pythonhosted.org/packages/f1/94/dfe435d90d0ef61ec0f2cc3d480eef78c59727c6c2ce039f433882f6131a/httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d", size = 113366, upload-time = "2026-05-25T22:17:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d4/13025f1a56e615dcb331e0bbe2d9a1143212b58c263385fc5d2e558f5bac/httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376", size = 464676, upload-time = "2026-05-25T22:17:02.014Z" }, + { url = "https://files.pythonhosted.org/packages/bf/95/4c1c26c0b985f8a3331682d802598f14e32dc41bf7509266eb2c04ad4801/httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d", size = 464235, upload-time = "2026-05-25T22:17:03.109Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/6735be2b0ca527718c431cdb8e5f70c3862c0844a687df0f572c51e11497/httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085", size = 449809, upload-time = "2026-05-25T22:17:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f9/5811c74f37a758c8a4aa3dc430375119d335947e883efc4664d8f3559a41/httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124", size = 452174, upload-time = "2026-05-25T22:17:05.476Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, ] [[package]] @@ -2122,14 +2392,14 @@ wheels = [ [[package]] name = "httpx-retries" -version = "0.4.6" +version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/13/5eac2df576c02280f79e4639a6d4c93a25cfe94458275f5aa55f5e6c8ea0/httpx_retries-0.4.6.tar.gz", hash = "sha256:a076d8a5ede5d5794e9c241da17b15b393b482129ddd2fdf1fa56a3fa1f28a7f", size = 13466, upload-time = "2026-02-17T16:16:05.995Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/f5/046cac13877ce9b55aebdbb3999e0e45b19b989a95c5fd1040fa04bd1f92/httpx_retries-0.5.0.tar.gz", hash = "sha256:d8c8e1e0852d84be3837aba0bcf78aeb89a4b77db95e8cc988c8c058830b3044", size = 15647, upload-time = "2026-04-20T01:21:47.154Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/97/63f56da4400034adde22adfe7524635dba068f17d6858f92ecd96f55b53e/httpx_retries-0.4.6-py3-none-any.whl", hash = "sha256:d66d912173b844e065ffb109345a453b922f4c2cd9c9e11139304cb33e7a1ee1", size = 8490, upload-time = "2026-02-17T16:16:04.137Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a8/aadeaa9a28510727d538636ee8688f0782a98523147852b29404ce696f1b/httpx_retries-0.5.0-py3-none-any.whl", hash = "sha256:d3124592979a9dc6197e666d1f02e9ab996a0c58fce59fad8db6201a6a87304e", size = 8908, upload-time = "2026-04-20T01:21:46.157Z" }, ] [[package]] @@ -2143,9 +2413,10 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.15.0" +version = "1.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "click", 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 = "filelock", 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 = "fsspec", 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 = "hf-xet", 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')" }, @@ -2156,9 +2427,9 @@ dependencies = [ { name = "typer", 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 = "typing-extensions", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/b6/e22bd20a25299c34b8c5922c1545a6320825b13906eb0f7298edfd034a0b/huggingface_hub-1.15.0.tar.gz", hash = "sha256:28abfdddda3927fd4de6a63cf26ab012498a2c24dae52baf150c5c6edf98a1d5", size = 784100, upload-time = "2026-05-15T11:42:52.149Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/d8/748ea0a47f0fa15227fe682f7a80826b4b7c096e4818044b8f56d6cb66d6/huggingface_hub-1.18.0.tar.gz", hash = "sha256:f0c5ecd1ef8c6a60f86f61ee278f2c1570ba9e279c9f54de9094210723b3613b", size = 812699, upload-time = "2026-06-05T09:26:33.401Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/11/0b64cc9024329b76d7547c19a67604a61d21d3ba678a69d1b220c29d5112/huggingface_hub-1.15.0-py3-none-any.whl", hash = "sha256:a4a59af04cbc41a3fe3fec429b171ef994ef8c971eda10136746f408dd4e3744", size = 663602, upload-time = "2026-05-15T11:42:50.487Z" }, + { url = "https://files.pythonhosted.org/packages/0b/03/40a05316cb6616e5b7efd7773656441ab04b4b022c2199e79bb4622a92a3/huggingface_hub-1.18.0-py3-none-any.whl", hash = "sha256:729be4a976fb706dcc02d176bcda8a3f32bdf21a294e8f4b3dda6fbcbc9c1ab1", size = 684411, upload-time = "2026-06-05T09:26:31.48Z" }, ] [[package]] @@ -2184,20 +2455,20 @@ wheels = [ [[package]] name = "identify" -version = "2.6.18" +version = "2.6.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/c4/7fb4db12296cdb11893d61c92048fe617ee853f8523b9b296ac03b43757e/identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", size = 99580, upload-time = "2026-03-15T18:39:50.319Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737", size = 99394, upload-time = "2026-03-15T18:39:48.915Z" }, + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, ] [[package]] name = "idna" -version = "3.15" +version = "3.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] @@ -2211,14 +2482,14 @@ wheels = [ [[package]] name = "importlib-metadata" -version = "8.5.0" +version = "8.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/72/c600ae4f68c28fc19f9c31b9403053e5dbb8cace2e6842c7b7c3e4d42fe9/importlib_metadata-8.9.0.tar.gz", hash = "sha256:58850626cef4bd2df100378b0f2aea9724a7b92f10770d547725b047078f99ee", size = 56140, upload-time = "2026-03-20T16:56:26.362Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f9/97f2ca8bb3ec6e4b1d64f983ebe98b9a192faddff67fac3d6303a537e670/importlib_metadata-8.9.0-py3-none-any.whl", hash = "sha256:e0f761b6ea91ced3b0844c14c9d955224d538105921f8e6754c00f6ca79fba7f", size = 27220, upload-time = "2026-03-20T16:56:25.07Z" }, ] [[package]] @@ -2245,11 +2516,10 @@ wheels = [ [[package]] name = "instructor" -version = "1.11.3" +version = "1.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp", 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 = "diskcache", 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 = "docstring-parser", 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 = "jinja2", 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 = "jiter", 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')" }, @@ -2261,9 +2531,9 @@ dependencies = [ { name = "tenacity", 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 = "typer", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/af/428b5d7a6a6eca5738c51706795a395099c141779cd1bbb9a6e2b0d3a94d/instructor-1.11.3.tar.gz", hash = "sha256:6f58fea6fadfa228c411ecdedad4662230c456718f4a770a97a806dcb36b3287", size = 69879936, upload-time = "2025-09-09T15:44:31.548Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/a4/832cfb15420360e26d2d85bd9d5fe1e4b839d52587574d389bc31284bf6f/instructor-1.15.1.tar.gz", hash = "sha256:c72406469d9025b742e83cf0c13e914b317db2089d08d889944e74fcd659ef94", size = 69948370, upload-time = "2026-04-03T01:51:30.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/5f/54783e5b1a497de204a0a59b5e22549f67f5f1aceaa08e00db21b1107ce4/instructor-1.11.3-py3-none-any.whl", hash = "sha256:9ecd7a3780a045506165debad2ddcc4a30e1057f06997973185f356b0a42c6e3", size = 155501, upload-time = "2025-09-09T15:44:26.139Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c8/36c5d9b80aaf40ba9a7084a8fc18c967db6bf248a4cc8d0f0816b14284be/instructor-1.15.1-py3-none-any.whl", hash = "sha256:be81d17ba2b154a04ab4720808f24f9d6b598f80992f82eaf9cc79006099cf6c", size = 178156, upload-time = "2026-04-03T01:51:23.098Z" }, ] [[package]] @@ -2379,26 +2649,26 @@ wheels = [ [[package]] name = "jaraco-functools" -version = "4.4.0" +version = "4.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/cf/ea4ef2920830dea3f5ab2ea4da6fb67724e6dca80ee2553788c3607243d0/jaraco_functools-4.5.0.tar.gz", hash = "sha256:3bb5665ea4a020cf78a7040e89154c77edadb3ca74f366479669c5999aa70b03", size = 20272, upload-time = "2026-05-15T21:34:10.025Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, + { url = "https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl", hash = "sha256:79ce39246eddbde4b3a03b77ea5f0f7878dc669b166a66cf3fa8e266aa3fa2f4", size = 10594, upload-time = "2026-05-15T21:34:08.595Z" }, ] [[package]] name = "jedi" -version = "0.19.2" +version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, + { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, ] [[package]] @@ -2424,27 +2694,43 @@ wheels = [ [[package]] name = "jiter" -version = "0.10.0" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/cf/fc33f5159ce132be1d8dd57251a1ec7a631c7df4bd11e1cd198308c6ae32/jiter-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc", size = 321971, upload-time = "2025-05-18T19:03:27.255Z" }, - { url = "https://files.pythonhosted.org/packages/68/a4/da3f150cf1d51f6c472616fb7650429c7ce053e0c962b41b68557fdf6379/jiter-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d", size = 345574, upload-time = "2025-05-18T19:03:28.63Z" }, - { url = "https://files.pythonhosted.org/packages/81/5a/0e73541b6edd3f4aada586c24e50626c7815c561a7ba337d6a7eb0a915b4/jiter-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5", size = 352174, upload-time = "2025-05-18T19:03:34.965Z" }, - { url = "https://files.pythonhosted.org/packages/41/22/5beb5ee4ad4ef7d86f5ea5b4509f680a20706c4a7659e74344777efb7739/jiter-0.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605", size = 523741, upload-time = "2025-05-18T19:03:38.168Z" }, - { url = "https://files.pythonhosted.org/packages/ea/10/768e8818538e5817c637b0df52e54366ec4cebc3346108a4457ea7a98f32/jiter-0.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5", size = 514527, upload-time = "2025-05-18T19:03:39.577Z" }, - { 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/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/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" }, - { url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225, upload-time = "2025-05-18T19:04:20.583Z" }, - { url = "https://files.pythonhosted.org/packages/43/84/c7d44c75767e18946219ba2d703a5a32ab37b0bc21886a97bc6062e4da42/jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070", size = 350235, upload-time = "2025-05-18T19:04:22.363Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, + { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, + { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, ] [[package]] @@ -2467,14 +2753,14 @@ wheels = [ [[package]] name = "joserfc" -version = "1.6.5" +version = "1.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/dc/5f768c2e391e9afabe5d18e3221346deb5fb6338565f1ccc9e7c6d7befdd/joserfc-1.6.5.tar.gz", hash = "sha256:1482a7db78fb4602e44ed89e51b599d052e091288c7c532c5b694e20149dec48", size = 231881, upload-time = "2026-05-06T04:58:13.408Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/90/25cb27518750218e4f850be63d8bbb2343efaad1c01c3571aaa4b3c33bd7/joserfc-1.7.1.tar.gz", hash = "sha256:77d0b76514879c68c6f433bc5b7357a4ab72008ff1e33d8379fd11d72bd8ca81", size = 233181, upload-time = "2026-06-08T07:21:33.412Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/3b/ad1cb22e75c963b1f07c8a2329bf47227ce7e4361df5eb2fb101b2ce33ef/joserfc-1.6.5-py3-none-any.whl", hash = "sha256:e9878a0f8243fe7b95e11fdda81374ca9f7a689e302751579d3dfdeec559675e", size = 70464, upload-time = "2026-05-06T04:58:11.668Z" }, + { url = "https://files.pythonhosted.org/packages/b3/00/fa62404c3e347f946faa13aa21085205f9cc06ad17671e37f81a51662ae8/joserfc-1.7.1-py3-none-any.whl", hash = "sha256:b3e3d655612e2e1ef67b2600f2f420e12e537b020208fab1761fad647319c164", size = 70423, upload-time = "2026-06-08T07:21:32.001Z" }, ] [[package]] @@ -2524,23 +2810,39 @@ sdist = { url = "https://files.pythonhosted.org/packages/ad/1d/68607c574dd78f030 wheels = [ { url = "https://files.pythonhosted.org/packages/8f/90/8391d14ac97e253d2637dab0eab370903510e0ba3a48510eff33df026742/jsonpath_rust_bindings-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0ca169ac219bc141775fb19df8165d4d0162e6ed77102e1ab19a74a80c1f9051", size = 814574, upload-time = "2025-11-16T19:01:53.739Z" }, { url = "https://files.pythonhosted.org/packages/b0/bd/1fb1e4c6635cfcc2936d9bfd8870c47ae2b1351d0bbd3ac241494e42446e/jsonpath_rust_bindings-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13446ad021abe05d622a01eaa648c238ef3b98e9fc0bd837a589bafb246ca3bc", size = 832886, upload-time = "2025-11-16T19:00:15.109Z" }, + { url = "https://files.pythonhosted.org/packages/cc/7d/77479e07955e1808390faa24b1ec6d57c3970bf96584bbe7a3f285a2c43a/jsonpath_rust_bindings-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9583e965fe5f8f21cd0d047244db9716a119e0e82a06f2336e6b14c9a9637af", size = 837021, upload-time = "2025-11-16T19:00:31.979Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fb/e8254b8c0bc112914b05d91c09ddc83f393d421c238ad5f25dcbc92b174d/jsonpath_rust_bindings-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36a40ed04d2db70897cde2ac92f6c9aae2ed1b426aa4c97a47f3e2be911ea4ba", size = 932671, upload-time = "2025-11-16T19:00:48.349Z" }, + { url = "https://files.pythonhosted.org/packages/c9/de/d71c31e2fcdc9a82bb6390ee804aee21a84237cd8284ca2767085957179a/jsonpath_rust_bindings-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:50f16c3dd6eb572dda74731508d2fca1abbb927ab4f6511fb65eeba6e59fd041", size = 960293, upload-time = "2025-11-16T19:01:05.211Z" }, { url = "https://files.pythonhosted.org/packages/3c/9e/159ab37a111f4a8ef7a781b50e6b9fe39663bb9e69e720f1827858f45e76/jsonpath_rust_bindings-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d656507b5913f9515ff136797c5850df907c5040fa1368baa428f7e829e33f0", size = 947441, upload-time = "2025-11-16T19:01:36.917Z" }, { url = "https://files.pythonhosted.org/packages/17/c8/ff82ee574f5508793599481f2632a02ceb25da23223b81d0a5d080de2396/jsonpath_rust_bindings-1.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a43107f6efc4e66ee046c338741429a268fd972e887721b01bf0f32e47387e30", size = 1067748, upload-time = "2025-11-16T19:02:30.136Z" }, + { url = "https://files.pythonhosted.org/packages/89/e5/97a4e4f3ed1bd069feba3f9810c94ae0ea1001a52c243ecf655b667bd14b/jsonpath_rust_bindings-1.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:dc0c3488f04dbd318fa876fb880e8cb7d1e53abcf8b0d9e697e10a0a15ac3158", size = 1149298, upload-time = "2025-11-16T19:02:46.232Z" }, { url = "https://files.pythonhosted.org/packages/a5/8f/613120a36b281619a13394eb8ede093941d939c530c6595d62070fa10f3d/jsonpath_rust_bindings-1.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbfeb05c7a6854104e97a0e3234f312004b3f4e678d14b68180a6a4f33f4d7c3", size = 1134382, upload-time = "2025-11-16T19:03:19.881Z" }, { 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" }, { url = "https://files.pythonhosted.org/packages/93/75/47695316d55a13d475490ca5aa41e02b8eab8b4eea696cc08536e2f05694/jsonpath_rust_bindings-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ddbf025592bf88fc5395d9d023d7bcc8fab977898c406e0a5722925c3b887c71", size = 814128, upload-time = "2025-11-16T19:02:08.847Z" }, { url = "https://files.pythonhosted.org/packages/69/0b/bbe0f2ba599a3aa59bcf44589188641528be507a2b7e45e8c2edfb17f77f/jsonpath_rust_bindings-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce1c6804706012c3c7a194903ef20befafa3cc913a4ef553696bc837ac738a66", size = 832484, upload-time = "2025-11-16T19:00:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/67/2f/fc61ad93957f01cbb683c78e842348f11832da98274574ff28b886da2cbe/jsonpath_rust_bindings-1.1.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa7e9d25b00c227c51e7a916a13fbf22cf483df622699dbc3ef051861ec1de85", size = 836786, upload-time = "2025-11-16T19:00:43.535Z" }, + { url = "https://files.pythonhosted.org/packages/11/e9/3859c3c118f02b5413ef6a1ddfd1b9f2ecdaf2d1a2eaa58e656bc8d4a887/jsonpath_rust_bindings-1.1.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebb9a05a2b80195ac47aec0ce98d861c102459d16225fefb0f7e0158196c4a58", size = 932463, upload-time = "2025-11-16T19:01:00.129Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f5/56e48adb9dad2a97172b905e305c2de478ae8748a0467996d9aae72f4667/jsonpath_rust_bindings-1.1.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ff4cd052f733d5f270329c552a04e08a1520053355d35f0be886714dff46955", size = 959317, upload-time = "2025-11-16T19:01:16.435Z" }, { url = "https://files.pythonhosted.org/packages/6f/77/d4ddd5710121ffa18f270d0af2c906786db0d5fd914ed47ce704beba9a75/jsonpath_rust_bindings-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7039a2f497674785a423076e803a1fa547c2f9cf568b25e2ac83ff5890b98f", size = 947180, upload-time = "2025-11-16T19:01:49.08Z" }, { url = "https://files.pythonhosted.org/packages/3c/38/7f3e03ae9655d1f7d97f34e2f8e95d55aa3f0790ba4743f648844048fab4/jsonpath_rust_bindings-1.1.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:a239166bd1418897de327c952a9d9ff912d1fabc9da82e688204ccfcd7b22584", size = 1067329, upload-time = "2025-11-16T19:02:41.053Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a6/2a96e232a6f0164320801c68bd99b407aea22d1a101892ccb4fc8a2d2198/jsonpath_rust_bindings-1.1.1-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:330f457556d06abc1ea36b6738eb172288afff6bd251350eaba42bed2f459fd3", size = 1149213, upload-time = "2025-11-16T19:02:57.714Z" }, { url = "https://files.pythonhosted.org/packages/c9/c1/4f7b7f5f78dcf23c7a2a208b3088875ae3596f22db5f0612367d95bdb5f7/jsonpath_rust_bindings-1.1.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:26955685acf0208b6061419cab4bd79fe869ebce57f3cec1e9b20f0e0af56b35", size = 1133989, upload-time = "2025-11-16T19:03:32.485Z" }, ] @@ -2564,7 +2866,7 @@ wheels = [ [[package]] name = "jsonschema" -version = "4.23.0" +version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs", 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')" }, @@ -2572,9 +2874,9 @@ dependencies = [ { name = "referencing", 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 = "rpds-py", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778, upload-time = "2024-07-08T18:40:05.546Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462, upload-time = "2024-07-08T18:40:00.165Z" }, + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, ] [package.optional-dependencies] @@ -2585,23 +2887,24 @@ format-nongpl = [ { name = "jsonpointer", 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 = "rfc3339-validator", 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 = "rfc3986-validator", 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 = "rfc3987-syntax", 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 = "uri-template", 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 = "webcolors", 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]] name = "jsonschema-path" -version = "0.3.4" +version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "attrs", 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 = "pathable", 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 = "referencing", 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 = "requests", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/79/cd02a4df6d9270efdc7d3feefe6edd730b0820c39eeaa107a2faee8322d5/jsonschema_path-0.5.0.tar.gz", hash = "sha256:493b156ba895c97602655b620a8456caa2ce08c1aa389f5a7addec065e6e855c", size = 19597, upload-time = "2026-05-19T20:45:00.971Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" }, + { url = "https://files.pythonhosted.org/packages/04/2c/9e69d73c4297508be9e3b64a970ea3971b3eb8db64ffc5802d40bd25981f/jsonschema_path-0.5.0-py3-none-any.whl", hash = "sha256:2790a070bc7abb08ea3dbe4d340ece4efadf639223001f020c7503229ba068e2", size = 24077, upload-time = "2026-05-19T20:44:59.225Z" }, ] [[package]] @@ -2618,7 +2921,7 @@ wheels = [ [[package]] name = "jupyter-client" -version = "8.8.0" +version = "8.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-core", 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')" }, @@ -2626,10 +2929,11 @@ dependencies = [ { name = "pyzmq", 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 = "tornado", 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 = "traitlets", 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 = "typing-extensions", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/dc/5512503b088997c2250b8bf18258fba9d9ce5ead641183700960d3c9d342/jupyter_client-8.9.1.tar.gz", hash = "sha256:a58f730dd9e728ba16ba1d62ebccf7ffe1ebbdbce4e95cfae941b7321ae1f4fa", size = 359256, upload-time = "2026-06-09T13:15:01.033Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl", hash = "sha256:0b7a295bc46e8751e9adae84781f726c851c1d911bd793edc4a3bde942e3da81", size = 109828, upload-time = "2026-06-09T13:14:58.835Z" }, ] [[package]] @@ -2647,7 +2951,7 @@ wheels = [ [[package]] name = "jupyter-events" -version = "0.12.0" +version = "0.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonschema", extra = ["format-nongpl"], 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')" }, @@ -2659,26 +2963,26 @@ dependencies = [ { name = "rfc3986-validator", 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 = "traitlets", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/c3/306d090461e4cf3cd91eceaff84bede12a8e52cd821c2d20c9a4fd728385/jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b", size = 62196, upload-time = "2025-02-03T17:23:41.485Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/f8/475c4241b2b75af0deaae453ed003c6c851766dbc44d332d8baf245dc931/jupyter_events-0.12.1.tar.gz", hash = "sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3", size = 62854, upload-time = "2026-04-20T23:17:50.66Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb", size = 19430, upload-time = "2025-02-03T17:23:38.643Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6c/6fcde0c8f616ed360ffd3587f7db9e225a7e62b583a04494d2f069cf64ea/jupyter_events-0.12.1-py3-none-any.whl", hash = "sha256:c366585253f537a627da52fa7ca7410c5b5301fe893f511e7b077c2d93ec8bcf", size = 19512, upload-time = "2026-04-20T23:17:48.927Z" }, ] [[package]] name = "jupyter-lsp" -version = "2.3.0" +version = "2.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-server", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/5a/9066c9f8e94ee517133cd98dba393459a16cd48bba71a82f16a65415206c/jupyter_lsp-2.3.0.tar.gz", hash = "sha256:458aa59339dc868fb784d73364f17dbce8836e906cd75fd471a325cba02e0245", size = 54823, upload-time = "2025-08-27T17:47:34.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/ff/1e4a61f5170a9a1d978f3ac3872449de6c01fc71eaf89657824c878b1549/jupyter_lsp-2.3.1.tar.gz", hash = "sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6", size = 55677, upload-time = "2026-04-02T08:10:06.749Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl", hash = "sha256:e914a3cb2addf48b1c7710914771aaf1819d46b2e5a79b0f917b5478ec93f34f", size = 76687, upload-time = "2025-08-27T17:47:33.15Z" }, + { url = "https://files.pythonhosted.org/packages/23/e8/9d61dcbd1dce8ef418f06befd4ac084b4720429c26b0b1222bc218685eff/jupyter_lsp-2.3.1-py3-none-any.whl", hash = "sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81", size = 77513, upload-time = "2026-04-02T08:10:01.753Z" }, ] [[package]] name = "jupyter-server" -version = "2.18.2" +version = "2.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", 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')" }, @@ -2700,9 +3004,9 @@ dependencies = [ { name = "traitlets", 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 = "websocket-client", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/15/1eacb0fcb79ef86e8a0a79a708e6ad7435f6f223097dd29a4ce861fabc44/jupyter_server-2.18.2.tar.gz", hash = "sha256:06b4f40d8a7a00bb39d5216859c81374a0e7cfefe6d8a5a7facc5a5c37c679a7", size = 753177, upload-time = "2026-05-06T07:04:36.274Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/a0/eb3c511f54df7b54ca5fc7bff3f4d2277d69052d6a7f521643dfed5279d6/jupyter_server-2.19.0.tar.gz", hash = "sha256:1731236bc32b680223e1ceb9d68209a845203475012ef68773a81434b46a31a7", size = 754561, upload-time = "2026-05-29T11:21:26.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/50/ecf4f70d65bdb7519b28a33d1b2fee8a4b4ba1ae1a92f15d97e877c5de21/jupyter_server-2.18.2-py3-none-any.whl", hash = "sha256:fa5e46539ded65791838035a2b6001f13e54d5f64b8b3752eb1e91fdd641a5b8", size = 391907, upload-time = "2026-05-06T07:04:34.014Z" }, + { url = "https://files.pythonhosted.org/packages/c1/78/d2881e68894cecdcd05912a9c585cfb776ef1fb38b62c8dba98f12ab3adc/jupyter_server-2.19.0-py3-none-any.whl", hash = "sha256:cb76591b76d7093584c2ad2ae72ac3d58614a4b597507a1bb04e1f9f683cf9ea", size = 392244, upload-time = "2026-05-29T11:21:23.871Z" }, ] [[package]] @@ -2719,7 +3023,7 @@ wheels = [ [[package]] name = "jupyterlab" -version = "4.5.7" +version = "4.5.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-lru", 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')" }, @@ -2736,9 +3040,9 @@ dependencies = [ { name = "tornado", 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 = "traitlets", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/22/8440ec827762146e7cdecf04335bd348795899d29dc6ae82238707353a2c/jupyterlab-4.5.7.tar.gz", hash = "sha256:55a9822c4754da305f41e113452c68383e214dcf96de760146af89ce5d5117b0", size = 23992763, upload-time = "2026-04-29T16:43:51.328Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/74/089613e6099e851a6130816f2df592c839d8565f8746a701edada05a33e4/jupyterlab-4.5.8.tar.gz", hash = "sha256:af54d7242cc689a1e6c3ad213cc9b6d9781787d9ec67c52ec9a8f4707088cadd", size = 23994076, upload-time = "2026-06-04T12:32:12.906Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/aa/537b8f7d80e799af19af35fb3ddfc970b951088a13c57dd9387dcfbb7f61/jupyterlab-4.5.7-py3-none-any.whl", hash = "sha256:fba4cb0e2c44a52859669d8c98b45de029d5e515f8407bf8534d2a8fc5f0964d", size = 12450123, upload-time = "2026-04-29T16:43:46.639Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d1/56a400100559cbf154a23cd29989261941ae5c9f743898fc10e8a5508b7c/jupyterlab-4.5.8-py3-none-any.whl", hash = "sha256:7d514c856d0d607601ec7692374da4f26e2aaf3b6e7cd363136b422a50588d6c", size = 12449443, upload-time = "2026-06-04T12:32:08.442Z" }, ] [[package]] @@ -2787,9 +3091,10 @@ wheels = [ [[package]] name = "kubernetes" -version = "35.0.0" +version = "36.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "aiohttp", 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 = "certifi", 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 = "durationpy", 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-dateutil", 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')" }, @@ -2800,23 +3105,23 @@ dependencies = [ { name = "urllib3", 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 = "websocket-client", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/8f/85bf51ad4150f64e8c665daf0d9dfe9787ae92005efb9a4d1cba592bd79d/kubernetes-35.0.0.tar.gz", hash = "sha256:3d00d344944239821458b9efd484d6df9f011da367ecb155dadf9513f05f09ee", size = 1094642, upload-time = "2026-01-16T01:05:27.76Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/57/8b538af5076bc3372949d76f70ba3449bdfe52f9e6488170fa5d4f7cbe70/kubernetes-36.0.2.tar.gz", hash = "sha256:03551fcb49cae1f708f63624041e37403545b7aaed10cbf54e2b01a37a5438e3", size = 2336738, upload-time = "2026-06-01T18:20:30.785Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/70/05b685ea2dffcb2adbf3cdcea5d8865b7bc66f67249084cf845012a0ff13/kubernetes-35.0.0-py2.py3-none-any.whl", hash = "sha256:39e2b33b46e5834ef6c3985ebfe2047ab39135d41de51ce7641a7ca5b372a13d", size = 2017602, upload-time = "2026-01-16T01:05:25.991Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/5c160dbdef7123f8cc97fd8ece7e0198627a426a2a49614845e9086feb8d/kubernetes-36.0.2-py2.py3-none-any.whl", hash = "sha256:faf9b5241b58de0c4a5069f2a0ffc8ac06fece7215156cd3d3ba081a78a858b6", size = 4617568, upload-time = "2026-06-01T18:20:28.737Z" }, ] [[package]] name = "langchain" -version = "1.2.14" +version = "1.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core", 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 = "langgraph", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/2b/0ca77ee988a9f1c1f1d923115d7c91221ab434067bc36f2f637201aeee81/langchain-1.2.14.tar.gz", hash = "sha256:fc5511e8f8af7efee9e5a144da4392d700d627b301d240470db97272940ad317", size = 574190, upload-time = "2026-03-31T13:50:37.398Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/3f/034eb6cbef90bfccc89b7f8ed0c1d853dc9cb0bea17c7a269534c647ba3a/langchain-1.3.4.tar.gz", hash = "sha256:d6e0654c22848925534f5c0a706f9be481bb09a619ec60a738fbd1e5502e457a", size = 606617, upload-time = "2026-06-02T20:04:49.411Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/87/324ae5fd9993f024339a452fc89e3fd808bccde87ef95c8dafab3de023c0/langchain-1.2.14-py3-none-any.whl", hash = "sha256:96da6d7338d5a6fc41eb4ec0db83f7ef5d03bb5efd17bb269f34ba4378ebdb4d", size = 112715, upload-time = "2026-03-31T13:50:35.997Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/9ffe99c7dc4891a0215ec59c423bea320f943c08a231bc5bae392a438a83/langchain-1.3.4-py3-none-any.whl", hash = "sha256:e51b05ab23d056bc6bf2d97d8c694fb92d6d5765126fef74565d007c27581672", size = 125286, upload-time = "2026-06-02T20:04:48.13Z" }, ] [[package]] @@ -2854,7 +3159,7 @@ wheels = [ [[package]] name = "langchain-community" -version = "0.3.27" +version = "0.3.31" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp", 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')" }, @@ -2870,14 +3175,14 @@ dependencies = [ { name = "sqlalchemy", 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 = "tenacity", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/76/200494f6de488217a196c4369e665d26b94c8c3642d46e2fd62f9daf0a3a/langchain_community-0.3.27.tar.gz", hash = "sha256:e1037c3b9da0c6d10bf06e838b034eb741e016515c79ef8f3f16e53ead33d882", size = 33237737, upload-time = "2025-07-02T18:47:02.329Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/49/2ff5354273809e9811392bc24bcffda545a196070666aef27bc6aacf1c21/langchain_community-0.3.31.tar.gz", hash = "sha256:250e4c1041539130f6d6ac6f9386cb018354eafccd917b01a4cff1950b80fd81", size = 33241237, upload-time = "2025-10-07T20:17:57.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/bc/f8c7dae8321d37ed39ac9d7896617c4203248240a4835b136e3724b3bb62/langchain_community-0.3.27-py3-none-any.whl", hash = "sha256:581f97b795f9633da738ea95da9cb78f8879b538090c9b7a68c0aed49c828f0d", size = 2530442, upload-time = "2025-07-02T18:47:00.246Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0a/b8848db67ad7c8d4652cb6f4cb78d49b5b5e6e8e51d695d62025aa3f7dbc/langchain_community-0.3.31-py3-none-any.whl", hash = "sha256:1c727e3ebbacd4d891b07bd440647668001cea3e39cbe732499ad655ec5cb569", size = 2532920, upload-time = "2025-10-07T20:17:54.91Z" }, ] [[package]] name = "langchain-core" -version = "1.3.3" +version = "1.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpatch", 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')" }, @@ -2890,9 +3195,9 @@ dependencies = [ { name = "typing-extensions", 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 = "uuid-utils", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d3/ae/8b74458fc3850ec3d150eb9f45e857db129dafa801fb5cf173dfc9f8bbf3/langchain_core-1.3.3.tar.gz", hash = "sha256:fa510a5db8efdc0c6ff41c0939fb5c00a0183c11f6b84233e892e3227ff69182", size = 915041, upload-time = "2026-05-05T19:02:36.612Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/13/446580dc9f26e4e524d57f727a9007b4c2484decd2c00269b7fd4f51326d/langchain_core-1.4.2.tar.gz", hash = "sha256:242abe763db71de05fe0d7ecff03f9cc6022fbceba8be15902fb89e35b7292f9", size = 935103, upload-time = "2026-06-08T18:19:41.611Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/01/4771b7ab2af1d1aba5b710bd8f13d9225c609425214b357590a17b01be77/langchain_core-1.3.3-py3-none-any.whl", hash = "sha256:18aae8506f37da7f74398492279a7d6efcee4f8e23c4c41c7af080eeb7ef7bd1", size = 543857, upload-time = "2026-05-05T19:02:34.52Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2c/92a6a6f5af07f1c347021d81aea307d405ed9d34a4f8ea6b78cfa3c3b189/langchain_core-1.4.2-py3-none-any.whl", hash = "sha256:a2906d339514e02a46d6c0888021dd2651ed5acc661a1f546fe33e1453adfcb9", size = 550103, upload-time = "2026-06-08T18:19:40.197Z" }, ] [[package]] @@ -2924,7 +3229,7 @@ wheels = [ [[package]] name = "langchain-litellm" -version = "0.6.5" +version = "0.6.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography", 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')" }, @@ -2932,9 +3237,9 @@ dependencies = [ { name = "langchain-core", 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 = "litellm", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/42/0b9eea0b57dd225850f9965c1d77d84ad7be1f5101c9040143e8751cfbd6/langchain_litellm-0.6.5.tar.gz", hash = "sha256:30741fda59803336d0d39788be441f6ccd2b4e41d7747ff0d2b002950a07453b", size = 339627, upload-time = "2026-05-08T12:48:43.116Z" } +sdist = { url = "https://files.pythonhosted.org/packages/17/70/e1f42ceccd5fdad78ed7fd1a150c12661b63b92647d768168cd9efcdcf15/langchain_litellm-0.6.6.tar.gz", hash = "sha256:fb4399ae4c239b5bb85c19574a5bb4c17988433d48ec716e62144f0dad4a63af", size = 346225, upload-time = "2026-05-21T11:27:56.869Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/48/99e81a0d33334f3bc7c310d15c19a2a972d0cf8d708c8369258a5db2d74e/langchain_litellm-0.6.5-py3-none-any.whl", hash = "sha256:dce2ebfddddd0dfd6b1ed473399ccc095dd2f5cb6adfe1336d7bbe489ef32b4b", size = 26359, upload-time = "2026-05-08T12:48:42.154Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d0/adda5508115c28d78b408cc8f6b1e3fa3b2cc83a551b62cd613bb705535c/langchain_litellm-0.6.6-py3-none-any.whl", hash = "sha256:d49d0353254e10e38c351d7eae3d7b34128a0d31a3a22928ac289a8987c21a30", size = 26393, upload-time = "2026-05-21T11:27:55.709Z" }, ] [[package]] @@ -2952,7 +3257,7 @@ wheels = [ [[package]] name = "langchain-nvidia-ai-endpoints" -version = "1.3.0" +version = "1.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp", 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')" }, @@ -2960,14 +3265,14 @@ dependencies = [ { name = "langchain-core", 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 = "requests", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/2f/29036df9a99212f27369a123d2b44b5eec0ffb1b15b1277bf71cc0a37606/langchain_nvidia_ai_endpoints-1.3.0.tar.gz", hash = "sha256:5223aa7988ee5044f38715ae757faa0af4ba64f2ed0c82851a99c052592eaa09", size = 58015, upload-time = "2026-05-07T23:06:33.579Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/26/cae8babfecb9b7b503dbf5d6c0d98df74149c3151247d20e7bbfd17ca461/langchain_nvidia_ai_endpoints-1.4.1.tar.gz", hash = "sha256:8835f7e56d559b370b87164f937c1eb048ab837f25de91598f00555a705c2d16", size = 58092, upload-time = "2026-06-03T19:34:15.117Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/34/dd21237e0534938061207ee733ef6da6c2dc62c9712932b379714817abc9/langchain_nvidia_ai_endpoints-1.3.0-py3-none-any.whl", hash = "sha256:cc2b356e96e86ffb92dcfe83980aa73227e1fad8f3a4cbdd76cdcf980c42e7cc", size = 63126, upload-time = "2026-05-07T23:06:32.585Z" }, + { url = "https://files.pythonhosted.org/packages/30/50/9dcb23c73270e775c38e8d264ccbf8b1b352dbac089bce3746756d895e46/langchain_nvidia_ai_endpoints-1.4.1-py3-none-any.whl", hash = "sha256:3edd1678a3e2c55789128e53ba32aab3dffe94cb201c70e6cea521fab7c261ff", size = 63203, upload-time = "2026-06-03T19:34:11.08Z" }, ] [[package]] name = "langchain-oci" -version = "0.2.6" +version = "0.2.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp", 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')" }, @@ -2975,40 +3280,41 @@ dependencies = [ { name = "langchain-core", 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 = "langchain-openai", 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 = "langgraph", 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 = "numpy", 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 = "oci", 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 = "oci-openai", 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 = "openai", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/c7/a43f7b3b5a5b542bc17972bc9a95ee40fd7029aab98c9504ff2bf456b6ef/langchain_oci-0.2.6.tar.gz", hash = "sha256:92538d3ee45e3323290fcc672e3f6618b13878b464abd8692ade9b7441b5863b", size = 85514, upload-time = "2026-05-13T21:22:42.244Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/8b/71ae3c8aba70770b088c0699cda2378cb05b37a1da919e251f89b443a585/langchain_oci-0.2.7.tar.gz", hash = "sha256:1fb7ef305008ebbb1fb53e32af4823daa754d256947b3462b2f85e147638dc55", size = 100132, upload-time = "2026-06-02T23:42:08.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/22/92cc0ac1194ea285668b02ef2bcc39d04a846dc6e2e943b2a1f1e968d777/langchain_oci-0.2.6-py3-none-any.whl", hash = "sha256:3451385da788926d5cffd19de8afb912e15bdb28fb76f3844d3d88a5683142b0", size = 107591, upload-time = "2026-05-13T21:22:41.056Z" }, + { url = "https://files.pythonhosted.org/packages/be/7c/5397d1b748fa27ac50339fd224de4c8a479a86f8f419bf56fd25bf38db76/langchain_oci-0.2.7-py3-none-any.whl", hash = "sha256:cf793058d3b76334b57e1c80203bb2ba42941a41ce236b5825d5dbc4fe188151", size = 126442, upload-time = "2026-06-02T23:42:07.484Z" }, ] [[package]] name = "langchain-openai" -version = "1.2.1" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core", 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 = "openai", 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 = "tiktoken", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/0e/d8e16c28aa67106d285e63b8ffc04c5af68341e345ce24a0751dbf2e167e/langchain_openai-1.2.1.tar.gz", hash = "sha256:ee4480b787706361b7125fad46930589a624df87aa158c6986ef1fad10d10675", size = 1146092, upload-time = "2026-04-24T19:46:43.328Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/1b/c506c7f41156d3a6b4582b4c487f480001b8741deecc6e2d4931fdf4cf2c/langchain_openai-1.2.2.tar.gz", hash = "sha256:8698ffcee9a086e91ab6d207f0026181a03effcbf86bf9aee1808ee35af69dcc", size = 1147539, upload-time = "2026-05-21T22:08:31.123Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/55/2865b18ee3a3dd11160b8c4b2cf37e75bf2a4a8d1d38868ffffc7b7cc180/langchain_openai-1.2.1-py3-none-any.whl", hash = "sha256:a80732185030d4f453dda6c25feef46f645f665423fdffe38ae3edf1ac3c6c4d", size = 98626, upload-time = "2026-04-24T19:46:41.971Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/7406c99afacafc8c2ce0fa4152f9f8b9598c93ceb291959821abd053b982/langchain_openai-1.2.2-py3-none-any.whl", hash = "sha256:7da39a3c70cbafa93853456199e39a264dc70651be79b12ac49b4f6a448bce2d", size = 99631, upload-time = "2026-05-21T22:08:29.527Z" }, ] [[package]] name = "langchain-protocol" -version = "0.0.15" +version = "0.0.16" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4f/24/9777489d6fbbee64af0c8f96d4f840239c408cf694f3394672807dafc490/langchain_protocol-0.0.15.tar.gz", hash = "sha256:9ab2d11ee73944754f10e037e717098d3a6796f0e58afa9cadda6154e7655ade", size = 5862, upload-time = "2026-05-01T22:30:04.748Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/e7/8300ba22d968653051fd06e3117d783872dddf3dcebdd6b1d386836eb43c/langchain_protocol-0.0.16.tar.gz", hash = "sha256:806c7cdd951b1c4f692fa40fce60821ff0f221d4360e27673ddf2c2b99c2b7ff", size = 5969, upload-time = "2026-05-28T23:05:11.121Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/7a/9c97a7b9cbe4c5dc6a44cdb1545450c28f0c8ce89b9c1f0ee7fbad896263/langchain_protocol-0.0.15-py3-none-any.whl", hash = "sha256:461eb794358f83d5e42635a5797799ffec7b4702314e34edf73ac21e75d3ef79", size = 6982, upload-time = "2026-05-01T22:30:03.877Z" }, + { url = "https://files.pythonhosted.org/packages/1f/9c/06dfcc88d02a6364e8d864c421ddd3736305cb0a6c853f75c302c80fe17c/langchain_protocol-0.0.16-py3-none-any.whl", hash = "sha256:3658c142c5d0fb3a023a4be442ce4c15c6d626aab6135eb79a76dc64ad19c3c3", size = 7037, upload-time = "2026-05-28T23:05:10.163Z" }, ] [[package]] @@ -3040,7 +3346,7 @@ wheels = [ [[package]] name = "langgraph" -version = "1.1.4" +version = "1.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core", 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')" }, @@ -3050,48 +3356,51 @@ dependencies = [ { 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 = "xxhash", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/ba/8a8f48ca1248ecff4844cb27247d10a85f05b4ac6b903298d36b2ca090fd/langgraph-1.1.4.tar.gz", hash = "sha256:c951a859f68a021c69a27500db4eafc1900fc7ac32a54f7fc31d277165d04bed", size = 545440, upload-time = "2026-03-31T12:56:45.344Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/43/dac5a2621c1e57f8eb7f0703f6f6fe34a5caf62f8f0fb4d2bb395bb454ea/langgraph-1.2.4.tar.gz", hash = "sha256:5df076973a2d23efb13eceb279d1e5b46feebcbbeded0a86a2ef669abd9e4399", size = 720374, upload-time = "2026-06-02T17:07:37.347Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/74/22ea4734247b59e7c98e575e31a1f463366b084e0dc83cf63715b079ff28/langgraph-1.1.4-py3-none-any.whl", hash = "sha256:77ebe7ed44a2699f13696bf41f1dabe7b5fa8e6ad51e3597f2f175492e8f3656", size = 168190, upload-time = "2026-03-31T12:56:44.221Z" }, + { url = "https://files.pythonhosted.org/packages/48/9e/31ca236104966d7bb14ea9e93cfd73350aea8c41008ddf057b65794ed10d/langgraph-1.2.4-py3-none-any.whl", hash = "sha256:ffe3e1e31dce28907640f82525858470f293506d2b272d07ea3b3ce97974b067", size = 245402, upload-time = "2026-06-02T17:07:35.977Z" }, ] [[package]] name = "langgraph-checkpoint" -version = "4.0.1" +version = "4.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core", 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 = "ormsgpack", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/44/a8df45d1e8b4637e29789fa8bae1db022c953cc7ac80093cfc52e923547e/langgraph_checkpoint-4.0.1.tar.gz", hash = "sha256:b433123735df11ade28829e40ce25b9be614930cd50245ff2af60629234befd9", size = 158135, upload-time = "2026-02-27T21:06:16.092Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/47/886af6f886f0bff2273164a45f008694e48a96ff3cd25ff0228f2aa9480e/langgraph_checkpoint-4.1.1.tar.gz", hash = "sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25", size = 184020, upload-time = "2026-05-22T16:57:38.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/4c/09a4a0c42f5d2fc38d6c4d67884788eff7fd2cfdf367fdf7033de908b4c0/langgraph_checkpoint-4.0.1-py3-none-any.whl", hash = "sha256:e3adcd7a0e0166f3b48b8cf508ce0ea366e7420b5a73aa81289888727769b034", size = 50453, upload-time = "2026-02-27T21:06:14.293Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b4/71425e3e38be92611300b9cc5e46a5bf98ab23f5ea8a75b73d02a2f1413c/langgraph_checkpoint-4.1.1-py3-none-any.whl", hash = "sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e", size = 56212, upload-time = "2026-05-22T16:57:37.203Z" }, ] [[package]] name = "langgraph-prebuilt" -version = "1.0.8" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core", 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 = "langgraph-checkpoint", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/06/dd61a5c2dce009d1b03b1d56f2a85b3127659fdddf5b3be5d8f1d60820fb/langgraph_prebuilt-1.0.8.tar.gz", hash = "sha256:0cd3cf5473ced8a6cd687cc5294e08d3de57529d8dd14fdc6ae4899549efcf69", size = 164442, upload-time = "2026-02-19T18:14:39.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833, upload-time = "2026-05-12T03:37:49.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/41/ec966424ad3f2ed3996d24079d3342c8cd6c0bd0653c12b2a917a685ec6c/langgraph_prebuilt-1.0.8-py3-none-any.whl", hash = "sha256:d16a731e591ba4470f3e313a319c7eee7dbc40895bcf15c821f985a3522a7ce0", size = 35648, upload-time = "2026-02-19T18:14:37.611Z" }, + { url = "https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl", hash = "sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9", size = 41043, upload-time = "2026-05-12T03:37:48.007Z" }, ] [[package]] name = "langgraph-sdk" -version = "0.3.12" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { 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 = "langchain-core", 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 = "langchain-protocol", 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 = "orjson", 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 = "websockets", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/a1/012f0e0f5c9fd26f92bdc9d244756ad673c428230156ef668e6ec7c18cee/langgraph_sdk-0.3.12.tar.gz", hash = "sha256:c9c9ec22b3c0fcd352e2b8f32a815164f69446b8648ca22606329f4ff4c59a71", size = 194932, upload-time = "2026-03-18T22:15:54.592Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/2b/bd8ac26d4e97f6df88ef05ce5b6a38945a3903e1025d926f4752aa88aa97/langgraph_sdk-0.4.2.tar.gz", hash = "sha256:b88f0f5f6328ac0680d6790614a905b2bcfa257f2276dba4e38f0e86db0aa738", size = 348327, upload-time = "2026-06-01T17:51:19.856Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/4d/4f796e86b03878ab20d9b30aaed1ad459eda71a5c5b67f7cfe712f3548f2/langgraph_sdk-0.3.12-py3-none-any.whl", hash = "sha256:44323804965d6ec2a07127b3cf08a0428ea6deaeb172c2d478d5cd25540e3327", size = 95834, upload-time = "2026-03-18T22:15:53.545Z" }, + { url = "https://files.pythonhosted.org/packages/a0/05/aac507337cceae773c2cc9ab91eb6301963af7aeeb55b4217a00e15aff17/langgraph_sdk-0.4.2-py3-none-any.whl", hash = "sha256:75fa5096c1177ce39c847096a8fe3745ffd480ddb412995f836e9f5f884c43dd", size = 160521, upload-time = "2026-06-01T17:51:18.849Z" }, ] [[package]] @@ -3157,7 +3466,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.83.14" +version = "1.88.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp", 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')" }, @@ -3173,18 +3482,9 @@ dependencies = [ { name = "tiktoken", 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 = "tokenizers", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8d/7c/c095649380adc96c8630273c1768c2ad1e74aa2ee1dd8dd05d218a60569f/litellm-1.83.14.tar.gz", hash = "sha256:24aef9b47cdc424c833e32f3727f411741c690832cd1fe4405e0077144fe09c9", size = 14836599, upload-time = "2026-04-26T03:16:10.176Z" } +sdist = { url = "https://files.pythonhosted.org/packages/16/ea/f99ececb7f22703fe120f1d8be9ffb749ec9453fbbbbbebc0d6a6b4d7864/litellm-1.88.1.tar.gz", hash = "sha256:89c6b74cc7912d6365793006ff951c0450fe847625008dfe49de8a7dc4529aa5", size = 13885969, upload-time = "2026-06-09T01:06:25.192Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/5c/1b5691575420135e90578543b2bf219497caa33cfd0af64cb38f30288450/litellm-1.83.14-py3-none-any.whl", hash = "sha256:92b11ba2a32cf80707ddf388d18526696c7999a21b418c5e3b6eda1243d2cfdb", size = 16457054, upload-time = "2026-04-26T03:16:05.72Z" }, -] - -[[package]] -name = "llguidance" -version = "1.7.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/91/6bc8bb503dc259e46d253b5424385a54fe06c38a4c7a12befe69a3c2455a/llguidance-1.7.6.tar.gz", hash = "sha256:db7febbe412ed2015501904646750071d7e00e6df7f85c4b956ad4f206fd2df7", size = 1156574, upload-time = "2026-06-03T20:13:25.316Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/fe/bb185f11bad82f2637e3cd8cbf6b200cbb6ed56ac395de47ea05a60d4649/llguidance-1.7.6-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:9c54c899db8cb4b4fba128a7d844730066576c70d806c95ada92b2bd2d6ab498", size = 3138127, upload-time = "2026-06-03T20:13:11.649Z" }, + { url = "https://files.pythonhosted.org/packages/42/9a/8f8909201b4bebaf96498c09226f6baa8540086a4c4188ad57d7dfbd97c1/litellm-1.88.1-py3-none-any.whl", hash = "sha256:369b84e57d9426582ddc35e731956ddb6618cda97cc44e4e4d2dfa75982a6e3a", size = 15276206, upload-time = "2026-06-09T01:06:16.72Z" }, ] [[package]] @@ -3198,35 +3498,51 @@ wheels = [ [[package]] name = "lxml" -version = "6.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/28/30/9abc9e34c657c33834eaf6cd02124c61bdf5944d802aa48e69be8da3585d/lxml-6.1.0.tar.gz", hash = "sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13", size = 4197006, upload-time = "2026-04-18T04:32:51.613Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/5d/3bccad330292946f97962df9d5f2d3ae129cce6e212732a781e856b91e07/lxml-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cec05be8c876f92a5aa07b01d60bbb4d11cfbdd654cad0561c0d7b5c043a61b9", size = 8526232, upload-time = "2026-04-18T04:27:40.389Z" }, - { url = "https://files.pythonhosted.org/packages/54/84/5a9ec07cbe1d2334a6465f863b949a520d2699a755738986dcd3b6b89e3f/lxml-6.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:942454ff253da14218f972b23dc72fa4edf6c943f37edd19cd697618b626fac5", size = 4923771, upload-time = "2026-04-18T04:32:17.402Z" }, - { url = "https://files.pythonhosted.org/packages/a7/23/851cfa33b6b38adb628e45ad51fb27105fa34b2b3ba9d1d4aa7a9428dfe0/lxml-6.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d036ee7b99d5148072ac7c9b847193decdfeac633db350363f7bce4fff108f0e", size = 5068101, upload-time = "2026-04-18T04:32:21.437Z" }, - { url = "https://files.pythonhosted.org/packages/b0/38/41bf99c2023c6b79916ba057d83e9db21d642f473cac210201222882d38b/lxml-6.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ae5d8d5427f3cc317e7950f2da7ad276df0cfa37b8de2f5658959e618ea8512", size = 5002573, upload-time = "2026-04-18T04:32:25.373Z" }, - { url = "https://files.pythonhosted.org/packages/c2/20/053aa10bdc39747e1e923ce2d45413075e84f70a136045bb09e5eaca41d3/lxml-6.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:363e47283bde87051b821826e71dde47f107e08614e1aa312ba0c5711e77738c", size = 5202816, upload-time = "2026-04-18T04:32:29.393Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2b/d44d0e5c79226017f4ab8c87a802ebe4f89f97e6585a8e4166dffcdd7b6e/lxml-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fcf3da95e93349e0647d48d4b36a12783105bcc74cb0c416952f9988410846a3", size = 5045444, upload-time = "2026-04-18T04:32:44.512Z" }, - { url = "https://files.pythonhosted.org/packages/dd/ee/12e6c1b39a77666c02eaa77f94a870aaf63c4ac3a497b2d52319448b01c6/lxml-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:976a6b39b1b13e8c354ad8d3f261f3a4ac6609518af91bdb5094760a08f132c4", size = 5226822, upload-time = "2026-04-18T04:32:53.437Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d4/9326838b59dc36dfae42eec9656b97520f9997eee1de47b8316aaeed169c/lxml-6.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d", size = 8570663, upload-time = "2026-04-18T04:27:48.253Z" }, - { 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/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/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/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/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/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/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" }, - { url = "https://files.pythonhosted.org/packages/b5/97/28b985c2983938d3cb696dd5501423afb90a8c3e869ef5d3c62569282c0f/lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfa1a34df366d9dc0d5eaf420f4cf2bb1e1bebe1066d1c2fc28c179f8a4004c", size = 4210749, upload-time = "2026-04-18T04:36:03.626Z" }, - { url = "https://files.pythonhosted.org/packages/29/67/dfab2b7d58214921935ccea7ce9b3df9b7d46f305d12f0f532ac7cf6b804/lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db88156fcf544cdbf0d95588051515cfdfd4c876fc66444eb98bceb5d6db76de", size = 4318463, upload-time = "2026-04-18T04:36:06.309Z" }, - { url = "https://files.pythonhosted.org/packages/32/a2/4ac7eb32a4d997dd352c32c32399aae27b3f268d440e6f9cfa405b575d2f/lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07f98f5496f96bf724b1e3c933c107f0cbf2745db18c03d2e13a291c3afd2635", size = 4251124, upload-time = "2026-04-18T04:36:09.056Z" }, - { url = "https://files.pythonhosted.org/packages/33/ef/d6abd850bb4822f9b720cfe36b547a558e694881010ff7d012191e8769c6/lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4642e04449a1e164b5ff71ffd901ddb772dfabf5c9adf1b7be5dffe1212bc037", size = 4401758, upload-time = "2026-04-18T04:36:11.803Z" }, +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/b0/83f481780d1548750b8ce2ec824073deef2f452d9cd1a6faff8507e3d16d/lxml-6.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2", size = 8526461, upload-time = "2026-05-18T19:17:25.862Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d2/edb71cf0e561581a7c5eb2626244320eb04e9f8ce6d563184fd668b45073/lxml-6.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510", size = 4923654, upload-time = "2026-05-18T19:17:42.917Z" }, + { url = "https://files.pythonhosted.org/packages/4c/77/1bc7eeb0de4577d783fb625aa092cc9357883bba35845a3666bf1259f3dc/lxml-6.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a", size = 5067921, upload-time = "2026-05-18T19:17:49.175Z" }, + { url = "https://files.pythonhosted.org/packages/1b/3c/c0690d74bd2bc17bc03b5b0d093569ead597dd0bfa088bf99eef8c24e19c/lxml-6.1.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d", size = 5002456, upload-time = "2026-05-18T19:17:59.715Z" }, + { url = "https://files.pythonhosted.org/packages/66/8d/d1b3271af0c0f1e27e8472a849e4d2c65bc7766884b9ad2da9e76e145c88/lxml-6.1.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8", size = 5202776, upload-time = "2026-05-18T19:18:08.924Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c0/ef73af53767e958fd87d437c170f272e2f6e6c0f854939f133a895f1e711/lxml-6.1.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6", size = 4659237, upload-time = "2026-05-18T19:18:18.657Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5e/e1158e40397585e91cb0472374a1f63d0926a1ddeaa92f13d1a1ffe306d5/lxml-6.1.1-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8", size = 5265904, upload-time = "2026-05-18T19:18:24.883Z" }, + { url = "https://files.pythonhosted.org/packages/a0/16/8687e5d1400ed1c0bc41dace232ebb7553952b618ea1f2e5fb6e2cfbbe23/lxml-6.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83", size = 5045225, upload-time = "2026-05-18T19:17:20.073Z" }, + { url = "https://files.pythonhosted.org/packages/ca/18/d877bd1ae2e5ffdfd4836565aba350db31feb2f2656d6ce70316ed66a05e/lxml-6.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6", size = 4712721, upload-time = "2026-05-18T19:17:40.512Z" }, + { url = "https://files.pythonhosted.org/packages/44/4d/1f44fd1d770b10dacbf6b5c6e520f4d6e0708744930f719dc04e67cab981/lxml-6.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c", size = 5252549, upload-time = "2026-05-18T19:17:51.236Z" }, + { url = "https://files.pythonhosted.org/packages/64/5d/1d66b84f850089254c230ef6ea6b267a5a54e2e179a5d960036a05d501d7/lxml-6.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08", size = 5226877, upload-time = "2026-05-18T19:18:00.875Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6e/c4add832b6fc1e887125b96f880d7b9b70aae5248718e046b1704bcac4b9/lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7", size = 8570821, upload-time = "2026-05-18T19:17:42.068Z" }, + { url = "https://files.pythonhosted.org/packages/42/95/bb63f0fd62e554fe078e1fb3c8fe9083c14ddc7ad7fa178d10e57e071ac7/lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc", size = 4930746, upload-time = "2026-05-18T19:18:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/eb/99/0013e8d9b5960f4f041cf0b73e2f80c23eb5205b1f7bfb20203243651359/lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc", size = 5093723, upload-time = "2026-05-18T19:18:34.168Z" }, + { url = "https://files.pythonhosted.org/packages/29/91/317b332636bfc7bddcff828d41b3307f50043f4b237e40849c333d80fa1a/lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383", size = 5005557, upload-time = "2026-05-18T19:18:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/42/2f/cc9bf06afe70f9c9093ae60855d9759da9db601ec4080f7473319666ffd7/lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b", size = 5631036, upload-time = "2026-05-18T19:18:44.858Z" }, + { url = "https://files.pythonhosted.org/packages/08/f6/af32e23e563971ffb0fb86be52bc5be5c2c118858ffc119bf6a9039b173d/lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818", size = 5240367, upload-time = "2026-05-18T19:18:49.217Z" }, + { url = "https://files.pythonhosted.org/packages/63/75/5d92da93729b7bad783689e6496049fa40927b45bec7bf183c981de3ca70/lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f", size = 4694874, upload-time = "2026-05-18T19:18:55.139Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b5/3aad415a9a25b822e783f15deeb4dffccf5113030f1afa2222dd929313d9/lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2", size = 5244492, upload-time = "2026-05-18T19:19:01.28Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a1/5fcf7eb9904b80086aa47dcf0027de07b1bb990afad2e6823144c368ae04/lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635", size = 5048232, upload-time = "2026-05-18T19:18:12.67Z" }, + { url = "https://files.pythonhosted.org/packages/77/74/1f601b63c7a69fcdf10fa9b148c81da8442204194f6c55509cc485c786b9/lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf", size = 4777023, upload-time = "2026-05-18T19:18:15.928Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b9/7a78f51aec95b1bf780d78e12705a9f6533284f8693dc5c0e6724fa53d3f/lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc", size = 5645773, upload-time = "2026-05-18T19:18:23.223Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6e/98a7b7ad54e4e74fa1f20fff776913980619d0ebe5558232d7da6580bdd8/lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955", size = 5233088, upload-time = "2026-05-18T19:18:31.433Z" }, + { url = "https://files.pythonhosted.org/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995, upload-time = "2026-05-18T19:18:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" }, + { url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" }, + { url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" }, + { url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" }, + { url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" }, + { url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" }, + { url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" }, + { url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" }, + { url = "https://files.pythonhosted.org/packages/40/44/d832e82af08723761556d004b1d04d281c09f9a8cecd7d3148548c9941a3/lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004", size = 4210769, upload-time = "2026-05-18T19:20:41.427Z" }, + { url = "https://files.pythonhosted.org/packages/6d/39/0dc5949f759ed7d951e0bb8c2f2d9d7aca1908d22352fa84a8afd2ea54af/lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e", size = 4318163, upload-time = "2026-05-18T19:20:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/e6/fb/8ab3845fe046ba4cbf74536bcf6801a774b7caf4350de1c5d37f1f0a9e90/lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2", size = 4250945, upload-time = "2026-05-18T19:20:47.385Z" }, + { url = "https://files.pythonhosted.org/packages/68/1b/7553ab136894374ffae8851ec06f98f511cd8e66246e41b6be059d0a7289/lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf", size = 4401664, upload-time = "2026-05-18T19:20:50.489Z" }, ] [[package]] @@ -3263,23 +3579,23 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] name = "marko" -version = "2.2.2" +version = "2.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/2f/050b6d485f052ddf17d76a41f9334d6fb2a8a85df35347a12d97ed3bc5c1/marko-2.2.2.tar.gz", hash = "sha256:6940308e655f63733ca518c47a68ec9510279dbb916c83616e4c4b5829f052e8", size = 143641, upload-time = "2026-01-05T11:04:41.935Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/cc/01b80dc58e4d44fe039403ef1ac0008bcb9375364ccd246a4b8bfec29b46/marko-2.2.3.tar.gz", hash = "sha256:e31ec2875383bc62f9093d16babed5a2c2cde601c00d834ea935a2222120ec19", size = 144531, upload-time = "2026-05-28T02:07:39.479Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/f8/36d79bac5701e6786f9880c61bbe57574760a13c1af84ab71e5ed21faecc/marko-2.2.2-py3-none-any.whl", hash = "sha256:f064ae8c10416285ad1d96048dc11e98ef04e662d3342ae416f662b70aa7959e", size = 42701, upload-time = "2026-01-05T11:04:40.75Z" }, + { url = "https://files.pythonhosted.org/packages/97/50/0a8fab45fa374820c27cc4c3178c4914c60902ba9d6404a692a979e20dbc/marko-2.2.3-py3-none-any.whl", hash = "sha256:8e1d7a0387281e59dfbc52a381b58c570156970e36b2bbe047f8a3a2f368cacc", size = 42951, upload-time = "2026-05-28T02:07:38.373Z" }, ] [[package]] @@ -3291,22 +3607,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, { url = "https://files.pythonhosted.org/packages/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" }, ] @@ -3324,14 +3648,14 @@ wheels = [ [[package]] name = "matplotlib-inline" -version = "0.2.1" +version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, ] [[package]] @@ -3345,7 +3669,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.26.0" +version = "1.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", 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')" }, @@ -3362,21 +3686,21 @@ dependencies = [ { name = "typing-inspection", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/3c/347cf965d313f5d41764e7d46bea6ffe7d9ef13b983cc429b0340962a082/mcp-1.27.2.tar.gz", hash = "sha256:8e02db104096d1c25b28e64bde29a5c32b31bc241710213e12fd4d84985bdfef", size = 621116, upload-time = "2026-05-29T17:16:04.039Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, + { url = "https://files.pythonhosted.org/packages/c9/11/252c6f971dc4f16af1d98a1c469d8ba523aab00d1bb76b4d3bc1ff32eacc/mcp-1.27.2-py3-none-any.whl", hash = "sha256:d6ff5160c6ca65d93013626efb3fc249de683c30b2d8570755ceddd490344de5", size = 220498, upload-time = "2026-05-29T17:16:02.442Z" }, ] [[package]] name = "mdit-py-plugins" -version = "0.5.0" +version = "0.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, ] [[package]] @@ -3388,20 +3712,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "miniaudio" -version = "1.71" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d8/d5/e5439dc08561f73656bfeb3340fc64ab63163e101426593d8fb9a025ff1e/miniaudio-1.71.tar.gz", hash = "sha256:ff51e2887bb673e2e757752b586b3dc924d59aa5fbcae9bbc45f4a111bd3262b", size = 1116480, upload-time = "2026-04-29T21:20:38.182Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/c1/4b13ac3c36a2574e0d70f322246d80259606cd24523279f542abc9ac6063/miniaudio-1.71-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5009b4e29cd43de3631d2d5ab09cc074192c085b4c8dd8a121b856ce1af6bab7", size = 351405, upload-time = "2026-04-29T21:20:10.533Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ac/30a324f758bed1b193e017ec25183cfb10a79e549656331f5d068a2d343a/miniaudio-1.71-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fc1a4f084cc1b4b25c567d22f54d1e46bfa505c17ed777c8b198e5c53d0f785", size = 351485, upload-time = "2026-04-29T21:20:17.761Z" }, - { url = "https://files.pythonhosted.org/packages/bd/d1/071a560000c8ce903dc919968ecce40fbe7a73213ac399051b887184f8a3/miniaudio-1.71-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d9dc15eff711bcfc62a9d05e0c78e4bc34821a455595e049629f2fea7491a523", size = 351488, upload-time = "2026-04-29T21:20:25.183Z" }, -] - [[package]] name = "mistune" version = "3.2.1" @@ -3413,7 +3723,7 @@ wheels = [ [[package]] name = "mlflow-skinny" -version = "3.10.1" +version = "3.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools", 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')" }, @@ -3433,104 +3743,13 @@ dependencies = [ { 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 = "requests", 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 = "sqlparse", 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 = "starlette", 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 = "typing-extensions", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/65/5b2c28e74c167ba8a5afe59399ef44291a0f140487f534db1900f09f59f6/mlflow_skinny-3.10.1.tar.gz", hash = "sha256:3d1c5c30245b6e7065b492b09dd47be7528e0a14c4266b782fe58f9bcd1e0be0", size = 2478631, upload-time = "2026-03-05T10:49:01.47Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/52/17460157271e70b0d8444d27f8ad730ef7d95fb82fac59dc19f11519b921/mlflow_skinny-3.10.1-py3-none-any.whl", hash = "sha256:df1dd507d8ddadf53bfab2423c76cdcafc235cd1a46921a06d1a6b4dd04b023c", size = 2987098, upload-time = "2026-03-05T10:48:59.566Z" }, -] - -[[package]] -name = "mlx" -version = "0.31.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mlx-metal", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/89/1e77ec3ff380e8fb9e7258047374d31452a0f9828a0e370f127b07dd8288/mlx-0.31.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4a3f181b367d404e44a6bd68ef5eb573930809ac60cacd51d0c851c629b1b651", size = 586911, upload-time = "2026-04-22T03:14:29.675Z" }, - { url = "https://files.pythonhosted.org/packages/6a/41/c1907f05f8a3fc54025fb78ad68d3c4a4b931664d03c0a24f7f431cc4087/mlx-0.31.2-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:70297cbef7479429f69c966bfed10da20a6f0c2aa997eec2b4f6ba1a07caf2ef", size = 586915, upload-time = "2026-04-22T03:14:31.403Z" }, - { url = "https://files.pythonhosted.org/packages/97/b0/61ac2c14773c786fecbda28067b0207a0c654cb4d10c548808c51284d700/mlx-0.31.2-cp311-cp311-macosx_26_0_arm64.whl", hash = "sha256:c0ff158b7ac93a4b5659adbc70053498b30a5964fc45f78596398e056a96c36a", size = 587030, upload-time = "2026-04-22T03:14:32.961Z" }, - { url = "https://files.pythonhosted.org/packages/c3/47/5f33906cb03d6a378a697cd2d2641a26b37dea17ee3d9124d7e39e8eca01/mlx-0.31.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:e5067aaf2be1f3d7bba5be52348775804f111173c1ed04639618fd713b1a530f", size = 584863, upload-time = "2026-04-22T03:14:38.211Z" }, - { url = "https://files.pythonhosted.org/packages/08/e7/a851a451b1327af9fb4df3991b9ae87d066b6f6630e854af55c288b0995a/mlx-0.31.2-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:edb9797db7d852477ca1c99708058654ee860d4148fe5765f0d55528e2b1aa22", size = 584860, upload-time = "2026-04-22T03:14:39.746Z" }, - { url = "https://files.pythonhosted.org/packages/3b/15/0d1dc0597644e5e7b011ca954ba0c47e13cd880a3b909b0c3f1b4d8bf8f1/mlx-0.31.2-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:51ca102db641b01e7cb083ce8ecb580e281530a141a7ca12544bb370641630ae", size = 584887, upload-time = "2026-04-22T03:14:41.585Z" }, - { url = "https://files.pythonhosted.org/packages/a3/3f/888f8664d4f8e23a1363a5f50024be5216e199ab7ad0ba20988c7ed6d729/mlx-0.31.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:1b3fb0dda955b0d552ce57bdd6f42b3309ab21b067e40587d6848443d307e91f", size = 584796, upload-time = "2026-04-22T03:14:47.215Z" }, - { url = "https://files.pythonhosted.org/packages/dd/14/e9cd18b51f9e1dbcb060eec0fafc2d2428c8e1eacd9b0a02d7c5ce75b661/mlx-0.31.2-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:34b0171cd9eb5c43fdd82091f6135d6ccc5a065363a4a3e68fac64fb4e53d37c", size = 584790, upload-time = "2026-04-22T03:14:48.519Z" }, - { url = "https://files.pythonhosted.org/packages/ca/20/c6c5fb998c7834d094b2bfb9f003b5246cb270f0266da055c55546c34999/mlx-0.31.2-cp313-cp313-macosx_26_0_arm64.whl", hash = "sha256:c05981684279a8935d58b0dde3ea5b02d210c3bad3319aa0e9934ec2df165752", size = 584795, upload-time = "2026-04-22T03:14:49.904Z" }, -] - -[[package]] -name = "mlx-audio" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "huggingface-hub", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "miniaudio", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "mlx", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "mlx-lm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "numpy", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "scipy", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "sounddevice", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "tqdm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "transformers", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/db/a9f95e3794eca373d681220c8b9f8f84451a0d14959f85cc341ca592394c/mlx_audio-0.4.3.tar.gz", hash = "sha256:8e87badf56a0f73bf91e3797b1195c01440a181cf0b64a2a08dc1bda4b037f54", size = 1144947, upload-time = "2026-04-28T20:18:12.09Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/13/840db21a4f46ebe6ba9837a38bc93d748e23b6b61986799c8040cd4bf728/mlflow_skinny-3.13.0.tar.gz", hash = "sha256:d2273bfa21f776359f7d6ab2267967e3a6732a5fb00996ad433d0e777dfa3b71", size = 2814837, upload-time = "2026-06-01T05:54:54.175Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/25/0a89073ed7b7cdf34299042bd03d867c12c0c8b43f597be61bea7f146793/mlx_audio-0.4.3-py3-none-any.whl", hash = "sha256:6b87bf42d79d9ceb6b9310a77656b9b76429c2d6ddd89f634b2786c58a2e4721", size = 1373582, upload-time = "2026-04-28T20:18:10.512Z" }, -] - -[[package]] -name = "mlx-lm" -version = "0.31.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "mlx", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "numpy", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "protobuf", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "pyyaml", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "sentencepiece", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "transformers", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/84/94/9a38d6b0c6fcca995b9136c94eb7da1e9c5165652edf228b96b29960fa7a/mlx_lm-0.31.3.tar.gz", hash = "sha256:61eb0e3ba09444f77f874aff295401d7ccd20b39495cbbce0c782a15474ce733", size = 304318, upload-time = "2026-04-22T07:37:27.922Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/02/9a67b8e4f87e3e2e5cd7b1ad79304b93c09a0db6af34bee75e6551c06c60/mlx_lm-0.31.3-py3-none-any.whl", hash = "sha256:758cfddf1180053b7613db76fad3d246a331a2a905808e1164a275621fc983b8", size = 408890, upload-time = "2026-04-22T07:37:25.965Z" }, -] - -[[package]] -name = "mlx-metal" -version = "0.31.2" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/69/fe3b783ebe999f3118234e1e940feb622518bfb1dea6ac5d13b1d36a8449/mlx_metal-0.31.2-py3-none-macosx_14_0_arm64.whl", hash = "sha256:b25385bcee18fc194092255b8b53b9a3d8489eb650e59160f1b57aadd07aa2dc", size = 40055588, upload-time = "2026-04-22T03:14:14.43Z" }, - { url = "https://files.pythonhosted.org/packages/4f/5d/4c690d5b93c30ba002656c37363159d978705bf8eb801b8481840fb942c2/mlx_metal-0.31.2-py3-none-macosx_15_0_arm64.whl", hash = "sha256:e9d4e5fce6ca10a87a0e388597f99519ad594d09e674708b5312bd8bd4f5997d", size = 40053220, upload-time = "2026-04-22T03:14:18.048Z" }, - { url = "https://files.pythonhosted.org/packages/99/82/11fd62a8d7a3e96e5c43220b17de0151e3f10101f8bb3b865f5bd9cdd074/mlx_metal-0.31.2-py3-none-macosx_26_0_arm64.whl", hash = "sha256:84ffb60ee503f03eb684f5fb168d5cff31e2a16b7f27c1731eaf7662bd6e9b46", size = 55792151, upload-time = "2026-04-22T03:14:22.059Z" }, -] - -[[package]] -name = "mlx-vlm" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "datasets", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "fastapi", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "llguidance", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "miniaudio", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "mlx", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "mlx-audio", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "mlx-lm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "numpy", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "opencv-python", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "pillow", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "requests", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "tqdm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "transformers", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "uvicorn", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/80/a3/70dce014f6a72efd2cecc07b6a68fc11c0694fbe54ea553b2e00499c7b36/mlx_vlm-0.5.0.tar.gz", hash = "sha256:24563cd1b3a399fd941b2359100628306e2754db1b48780516d1283138258793", size = 1033154, upload-time = "2026-05-06T21:09:33.594Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/66/fb955ccc442aa556e5e9d8836fb9041a7aadff5a88fa80c285e53dc19bf5/mlx_vlm-0.5.0-py3-none-any.whl", hash = "sha256:3351d6ccf609cbf57a4c8cd8308e9a1ce469883d8679d9968c6c6f77af016419", size = 1218132, upload-time = "2026-05-06T21:09:32.071Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fd/f2739de1b6a09da981927aa90db87340cbe4b3cf6cd175fd5e6e4366208e/mlflow_skinny-3.13.0-py3-none-any.whl", hash = "sha256:ced3d9a580564fae093d14732df8531fb180574f6483d4c642b6083879eb86fc", size = 3365675, upload-time = "2026-06-01T05:54:52.166Z" }, ] [[package]] @@ -3543,13 +3762,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/b4/9cd284bd6062d711e13d26c04d4778ab3f690c1c38a4563e3c767ec8802e/mmh3-5.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082", size = 40079, upload-time = "2026-03-05T15:54:02.743Z" }, { url = "https://files.pythonhosted.org/packages/ee/93/723e317dd9e041c4dc4566a2eb53b01ad94de31750e0b834f1643905e97c/mmh3-5.2.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d", size = 103082, upload-time = "2026-03-05T15:54:06.387Z" }, { url = "https://files.pythonhosted.org/packages/61/b5/f96121e69cc48696075071531cf574f112e1ffd08059f4bffb41210e6fc5/mmh3-5.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e", size = 106054, upload-time = "2026-03-05T15:54:07.506Z" }, + { url = "https://files.pythonhosted.org/packages/82/49/192b987ec48d0b2aecf8ac285a9b11fbc00030f6b9c694664ae923458dde/mmh3-5.2.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d", size = 112910, upload-time = "2026-03-05T15:54:09.403Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a1/03e91fd334ed0144b83343a76eb11f17434cd08f746401488cfeafb2d241/mmh3-5.2.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4", size = 120551, upload-time = "2026-03-05T15:54:10.587Z" }, { url = "https://files.pythonhosted.org/packages/93/b9/b89a71d2ff35c3a764d1c066c7313fc62c7cc48fa48a4b3b0304a4a0146f/mmh3-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15", size = 99096, upload-time = "2026-03-05T15:54:11.76Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0e/1524566fe8eaf871e4f7bc44095929fcd2620488f402822d848df19d679c/mmh3-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2", size = 106239, upload-time = "2026-03-05T15:54:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/04/94/21adfa7d90a7a697137ad6de33eeff6445420ca55e433a5d4919c79bc3b5/mmh3-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1", size = 109797, upload-time = "2026-03-05T15:54:15.819Z" }, { url = "https://files.pythonhosted.org/packages/b5/e6/1aacc3a219e1aa62fa65669995d4a3562b35be5200ec03680c7e4bec9676/mmh3-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38", size = 97228, upload-time = "2026-03-05T15:54:16.992Z" }, { url = "https://files.pythonhosted.org/packages/92/94/bc5c3b573b40a328c4d141c20e399039ada95e5e2a661df3425c5165fd84/mmh3-5.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1", size = 56087, upload-time = "2026-03-05T15:54:21.92Z" }, { 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" }, @@ -3558,7 +3785,11 @@ 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" }, ] @@ -3582,11 +3813,11 @@ dev = [] [[package]] name = "more-itertools" -version = "10.8.0" +version = "11.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, ] [[package]] @@ -3630,26 +3861,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, { 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" }, ] @@ -3707,7 +3962,7 @@ wheels = [ [[package]] name = "myst-parser" -version = "5.0.0" +version = "5.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils", 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')" }, @@ -3715,25 +3970,17 @@ dependencies = [ { name = "markdown-it-py", 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 = "mdit-py-plugins", 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 = "sphinx", 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')" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/33/fa/7b45eef11b7971f0beb29d27b7bfe0d747d063aa29e170d9edd004733c8a/myst_parser-5.0.0.tar.gz", hash = "sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a", size = 98535, upload-time = "2026-01-15T09:08:18.036Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/ac/686789b9145413f1a61878c407210e41bfdb097976864e0913078b24098c/myst_parser-5.0.0-py3-none-any.whl", hash = "sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211", size = 84533, upload-time = "2026-01-15T09:08:16.788Z" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] - -[[package]] -name = "narwhals" -version = "2.22.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/62/3c/c4ef2164a71c1a63d7f1ae411c4082c5fa872405106db60a4b7114989ad7/narwhals-2.22.1.tar.gz", hash = "sha256:d62920805a0a43b7ff8b54b0c0d3142d796f8a9301836ada37e573d6a33cbcd9", size = 647493, upload-time = "2026-06-05T12:34:34.051Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02", size = 101182, upload-time = "2026-05-13T09:38:19.361Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/ca/36339329c4604adbcc99c899b7eb1ce1a555c499b6a6860757dc9bfed36d/narwhals-2.22.1-py3-none-any.whl", hash = "sha256:60567d774edf77db53906f89d9fbd164e66e56d66d388e1e6990f17ac33cfb53", size = 454815, upload-time = "2026-06-05T12:34:32.289Z" }, + { url = "https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl", hash = "sha256:9c91c52b3cdb4d94a6506e4fab4e2f296c7623a0da0dcbe6de1565c3dad67a8a", size = 85817, upload-time = "2026-05-13T09:38:17.904Z" }, ] [[package]] name = "nbclient" -version = "0.10.4" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-client", 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')" }, @@ -3741,9 +3988,9 @@ dependencies = [ { name = "nbformat", 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 = "traitlets", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/a5/b3bae4b590c0cbcada2c63a34f7580024e834a8ba213e949a2f906705787/nbclient-0.11.0.tar.gz", hash = "sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a", size = 62535, upload-time = "2026-06-05T07:52:41.746Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, + { url = "https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl", hash = "sha256:ef7fa0d59d6e1d41103933d8a445a18d5de860ca6b613b87b8574accdb3c2895", size = 25288, upload-time = "2026-06-05T07:52:40.115Z" }, ] [[package]] @@ -4300,7 +4547,8 @@ version = "0.5.0" source = { editable = "packages/nemo_nb" } dependencies = [ { 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 = "sphinx", 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 = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "typer", 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')" }, ] @@ -4369,9 +4617,7 @@ all = [ { name = "nemo-safe-synthesizer", 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 = "nemoguardrails", extra = ["tracing"], 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 = "ngcsdk", 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 = "nmp-auth", 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 = "nmp-common", 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 = "nmp-guardrails", 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 = "nvidia-nat-config-optimizer", 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 = "nvidia-nat-core", 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 = "nvidia-nat-langchain", 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')" }, @@ -4762,11 +5008,6 @@ nmp-common = [ { name = "sqlalchemy", 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 = "structlog", 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')" }, ] -platform-seed-service = [ - { name = "nmp-auth", 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 = "nmp-common", 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 = "nmp-guardrails", 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')" }, -] plugins = [ { name = "anthropic", 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 = "boto3", 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')" }, @@ -4852,9 +5093,7 @@ services = [ { name = "nemo-safe-synthesizer", 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 = "nemoguardrails", extra = ["tracing"], 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 = "ngcsdk", 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 = "nmp-auth", 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 = "nmp-common", 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 = "nmp-guardrails", 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 = "nvidia-nat-config-optimizer", 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 = "nvidia-nat-core", 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 = "nvidia-nat-langchain", 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')" }, @@ -5121,9 +5360,9 @@ requires-dist = [ { name = "langchain-aws", marker = "extra == 'nemo-agents-plugin'", specifier = "==1.1.0" }, { name = "langchain-aws", marker = "extra == 'plugins'", specifier = "==1.1.0" }, { name = "langchain-aws", marker = "extra == 'services'", specifier = "==1.1.0" }, - { name = "langchain-community", marker = "extra == 'all'", specifier = ">=0.3.27,<0.4" }, - { name = "langchain-community", marker = "extra == 'evaluator-service'", specifier = ">=0.3.27,<0.4" }, - { name = "langchain-community", marker = "extra == 'services'", specifier = ">=0.3.27,<0.4" }, + { name = "langchain-community", marker = "extra == 'all'", specifier = ">=0.3.31,<0.4" }, + { name = "langchain-community", marker = "extra == 'evaluator-service'", specifier = ">=0.3.31,<0.4" }, + { name = "langchain-community", marker = "extra == 'services'", specifier = ">=0.3.31,<0.4" }, { name = "langchain-nvidia-ai-endpoints", marker = "extra == 'all'", specifier = ">=1.0.0,<2.0.0" }, { name = "langchain-nvidia-ai-endpoints", marker = "extra == 'evaluator-service'", specifier = ">=1.0.0,<2.0.0" }, { name = "langchain-nvidia-ai-endpoints", marker = "extra == 'guardrails-service'", specifier = ">=1.0.0,<2.0.0" }, @@ -5191,9 +5430,6 @@ requires-dist = [ { name = "ngcsdk", marker = "extra == 'files-service'", specifier = ">=4.9.10" }, { name = "ngcsdk", marker = "extra == 'nemo-platform-sdk'", specifier = ">=4.8.2" }, { name = "ngcsdk", marker = "extra == 'services'", specifier = ">=4.9.10" }, - { name = "nmp-auth", marker = "extra == 'all'", editable = "services/core/auth" }, - { name = "nmp-auth", marker = "extra == 'platform-seed-service'", editable = "services/core/auth" }, - { name = "nmp-auth", marker = "extra == 'services'", editable = "services/core/auth" }, { name = "nmp-common", editable = "packages/nmp_common" }, { name = "nmp-common", marker = "extra == 'all'", editable = "packages/nmp_common" }, { name = "nmp-common", marker = "extra == 'auth-service'", editable = "packages/nmp_common" }, @@ -5213,14 +5449,10 @@ requires-dist = [ { name = "nmp-common", marker = "extra == 'nemo-data-designer-plugin'", editable = "packages/nmp_common" }, { name = "nmp-common", marker = "extra == 'nemo-evaluator-plugin'", editable = "packages/nmp_common" }, { name = "nmp-common", marker = "extra == 'nemo-safe-synthesizer-plugin'", editable = "packages/nmp_common" }, - { name = "nmp-common", marker = "extra == 'platform-seed-service'", editable = "packages/nmp_common" }, { name = "nmp-common", marker = "extra == 'plugins'", editable = "packages/nmp_common" }, { name = "nmp-common", marker = "extra == 'secrets-service'", editable = "packages/nmp_common" }, { name = "nmp-common", marker = "extra == 'services'", editable = "packages/nmp_common" }, { name = "nmp-common", marker = "extra == 'studio-service'", editable = "packages/nmp_common" }, - { name = "nmp-guardrails", marker = "extra == 'all'", editable = "services/guardrails" }, - { name = "nmp-guardrails", marker = "extra == 'platform-seed-service'", editable = "services/guardrails" }, - { name = "nmp-guardrails", marker = "extra == 'services'", editable = "services/guardrails" }, { name = "nvidia-ml-py", marker = "extra == 'nemo-platform-sdk'", specifier = ">=13.0.0" }, { name = "nvidia-ml-py", marker = "extra == 'nmp-common'", specifier = ">=13.0.0" }, { name = "nvidia-nat-config-optimizer", marker = "extra == 'all'", specifier = ">=1.7.0,<1.8" }, @@ -5512,7 +5744,7 @@ requires-dist = [ { name = "yara-python", marker = "extra == 'guardrails-service'", specifier = "==4.5.1" }, { name = "yara-python", marker = "extra == 'services'", specifier = "==4.5.1" }, ] -provides-extras = ["aiohttp", "all", "auditor-service", "auth-service", "core-service", "customizer-service", "data-designer-nemo", "entities-service", "evaluator-service", "files-service", "guardrails-service", "hello-world-service", "inference-gateway-service", "intake-service", "jobs-service", "models-service", "nemo-agents-example-calculator", "nemo-agents-plugin", "nemo-anonymizer-plugin", "nemo-auditor-plugin", "nemo-data-designer-plugin", "nemo-evaluator-plugin", "nemo-evaluator-sdk", "nemo-guardrails-plugin", "nemo-platform-plugin", "nemo-platform-sdk", "nemo-safe-synthesizer-plugin", "nemo-switchyard", "nmp-common", "platform-seed-service", "plugins", "secrets-service", "services", "studio-service", "switchyard"] +provides-extras = ["aiohttp", "all", "auditor-service", "auth-service", "core-service", "customizer-service", "data-designer-nemo", "entities-service", "evaluator-service", "files-service", "guardrails-service", "hello-world-service", "inference-gateway-service", "intake-service", "jobs-service", "models-service", "nemo-agents-example-calculator", "nemo-agents-plugin", "nemo-anonymizer-plugin", "nemo-auditor-plugin", "nemo-data-designer-plugin", "nemo-evaluator-plugin", "nemo-evaluator-sdk", "nemo-guardrails-plugin", "nemo-platform-plugin", "nemo-platform-sdk", "nemo-safe-synthesizer-plugin", "nemo-switchyard", "nmp-common", "plugins", "secrets-service", "services", "studio-service", "switchyard"] [[package]] name = "nemo-platform-ext" @@ -6498,7 +6730,7 @@ wheels = [ [[package]] name = "ngcsdk" -version = "4.16.0" +version = "4.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles", 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')" }, @@ -6522,7 +6754,7 @@ dependencies = [ { name = "validators", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/c1/1f4195a83c2a41f3c130096f05ae22d8291afa72d00ef01ae9dbe91da69c/ngcsdk-4.16.0-py3-none-any.whl", hash = "sha256:3fe7267fab02b5e4c63521ade365a708238113a1b19802e53fa699b548e13fce", size = 3081403, upload-time = "2026-04-01T20:43:27.362Z" }, + { url = "https://files.pythonhosted.org/packages/02/07/413ee98909fb863b8209965c42d9707d26cefb7f309b82922946e7cc6e0e/ngcsdk-4.19.1-py3-none-any.whl", hash = "sha256:945455d06f9a215772660472d2ff4432c67a53986f86a384a98fbee62b01e434", size = 3137086, upload-time = "2026-06-01T20:03:24.666Z" }, ] [[package]] @@ -6934,7 +7166,7 @@ requires-dist = [ { name = "huggingface-hub", specifier = ">=1.0.1,<2.0.0" }, { name = "jsonpath-ng", specifier = ">=1.6.0" }, { name = "kubernetes", specifier = ">=31.0.0" }, - { name = "langchain-community", specifier = ">=0.3.27,<0.4" }, + { name = "langchain-community", specifier = ">=0.3.31,<0.4" }, { name = "langchain-nvidia-ai-endpoints", specifier = ">=1.0.0,<2.0.0" }, { name = "nemo-evaluator-sdk", editable = "packages/nemo_evaluator_sdk" }, { name = "nmp-common", editable = "packages/nmp_common" }, @@ -7645,7 +7877,7 @@ wheels = [ [[package]] name = "notebook" -version = "7.5.6" +version = "7.5.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-server", 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')" }, @@ -7654,9 +7886,9 @@ dependencies = [ { name = "notebook-shim", 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 = "tornado", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/c2/cf59bd2e6f2c8b976b52477e3e53bf6f97bc714ed046a51821afb428eaee/notebook-7.5.6.tar.gz", hash = "sha256:621174aade80108f0020b0f00738000b215f75fa3cd90771ad7aa0f24536a4e1", size = 14170814, upload-time = "2026-04-30T11:46:26.613Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/c4/f71f8716f2903e9e817a47f534b9fd84831e155e2acb32c26691c8e06243/notebook-7.5.7.tar.gz", hash = "sha256:d6d59288a25303b25e1dcb71e9b017ec3a785f7d92f38b9bc288ca1970d5b0a8", size = 14171612, upload-time = "2026-06-04T18:33:45.224Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/d6/1fd0646b9bbd9efbb0b8ae21b2325fbef515769a5621c03e31d8eb8da587/notebook-7.5.6-py3-none-any.whl", hash = "sha256:4dde3f8fb55fa8fb7946d58c6e869ce9baf46d00fc070664f62604569d0faca0", size = 14581730, upload-time = "2026-04-30T11:46:22.342Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4d/b3347f7073a377273531efe4ffc738fc910e93718fd2838c7ebf6736c6af/notebook-7.5.7-py3-none-any.whl", hash = "sha256:1f95f79d117e47d20b5555b5c85a397d2cfecf136978aaab767cf0314b09165b", size = 14583767, upload-time = "2026-06-04T18:33:40.987Z" }, ] [[package]] @@ -7673,7 +7905,7 @@ wheels = [ [[package]] name = "nox" -version = "2026.2.9" +version = "2026.4.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete", 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')" }, @@ -7684,156 +7916,170 @@ dependencies = [ { name = "packaging", 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 = "virtualenv", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/8e/55a9679b31f1efc48facedd2448eb53c7f1e647fb592aa1403c9dd7a4590/nox-2026.2.9.tar.gz", hash = "sha256:1bc8a202ee8cd69be7aaada63b2a7019126899a06fc930a7aee75585bf8ee41b", size = 4031165, upload-time = "2026-02-10T04:38:58.878Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/6b/e672c862a43cfca704d32359221fa3780226daa1e5db5dfc401bcc8be9c9/nox-2026.4.10.tar.gz", hash = "sha256:2d0af5374f3f37a295428c927d1b04a8182aa01762897d172446dda2f1ce9692", size = 4034839, upload-time = "2026-04-10T17:42:42.209Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/58/0d5e5a044f1868bdc45f38afdc2d90ff9867ce398b4e8fa9e666bfc9bfba/nox-2026.2.9-py3-none-any.whl", hash = "sha256:1b7143bc8ecdf25f2353201326152c5303ae4ae56ca097b1fb6179ad75164c47", size = 74615, upload-time = "2026-02-10T04:38:57.266Z" }, + { url = "https://files.pythonhosted.org/packages/7f/95/4df134a100b5a9a12378d5301b934366686ef6fbdaffcd21211d5654970e/nox-2026.4.10-py3-none-any.whl", hash = "sha256:082c117627590d9b90aa21f86df89b310b07c5842539524203bcb3c719f116c1", size = 75536, upload-time = "2026-04-10T17:42:40.664Z" }, ] [[package]] name = "numpy" -version = "2.4.4" +version = "2.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, - { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, - { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, - { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, - { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, - { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, - { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, - { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, - { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, - { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, - { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, - { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, - { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, - { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, - { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, - { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, - { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, - { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, - { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, - { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, ] [[package]] -name = "nvidia-cublas-cu12" -version = "12.8.4.1" +name = "nvidia-cublas" +version = "13.1.1.3" source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, ] [[package]] -name = "nvidia-cuda-cupti-cu12" -version = "12.8.90" +name = "nvidia-cuda-cupti" +version = "13.0.85" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, ] [[package]] -name = "nvidia-cuda-nvrtc-cu12" -version = "12.8.93" +name = "nvidia-cuda-nvrtc" +version = "13.0.88" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, ] [[package]] -name = "nvidia-cuda-runtime-cu12" -version = "12.8.90" +name = "nvidia-cuda-runtime" +version = "13.0.96" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, ] [[package]] -name = "nvidia-cudnn-cu12" -version = "9.10.2.21" +name = "nvidia-cudnn-cu13" +version = "9.20.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, ] [[package]] -name = "nvidia-cufft-cu12" -version = "11.3.3.83" +name = "nvidia-cufft" +version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, ] [[package]] -name = "nvidia-cufile-cu12" -version = "1.13.1.3" +name = "nvidia-cufile" +version = "1.15.1.6" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, ] [[package]] -name = "nvidia-curand-cu12" -version = "10.3.9.90" +name = "nvidia-curand" +version = "10.4.0.35" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, ] [[package]] -name = "nvidia-cusolver-cu12" -version = "11.7.3.90" +name = "nvidia-cusolver" +version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, ] [[package]] -name = "nvidia-cusparse-cu12" -version = "12.5.8.93" +name = "nvidia-cusparse" +version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, ] [[package]] -name = "nvidia-cusparselt-cu12" -version = "0.7.1" +name = "nvidia-cusparselt-cu13" +version = "0.8.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, ] [[package]] name = "nvidia-ml-py" -version = "13.595.45" +version = "13.610.43" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/49/c29f6e30d8662d2e94fef17739ea7309cc76aba269922ae999e4cc07f268/nvidia_ml_py-13.595.45.tar.gz", hash = "sha256:c9f34897fe0441ff35bc8f35baf80f830a20b0f4e6ce71e0a325bc0e66acf079", size = 50780, upload-time = "2026-03-19T16:59:44.956Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/b5/a8fbc356f768fa5c9cfd646668fd7d34bf55bdd1c6e20754642a64d930d4/nvidia_ml_py-13.610.43.tar.gz", hash = "sha256:65437eb73d68d0c62c931ca4d45038472faff03bd0b8729abba4b899f70d60f2", size = 52109, upload-time = "2026-06-01T18:54:08.829Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/24/fc256107d23597fa33d319505ce77160fa1a2349c096d01901ffc7cb7fc4/nvidia_ml_py-13.595.45-py3-none-any.whl", hash = "sha256:b65a7977f503d56154b14d683710125ef93594adb63fbf7e559336e3318f1376", size = 51776, upload-time = "2026-03-19T16:59:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl", hash = "sha256:f13c72698edef492f985cc225f14faafe68ae065a2e407f45bdf6f4b9b43fde8", size = 53163, upload-time = "2026-06-01T18:54:07.704Z" }, ] [[package]] @@ -7962,35 +8208,39 @@ wheels = [ ] [[package]] -name = "nvidia-nccl-cu12" -version = "2.27.5" +name = "nvidia-nccl-cu13" +version = "2.29.7" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, + { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, + { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, ] [[package]] -name = "nvidia-nvjitlink-cu12" -version = "12.8.93" +name = "nvidia-nvjitlink" +version = "13.0.88" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, ] [[package]] -name = "nvidia-nvshmem-cu12" +name = "nvidia-nvshmem-cu13" version = "3.4.5" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, ] [[package]] -name = "nvidia-nvtx-cu12" -version = "12.8.90" +name = "nvidia-nvtx" +version = "13.0.85" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, ] [[package]] @@ -8004,20 +8254,21 @@ wheels = [ [[package]] name = "oci" -version = "2.174.0" +version = "2.178.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi", 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 = "circuitbreaker", 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 = "crc32c", 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 = "cryptography", 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 = "pyopenssl", 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-dateutil", 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 = "pytz", 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 = "urllib3", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/45/5edb442e8197860b4fc26fd82305abf3df356827862ee11febfd8bf6ebbe/oci-2.174.0.tar.gz", hash = "sha256:f960e413a7f0e59ca5523b57349165f992812bd2738abc34bd9fecbce4722733", size = 17352965, upload-time = "2026-05-12T00:52:26.527Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/c8/25eb226edcd2ede3fb5bedf5bfa5054cde98006414b3cd76b82111ec8126/oci-2.178.0.tar.gz", hash = "sha256:d3a19859d80aa5c4988905e1a30b46dcc2af146c76f3d8c813129d71247d1a94", size = 17465409, upload-time = "2026-06-09T13:56:14.586Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/37/37a7d97a32e897b066f367448851471f52fcf8d46a16255a4532b2684821/oci-2.174.0-py3-none-any.whl", hash = "sha256:36c377fb59452b607686d73c1ae1604f2c19e3cabd7d12abe43a4404b10a17c5", size = 35404400, upload-time = "2026-05-12T00:52:17.116Z" }, + { url = "https://files.pythonhosted.org/packages/74/21/0f8f654086cbd878da1263a0ca5a81b0f4584fb98d56c6f262c25d2cb949/oci-2.178.0-py3-none-any.whl", hash = "sha256:830cb97cbcac818f8eb8d05d4abbc00192f4bcef10260b14d0978f649799a26e", size = 35670909, upload-time = "2026-06-09T13:56:04.592Z" }, ] [[package]] @@ -8037,32 +8288,31 @@ wheels = [ [[package]] name = "onnxruntime" -version = "1.24.4" +version = "1.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flatbuffers", 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 = "numpy", 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 = "packaging", 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 = "sympy", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/60/69/6c40720201012c6af9aa7d4ecdd620e521bd806dc6269d636fdd5c5aeebe/onnxruntime-1.24.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0bdfce8e9a6497cec584aab407b71bf697dac5e1b7b7974adc50bf7533bdb3a2", size = 17332131, upload-time = "2026-03-17T22:05:49.005Z" }, - { url = "https://files.pythonhosted.org/packages/38/e9/8c901c150ce0c368da38638f44152fb411059c0c7364b497c9e5c957321a/onnxruntime-1.24.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:046ff290045a387676941a02a8ae5c3ebec6b4f551ae228711968c4a69d8f6b7", size = 15152472, upload-time = "2026-03-17T22:03:26.176Z" }, - { url = "https://files.pythonhosted.org/packages/d5/b6/7a4df417cdd01e8f067a509e123ac8b31af450a719fa7ed81787dd6057ec/onnxruntime-1.24.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e54ad52e61d2d4618dcff8fa1480ac66b24ee2eab73331322db1049f11ccf330", size = 17222993, upload-time = "2026-03-17T22:04:34.485Z" }, - { url = "https://files.pythonhosted.org/packages/d7/38/31db1b232b4ba960065a90c1506ad7a56995cd8482033184e97fadca17cc/onnxruntime-1.24.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cad1c2b3f455c55678ab2a8caa51fb420c25e6e3cf10f4c23653cdabedc8de78", size = 17341875, upload-time = "2026-03-17T22:05:51.669Z" }, - { url = "https://files.pythonhosted.org/packages/aa/60/c4d1c8043eb42f8a9aa9e931c8c293d289c48ff463267130eca97d13357f/onnxruntime-1.24.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a5c5a544b22f90859c88617ecb30e161ee3349fcc73878854f43d77f00558b5", size = 15172485, upload-time = "2026-03-17T22:03:32.182Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ab/5b68110e0460d73fad814d5bd11c7b1ddcce5c37b10177eb264d6a36e331/onnxruntime-1.24.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d640eb9f3782689b55cfa715094474cd5662f2f137be6a6f847a594b6e9705c", size = 17244912, upload-time = "2026-03-17T22:04:37.251Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f0/8a21ec0a97e40abb7d8da1e8b20fb9e1af509cc6d191f6faa75f73622fb2/onnxruntime-1.24.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e99a48078baaefa2b50fe5836c319499f71f13f76ed32d0211f39109147a49e0", size = 17341922, upload-time = "2026-03-17T22:03:56.364Z" }, - { url = "https://files.pythonhosted.org/packages/8b/25/d7908de8e08cee9abfa15b8aa82349b79733ae5865162a3609c11598805d/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4aaed1e5e1aaacf2343c838a30a7c3ade78f13eeb16817411f929d04040a13", size = 15172290, upload-time = "2026-03-17T22:03:37.124Z" }, - { url = "https://files.pythonhosted.org/packages/7f/72/105ec27a78c5aa0154a7c0cd8c41c19a97799c3b12fc30392928997e3be3/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e30c972bc02e072911aabb6891453ec73795386c0af2b761b65444b8a4c4745f", size = 17244738, upload-time = "2026-03-17T22:04:40.625Z" }, - { url = "https://files.pythonhosted.org/packages/b4/af/a479a536c4398ffaf49fbbe755f45d5b8726bdb4335ab31b537f3d7149b8/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1700f559c8086d06b2a4d5de51e62cb4ff5e2631822f71a36db8c72383db71ee", size = 15176861, upload-time = "2026-03-17T22:03:40.143Z" }, - { url = "https://files.pythonhosted.org/packages/be/13/19f5da70c346a76037da2c2851ecbf1266e61d7f0dcdb887c667210d4608/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c74e268dc808e61e63784d43f9ddcdaf50a776c2819e8bd1d1b11ef64bf7e36", size = 17247454, upload-time = "2026-03-17T22:04:46.643Z" }, + { url = "https://files.pythonhosted.org/packages/d4/81/29a9eb470994a75eb7b3ccf32be314d7c66675a00ac7b50294816cc2db27/onnxruntime-1.26.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ee1109ef4ef27cad90e823399e61e03b3c6c7bfe0fb820b4baf3678c15be8b3c", size = 18005108, upload-time = "2026-05-08T19:08:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/66/c7/73efa6c8a4000c38fcc14947d84f234a17e5d66f203b37b7f1ad4a7b46eb/onnxruntime-1.26.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35c7c7b0ac2e02001d28fab6c9fc24e9abc5e6faa35e6e19c63cecf1406ba89f", size = 16043752, upload-time = "2026-05-08T19:07:10.707Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3f/8de630f595daf6ce884d4dd95afd2a60e70ec6572e52bfee3aa2229befab/onnxruntime-1.26.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11a8df4dcfe9ad5ff0bd71a7571dbed019fabc7594676c89fe8b86ea029c246f", size = 18176043, upload-time = "2026-05-08T19:07:33.735Z" }, + { url = "https://files.pythonhosted.org/packages/81/b1/d111b1df656761f980d9e298a60039a9cb66036b1d039e777537743d0ac3/onnxruntime-1.26.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05b028781b322ad74b57ce5b50aa5280bb1fe96ceec334628ade681e0b24c1ac", size = 18016624, upload-time = "2026-05-12T00:41:01.735Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a0/3f9d896a0385a36bd04345d6d0b802821a5782adde562e7e135f6bb71c73/onnxruntime-1.26.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91f2bb870a4b9224eba0a6728c1fa7a9e552b8e59e1083c51fbbc3d013f2b5c0", size = 16052692, upload-time = "2026-05-08T19:07:13.829Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/2a4e04f8dbeffad19bbcced4bcd4289bf478921518437404d6b92bdf213b/onnxruntime-1.26.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b6dd70599005bd1bf29779f04a91978b92b5e719c11a20068a8f8e535f725b6", size = 18185439, upload-time = "2026-05-08T19:07:36.299Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a2/c801242685e0ce48a4ca51dfafbb588765e0446397e123be53ba5598f3f5/onnxruntime-1.26.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccce19c5f771b8268902f77d9fed9e88f9499465d6780808faa6611a789d33f0", size = 18016563, upload-time = "2026-05-08T19:07:28.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/64/0492c0b1db04e29b2630c87cfa36f9d6872b1ca8614b90c5cad58fac7d76/onnxruntime-1.26.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdbed8cf3b672b66acb032f33a253bc27f42bce6ece48ae3fab4fa483a5e96e0", size = 16052634, upload-time = "2026-05-08T19:07:16.885Z" }, + { url = "https://files.pythonhosted.org/packages/3d/26/4d09ddc755a84fc8d5e192991626b0e0680e8f6c5d58f4f1d05c42bc48cf/onnxruntime-1.26.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c07af6fc6d5557835f2b6ee7a96d8b3235d0c57a8e230efdedaee106a8a3cbc6", size = 18185632, upload-time = "2026-05-08T19:07:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f5/47b0676408abec652c14b84d7173e389837832d850c24f87184277313e8d/onnxruntime-1.26.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e016edc15d3c19f36807e1c6b10be5b27807688c32720f91b5ae480a95215d0", size = 16057265, upload-time = "2026-05-08T19:07:19.603Z" }, + { url = "https://files.pythonhosted.org/packages/3b/45/33ab6deeef010ca844c877dd618cebc079590bbe52d2a3678e7223b1b908/onnxruntime-1.26.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5fc48a91a046a6a5c9b147f83fb41d65d24d24923373b222cdd248f0f4f4aac", size = 18197590, upload-time = "2026-05-08T19:07:41.422Z" }, ] [[package]] name = "openai" -version = "2.35.0" +version = "2.41.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", 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')" }, @@ -8074,9 +8324,9 @@ dependencies = [ { name = "tqdm", 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 = "typing-extensions", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/4c/35a5216fe5f1cd4d7002b037ba47cff10b71cbd4bddcb601262c664d08de/openai-2.35.0.tar.gz", hash = "sha256:607f62257d6be167240c6b82db052fabf940e3c4d9ad3e8629364e837a601395", size = 751972, upload-time = "2026-05-06T16:36:55.166Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/a6/5815fe2e2aca74b36c650d1bd43b69827cee568073d0d2d9b6fc5aaac80c/openai-2.41.0.tar.gz", hash = "sha256:db5c362acd6604b84f076abbefa66826ea4b46ecba2954ed866e6a149a1352c0", size = 783525, upload-time = "2026-06-03T22:39:40.719Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/b7/c43595f7f441cbc62ac3144a080d71952566b213f7a21bca0564d69e39fd/openai-2.35.0-py3-none-any.whl", hash = "sha256:164fd0477d001e784369f7cd81ccadb8db3c22f16b33973d8f95e3095c7f71d8", size = 1300139, upload-time = "2026-05-06T16:36:53.108Z" }, + { url = "https://files.pythonhosted.org/packages/be/51/d82bb424e8aa372190c5233253a2ceb399a778747d18b42cff487411e663/openai-2.41.0-py3-none-any.whl", hash = "sha256:20cc7952e8501c7e5773dd2ef7be437bae9cb549044902e1041a83a54516e375", size = 1353378, upload-time = "2026-06-03T22:39:38.964Z" }, ] [[package]] @@ -8091,17 +8341,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, ] -[[package]] -name = "opencv-python" -version = "4.13.0.92" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/6f/5a28fef4c4a382be06afe3938c64cc168223016fa520c5abaf37e8862aa5/opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:caf60c071ec391ba51ed00a4a920f996d0b64e3e46068aac1f646b5de0326a19", size = 46247052, upload-time = "2026-02-05T07:01:25.046Z" }, -] - [[package]] name = "openevals" version = "0.2.0" @@ -8119,68 +8358,67 @@ wheels = [ [[package]] name = "openinference-semantic-conventions" -version = "0.1.29" +version = "0.1.30" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/6b/9ed67f9ce8c92436b297207abde730800b00bdec7e114f71b8dfe91cd26b/openinference_semantic_conventions-0.1.29.tar.gz", hash = "sha256:bbeb6472777a45a574169894bb9c4d80c6832a8befd32ab238cb875438ce1044", size = 12959, upload-time = "2026-04-22T00:39:27.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/51/8ba1182ee86fc79793d5ff2d11e7fdcda10ded2d01f3e46ca6fcf0568213/openinference_semantic_conventions-0.1.30.tar.gz", hash = "sha256:81fece76e09c83789e35c393b8b30523481eeabf1008745b955631a53e3221d9", size = 13391, upload-time = "2026-05-22T21:10:44.065Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/7b/45ad1b95315b5563baa7338c8e8088bb1af66905c46e1bd1fe6ecbe30ea8/openinference_semantic_conventions-0.1.29-py3-none-any.whl", hash = "sha256:f45e0b1cf79fe407af4722bcf391a01565f0878c95be3ebcc9382245d0367cc5", size = 10582, upload-time = "2026-04-22T00:39:27.066Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/5b7e78cf0de38589b821bbe8e9c29c59a6e76edfb980488d0854cbb90f7c/openinference_semantic_conventions-0.1.30-py3-none-any.whl", hash = "sha256:36d946d3f95f699b7c4b12324ae9c1f02d6c7750df11eece56aa159cff430b3d", size = 10911, upload-time = "2026-05-22T21:10:43.04Z" }, ] [[package]] name = "opentelemetry-api" -version = "1.40.0" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", 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 = "typing-extensions", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, ] [[package]] name = "opentelemetry-distro" -version = "0.61b0" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api", 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 = "opentelemetry-instrumentation", 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 = "opentelemetry-sdk", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/00/1f8acc51326956a596fefaf67751380001af36029132a7a07d4debce3c06/opentelemetry_distro-0.61b0.tar.gz", hash = "sha256:975b845f50181ad53753becf4fd4b123b54fa04df5a9d78812264436d6518981", size = 2590, upload-time = "2026-03-04T14:20:12.453Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/97/87080029d9309841dd97db34130f9410cda77162843f81d09ad257dce1ef/opentelemetry_distro-0.63b1.tar.gz", hash = "sha256:f435098abc7953f58226e8bf79e4c90bc6b32e50aa75d6fa074201db8243b577", size = 2333, upload-time = "2026-05-21T16:36:11.285Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/2c/efcc995cd7484e6e55b1d26bd7fa6c55ca96bd415ff94310b52c19f330b0/opentelemetry_distro-0.61b0-py3-none-any.whl", hash = "sha256:f21d1ac0627549795d75e332006dd068877f00e461b1b2e8fe4568d6eb7b9590", size = 3349, upload-time = "2026-03-04T14:18:57.788Z" }, + { url = "https://files.pythonhosted.org/packages/35/97/16619e2e0e5192f2d1b8da2aaaefface05463cc1cfca6b81d3a3108ccedd/opentelemetry_distro-0.63b1-py3-none-any.whl", hash = "sha256:b405b04ad70e430390265eb38e82e067a84ca1f49a21429eaadb930c13330d66", size = 2777, upload-time = "2026-05-21T16:34:51.441Z" }, ] [[package]] name = "opentelemetry-exporter-otlp" -version = "1.40.0" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-exporter-otlp-proto-grpc", 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 = "opentelemetry-exporter-otlp-proto-http", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/37/b6708e0eff5c5fb9aba2e0ea09f7f3bcbfd12a592d2a780241b5f6014df7/opentelemetry_exporter_otlp-1.40.0.tar.gz", hash = "sha256:7caa0870b95e2fcb59d64e16e2b639ecffb07771b6cd0000b5d12e5e4fef765a", size = 6152, upload-time = "2026-03-04T14:17:23.235Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/94/8637919a5d01f81dacf510234bc0110b944f4687a6e96b0a02adf2f6bdce/opentelemetry_exporter_otlp-1.42.1.tar.gz", hash = "sha256:2d9ebaed714377a67d224d46795ddcc11d2c877fa5de35fda70b6f3b010729a9", size = 6086, upload-time = "2026-05-21T16:32:51.963Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/fc/aea77c28d9f3ffef2fdafdc3f4a235aee4091d262ddabd25882f47ce5c5f/opentelemetry_exporter_otlp-1.40.0-py3-none-any.whl", hash = "sha256:48c87e539ec9afb30dc443775a1334cc5487de2f72a770a4c00b1610bf6c697d", size = 7023, upload-time = "2026-03-04T14:17:03.612Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4d/c26080295a36fd22e201fefd7cb9c22cd203189b1af8cd73b158382b7ad8/opentelemetry_exporter_otlp-1.42.1-py3-none-any.whl", hash = "sha256:aedd54545bb0587cd45210abdc8be545af9c01413f3307786e276df1e3c83bee", size = 6733, upload-time = "2026-05-21T16:32:31.261Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.40.0" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/bc/1559d46557fe6eca0b46c88d4c2676285f1f3be2e8d06bb5d15fbffc814a/opentelemetry_exporter_otlp_proto_common-1.40.0.tar.gz", hash = "sha256:1cbee86a4064790b362a86601ee7934f368b81cd4cc2f2e163902a6e7818a0fa", size = 20416, upload-time = "2026-03-04T14:17:23.801Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/ca/8f122055c97a932311a3f640273f084e738008933503d0c2563cd5d591fc/opentelemetry_exporter_otlp_proto_common-1.40.0-py3-none-any.whl", hash = "sha256:7081ff453835a82417bf38dccf122c827c3cbc94f2079b03bba02a3165f25149", size = 18369, upload-time = "2026-03-04T14:17:04.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/2375e7612e1121a4518c17603b6e0b03ad94f565aafad53f464dc5be2bf6/opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl", hash = "sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140", size = 17327, upload-time = "2026-05-21T16:32:33.387Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.40.0" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos", 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')" }, @@ -8191,14 +8429,14 @@ dependencies = [ { name = "opentelemetry-sdk", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "typing-extensions", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8f/7f/b9e60435cfcc7590fa87436edad6822240dddbc184643a2a005301cc31f4/opentelemetry_exporter_otlp_proto_grpc-1.40.0.tar.gz", hash = "sha256:bd4015183e40b635b3dab8da528b27161ba83bf4ef545776b196f0fb4ec47740", size = 25759, upload-time = "2026-03-04T14:17:24.4Z" } +sdist = { url = "https://files.pythonhosted.org/packages/87/87/ca7fc790dfdbcf4f9e9aab14a39ef1b7508ead13707e283de0b3131478d2/opentelemetry_exporter_otlp_proto_grpc-1.42.1.tar.gz", hash = "sha256:975c4461f167dd8ed8857d68d3b6b25f3d272eab896f6a9470d0f5b90e2faf15", size = 27140, upload-time = "2026-05-21T16:32:56.162Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/6f/7ee0980afcbdcd2d40362da16f7f9796bd083bf7f0b8e038abfbc0300f5d/opentelemetry_exporter_otlp_proto_grpc-1.40.0-py3-none-any.whl", hash = "sha256:2aa0ca53483fe0cf6405087a7491472b70335bc5c7944378a0a8e72e86995c52", size = 20304, upload-time = "2026-03-04T14:17:05.942Z" }, + { url = "https://files.pythonhosted.org/packages/89/2b/28ba5b128f47fe8c3bab541000d6feb4b5a9bd26623ca013406f01c0fb60/opentelemetry_exporter_otlp_proto_grpc-1.42.1-py3-none-any.whl", hash = "sha256:0ae1177e2038b18a929b3098215243631ef91136cba26b7e2b12790ceb7e87cc", size = 19617, upload-time = "2026-05-21T16:32:34.278Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.40.0" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos", 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')" }, @@ -8209,28 +8447,28 @@ dependencies = [ { name = "requests", 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 = "typing-extensions", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/fa/73d50e2c15c56be4d000c98e24221d494674b0cc95524e2a8cb3856d95a4/opentelemetry_exporter_otlp_proto_http-1.40.0.tar.gz", hash = "sha256:db48f5e0f33217588bbc00274a31517ba830da576e59503507c839b38fa0869c", size = 17772, upload-time = "2026-03-04T14:17:25.324Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", size = 25406, upload-time = "2026-05-21T16:32:56.807Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/3a/8865d6754e61c9fb170cdd530a124a53769ee5f740236064816eb0ca7301/opentelemetry_exporter_otlp_proto_http-1.40.0-py3-none-any.whl", hash = "sha256:a8d1dab28f504c5d96577d6509f80a8150e44e8f45f82cdbe0e34c99ab040069", size = 19960, upload-time = "2026-03-04T14:17:07.153Z" }, + { url = "https://files.pythonhosted.org/packages/d3/96/82cb223a1502f0787d4bbff12907f5f8d870a50731febcd5818d93ef9555/opentelemetry_exporter_otlp_proto_http-1.42.1-py3-none-any.whl", hash = "sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d", size = 21793, upload-time = "2026-05-21T16:32:35.277Z" }, ] [[package]] name = "opentelemetry-exporter-prometheus" -version = "0.61b0" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api", 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 = "opentelemetry-sdk", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "prometheus-client", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4a/20/9e818fd364d12e8d0cfdce4a3b2d82e24d98c4ceebb315de6b6770b5f214/opentelemetry_exporter_prometheus-0.61b0.tar.gz", hash = "sha256:7c4919bd8e79abd62b610767e80f42c9c3a06c5183f4dd9141eedeb57aea284b", size = 15136, upload-time = "2026-03-04T14:17:26.275Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/2a/dfeddff262b12eff0c72f4ad9e258aab8889f48c4dc1417a0377a13bc427/opentelemetry_exporter_prometheus-0.63b1.tar.gz", hash = "sha256:31902e22c89431058a95b6dcdb644f9309f226aa4872cc755f0a780d2895e97f", size = 15234, upload-time = "2026-05-21T16:32:57.797Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/4a/b65d40e94d1d930aee73a1a2857211ee6ab10ce3686cbdae5eea78cd9d34/opentelemetry_exporter_prometheus-0.61b0-py3-none-any.whl", hash = "sha256:3013b41f4370143d48d219a2351473761423e5882fa4c213811eaefacba39cb7", size = 13149, upload-time = "2026-03-04T14:17:08.983Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ec/d7c7435e9000fb69837cf7753b7cbbbdeb5d0585203daf1b6ebf8fa93e02/opentelemetry_exporter_prometheus-0.63b1-py3-none-any.whl", hash = "sha256:0efd00aa6b1939345ddcc6de141b83ebffa2b4401a37a68f880e54217602701d", size = 12466, upload-time = "2026-05-21T16:32:36.622Z" }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.61b0" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api", 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')" }, @@ -8238,14 +8476,14 @@ dependencies = [ { name = "packaging", 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 = "wrapt", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/37/6bf8e66bfcee5d3c6515b79cb2ee9ad05fe573c20f7ceb288d0e7eeec28c/opentelemetry_instrumentation-0.61b0.tar.gz", hash = "sha256:cb21b48db738c9de196eba6b805b4ff9de3b7f187e4bbf9a466fa170514f1fc7", size = 32606, upload-time = "2026-03-04T14:20:16.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/6d/4de72d97ff54db1ed270c7a59c9b904b917c0ac7af429c086c388b824ddb/opentelemetry_instrumentation-0.63b1.tar.gz", hash = "sha256:32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541", size = 41081, upload-time = "2026-05-21T16:36:14.206Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/3e/f6f10f178b6316de67f0dfdbbb699a24fbe8917cf1743c1595fb9dcdd461/opentelemetry_instrumentation-0.61b0-py3-none-any.whl", hash = "sha256:92a93a280e69788e8f88391247cc530fd81f16f2b011979d4d6398f805cfbc63", size = 33448, upload-time = "2026-03-04T14:19:02.447Z" }, + { url = "https://files.pythonhosted.org/packages/35/a1/9314e621c143e4d82a5bf7a43c2ff7a745d31023506336857607c8c543cc/opentelemetry_instrumentation-0.63b1-py3-none-any.whl", hash = "sha256:f1986716d52cc316ea5f60189098726a9071d8ecc0eee96c9ed110be08bade9c", size = 35577, upload-time = "2026-05-21T16:34:56.818Z" }, ] [[package]] name = "opentelemetry-instrumentation-asgi" -version = "0.61b0" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref", 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')" }, @@ -8254,14 +8492,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions", 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 = "opentelemetry-util-http", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/3e/143cf5c034e58037307e6a24f06e0dd64b2c49ae60a965fc580027581931/opentelemetry_instrumentation_asgi-0.61b0.tar.gz", hash = "sha256:9d08e127244361dc33976d39dd4ca8f128b5aa5a7ae425208400a80a095019b5", size = 26691, upload-time = "2026-03-04T14:20:21.038Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/b5/7ea3a9fd1b80e89786c14250bfaecf32a753c3fd08232690f4da8dc16e29/opentelemetry_instrumentation_asgi-0.63b1.tar.gz", hash = "sha256:267b422416d768f3c7f4054883b41d9c3a7c943d86d20032b738c99a3dbb5862", size = 26151, upload-time = "2026-05-21T16:36:18.368Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/78/154470cf9d741a7487fbb5067357b87386475bbb77948a6707cae982e158/opentelemetry_instrumentation_asgi-0.61b0-py3-none-any.whl", hash = "sha256:e4b3ce6b66074e525e717efff20745434e5efd5d9df6557710856fba356da7a4", size = 16980, upload-time = "2026-03-04T14:19:10.894Z" }, + { url = "https://files.pythonhosted.org/packages/57/7e/83986f27b421de04fab1e1a84e892621dac42e6432a9c66779505f4d1381/opentelemetry_instrumentation_asgi-0.63b1-py3-none-any.whl", hash = "sha256:1a22453dfa965f14799b10a674b8acbcb897a8a75c79136060af54214cc7886e", size = 15906, upload-time = "2026-05-21T16:35:04.162Z" }, ] [[package]] name = "opentelemetry-instrumentation-fastapi" -version = "0.61b0" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api", 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')" }, @@ -8270,14 +8508,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions", 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 = "opentelemetry-util-http", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/37/35/aa727bb6e6ef930dcdc96a617b83748fece57b43c47d83ba8d83fbeca657/opentelemetry_instrumentation_fastapi-0.61b0.tar.gz", hash = "sha256:3a24f35b07c557ae1bbc483bf8412221f25d79a405f8b047de8b670722e2fa9f", size = 24800, upload-time = "2026-03-04T14:20:32.759Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/d6/0c128fac2e34b7d526a8d3c6edc45b875a97f8a987861b00511151b6337d/opentelemetry_instrumentation_fastapi-0.63b1.tar.gz", hash = "sha256:cc42dff56c96d0a2921510c4abab2a4c2e27fe64b26dc1254727fb550df100ba", size = 25387, upload-time = "2026-05-21T16:36:32.071Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/05/acfeb2cccd434242a0a7d0ea29afaf077e04b42b35b485d89aee4e0d9340/opentelemetry_instrumentation_fastapi-0.61b0-py3-none-any.whl", hash = "sha256:a1a844d846540d687d377516b2ff698b51d87c781b59f47c214359c4a241047c", size = 13485, upload-time = "2026-03-04T14:19:30.351Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/2eae63f13f36d7a8ab5bf03d06ecaf169c2069b524547f24947be6d92094/opentelemetry_instrumentation_fastapi-0.63b1-py3-none-any.whl", hash = "sha256:52ee2cde9a2ac094bdd45d79f85860e03a972928a2553006071fe61d94cf7281", size = 12795, upload-time = "2026-05-21T16:35:28.68Z" }, ] [[package]] name = "opentelemetry-instrumentation-httpx" -version = "0.61b0" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api", 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')" }, @@ -8286,14 +8524,14 @@ dependencies = [ { name = "opentelemetry-util-http", 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 = "wrapt", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/2a/e2becd55e33c29d1d9ef76e2579040ed1951cb33bacba259f6aff2fdd2a6/opentelemetry_instrumentation_httpx-0.61b0.tar.gz", hash = "sha256:6569ec097946c5551c2a4252f74c98666addd1bf047c1dde6b4ef426719ff8dd", size = 24104, upload-time = "2026-03-04T14:20:34.752Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/27/c2b4335bca030e893acbe5ff2b4f434868773bf94508be7e6bf5af981b24/opentelemetry_instrumentation_httpx-0.63b1.tar.gz", hash = "sha256:f41ec82f25c3abcdada621052db3e5fd648e3b43d55eec4b9c0c5d3ecb7b4ff4", size = 23557, upload-time = "2026-05-21T16:36:34.583Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/88/dde310dce56e2d85cf1a09507f5888544955309edc4b8d22971d6d3d1417/opentelemetry_instrumentation_httpx-0.61b0-py3-none-any.whl", hash = "sha256:dee05c93a6593a5dc3ae5d9d5c01df8b4e2c5d02e49275e5558534ee46343d5e", size = 17198, upload-time = "2026-03-04T14:19:33.585Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b8/f536780996195c3b9f2354998554671e05a7a262df8c043f63fe9e5a6f0b/opentelemetry_instrumentation_httpx-0.63b1-py3-none-any.whl", hash = "sha256:14df6e99d81be9a8cd238f6639b6fa52404c4d3ce219058fcb5dc8c0f2211f86", size = 16336, upload-time = "2026-05-21T16:35:32.221Z" }, ] [[package]] name = "opentelemetry-instrumentation-requests" -version = "0.61b0" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api", 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')" }, @@ -8301,14 +8539,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions", 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 = "opentelemetry-util-http", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/c7/7a47cb85c7aa93a9c820552e414889185bcf91245271d12e5d443e5f834d/opentelemetry_instrumentation_requests-0.61b0.tar.gz", hash = "sha256:15f879ce8fb206bd7e6fdc61663ea63481040a845218c0cf42902ce70bd7e9d9", size = 18379, upload-time = "2026-03-04T14:20:46.959Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/39/7b14ef15c7c74b0da7d32b449732795a5cf7495897b72fc0b48280b96f50/opentelemetry_instrumentation_requests-0.63b1.tar.gz", hash = "sha256:513fcaa3d93debbdb359c00ce1a137a34a89ee908c51ac43beb7e8c18ac2b3cd", size = 18098, upload-time = "2026-05-21T16:36:46.02Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/a1/a7a133b273d1f53950f16a370fc94367eff472c9c2576e8e9e28c62dcc9f/opentelemetry_instrumentation_requests-0.61b0-py3-none-any.whl", hash = "sha256:cce19b379949fe637eb73ba39b02c57d2d0805447ca6d86534aa33fcb141f683", size = 14207, upload-time = "2026-03-04T14:19:51.765Z" }, + { url = "https://files.pythonhosted.org/packages/f8/18/a5e35fe8c9ad8041b71dd712658589de5d692aaa17d7cbce7f87a5cb0d0f/opentelemetry_instrumentation_requests-0.63b1-py3-none-any.whl", hash = "sha256:935c980a11e33bfd7ed969c741e4bd7c84077045651469f10e163534368d87f7", size = 13378, upload-time = "2026-05-21T16:35:52.166Z" }, ] [[package]] name = "opentelemetry-instrumentation-sqlalchemy" -version = "0.61b0" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api", 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')" }, @@ -8317,85 +8555,86 @@ dependencies = [ { name = "packaging", 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 = "wrapt", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/4f/3a325b180944610697a0a926d49d782b41a86120050d44fefb2715b630ac/opentelemetry_instrumentation_sqlalchemy-0.61b0.tar.gz", hash = "sha256:13a3a159a2043a52f0180b3757fbaa26741b0e08abb50deddce4394c118956e6", size = 15343, upload-time = "2026-03-04T14:20:47.648Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/97/e5cb3ad027aebf7128faadeefe4d4cb0fc07ed32ef95e8fc9d828a077a85/opentelemetry_instrumentation_sqlalchemy-0.63b1.tar.gz", hash = "sha256:621f9eb800ea24a98b4eda968373e3909bfede0ff47f77b96f8b8a18bc2a2a1a", size = 18006, upload-time = "2026-05-21T16:36:46.855Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/97/b906a930c6a1a20c53ecc8b58cabc2cdd0ce560a2b5d44259084ffe4333e/opentelemetry_instrumentation_sqlalchemy-0.61b0-py3-none-any.whl", hash = "sha256:f115e0be54116ba4c327b8d7b68db4045ee18d44439d888ab8130a549c50d1c1", size = 14547, upload-time = "2026-03-04T14:19:53.088Z" }, + { url = "https://files.pythonhosted.org/packages/3c/bc/c0984c4c51da64cc2c37ce031b4fb7fab61d223f2188a6bc6b5f18035ae3/opentelemetry_instrumentation_sqlalchemy-0.63b1-py3-none-any.whl", hash = "sha256:d417414f6517963e9c1ee91ec971b94938b46904499114d035a43937bd62b6a1", size = 14410, upload-time = "2026-05-21T16:35:53.342Z" }, ] [[package]] name = "opentelemetry-instrumentation-system-metrics" -version = "0.61b0" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api", 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 = "opentelemetry-instrumentation", 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 = "opentelemetry-semantic-conventions", 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 = "psutil", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/68/a403ade03a7ccba3d113a02c041942ab8feb4471101eb3a02da6403e9258/opentelemetry_instrumentation_system_metrics-0.61b0.tar.gz", hash = "sha256:3eb55f9a058797cf915946cbb7445e00b31316ac3e55050475792edf3367c321", size = 17637, upload-time = "2026-03-04T14:20:49.591Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/a4/c91a0e085808724ad4cb3bdd76bf9dac872c8a8910b24767cf95ffde67a5/opentelemetry_instrumentation_system_metrics-0.63b1.tar.gz", hash = "sha256:d6d4d7a1a854be4165143cf6420ee5894188762eb367d7bf9da5be4a83a4b632", size = 17412, upload-time = "2026-05-21T16:36:49.276Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/2b/3142c6e0f3c9a5be3e5187933bc28b0c8b7e77c04937aec317eee96e8fdb/opentelemetry_instrumentation_system_metrics-0.61b0-py3-none-any.whl", hash = "sha256:7d4fe3e0ce14e0e6eb18f5826100d6cc1af662e5a8ebc74e9b91fe23f192f3e8", size = 14909, upload-time = "2026-03-04T14:19:56.306Z" }, + { url = "https://files.pythonhosted.org/packages/29/37/b8bddfab16c0a36af1941ec041b5bc8fe3c3d802997b4e819037908a90d5/opentelemetry_instrumentation_system_metrics-0.63b1-py3-none-any.whl", hash = "sha256:995051f47876d79461aed8b7aa205d4584d90794ef864342cc748929c389bb42", size = 14006, upload-time = "2026-05-21T16:35:56.979Z" }, ] [[package]] name = "opentelemetry-processor-baggage" -version = "0.61b0" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api", 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 = "opentelemetry-sdk", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "wrapt", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/0d/4afee20490ef53a449b1781b0671d84858742a2ccfb01c08de398a5d1ccd/opentelemetry_processor_baggage-0.61b0.tar.gz", hash = "sha256:4d1d2a624e3aa9a8b6c6d1f560ba2951f97acf875f57502a274c5078043a69d5", size = 7573, upload-time = "2026-03-04T14:20:54.941Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/43/69a8dcb2a540809c02bfc31d01b2e8228b4dd28c363d6d59577bcf9f1361/opentelemetry_processor_baggage-0.63b1.tar.gz", hash = "sha256:334b77963ea5807efd6f05664a6064aa92fc6c03571edbf1f749b9dee370d567", size = 8834, upload-time = "2026-05-21T16:36:54.591Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/24/0ef2cf49e6ac9b2b422400abbf528230a409c9e174572f2d13e2dff7ec7c/opentelemetry_processor_baggage-0.61b0-py3-none-any.whl", hash = "sha256:f6b5937e93bda8f380d8f5f667355c7d127e9296b38dfacf39fd328ab410262c", size = 8881, upload-time = "2026-03-04T14:20:05.25Z" }, + { url = "https://files.pythonhosted.org/packages/23/44/3179fc05c69ca82657565d3223feb14d5208ede33e6838e42e7fe2dc01b8/opentelemetry_processor_baggage-0.63b1-py3-none-any.whl", hash = "sha256:b205c343720ce4d5e420204e09862a043917ee433b2304d87bb6f388084f3c15", size = 9488, upload-time = "2026-05-21T16:36:06.756Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.40.0" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/77/dd38991db037fdfce45849491cb61de5ab000f49824a00230afb112a4392/opentelemetry_proto-1.40.0.tar.gz", hash = "sha256:03f639ca129ba513f5819810f5b1f42bcb371391405d99c168fe6937c62febcd", size = 45667, upload-time = "2026-03-04T14:17:31.194Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", size = 45839, upload-time = "2026-05-21T16:33:03.937Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/b2/189b2577dde745b15625b3214302605b1353436219d42b7912e77fa8dc24/opentelemetry_proto-1.40.0-py3-none-any.whl", hash = "sha256:266c4385d88923a23d63e353e9761af0f47a6ed0d486979777fe4de59dc9b25f", size = 72073, upload-time = "2026-03-04T14:17:16.673Z" }, + { url = "https://files.pythonhosted.org/packages/41/9d/171c02c84a76940b7e601805b3bb536985aded9168fbcc9ba52f0a730fa2/opentelemetry_proto-1.42.1-py3-none-any.whl", hash = "sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c", size = 71782, upload-time = "2026-05-21T16:32:44.867Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.40.0" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api", 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 = "opentelemetry-semantic-conventions", 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 = "typing-extensions", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/fd/3c3125b20ba18ce2155ba9ea74acb0ae5d25f8cd39cfd37455601b7955cc/opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2", size = 184252, upload-time = "2026-03-04T14:17:31.87Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/c5/6a852903d8bfac758c6dc6e9a68b015d3c33f2f1be5e9591e0f4b69c7e0a/opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1", size = 141951, upload-time = "2026-03-04T14:17:17.961Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.61b0" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api", 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 = "typing-extensions", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/c0/4ae7973f3c2cfd2b6e321f1675626f0dab0a97027cc7a297474c9c8f3d04/opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a", size = 145755, upload-time = "2026-03-04T14:17:32.664Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" }, ] [[package]] name = "opentelemetry-util-http" -version = "0.61b0" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/3c/f0196223efc5c4ca19f8fad3d5462b171ac6333013335ce540c01af419e9/opentelemetry_util_http-0.61b0.tar.gz", hash = "sha256:1039cb891334ad2731affdf034d8fb8b48c239af9b6dd295e5fabd07f1c95572", size = 11361, upload-time = "2026-03-04T14:20:57.01Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/d8/7bf5e4cec0578ac3c28c18eb7b88f34279139cbc8c568d6aa02b9c5ae53e/opentelemetry_util_http-0.63b1.tar.gz", hash = "sha256:ba1268f00922ee522dba2ae38458060f99486e7385a8056985901ca9685adfff", size = 11102, upload-time = "2026-05-21T16:36:56.675Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/e5/c08aaaf2f64288d2b6ef65741d2de5454e64af3e050f34285fb1907492fe/opentelemetry_util_http-0.61b0-py3-none-any.whl", hash = "sha256:8e715e848233e9527ea47e275659ea60a57a75edf5206a3b937e236a6da5fc33", size = 9281, upload-time = "2026-03-04T14:20:08.364Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/34e047e8f6a3c67e5220acf1af7b9f62868c25d77791bca74457bd2180a6/opentelemetry_util_http-0.63b1-py3-none-any.whl", hash = "sha256:6284194028c59cd439f8acfe388145069a6127f11dc077e1344a2094adacc3f8", size = 8205, upload-time = "2026-05-21T16:36:09.736Z" }, ] [[package]] @@ -8418,28 +8657,40 @@ wheels = [ [[package]] name = "orjson" -version = "3.11.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832, upload-time = "2026-03-31T16:16:27.878Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/41/5aa7fa3b0f4dc6b47dcafc3cea909299c37e40e9972feabc8b6a74e2730d/orjson-3.11.8-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:003646067cc48b7fcab2ae0c562491c9b5d2cbd43f1e5f16d98fd118c5522d34", size = 229229, upload-time = "2026-03-31T16:14:50.424Z" }, - { url = "https://files.pythonhosted.org/packages/0a/d7/57e7f2458e0a2c41694f39fc830030a13053a84f837a5b73423dca1f0938/orjson-3.11.8-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ed193ce51d77a3830cad399a529cd4ef029968761f43ddc549e1bc62b40d88f8", size = 128871, upload-time = "2026-03-31T16:14:51.888Z" }, - { url = "https://files.pythonhosted.org/packages/53/4a/e0fdb9430983e6c46e0299559275025075568aad5d21dd606faee3703924/orjson-3.11.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30491bc4f862aa15744b9738517454f1e46e56c972a2be87d70d727d5b2a8f8", size = 132104, upload-time = "2026-03-31T16:14:53.142Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fc/55e667ec9c85694038fcff00573d221b085d50777368ee3d77f38668bf3c/orjson-3.11.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f8952d6d2505c003e8f0224ff7858d341fa4e33fef82b91c4ff0ef070f2393c", size = 133580, upload-time = "2026-03-31T16:15:00.519Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a6/c08c589a9aad0cb46c4831d17de212a2b6901f9d976814321ff8e69e8785/orjson-3.11.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0022bb50f90da04b009ce32c512dc1885910daa7cb10b7b0cba4505b16db82a8", size = 142042, upload-time = "2026-03-31T16:15:01.906Z" }, - { url = "https://files.pythonhosted.org/packages/90/6c/0fb6e8a24e682e0958d71711ae6f39110e4b9cd8cab1357e2a89cb8e1951/orjson-3.11.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5c370674ebabe16c6ccac33ff80c62bf8a6e59439f5e9d40c1f5ab8fd2215b7", size = 136425, upload-time = "2026-03-31T16:15:07.052Z" }, - { 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/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/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/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/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" }, +version = "3.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/51/3fb9e65ae76ee97bd611869a503fa3fc0a6e81dd8b737cf3003f682df7ff/orjson-3.11.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f", size = 228522, upload-time = "2026-05-06T15:09:35.362Z" }, + { url = "https://files.pythonhosted.org/packages/16/fa/9d54b07cb3f3b0bfd57841478e42d7a0ece4a9f49f9907eecf5a45461687/orjson-3.11.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c", size = 128463, upload-time = "2026-05-06T15:09:37.063Z" }, + { url = "https://files.pythonhosted.org/packages/88/b1/6ceafc2eefd0a553e3be77ce6c49d107e772485d9568629376171c50e634/orjson-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5", size = 132306, upload-time = "2026-05-06T15:09:38.299Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/f11311285324a40aab1e3031385c50b635a7cd0734fdaf60c7e89a696f60/orjson-3.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c", size = 127988, upload-time = "2026-05-06T15:09:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/05/94/b0d27090ea8a2095db3c2bd1b1c96f96f19bbb494d7fef33130e846e613d/orjson-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62", size = 145937, upload-time = "2026-05-06T15:09:42.249Z" }, + { url = "https://files.pythonhosted.org/packages/09/eb/75d50c29c05b8054013e221e598820a365c8e64065312e75e202ed880709/orjson-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877", size = 132758, upload-time = "2026-05-06T15:09:43.945Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/360686f39348aa88827cb6fbf7dc606fd41c831a35235e1abf1db8e3a9e6/orjson-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1", size = 133971, upload-time = "2026-05-06T15:09:45.239Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/3178eb16f3221aeef068b6f1f1ebe05f656ea5c6dffe9f6c917329fe17a3/orjson-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd", size = 141685, upload-time = "2026-05-06T15:09:46.858Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f1/ff2f19ed0225f9680fafa42febca3570dd59444ebf190980738d376214c2/orjson-3.11.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff", size = 415167, upload-time = "2026-05-06T15:09:48.312Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4081492586d75b073d60c5271a8d0f05a0955cabf1e34c8473f6fcd84235/orjson-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2", size = 136959, upload-time = "2026-05-06T15:09:51.311Z" }, + { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, + { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" }, + { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" }, + { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" }, + { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" }, + { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" }, + { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" }, + { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" }, + { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" }, + { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" }, ] [[package]] @@ -8450,18 +8701,24 @@ sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9 wheels = [ { url = "https://files.pythonhosted.org/packages/4b/08/8b68f24b18e69d92238aa8f258218e6dfeacf4381d9d07ab8df303f524a9/ormsgpack-1.12.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9", size = 378266, upload-time = "2026-01-18T20:55:59.876Z" }, { url = "https://files.pythonhosted.org/packages/0d/24/29fc13044ecb7c153523ae0a1972269fcd613650d1fa1a9cec1044c6b666/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a", size = 203035, upload-time = "2026-01-18T20:55:30.59Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c2/00169fb25dd8f9213f5e8a549dfb73e4d592009ebc85fbbcd3e1dcac575b/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5", size = 210539, upload-time = "2026-01-18T20:55:48.569Z" }, { url = "https://files.pythonhosted.org/packages/1b/33/543627f323ff3c73091f51d6a20db28a1a33531af30873ea90c5ac95a9b5/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181", size = 212401, upload-time = "2026-01-18T20:56:10.101Z" }, { url = "https://files.pythonhosted.org/packages/e8/5d/f70e2c3da414f46186659d24745483757bcc9adccb481a6eb93e2b729301/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b", size = 387082, upload-time = "2026-01-18T20:56:12.047Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d6/06e8dc920c7903e051f30934d874d4afccc9bb1c09dcaf0bc03a7de4b343/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92", size = 482346, upload-time = "2026-01-18T20:56:05.152Z" }, { url = "https://files.pythonhosted.org/packages/66/c4/f337ac0905eed9c393ef990c54565cd33644918e0a8031fe48c098c71dbf/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a", size = 425181, upload-time = "2026-01-18T20:55:37.83Z" }, { url = "https://files.pythonhosted.org/packages/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" }, ] @@ -8476,11 +8733,11 @@ wheels = [ [[package]] name = "packaging" -version = "26.0" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] @@ -8548,29 +8805,29 @@ wheels = [ [[package]] name = "parso" -version = "0.8.6" +version = "0.8.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, ] [[package]] name = "pathable" -version = "0.4.4" +version = "0.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/f3/5a20387de9bcd0607871bfc2198ee0e15836da7baa4592ccd7f24c27c986/pathable-0.6.0.tar.gz", hash = "sha256:6404b8b82aef5ff0fd478934137128b99b12212ba35afdde5525ca4f8388ea58", size = 18970, upload-time = "2026-05-19T18:15:11.911Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e8/6d75ffd9784bce2e93d1ae4415649427e39a53bb172d4672b2b59c6f0a7b/pathable-0.6.0-py3-none-any.whl", hash = "sha256:82c4ca6c98c502ad12e0d4e9779b6210afee93c38990988c8c5d1b49bdcdf566", size = 18983, upload-time = "2026-05-19T18:15:10.728Z" }, ] [[package]] name = "pathspec" -version = "1.0.4" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] @@ -8652,11 +8909,11 @@ wheels = [ [[package]] name = "pip" -version = "26.1.1" +version = "26.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/48/cb9b7a682f6fe01a4221e1728941dd4ac3cd9090a17db3779d6ff490b602/pip-26.1.1.tar.gz", hash = "sha256:d36762751d156a4ee895de8af39aa0abeeeb577f93a2eca6ab62467bbf0f8a78", size = 1840400, upload-time = "2026-05-04T19:02:21.248Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/91/47e7d486260f618783899587af63ccf7980fb60245c3e63dd4571c6b57ad/pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605", size = 1840799, upload-time = "2026-05-31T17:33:58.56Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl", hash = "sha256:99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb", size = 1812777, upload-time = "2026-05-04T19:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/5d/95/6b5cb3461ea5673ba0995989746db58eb18b91b54dbf331e72f569540946/pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab", size = 1813144, upload-time = "2026-05-31T17:33:56.772Z" }, ] [[package]] @@ -8679,11 +8936,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.9.4" +version = "4.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, ] [[package]] @@ -8743,11 +9000,11 @@ wheels = [ [[package]] name = "prometheus-client" -version = "0.24.1" +version = "0.25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" }, ] [[package]] @@ -8777,35 +9034,63 @@ wheels = [ [[package]] name = "propcache" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, - { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, - { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, - { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, - { 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/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/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/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/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/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/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" }, +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" }, + { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573, upload-time = "2026-05-08T20:59:50.778Z" }, + { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645, upload-time = "2026-05-08T20:59:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563, upload-time = "2026-05-08T20:59:53.866Z" }, + { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888, upload-time = "2026-05-08T20:59:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253, upload-time = "2026-05-08T20:59:57.075Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558, upload-time = "2026-05-08T20:59:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007, upload-time = "2026-05-08T20:59:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355, upload-time = "2026-05-08T21:00:01.144Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057, upload-time = "2026-05-08T21:00:02.401Z" }, + { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938, upload-time = "2026-05-08T21:00:03.638Z" }, + { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731, upload-time = "2026-05-08T21:00:04.881Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] [[package]] @@ -8816,6 +9101,7 @@ 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" }, ] @@ -8838,25 +9124,37 @@ wheels = [ [[package]] name = "psycopg2-binary" -version = "2.9.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/34/aa03d327739c1be70e09d01182619aca8ebab5970cd0cfa50dd8b9cec2ac/psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", size = 3863957, upload-time = "2025-10-10T11:11:16.932Z" }, - { url = "https://files.pythonhosted.org/packages/48/89/3fdb5902bdab8868bbedc1c6e6023a4e08112ceac5db97fc2012060e0c9a/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", size = 4410955, upload-time = "2025-10-10T11:11:21.21Z" }, - { url = "https://files.pythonhosted.org/packages/91/7e/b8441e831a0f16c159b5381698f9f7f7ed54b77d57bc9c5f99144cc78232/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", size = 4165012, upload-time = "2025-10-10T11:11:29.51Z" }, - { url = "https://files.pythonhosted.org/packages/76/a1/2f5841cae4c635a9459fe7aca8ed771336e9383b6429e05c01267b0774cf/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", size = 3650985, upload-time = "2025-10-10T11:11:34.975Z" }, - { url = "https://files.pythonhosted.org/packages/c8/31/36a1d8e702aa35c38fc117c2b8be3f182613faa25d794b8aeaab948d4c03/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", size = 3345842, upload-time = "2025-10-10T11:11:45.366Z" }, - { 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/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/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/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/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/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/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" }, +version = "2.9.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/71/c85409ee0d78890f0660eff262e815e7dd2bb741a17611d82e9e8cd9dc5e/psycopg2_binary-2.9.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326", size = 3822407, upload-time = "2026-04-20T23:34:05.977Z" }, + { url = "https://files.pythonhosted.org/packages/3c/ed/60486c2c7f0d4d1ede2bfb1ed27e2498477ce646bc7f6b2759906303117e/psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06", size = 4578425, upload-time = "2026-04-20T23:34:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b9/656cb03fad9f4f49f2145c334b1126ee75189929ca4e6187d485a2d59951/psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8", size = 4273709, upload-time = "2026-04-20T23:34:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/99/66/08cf0da0e25cc6fb142c89be45fc8418792858f0c4cbff5e24530ff02cd6/psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3", size = 5893779, upload-time = "2026-04-20T23:34:13.905Z" }, + { url = "https://files.pythonhosted.org/packages/17/d7/eecd9ce8e146d3721115d82d3836efdbb712187e4590325df549989d18f4/psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39", size = 4109308, upload-time = "2026-04-20T23:34:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/b1dc289b362cc8d45697b57eefbd673186f49a4ea0906928988e3affcc98/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c", size = 3654405, upload-time = "2026-04-20T23:34:19.303Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/4c4aea6473214dbdbd0fbba11aa4691e76dc01722c55724c5951719865ff/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a", size = 3299187, upload-time = "2026-04-20T23:34:21.206Z" }, + { url = "https://files.pythonhosted.org/packages/ba/5d/b03b99986446a4f57b170ed9a2579fb7ff9783ca0fa5226b19db99737fee/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c", size = 3047716, upload-time = "2026-04-20T23:34:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/14/86/382ee4afbd1d97500c9d2862b20c2fdeddf4b7335e984df3fb4309f64108/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be", size = 3349237, upload-time = "2026-04-20T23:34:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" }, + { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" }, + { url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990, upload-time = "2026-04-20T23:35:00.821Z" }, + { url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696, upload-time = "2026-04-20T23:35:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982, upload-time = "2026-04-20T23:35:05.583Z" }, + { url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867, upload-time = "2026-04-20T23:35:07.902Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578, upload-time = "2026-04-20T23:35:10.149Z" }, + { url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816, upload-time = "2026-04-20T23:35:12.56Z" }, + { url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307, upload-time = "2026-04-20T23:35:15.029Z" }, + { url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968, upload-time = "2026-04-20T23:35:16.757Z" }, + { url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369, upload-time = "2026-04-20T23:35:18.933Z" }, ] [[package]] @@ -8879,15 +9177,15 @@ wheels = [ [[package]] name = "py-key-value-aio" -version = "0.4.4" +version = "0.4.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beartype", 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 = "typing-extensions", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/e2/d689d922894a7ecde73b6daeaf9b13dab5aae06fe6aaaf7514722644d382/py_key_value_aio-0.4.5.tar.gz", hash = "sha256:c6563a2c6abe5da5e20f4f9e875c2a9b425a2244a54fadbf46cf140a9eea45d7", size = 107547, upload-time = "2026-05-27T16:37:08.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" }, + { url = "https://files.pythonhosted.org/packages/f6/95/b8ba862968712caa12a19666175334fa979e1f198b896a430adb3bacfe87/py_key_value_aio-0.4.5-py3-none-any.whl", hash = "sha256:ab862adbcb8c72547d1c57821f22cbbb71ab86509039c96f36e914e0336c8dd7", size = 170005, upload-time = "2026-05-27T16:37:06.629Z" }, ] [package.optional-dependencies] @@ -8904,28 +9202,37 @@ memory = [ [[package]] name = "py-rust-stemmers" -version = "0.1.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/63/4fbc14810c32d2a884e2e94e406a7d5bf8eee53e1103f558433817230342/py_rust_stemmers-0.1.5.tar.gz", hash = "sha256:e9c310cfb5c2470d7c7c8a0484725965e7cab8b1237e106a0863d5741da3e1f7", size = 9388, upload-time = "2025-02-19T13:56:28.708Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/d1/e16b587dc0ebc42916b1caad994bc37fbb19ad2c7e3f5f3a586ba2630c16/py_rust_stemmers-0.1.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:910d87d39ba75da1fe3d65df88b926b4b454ada8d73893cbd36e258a8a648158", size = 272019, upload-time = "2025-02-19T13:55:10.268Z" }, - { url = "https://files.pythonhosted.org/packages/41/66/8777f125720acb896b336e6f8153e3ec39754563bc9b89523cfe06ba63da/py_rust_stemmers-0.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31ff4fb9417cec35907c18a6463e3d5a4941a5aa8401f77fbb4156b3ada69e3f", size = 310547, upload-time = "2025-02-19T13:55:11.521Z" }, - { url = "https://files.pythonhosted.org/packages/62/4c/c05c266ed74c063ae31dc5633ed63c48eb3b78034afcc80fe755d0cb09e7/py_rust_stemmers-0.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:804944eeb5c5559443d81f30c34d6e83c6292d72423f299e42f9d71b9d240941", size = 324420, upload-time = "2025-02-19T13:55:15.292Z" }, - { url = "https://files.pythonhosted.org/packages/7f/65/feb83af28095397466e6e031989ff760cc89b01e7da169e76d4cf16a2252/py_rust_stemmers-0.1.5-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c52c5c326de78c70cfc71813fa56818d1bd4894264820d037d2be0e805b477bd", size = 324791, upload-time = "2025-02-19T13:55:16.45Z" }, - { url = "https://files.pythonhosted.org/packages/20/3e/162be2f9c1c383e66e510218d9d4946c8a84ee92c64f6d836746540e915f/py_rust_stemmers-0.1.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8f374c0f26ef35fb87212686add8dff394bcd9a1364f14ce40fe11504e25e30", size = 488014, upload-time = "2025-02-19T13:55:18.486Z" }, - { url = "https://files.pythonhosted.org/packages/7b/31/2a48960a072e54d7cc244204d98854d201078e1bb5c68a7843a3f6d21ced/py_rust_stemmers-0.1.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85944262c248ea30444155638c9e148a3adc61fe51cf9a3705b4055b564ec95d", size = 493269, upload-time = "2025-02-19T13:55:21.532Z" }, - { 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/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/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/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/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" }, +version = "0.1.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/c1/9763f9fb1cd73f9c317a83feeed6e0d4af320c6bbddab47b4a94f3a47d0c/py_rust_stemmers-0.1.8.tar.gz", hash = "sha256:6b0f6f48bc54d607aed802de872fcd5a71bae969a6760976dc78ce55e8eaf3da", size = 9732, upload-time = "2026-05-22T11:00:24.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/0a/c88c9a7b5c94acc1175a33964637aff9cf8fa4c2e595846ab1df04c1f0bf/py_rust_stemmers-0.1.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1686fc009869ff8bcc1d5a305f071eeb8c3b3612a9827bcadd4e61fdb5727179", size = 275775, upload-time = "2026-05-22T10:59:32.979Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e2/e685cd31655a1ac56ebe0d571d221c199b1971eb5a2fdad88c889dc25983/py_rust_stemmers-0.1.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:769f37882905da2311cb720681b112eb70a4e6bd56fb424d473427b5379c8396", size = 314523, upload-time = "2026-05-22T10:59:34.436Z" }, + { url = "https://files.pythonhosted.org/packages/65/93/a6c0f30109c259199ac171cb6a0c69addefdba454ee0a8d51bb94e767c11/py_rust_stemmers-0.1.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3007ad4ec51e0c352ae410234a24a9ac75fab0c1e06c585fbac9fcced69385f8", size = 318808, upload-time = "2026-05-22T10:59:35.719Z" }, + { url = "https://files.pythonhosted.org/packages/59/87/ecaffed03e4b78d35ffb44740ca779e57d9f49d7d764f3f56b633b1e1c8c/py_rust_stemmers-0.1.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a1e11d22a240318dc917266eb3c85919455b6ea834445b95997712d9ede6b93", size = 319990, upload-time = "2026-05-22T10:59:36.84Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0d/2976bb288240e25110be687e6be5ecb0623a17f667f186e07033e429985f/py_rust_stemmers-0.1.8-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:08c258deab6d994551a92e9468ce88e58f97e636e73d9c5763978a57d7675a13", size = 320291, upload-time = "2026-05-22T10:59:38.263Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fb/7b1a93f63600633b2c741714f0f6024b2caff54e5aed77c5f6e0be384947/py_rust_stemmers-0.1.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eee4af7ada2ce9cb3ec59ffe8458148c3933a86507d816bf954ee506a0e45b61", size = 492171, upload-time = "2026-05-22T10:59:39.537Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3b/8e829e709542f928beb0613f4dffca4797a817f740c1be07eabd11bd2db4/py_rust_stemmers-0.1.8-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f16deb1557b8253d8c11693047bec4ed67d6b09ae0f84c8b896ea03ac2fc8925", size = 595398, upload-time = "2026-05-22T10:59:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/27/8b/b3972f0fc14e6bfc602a9260a1747742aaf86737ad57872998b085a2f1aa/py_rust_stemmers-0.1.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:870afb2d1d4731bd2d74b715b34439b29734e4dc94c55342096f07669f7f9fa0", size = 537820, upload-time = "2026-05-22T10:59:42.307Z" }, + { url = "https://files.pythonhosted.org/packages/73/15/ae60b9010924adac465f418822d9c514690aba6846edd67b6e2b5c227745/py_rust_stemmers-0.1.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51d0042d2a92ef0f7048bfc06b6c2a02306af31ea47f09d24b34e4b7e63c4e80", size = 275449, upload-time = "2026-05-22T10:59:45.547Z" }, + { url = "https://files.pythonhosted.org/packages/ec/7c/94be8b932179823d66e0d2be03a94706132a7d16a640d5e5710de1cb1b8f/py_rust_stemmers-0.1.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d3d34094b9b6078a8ea6fe1c7044e5fd32f14e76c94818c5008f49ae075f08", size = 316676, upload-time = "2026-05-22T10:59:46.522Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a4/8bd5c9f31207136830457d819e3f98bb21c54c0cdc40d6f1845ce4efdf7c/py_rust_stemmers-0.1.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:40c86be90cee4a709ad84fde4db7f11ca44d65630a56b77ec86fe84c23adfc09", size = 319458, upload-time = "2026-05-22T10:59:47.914Z" }, + { url = "https://files.pythonhosted.org/packages/f9/95/95da2b353b164a3a2b8a1c799866a58060693be4f1dc21065663dc67dc17/py_rust_stemmers-0.1.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:515884bcfb47b10335146648f276930d0c1201ae5e8b7b400fb46d8ea05c0ec2", size = 323541, upload-time = "2026-05-22T10:59:48.894Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ce/f34403b68808519dfa3220e1d94a40f26d5025f27e28893e2388ab9cfde5/py_rust_stemmers-0.1.8-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:fa42f5f8feb694aaaa869eedf477fcaf66f67a192cd64d94302d06920c33864a", size = 323873, upload-time = "2026-05-22T10:59:49.872Z" }, + { url = "https://files.pythonhosted.org/packages/57/01/fb8527f6474d576975415405c985a97260e0403829e062103d334230b7d2/py_rust_stemmers-0.1.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e86ad68fe297a6652f0f0390625ea81858b6f27862fd4c5ee1214bf5af29b9d", size = 494761, upload-time = "2026-05-22T10:59:51.021Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ac/73816237dbec20a7299abf901e2f7b6061d238754e033b48e423603f5336/py_rust_stemmers-0.1.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4b90fc81411943b114e8eb4988a876ba3b12bd2d20741559803eddc4131575dc", size = 596141, upload-time = "2026-05-22T10:59:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/52/0a/dd48debf386a206ee1c6ad75a0827eac89428441291c90d98bc3803fccf1/py_rust_stemmers-0.1.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56cc2c2df742fa6529285b7d204720f34b7da789ed78eb578442f93c6de97d89", size = 541633, upload-time = "2026-05-22T10:59:53.18Z" }, + { url = "https://files.pythonhosted.org/packages/c9/46/21d784a3f1db6a23051ffd5826d8ee667d26a64587c1cfbda0443ed87fff/py_rust_stemmers-0.1.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6c92733b020534470ca5a0d7fe8b85c85622ff383d4f37fec75a1c677aa84921", size = 275628, upload-time = "2026-05-22T10:59:56.687Z" }, + { url = "https://files.pythonhosted.org/packages/57/d5/701c73a4f6a7fecfd96a6588f0cafe98d6b0acde93adf8a2e45535f3d1d5/py_rust_stemmers-0.1.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ab605a86c950ba7e8ab1392cf91296c0bec3084babb897a4aecf90a10c82395", size = 316656, upload-time = "2026-05-22T10:59:57.67Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0d/c58fe98153cfdb6abf4dfb6ac335c923000d4af4e736080c3a3045b7aea7/py_rust_stemmers-0.1.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:21ed8055cec1f78d666afad8ffd7a51775ba419d2c615b8a1df7b32ca7f33e2b", size = 319377, upload-time = "2026-05-22T10:59:58.664Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d7/e60d04849e90aa3ad457211cc4999c30401f433341f9a5588c12b81f9877/py_rust_stemmers-0.1.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae773e1d01e9aa328d175f461475d0cd7074a82bfcc71de6dc5765e51f1cc9f7", size = 323719, upload-time = "2026-05-22T10:59:59.845Z" }, + { url = "https://files.pythonhosted.org/packages/6a/48/c0e4fb955db784cc354e0756354602f7043ff4c10fcbd9d901a2f8fe3239/py_rust_stemmers-0.1.8-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5cc8fab9d0f1b274a26935a632362b8278f03e81b65e8b8644d5ca3f62a5a1a4", size = 324110, upload-time = "2026-05-22T11:00:01.26Z" }, + { url = "https://files.pythonhosted.org/packages/48/eb/981b26baff37cf7a26ee206763cc4d2fb3e1db8f0f86ec030074431fae05/py_rust_stemmers-0.1.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35570098da02eb439afcd7270a12bf850bbe874b85cb912e0fb2d87a6e703920", size = 494645, upload-time = "2026-05-22T11:00:02.737Z" }, + { url = "https://files.pythonhosted.org/packages/6d/af/f16e805b7aefc2257b192b83a89300c8360b0fdffd3dfefa92dee4ec9b15/py_rust_stemmers-0.1.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0a68745d4b3c7f5abc778ca967e8711df6154873abcfe4e62a6631fa2363cc32", size = 596124, upload-time = "2026-05-22T11:00:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/76/8c/e7a2c940ba00e0792ae346aed5e755d51d37cf6d6853f6b141e5380e285d/py_rust_stemmers-0.1.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7cc0cc0b8eb45d2158c28ea43e2f338c110aad63052ad3bd00bc7446a595e12f", size = 541771, upload-time = "2026-05-22T11:00:06.081Z" }, + { url = "https://files.pythonhosted.org/packages/76/fe/04436ffe3aa4c02a40500835fc1a80d52375c738aa7ef66ebe0c4ccc2900/py_rust_stemmers-0.1.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:234fdcb58f4d907877ed03c9358668a149b5a66d096abcf43c324a4f5697d36d", size = 276111, upload-time = "2026-05-22T11:00:21.026Z" }, + { url = "https://files.pythonhosted.org/packages/45/24/6b32c86dd4eecdc309bfe6c15529a11e90b1e2c7af015366498c14e925f7/py_rust_stemmers-0.1.8-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dca0ae40715238582d6f1824b61d09ea3982359a061b69798ab5732b3ba0d4c5", size = 314816, upload-time = "2026-05-22T11:00:22.207Z" }, + { url = "https://files.pythonhosted.org/packages/22/78/3bf351dbcc7f51eb03a506c0bcf8aead8b1401cf26aaa1328968471531aa/py_rust_stemmers-0.1.8-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfc185b599e646a0e39d11df3f5e6d15edefb110496601556385d33b55fed5de", size = 320180, upload-time = "2026-05-22T11:00:23.387Z" }, ] [[package]] @@ -8997,7 +9304,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types", 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')" }, @@ -9005,9 +9312,9 @@ dependencies = [ { name = "typing-extensions", 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 = "typing-inspection", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [package.optional-dependencies] @@ -9017,38 +9324,54 @@ email = [ [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { 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/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/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/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/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/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, ] [[package]] @@ -9066,15 +9389,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.8.1" +version = "2.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { 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-dotenv", 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 = "typing-inspection", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550, upload-time = "2025-02-27T10:10:32.338Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839, upload-time = "2025-02-27T10:10:30.711Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, ] [[package]] @@ -9097,11 +9421,11 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.12.1" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, ] [package.optional-dependencies] @@ -9209,15 +9533,15 @@ wheels = [ [[package]] name = "pytest-asyncio" -version = "1.3.0" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest", 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 = "typing-extensions", marker = "(python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, ] [[package]] @@ -9236,14 +9560,15 @@ wheels = [ [[package]] name = "pytest-env" -version = "1.2.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest", 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-dotenv", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/12/9c87d0ca45d5992473208bcef2828169fa7d39b8d7fc6e3401f5c08b8bf7/pytest_env-1.2.0.tar.gz", hash = "sha256:475e2ebe8626cee01f491f304a74b12137742397d6c784ea4bc258f069232b80", size = 8973, upload-time = "2025-10-09T19:15:47.42Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/69/4db1c30625af0621df8dbe73797b38b6d1b04e15d021dd5d26a6d297f78c/pytest_env-1.6.0.tar.gz", hash = "sha256:ac02d6fba16af54d61e311dd70a3c61024a4e966881ea844affc3c8f0bf207d3", size = 16163, upload-time = "2026-03-12T22:39:43.78Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/98/822b924a4a3eb58aacba84444c7439fce32680592f394de26af9c76e2569/pytest_env-1.2.0-py3-none-any.whl", hash = "sha256:d7e5b7198f9b83c795377c09feefa45d56083834e60d04767efd64819fc9da00", size = 6251, upload-time = "2025-10-09T19:15:46.077Z" }, + { url = "https://files.pythonhosted.org/packages/27/16/ad52f56b96d851a2bcfdc1e754c3531341885bd7177a128c13ff2ca72ab4/pytest_env-1.6.0-py3-none-any.whl", hash = "sha256:1e7f8a62215e5885835daaed694de8657c908505b964ec8097a7ce77b403d9a3", size = 10400, upload-time = "2026-03-12T22:39:41.887Z" }, ] [[package]] @@ -9272,15 +9597,15 @@ wheels = [ [[package]] name = "pytest-rerunfailures" -version = "16.1" +version = "16.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging", 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 = "pytest", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/04/71e9520551fc8fe2cf5c1a1842e4e600265b0815f2016b7c27ec85688682/pytest_rerunfailures-16.1.tar.gz", hash = "sha256:c38b266db8a808953ebd71ac25c381cb1981a78ff9340a14bcb9f1b9bff1899e", size = 30889, upload-time = "2025-10-10T07:06:01.238Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/f0/74f8e685be7ecd1572c1256132f18fce3a665d7e07649a3f23b7eb2d3bec/pytest_rerunfailures-16.3.tar.gz", hash = "sha256:37c9b1231c8083e9f4e724f50f7a21241822f9516c15c700ebbf218d6452355c", size = 34148, upload-time = "2026-05-22T06:51:22.292Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/54/60eabb34445e3db3d3d874dc1dfa72751bfec3265bd611cb13c8b290adea/pytest_rerunfailures-16.1-py3-none-any.whl", hash = "sha256:5d11b12c0ca9a1665b5054052fcc1084f8deadd9328962745ef6b04e26382e86", size = 14093, upload-time = "2025-10-10T07:06:00.019Z" }, + { url = "https://files.pythonhosted.org/packages/f8/98/58a71d68d3126d7f6a6ed1944c37ec207a4ff3dc66cad3bed7b59d38df61/pytest_rerunfailures-16.3-py3-none-any.whl", hash = "sha256:6bdfb8ffb46c46072e6c16bdedee38b6c13eac620d9415ed5b63152cbf283170", size = 15396, upload-time = "2026-05-22T06:51:20.547Z" }, ] [[package]] @@ -9350,15 +9675,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.2.1" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock", 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 = "platformdirs", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/88/815e53084c5079a59df912825a279f41dd2e0df82281770eadc732f5352c/python_discovery-1.2.1.tar.gz", hash = "sha256:180c4d114bff1c32462537eac5d6a332b768242b76b69c0259c7d14b1b680c9e", size = 58457, upload-time = "2026-03-26T22:30:44.496Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/12/38c1a0b1e64806780c9563e3fc9f6e472251839662587cfbe9bfaf2ae10a/python_discovery-1.4.0.tar.gz", hash = "sha256:eb8bc7daad3c226c147e45bb4e970a1feb1bf4048ee178e6db59e197b8010ce3", size = 68455, upload-time = "2026-05-28T01:15:37.639Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl", hash = "sha256:b6a957b24c1cd79252484d3566d1b49527581d46e789aaf43181005e56201502", size = 31674, upload-time = "2026-03-26T22:30:43.396Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8d/3d316429f65029532bb1e28ff77b797d86b5ac3915bb44ca4e19aa283d43/python_discovery-1.4.0-py3-none-any.whl", hash = "sha256:26ed78d703e234879a66244c7d4114563fb13ec5cd30a2d1357e5fb4850782da", size = 33217, upload-time = "2026-05-28T01:15:36.573Z" }, ] [[package]] @@ -9424,11 +9749,11 @@ wheels = [ [[package]] name = "pytz" -version = "2026.1.post1" +version = "2026.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, + { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, ] [[package]] @@ -9439,16 +9764,19 @@ sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd77 wheels = [ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, { url = "https://files.pythonhosted.org/packages/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" }, @@ -9462,11 +9790,13 @@ 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" }, @@ -9533,16 +9863,16 @@ wheels = [ [[package]] name = "referencing" -version = "0.36.2" +version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs", 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 = "rpds-py", 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 = "typing-extensions", marker = "(python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] [[package]] @@ -9554,32 +9884,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/dc/c1f2df4027e82fc54b5a473e4b250f5139faca49a0fbe29a48668d228f34/regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48", size = 489445, upload-time = "2026-05-09T23:12:06.111Z" }, { url = "https://files.pythonhosted.org/packages/58/b6/14b2c84ff90ddb370c81d27503f4a0fcf071496416f4855f6cc8c5d81c35/regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555", size = 289212, upload-time = "2026-05-09T23:12:09.266Z" }, { url = "https://files.pythonhosted.org/packages/03/d0/4db86529117320de0c84afd90e70bb47434625875e34fcef9d8c127c5b16/regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919", size = 792310, upload-time = "2026-05-09T23:12:11.416Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/fe4800cd322f862ecffd2d553409b20d80650e5ed71b9d178f853d020b82/regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451", size = 861721, upload-time = "2026-05-09T23:12:13.681Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d0/b3618a895dd8feb897c61bb2954edd265e1767d82a01d53065d5871127a3/regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c", size = 906460, upload-time = "2026-05-09T23:12:15.443Z" }, { url = "https://files.pythonhosted.org/packages/33/6f/1481597e859ef19508b345eec4afd1416ed6e6b459c75a64026ef193aecf/regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc", size = 799843, upload-time = "2026-05-09T23:12:16.892Z" }, + { url = "https://files.pythonhosted.org/packages/73/59/955734c803f59108deccba3597ae440c76b62a652733c0006e6243758420/regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d", size = 773610, upload-time = "2026-05-09T23:12:19.127Z" }, { url = "https://files.pythonhosted.org/packages/68/8f/70c04a236d651c81881dac42ef8538bddda6121434509d0a22d9e601503b/regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9", size = 781645, upload-time = "2026-05-09T23:12:20.806Z" }, + { url = "https://files.pythonhosted.org/packages/1d/96/05c7434d88185e5d27fe54aeb74df86bd77cd79f52f0b4eae54faa8fea70/regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2", size = 854473, upload-time = "2026-05-09T23:12:22.465Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/6e3d8202d981f3117004bf341ee74893ba4ba8a9fbaf4b94615846550a08/regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf", size = 763311, upload-time = "2026-05-09T23:12:24.351Z" }, + { url = "https://files.pythonhosted.org/packages/93/c7/e7737f1526b3fb32bd4c337fd6c71c3ebb5c8296fc34d11197e0955d2e35/regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611", size = 844593, upload-time = "2026-05-09T23:12:26.341Z" }, { url = "https://files.pythonhosted.org/packages/a5/27/0daffb1a535bb39f422c3d200f4ab023c71110ad66a32b366bee708baba0/regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c", size = 789167, upload-time = "2026-05-09T23:12:27.975Z" }, { 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" }, ] [[package]] name = "requests" -version = "2.33.1" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi", 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')" }, @@ -9587,9 +9941,9 @@ dependencies = [ { name = "idna", 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 = "urllib3", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] @@ -9619,28 +9973,28 @@ wheels = [ [[package]] name = "responses" -version = "0.26.0" +version = "0.26.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { 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 = "requests", 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 = "urllib3", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/b4/b7e040379838cc71bf5aabdb26998dfbe5ee73904c92c1c161faf5de8866/responses-0.26.0.tar.gz", hash = "sha256:c7f6923e6343ef3682816ba421c006626777893cb0d5e1434f674b649bac9eb4", size = 81303, upload-time = "2026-02-19T14:38:05.574Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/58/1fb6de3503428196df78638f991ec8095274f1ee9723e272ee4d9ff0092b/responses-0.26.1.tar.gz", hash = "sha256:2eb3218553cc8f79b57d257bac23af5e1bf381f5b9390b1767816f0843e01dc2", size = 83088, upload-time = "2026-05-21T19:56:39.747Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/04/7f73d05b556da048923e31a0cc878f03be7c5425ed1f268082255c75d872/responses-0.26.0-py3-none-any.whl", hash = "sha256:03ec4409088cd5c66b71ecbbbd27fe2c58ddfad801c66203457b3e6a04868c37", size = 35099, upload-time = "2026-02-19T14:38:03.847Z" }, + { url = "https://files.pythonhosted.org/packages/3a/31/6a620b4427d546b9e7cca8b3b8c5f0559d9cef2bb9eedcda7f73c1473c19/responses-0.26.1-py3-none-any.whl", hash = "sha256:8aacc4586eb08fb2208ef64a9eb4258d9b0c6e6f4260845f2f018ab847495345", size = 35502, upload-time = "2026-05-21T19:56:38.046Z" }, ] [[package]] name = "respx" -version = "0.22.0" +version = "0.23.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/98/4e55c9c486404ec12373708d015ebce157966965a5ebe7f28ff2c784d41b/respx-0.23.1.tar.gz", hash = "sha256:242dcc6ce6b5b9bf621f5870c82a63997e8e82bc7c947f9ffe272b8f3dd5a780", size = 29243, upload-time = "2026-04-08T14:37:16.008Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, + { url = "https://files.pythonhosted.org/packages/1d/4a/221da6ca167db45693d8d26c7dc79ccfc978a440251bf6721c9aaf251ac0/respx-0.23.1-py2.py3-none-any.whl", hash = "sha256:b18004b029935384bccfa6d7d9d74b4ec9af73a081cc28600fffc0447f4b8c1a", size = 25557, upload-time = "2026-04-08T14:37:14.613Z" }, ] [[package]] @@ -9664,56 +10018,68 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, ] +[[package]] +name = "rfc3987-syntax" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lark", 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')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, +] + [[package]] name = "rich" -version = "14.3.3" +version = "14.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", 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 = "pygments", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/67/cae617f1351490c25a4b8ac3b8b63a4dda609295d8222bad12242dfdc629/rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9", size = 230524, upload-time = "2026-04-11T02:57:45.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" }, ] [[package]] name = "rich-argparse" -version = "1.7.2" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "rich", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/f7/1c65e0245d4c7009a87ac92908294a66e7e7635eccf76a68550f40c6df80/rich_argparse-1.7.2.tar.gz", hash = "sha256:64fd2e948fc96e8a1a06e0e72c111c2ce7f3af74126d75c0f5f63926e7289cd1", size = 38500, upload-time = "2025-11-01T10:35:44.232Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/e5/1064c43203a357d668cd42435f7a15fe6af51512d85b2104fecb937aa861/rich_argparse-1.8.0.tar.gz", hash = "sha256:679df3d832fa94ad6e4bdb07ded088cd7ea2dddc58ae9b2b46346a40b06cbc0c", size = 38940, upload-time = "2026-05-01T15:18:43.604Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/80/97b6f357ac458d9ad9872cc3183ca09ef7439ac89e030ea43053ba1294b6/rich_argparse-1.7.2-py3-none-any.whl", hash = "sha256:0559b1f47a19bbeb82bf15f95a057f99bcbbc98385532f57937f9fc57acc501a", size = 25476, upload-time = "2025-11-01T10:35:42.681Z" }, + { url = "https://files.pythonhosted.org/packages/0b/35/1cceccc5fcb50fa2ed53e2aa278cd032f3902682a73e763fb1ac3be8e6fa/rich_argparse-1.8.0-py3-none-any.whl", hash = "sha256:d2a3ce7854654e2253c578763ab0a32f05016f23a55fadba7b9a91b6c0e92142", size = 25616, upload-time = "2026-05-01T15:18:42.395Z" }, ] [[package]] name = "rich-rst" -version = "1.3.2" +version = "2.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils", 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 = "pygments", 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 = "rich", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/56/3191bae66b08ccc637ea8120426068bcb361cc323c96404c310886937067/rich_rst-2.0.1.tar.gz", hash = "sha256:cbe236ed0901d1ec8427cc6a50bf0a34353ba28ad014dc24def68bfe7f3b9e68", size = 300570, upload-time = "2026-05-16T00:47:57.362Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3d/55c17d3ebdf3cd81356002afe5bef9bb8af631db2819785b6eac845b925b/rich_rst-2.0.1-py3-none-any.whl", hash = "sha256:7ee15f345ce25fa02b582c272a6cdbaf0c21243e38061cea273cff659bf3ef61", size = 272922, upload-time = "2026-05-16T00:47:55.508Z" }, ] [[package]] name = "rich-toolkit" -version = "0.19.7" +version = "0.20.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", 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 = "rich", 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 = "typing-extensions", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/ba/dae9e3096651042754da419a4042bc1c75e07d615f9b15066d738838e4df/rich_toolkit-0.19.7.tar.gz", hash = "sha256:133c0915872da91d4c25d85342d5ec1dfacc69b63448af1a08a0d4b4f23ef46e", size = 195877, upload-time = "2026-02-24T16:06:20.555Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/63/3e427c62f1992945c997d4ec31e2fcb37d26aadbe5aa44ae5b29f7f64d26/rich_toolkit-0.20.1.tar.gz", hash = "sha256:c7336ae281f435c785acecaedc4b71d4b663dc73d9c8079fea96372527e822a4", size = 203473, upload-time = "2026-06-05T08:56:57.679Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/3c/c923619f6d2f5fafcc96fec0aaf9550a46cd5b6481f06e0c6b66a2a4fed0/rich_toolkit-0.19.7-py3-none-any.whl", hash = "sha256:0288e9203728c47c5a4eb60fd2f0692d9df7455a65901ab6f898437a2ba5989d", size = 32963, upload-time = "2026-02-24T16:06:22.066Z" }, + { url = "https://files.pythonhosted.org/packages/00/88/309f07d08155da2ba1d5ceb42d270fb42fbe34a807684543e3ffc10fe713/rich_toolkit-0.20.1-py3-none-any.whl", hash = "sha256:2a6d5f8e15759b9eba5a9ee63da10b275359ead20e5a0fc92bd5b4dbae8ce4bf", size = 35525, upload-time = "2026-06-05T08:56:58.586Z" }, ] [[package]] @@ -9724,23 +10090,39 @@ sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b wheels = [ { url = "https://files.pythonhosted.org/packages/52/66/ba7f561b6062402022887706a7f2b2c2e2e2a28f1e3839202b0a2f77e36d/rignore-0.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182f4e5e4064d947c756819446a7d4cdede8e756b8c81cf9e509683fe38778d7", size = 823882, upload-time = "2025-11-05T20:42:23.488Z" }, { url = "https://files.pythonhosted.org/packages/f5/81/4087453df35a90b07370647b19017029324950c1b9137d54bf1f33843f17/rignore-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16b63047648a916a87be1e51bb5c009063f1b8b6f5afe4f04f875525507e63dc", size = 899362, upload-time = "2025-11-05T20:40:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c9/390a8fdfabb76d71416be773bd9f162977bd483084f68daf19da1dec88a6/rignore-0.7.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba5524f5178deca4d7695e936604ebc742acb8958f9395776e1fcb8133f8257a", size = 873633, upload-time = "2025-11-05T20:41:06.193Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/79404fcb0faa76edfbc9df0901f8ef18568d1104919ebbbad6d608c888d1/rignore-0.7.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62020dbb89a1dd4b84ab3d60547b3b2eb2723641d5fb198463643f71eaaed57d", size = 1167633, upload-time = "2025-11-05T20:41:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/b3466d32d445d158a0aceb80919085baaae495b1f540fb942f91d93b5e5b/rignore-0.7.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b34acd532769d5a6f153a52a98dcb81615c949ab11697ce26b2eb776af2e174d", size = 941434, upload-time = "2025-11-05T20:41:38.151Z" }, { url = "https://files.pythonhosted.org/packages/e8/40/9cd949761a7af5bc27022a939c91ff622d29c7a0b66d0c13a863097dde2d/rignore-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c5e53b752f9de44dff7b3be3c98455ce3bf88e69d6dc0cf4f213346c5e3416c", size = 959461, upload-time = "2025-11-05T20:42:08.476Z" }, { url = "https://files.pythonhosted.org/packages/6c/31/1ecff992fc3f59c4fcdcb6c07d5f6c1e6dfb55ccda19c083aca9d86fa1c6/rignore-0.7.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e01cad2b0b92f6b1993f29fc01f23f2d78caf4bf93b11096d28e9d578eb08ce", size = 1079173, upload-time = "2025-11-05T21:40:12.007Z" }, + { url = "https://files.pythonhosted.org/packages/17/18/162eedadb4c2282fa4c521700dbf93c9b14b8842e8354f7d72b445b8d593/rignore-0.7.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5991e46ab9b4868334c9e372ab0892b0150f3f586ff2b1e314272caeb38aaedb", size = 1139012, upload-time = "2025-11-05T21:40:29.399Z" }, { url = "https://files.pythonhosted.org/packages/9f/22/1c1a65047df864def9a047dbb40bc0b580b8289a4280e62779cd61ae21f2/rignore-0.7.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aaf938530dcc0b47c4cfa52807aa2e5bfd5ca6d57a621125fe293098692f6345", size = 1128182, upload-time = "2025-11-05T21:41:04.239Z" }, { 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" }, { url = "https://files.pythonhosted.org/packages/35/af/c69c0c51b8f9f7914d95c4ea91c29a2ac067572048cae95dd6d2efdbe05d/rignore-0.7.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:392dcabfecbe176c9ebbcb40d85a5e86a5989559c4f988c2741da7daf1b5be25", size = 825976, upload-time = "2025-11-05T20:42:35.118Z" }, { url = "https://files.pythonhosted.org/packages/f1/d2/1b264f56132264ea609d3213ab603d6a27016b19559a1a1ede1a66a03dcd/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22baa462abdc36fdd5a5e2dae423107723351b85ff093762f9261148b9d0a04a", size = 899739, upload-time = "2025-11-05T20:41:01.518Z" }, + { url = "https://files.pythonhosted.org/packages/55/e4/b3c5dfdd8d8a10741dfe7199ef45d19a0e42d0c13aa377c83bd6caf65d90/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53fb28882d2538cb2d231972146c4927a9d9455e62b209f85d634408c4103538", size = 874843, upload-time = "2025-11-05T20:41:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/10/d6f3750233881a2a154cefc9a6a0a9b19da526b19f7f08221b552c6f827d/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87409f7eeb1103d6b77f3472a3a0d9a5953e3ae804a55080bdcb0120ee43995b", size = 1170348, upload-time = "2025-11-05T20:41:34.21Z" }, + { url = "https://files.pythonhosted.org/packages/6e/10/ad98ca05c9771c15af734cee18114a3c280914b6e34fde9ffea2e61e88aa/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684014e42e4341ab3ea23a203551857fcc03a7f8ae96ca3aefb824663f55db32", size = 942315, upload-time = "2025-11-05T20:41:48.508Z" }, { url = "https://files.pythonhosted.org/packages/de/00/ab5c0f872acb60d534e687e629c17e0896c62da9b389c66d3aa16b817aa8/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77356ebb01ba13f8a425c3d30fcad40e57719c0e37670d022d560884a30e4767", size = 961047, upload-time = "2025-11-05T20:42:19.403Z" }, { url = "https://files.pythonhosted.org/packages/33/b8/133aa4002cee0ebbb39362f94e4898eec7fbd09cec9fcbce1cd65b355b7f/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2673225dcec7f90497e79438c35e34638d0d0391ccea3cbb79bfb9adc0dc5bd7", size = 1079656, upload-time = "2025-11-05T21:40:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/67/56/36d5d34210e5e7dfcd134eed8335b19e80ae940ee758f493e4f2b344dd70/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:c081f17290d8a2b96052b79207622aa635686ea39d502b976836384ede3d303c", size = 1139789, upload-time = "2025-11-05T21:40:42.119Z" }, { url = "https://files.pythonhosted.org/packages/ce/8b/a1299085b28a2f6135e30370b126e3c5055b61908622f2488ade67641479/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:d8955b57e42f2a5434670d5aa7b75eaf6e74602ccd8955dddf7045379cd762fb", size = 1129444, upload-time = "2025-11-05T21:41:17.906Z" }, ] @@ -9767,35 +10149,55 @@ sdist = { url = "https://files.pythonhosted.org/packages/e2/c5/9136736c37022a6ad [[package]] name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, - { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, - { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, - { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, - { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, - { url = "https://files.pythonhosted.org/packages/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/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/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/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/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/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/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" }, - { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, - { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, - { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, - { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +version = "2026.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/95/f8203fd997484b1690a6869cd0e503b6c3c6be55b0ecc36d1a491fe742f0/rpds_py-2026.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:99ab6ba7bfa2cb0f96a04e3652355bf04e3f51aceb1e943b8541dab7ba4828cc", size = 348460, upload-time = "2026-05-28T11:58:52.374Z" }, + { url = "https://files.pythonhosted.org/packages/33/8c/b47326ad2f0be545a5e5c1a55937a12afaea7d392ba2837bb9680f57e6c9/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0efbe45632665e53e3db8fe1e5692db58fc5cb9bab4459d570b83efefe11164", size = 381031, upload-time = "2026-05-28T11:58:53.775Z" }, + { url = "https://files.pythonhosted.org/packages/22/0b/e83bbd97ffac6f6389b605cd4e1c8ac5761dc7e977769c9255d8c5adb7bd/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:01d17b29c0c23d82b1f4751147ec49cf451f1fc2554eb9ef5f957e55d2656ead", size = 387121, upload-time = "2026-05-28T11:58:55.243Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0e/d285d1bc8864245919c61e1ca82263e4a66d337759c3a4cef72766ff9afc/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7559f72b94ae52659086c595dfa017cde03155f7832071d30959049052cb3ece", size = 501026, upload-time = "2026-05-28T11:58:56.788Z" }, + { url = "https://files.pythonhosted.org/packages/86/06/ccb2109a1e543437b5e43816f2b43b9554cc6783145528a4e3711e05c011/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e25b7088f9ccbfc0dfcaa52bf969300ca229e10ecf758974ebcbb080a4b37bb", size = 391865, upload-time = "2026-05-28T11:58:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/3d/33/237173db1cfef10105b3839a24de00eb8d2a523711add4632447cdf0aedd/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613fc4ee9eaef26dc5840666214dd6fbcebcf32f46e76f4abc473059f4e13dda", size = 378012, upload-time = "2026-05-28T11:58:59.589Z" }, + { url = "https://files.pythonhosted.org/packages/97/64/1eae54e34d5161f9969295e80bd6b62a55f2b6ac5f2a5b60d02c2140e758/rpds_py-2026.5.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:85264a90ff4c05c1568dd65f5921c837614b67c60358fb4c17df3b7f2e90690a", size = 391111, upload-time = "2026-05-28T11:59:01.104Z" }, + { url = "https://files.pythonhosted.org/packages/16/0f/007ec21283b5b040b4ec3bd95e0402591e22bfa7d5c93dfe01c465c2d2d7/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05fa4f41f37ec97c9c260441a940450a192f78d774d2b097eee1379f1e1246a", size = 556487, upload-time = "2026-05-28T11:59:04.012Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d5/9937dce4d6bda74157b954e7d1460db05a22f5929dccfeeba1ed27a93df0/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8895840ac4809e5f60c88fd07617cd71326e73d6e5a8aa783c5c0f7c24985de2", size = 584053, upload-time = "2026-05-28T11:59:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/a3/43/35e3f136343aef451e545ce8c38d36c2f93c0ed88703db8b64ba2b205c68/rpds_py-2026.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c", size = 345775, upload-time = "2026-05-28T11:59:13.827Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/0f2160c5982d3157734d5cb3ed63d8b2d583a73c9864f77b666449f32cf8/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08", size = 376329, upload-time = "2026-05-28T11:59:15.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/11/ee0ba42aff83bf4effdbc576673c6be64c5e173978c3f6d537e94482f77d/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb", size = 383539, upload-time = "2026-05-28T11:59:16.665Z" }, + { url = "https://files.pythonhosted.org/packages/11/df/d94aa6a499d4ac40afe2d7620f2c597fd3c0f182e854ad7cf3f596a81cb6/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1", size = 494674, upload-time = "2026-05-28T11:59:17.991Z" }, + { url = "https://files.pythonhosted.org/packages/1f/75/33d30f43bb2f458de11979486a591b1bf6e5651765ed1704c6197c2dc773/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5", size = 389268, upload-time = "2026-05-28T11:59:19.434Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1e/2c9096fc19d5fd084b0184ca2b651e659aa0a37e6fdbecf6ece47f147fe1/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644", size = 376280, upload-time = "2026-05-28T11:59:21Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e5/61ec9f8be8211ea7f48448195549e4aaf02004083475493b0e137702ecb2/rpds_py-2026.5.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4", size = 387233, upload-time = "2026-05-28T11:59:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/72/e6/4d5718c5cf26c522dc7c9999e238da1e77380b81d0c5d1df11e271ddfeb1/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4", size = 553113, upload-time = "2026-05-28T11:59:25.184Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c1/7d4c26f167f8c41501cc073d30ee22082b16ce358cf5b00ec97cbc7804ea/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732", size = 582436, upload-time = "2026-05-28T11:59:28.11Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" }, + { url = "https://files.pythonhosted.org/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" }, + { url = "https://files.pythonhosted.org/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" }, + { url = "https://files.pythonhosted.org/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" }, + { url = "https://files.pythonhosted.org/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" }, + { url = "https://files.pythonhosted.org/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" }, + { url = "https://files.pythonhosted.org/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" }, + { url = "https://files.pythonhosted.org/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" }, + { url = "https://files.pythonhosted.org/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" }, + { url = "https://files.pythonhosted.org/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" }, + { url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/3eb9ccdb9f143b8c9b003978898cb497f942a324c077401e6b8834238e63/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad3773236e95f7f33991eb125224b7da66f206504d032a253a02da7e134519fb", size = 350195, upload-time = "2026-05-28T12:01:54.901Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/dbda232bc4f3ed732120692ab0d2c8402cb020516556d8bee622dcef2413/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04df86b3f0fade39ec8fd0e0aab089b1da9fbd2b48df778a57ef96f5e7d38df", size = 381850, upload-time = "2026-05-28T12:01:56.601Z" }, + { url = "https://files.pythonhosted.org/packages/40/30/32e769839a358f78810c234f160f2cc21d1e4e47e1c0e0e0d535be5a0219/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6142dbd80c4df62a5d899f0d616d417f84e0bc8d32526c8e5589019d75d028a7", size = 387899, upload-time = "2026-05-28T12:01:58.212Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/ec84d243aadb3b34b71dd26a010d0930b2d284ff5fc9a69fec53810ee6fd/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b35217adefe87f2fe4db7e9766cabe84744bfe9616d9667be18988928c7f2dc", size = 501618, upload-time = "2026-05-28T12:01:59.888Z" }, + { url = "https://files.pythonhosted.org/packages/74/25/b60e52686bbff777a64f9e4f4d3dd57980dc846913777177a2c92e4937aa/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b95d5e11fc712b752081183a55a244c03cd00570489edd7014d8899f8ceb8162", size = 394003, upload-time = "2026-05-28T12:02:01.482Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c7/b3a6a588cc2219510ef3f42e207483a93950bedd1e3a0fd4015c95cff9e5/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141c9498daf2ace9eda35d2b0e376f9ea8b058d84f2aef4f96fccfd449a2f251", size = 379778, upload-time = "2026-05-28T12:02:03.197Z" }, + { url = "https://files.pythonhosted.org/packages/31/00/c7dba3fc8a3da8cb3f6db1eb3386be4d79c2e97c6890d20eb9ac66ae8c43/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:6f249f8b860a200ad35193af961183ebe9132710484e6f6ce0cf89fd83c63a9a", size = 392359, upload-time = "2026-05-28T12:02:04.817Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6f/93831a3bfe789542ed0c1d0d74b78b440f055d6dc3ea4640eba2d95e6e23/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c74005a7bb87752acf351c93897ec63ad77a07a0da7ecad9c050e32e7286ba34", size = 557243, upload-time = "2026-05-28T12:02:08.013Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ea/e7b0251441da9adfeaebcf29601d10f2a1455fcf0772fae9e7e19032bd96/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8c43a8a973270fd173bf48cdf80bbe66312421cba68d40845034f174f2389049", size = 586326, upload-time = "2026-05-28T12:02:11.47Z" }, ] [[package]] @@ -9813,10 +10215,16 @@ 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" }, ] @@ -9851,44 +10259,20 @@ wheels = [ [[package]] name = "safetensors" -version = "0.8.0rc1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/a1/d46f642820c00443e86343b9548647a82c78f8818857652084852757f805/safetensors-0.8.0rc1.tar.gz", hash = "sha256:a4bacbcd2ab9efe4eb5f1ea44afc9ac5f3b40e103ebde146e370e885ba46f2fc", size = 325883, upload-time = "2026-06-01T09:54:53.914Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/5d/2aa9139997bf1681778b96188522480c862af0b4efd978eaae5171296105/safetensors-0.8.0rc1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ba66ebb7eaa5914ff41cd2b2cd7ccd22c844854be2a5179289e748c70ffa90a4", size = 484485, upload-time = "2026-06-01T09:54:46.523Z" }, - { url = "https://files.pythonhosted.org/packages/01/07/c78d1bb09f83885467d4472b14598efc933bd37e0de443f18d23758b0ff3/safetensors-0.8.0rc1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a72817e309ed17a6b805168bca500af711c8bf50cccf7bf790ad247788c676c", size = 503240, upload-time = "2026-06-01T09:54:37.393Z" }, - { url = "https://files.pythonhosted.org/packages/a8/1a/5ef0b186d4edda2ecdd1b4f4b384c03afa64ce17b4582c33329c32bad9ca/safetensors-0.8.0rc1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c945f1ec6fc5a04abc174e79bce69c4613ae536c5276a3812a2a89b62ae009d7", size = 511782, upload-time = "2026-06-01T09:54:38.903Z" }, - { url = "https://files.pythonhosted.org/packages/ac/30/699638f35a524de781ba16cc5c54f31430fd04387960b1d7dc0d5d5fa305/safetensors-0.8.0rc1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba0397739b71400eab5d1f492d68484595cf8b5d13dbfef274e3bcf518348bd8", size = 633524, upload-time = "2026-06-01T09:54:40.169Z" }, - { url = "https://files.pythonhosted.org/packages/4f/bc/12ecd32fec076e836c25e57ac991e09d26fd448d361228057d3d5b3e4d1b/safetensors-0.8.0rc1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f61b7d2c1babc6271d778138788c57c8a95898be6ad3b559971fce20ec1c9c23", size = 545342, upload-time = "2026-06-01T09:54:42.585Z" }, - { url = "https://files.pythonhosted.org/packages/0c/8f/23e0eca29cbaba9f89917e6ca2840f6a80bbcdf659a0e2cd59d311549b7b/safetensors-0.8.0rc1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53f59971f435fb5c23bb7bfa3c00e6cdbb26486d41f3aaf04c6d9e2d91bc8e4c", size = 516088, upload-time = "2026-06-01T09:54:45.356Z" }, - { url = "https://files.pythonhosted.org/packages/18/0b/490c76d05f9dd7041bdf29d72b43084ed1498dadebaca5955491e488f2be/safetensors-0.8.0rc1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:b070ba9428e6b2b3b820152b1e1583931cfbd692cda595442d5fbc8b5a46e824", size = 513718, upload-time = "2026-06-01T09:54:41.477Z" }, - { url = "https://files.pythonhosted.org/packages/73/37/1c2cd01fcf86703433bd2df7371f6d864449d31bfc0289d2575d9e4b001c/safetensors-0.8.0rc1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87aad206a0bb02fa3ddaeb2feb1c6f7f39a87bea1976750ba28a857fbfcd6b31", size = 678511, upload-time = "2026-06-01T09:54:48.861Z" }, - { url = "https://files.pythonhosted.org/packages/99/55/15c17603d8b575253431a574af4ffff447550a9ad6023f2ea643dad7af47/safetensors-0.8.0rc1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:6ea00be4f7066ba834064fd7b104ba4b10d2e42cd915c9ece31f0e2b42e6b6d2", size = 786778, upload-time = "2026-06-01T09:54:50.231Z" }, - { url = "https://files.pythonhosted.org/packages/9b/03/53ecffb459f5c58b8712e9fde57e8e2a011274eb7ec1f8de3db4641039a2/safetensors-0.8.0rc1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cf949f6a37287572de1c47294b36131bf49528436724eec2f96015f75a3d0bc8", size = 722435, upload-time = "2026-06-01T09:54:52.767Z" }, -] - -[[package]] -name = "scikit-learn" -version = "1.9.0" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "joblib", 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 = "narwhals", 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 = "numpy", 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 = "scipy", 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 = "threadpoolctl", 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')" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/e2/ff880f62677a17d035817d543cb0fc8727d01eccbee81c5f7fc733a9d856/scikit_learn-1.9.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f401448645a3e7bc115aa3c094097865155b34bff1cba8101857d9104e99074c", size = 8256782, upload-time = "2026-06-02T11:53:08.904Z" }, - { url = "https://files.pythonhosted.org/packages/25/64/eb40435e1a508ab1b4e284ce43ae80f6a162e5be5e38ed5a6fab467a9ea4/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd3a8ef0c758555a3b23c03adaa858af32f7736785ded50ad5991f59c4ed03fa", size = 8992419, upload-time = "2026-06-02T11:53:11.551Z" }, - { url = "https://files.pythonhosted.org/packages/8d/da/4810a28e473185429e45a57eebcc91fc991b33d889cc0676063e671db03d/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7e254636164090da847715a27f8e5478feb98c40a9e0ee90cbd277de9e5ceb8", size = 9281411, upload-time = "2026-06-02T11:53:15.063Z" }, - { url = "https://files.pythonhosted.org/packages/cc/d5/2b5148f2279196775e1db2aeb85d14b70ac80e7e32b3b28e7ebeafb0901d/scikit_learn-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5be45aa4a42a68a533913a6ed736cf309de2226411c79ef8d609a5456f1939b1", size = 8261512, upload-time = "2026-06-02T11:53:27.183Z" }, - { url = "https://files.pythonhosted.org/packages/a0/ee/5adbc77656b71f9456a2f5a7a9fdb4bcf9207a6b962889f1c2f9323afa4e/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e50ed4da51974e86e940690e9a3d82e729b62b5a49f7c9bac534d515d39d86f", size = 8837603, upload-time = "2026-06-02T11:53:30.328Z" }, - { url = "https://files.pythonhosted.org/packages/6c/c2/63fdda36c56437eeb44aaf9493c8bcd62ce230ab1598924fc626ffbfa943/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8", size = 9132097, upload-time = "2026-06-02T11:53:33.456Z" }, - { url = "https://files.pythonhosted.org/packages/3e/04/5acd7ae280c5f93b6ac5ef6cdec14eef4c8d1cd91d85b3292989c94d96b1/scikit_learn-1.9.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5b934c45c252844a91d69fda3a34cff5e7307e1db10d77cb10a3980312c74713", size = 8228299, upload-time = "2026-06-02T11:53:44.817Z" }, - { url = "https://files.pythonhosted.org/packages/0c/39/ffe829a5b8ecb40a518724a997794657fdc354ada5e8fe8e64d998c0bac9/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:38c3dcb9a1ffb85505ec53d54c7b4aea0cff70050425a7760c2af661ac85df05", size = 8789690, upload-time = "2026-06-02T11:53:47.461Z" }, - { url = "https://files.pythonhosted.org/packages/1f/88/8dab5de10c638c083772a6be83a3d8106ced492f74a928c8693638e5bb50/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da76d09304a4706db7cc1e3ebaa3b6b98a67365cc11d2996c4f1e58ba47df714", size = 9087723, upload-time = "2026-06-02T11:53:50.702Z" }, + { 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" }, ] [[package]] @@ -9969,25 +10353,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, ] -[[package]] -name = "sentence-transformers" -version = "5.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "huggingface-hub", 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 = "numpy", 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 = "scikit-learn", 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 = "scipy", 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 = "torch", 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 = "tqdm", 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 = "transformers", 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 = "typing-extensions", 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')" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/d4/7ef93157485e978c016f49da05363c1e4e7237beb5343b64b5631101f0f1/sentence_transformers-5.5.1.tar.gz", hash = "sha256:02b7740dfc60bdbbcb6061625f5d97a5c1a4e2d3baac5f9391b912bb5eae2290", size = 445161, upload-time = "2026-05-20T07:37:44.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/03/ee99a6b030e7a2e056547729f8a4709dd93e13d9c6f07590f74c395c4017/sentence_transformers-5.5.1-py3-none-any.whl", hash = "sha256:4fe11d433badc5282d32f7fc08bc714216b7a5aca426f9df77a45a554756deb7", size = 588887, upload-time = "2026-05-20T07:37:43.004Z" }, -] - [[package]] name = "sentencepiece" version = "0.2.1" @@ -10014,15 +10379,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.57.0" +version = "2.62.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi", 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 = "urllib3", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4f/87/46c0406d8b5ddd026f73adaf5ab75ce144219c41a4830b52df4b9ab55f7f/sentry_sdk-2.57.0.tar.gz", hash = "sha256:4be8d1e71c32fb27f79c577a337ac8912137bba4bcbc64a4ec1da4d6d8dc5199", size = 435288, upload-time = "2026-03-31T09:39:29.264Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/5d/a343201726150e05f2036eeb6e493e2e2f8bf8a66f5aa70f2f4ac96f9ca3/sentry_sdk-2.62.0.tar.gz", hash = "sha256:3c870b9f50d9fd15b58c817dbde1c7cfaa9fe3f05df0a4c6edd5571cb82f5491", size = 463986, upload-time = "2026-06-08T13:23:49.223Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/64/982e07b93219cb52e1cca5d272cb579e2f3eb001956c9e7a9a6d106c9473/sentry_sdk-2.57.0-py2.py3-none-any.whl", hash = "sha256:812c8bf5ff3d2f0e89c82f5ce80ab3a6423e102729c4706af7413fd1eb480585", size = 456489, upload-time = "2026-03-31T09:39:27.524Z" }, + { url = "https://files.pythonhosted.org/packages/3d/07/05440381627877aae223fd68f330df9b9fc6641d08bf65328b55235617a2/sentry_sdk-2.62.0-py3-none-any.whl", hash = "sha256:27f61d13a86c3c1648dec666dd5a64f79772dd6a84b446f11866601ecab24f6f", size = 490586, upload-time = "2026-06-08T13:23:47.486Z" }, ] [[package]] @@ -10072,14 +10437,14 @@ wheels = [ [[package]] name = "smart-open" -version = "7.0.5" +version = "7.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wrapt", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/d8/1481294b2d110b805c0f5d23ef34158b7d5d4283633c0d34c69ea89bb76b/smart_open-7.0.5.tar.gz", hash = "sha256:d3672003b1dbc85e2013e4983b88eb9a5ccfd389b0d4e5015f39a9ee5620ec18", size = 71693, upload-time = "2024-10-04T13:58:32.442Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/65/3ada667d32675399001bf022ad3d9f3989b57101351ebc71d6fbe2384634/smart_open-7.6.1.tar.gz", hash = "sha256:4347996e7ba21db7cd1e059632e0b30395407e4f6c660d2ddffc8f2a9ae5f990", size = 54754, upload-time = "2026-05-09T06:23:37.06Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/bc/706838af28a542458bffe74a5d0772ca7f207b5495cd9fccfce61ef71f2a/smart_open-7.0.5-py3-none-any.whl", hash = "sha256:8523ed805c12dff3eaa50e9c903a6cb0ae78800626631c5fe7ea073439847b89", size = 61387, upload-time = "2024-10-04T13:58:35.073Z" }, + { url = "https://files.pythonhosted.org/packages/14/78/0f68b93564b8c6b6987a0696c582ba2591a381ab2f733a501909e949f241/smart_open-7.6.1-py3-none-any.whl", hash = "sha256:b4de6aebef023aca91cc9fb372052e1343ba3f152de215bd22391a663e3ddd21", size = 64845, upload-time = "2026-05-09T06:23:35.386Z" }, ] [[package]] @@ -10102,60 +10467,87 @@ wheels = [ [[package]] name = "snowballstemmer" -version = "3.0.1" +version = "3.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, ] [[package]] -name = "sounddevice" -version = "0.5.5" +name = "soupsieve" +version = "2.8.4" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2a/f9/2592608737553638fca98e21e54bfec40bf577bb98a61b2770c912aab25e/sounddevice-0.5.5.tar.gz", hash = "sha256:22487b65198cb5bf2208755105b524f78ad173e5ab6b445bdab1c989f6698df3", size = 143191, upload-time = "2026-01-23T18:36:43.529Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/0a/478e441fd049002cf308520c0d62dd8333e7c6cc8d997f0dda07b9fbcc46/sounddevice-0.5.5-py3-none-any.whl", hash = "sha256:30ff99f6c107f49d25ad16a45cacd8d91c25a1bcdd3e81a206b921a3a6405b1f", size = 32807, upload-time = "2026-01-23T18:36:35.649Z" }, - { url = "https://files.pythonhosted.org/packages/56/f9/c037c35f6d0b6bc3bc7bfb314f1d6f1f9a341328ef47cd63fc4f850a7b27/sounddevice-0.5.5-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:05eb9fd6c54c38d67741441c19164c0dae8ce80453af2d8c4ad2e7823d15b722", size = 108557, upload-time = "2026-01-23T18:36:37.41Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, ] [[package]] -name = "soupsieve" -version = "2.8.3" +name = "sphinx" +version = "9.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +resolution-markers = [ + "python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", +] +dependencies = [ + { name = "alabaster", marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "babel", marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "docutils", marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "imagesize", marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "jinja2", marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "packaging", marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pygments", marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "requests", marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "roman-numerals", marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "snowballstemmer", marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sphinxcontrib-applehelp", marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sphinxcontrib-devhelp", marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sphinxcontrib-htmlhelp", marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sphinxcontrib-jsmath", marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sphinxcontrib-qthelp", marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sphinxcontrib-serializinghtml", marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, ] [[package]] name = "sphinx" -version = "9.0.4" +version = "9.1.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", +] dependencies = [ - { name = "alabaster", 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 = "babel", 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 = "docutils", 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 = "imagesize", 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 = "jinja2", 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 = "packaging", 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 = "pygments", 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 = "requests", 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 = "roman-numerals", 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 = "snowballstemmer", 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 = "sphinxcontrib-applehelp", 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 = "sphinxcontrib-devhelp", 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 = "sphinxcontrib-htmlhelp", 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 = "sphinxcontrib-jsmath", 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 = "sphinxcontrib-qthelp", 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 = "sphinxcontrib-serializinghtml", 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 = "alabaster", marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "babel", marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "docutils", marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "imagesize", marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "jinja2", marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "packaging", marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pygments", marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "requests", marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "roman-numerals", marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "snowballstemmer", marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sphinxcontrib-applehelp", marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sphinxcontrib-devhelp", marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sphinxcontrib-htmlhelp", marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sphinxcontrib-jsmath", marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sphinxcontrib-qthelp", marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sphinxcontrib-serializinghtml", marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, ] [[package]] @@ -10163,7 +10555,8 @@ name = "sphinx-design" version = "0.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sphinx", 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 = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/7b/804f311da4663a4aecc6cf7abd83443f3d4ded970826d0c958edc77d4527/sphinx_design-0.7.0.tar.gz", hash = "sha256:d2a3f5b19c24b916adb52f97c5f00efab4009ca337812001109084a740ec9b7a", size = 2203582, upload-time = "2026-01-19T13:12:53.297Z" } wheels = [ @@ -10226,34 +10619,30 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.48" +version = "2.0.50" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "typing-extensions", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/6d/b8b78b5b80f3c3ab3f7fa90faa195ec3401f6d884b60221260fd4d51864c/sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc", size = 2157184, upload-time = "2026-03-02T15:38:28.161Z" }, - { url = "https://files.pythonhosted.org/packages/21/4b/4f3d4a43743ab58b95b9ddf5580a265b593d017693df9e08bd55780af5bb/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c", size = 3313555, upload-time = "2026-03-02T15:58:57.21Z" }, - { url = "https://files.pythonhosted.org/packages/21/dd/3b7c53f1dbbf736fd27041aee68f8ac52226b610f914085b1652c2323442/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7", size = 3313057, upload-time = "2026-03-02T15:52:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cc/3e600a90ae64047f33313d7d32e5ad025417f09d2ded487e8284b5e21a15/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d", size = 3265431, upload-time = "2026-03-02T15:58:59.096Z" }, - { url = "https://files.pythonhosted.org/packages/8b/19/780138dacfe3f5024f4cf96e4005e91edf6653d53d3673be4844578faf1d/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571", size = 3287646, upload-time = "2026-03-02T15:52:31.569Z" }, - { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, - { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, - { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, - { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, - { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, - { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, - { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, - { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, - { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/57/da/6fbf010c8ebb347679d0d100b22fe9ba5e13fd04046c5df7280d2f0bf706/sqlalchemy-2.0.50.tar.gz", hash = "sha256:af5607d11ef90fd6a5c0549fe0045dce1663d427426bcfb506dcb5346a85a3b9", size = 9907424, upload-time = "2026-05-24T19:20:04.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/5d/3172686af1770e4de2805f919a51441085f589ddadf3dd76ec582f84f497/sqlalchemy-2.0.50-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa6e403663a9c43c8fef7ce4bdb4cf48bcd8d352e91deda2a99f963270bd508", size = 2161366, upload-time = "2026-05-24T20:00:02.061Z" }, + { url = "https://files.pythonhosted.org/packages/0f/90/e98dedea3c3e663a17afcd003a34ba45efdac2cea3b6f2e4585e2b1e2537/sqlalchemy-2.0.50-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51b637a84f9fa35ae1f9017e786cb142974a25305085e1b378b3647a67f65ad3", size = 3318926, upload-time = "2026-05-24T20:07:42.369Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4f/501308c2babb62c11753ecb4ee88ba9eef019419a4d6cbf7cb13e2bad353/sqlalchemy-2.0.50-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2dab927761d9108550f0cf8e66ff21af56f907a0ce0a689793db615e2b55f62c", size = 3319199, upload-time = "2026-05-24T20:14:28.551Z" }, + { url = "https://files.pythonhosted.org/packages/ac/39/d88996c5e03ed6248c3a788d20f0b8d8b376b9f8a495e4bab9df7c72d2f8/sqlalchemy-2.0.50-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:545eae198d37bcf837a10ede3684e2af32458d6f35c597c35c2de7502dc38fc4", size = 3270301, upload-time = "2026-05-24T20:07:44.917Z" }, + { url = "https://files.pythonhosted.org/packages/42/1b/1ae0e65161b51cc43e5ca75430ef79d80e23b5042d645586c2c342c3b92e/sqlalchemy-2.0.50-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fec460e18cdbb4c7773531122ce9a27e96c6ca17af3933941d94da475ad2c86", size = 3293465, upload-time = "2026-05-24T20:14:30.501Z" }, + { url = "https://files.pythonhosted.org/packages/be/b0/a9d19b43f38f878b1278bca5b00b909f7540d41494396dd2561f9ad0956d/sqlalchemy-2.0.50-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae23d8b9d344d30d0a92f06d45825024a5790f1c1dd4cf452636a50d3e58cb", size = 2159807, upload-time = "2026-05-24T19:27:53.086Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/191dd58a248fd2cfd4780fa82c375c505e4ad98c8b522fa69ec492130d77/sqlalchemy-2.0.50-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47b71b933e7b4ebad407c8fdfd70d2c4f08b78b3238bb30eebdd6eb32ca51b89", size = 3343358, upload-time = "2026-05-24T20:09:29.279Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2b/514fce8a7df81cf5bad7ff7865de7ac0c5776a38cc043475c4703eb7fe8b/sqlalchemy-2.0.50-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:110fdac56ace278949f00de805edacbd6141e382d992f9ba28238b3a0827a600", size = 3357994, upload-time = "2026-05-24T20:17:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/35/a6/a0e283f5494f92b0d77e319ff77e437b1ffe4a051ba67c81d53234825475/sqlalchemy-2.0.50-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5e4ac70e9e757f6b3e87c0491ff034442ecd8dfd36d041a50564c322dafc0e", size = 3289399, upload-time = "2026-05-24T20:09:32.239Z" }, + { url = "https://files.pythonhosted.org/packages/b7/96/1b07325ba71752d6a028b77d07bed1483ad545f794e8b1dc89b3ba3b3c68/sqlalchemy-2.0.50-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:724f3dcbe53dd0151e3cb5e7ec4ba4c620bede579caacd16275dc35ce06e8615", size = 3321216, upload-time = "2026-05-24T20:17:15.581Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c4/c42356b527296e9862f67990efce31ef78b4cf69cd3f80873a528a060320/sqlalchemy-2.0.50-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06a9210bdc5f4298cff0781087e2ff45683922252dacc452846373a58761f093", size = 2156697, upload-time = "2026-05-24T19:27:54.764Z" }, + { url = "https://files.pythonhosted.org/packages/60/a1/b1a70e3c4365ac7fe9e347f3710f19b562c866fb96d45e3c891588789a7b/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b53784972ade4f8174b9aa661f31a06f8a936d2cfdd602913ff3c6dd40ae873", size = 3284260, upload-time = "2026-05-24T20:09:34.195Z" }, + { url = "https://files.pythonhosted.org/packages/3f/4a/f3ac3caa19f263d57b0a47f8c91bbf56583dc2d3fc63acfbf644abb24fe0/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31648fa14460537e768a7303b078e4344d208e0d23e06867c1f376a227ed82db", size = 3302280, upload-time = "2026-05-24T20:17:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/66/55/ccada3e3d62254587819749a0bc69f41173eb48a6e385d10e66d32a9c88e/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03f4323c980ad0e918cc9e5369b015f759f4e534db5bbaf4dc36832c10d05064", size = 3231580, upload-time = "2026-05-24T20:09:36.406Z" }, + { url = "https://files.pythonhosted.org/packages/05/f6/6809349130a2de0e109e7f00fd7d431da9565b9b2868b32ee684754f672b/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2b9dcc43afef8ac157cd92fce96985d6b8b0cfbd3df4d666f66b4d55a75d202f", size = 3269375, upload-time = "2026-05-24T20:17:20.34Z" }, + { url = "https://files.pythonhosted.org/packages/d0/10/f7220e9b784d295d241c86ed99aeb537f92afcd469a64861f2717e9bb077/sqlalchemy-2.0.50-py3-none-any.whl", hash = "sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9", size = 1943861, upload-time = "2026-05-24T19:59:01.119Z" }, ] [[package]] @@ -10281,15 +10670,16 @@ wheels = [ [[package]] name = "sqlmodel" -version = "0.0.37" +version = "0.0.38" source = { registry = "https://pypi.org/simple" } dependencies = [ { 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 = "sqlalchemy", 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 = "typing-extensions", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/26/1d2faa0fd5a765267f49751de533adac6b9ff9366c7c6e7692df4f32230f/sqlmodel-0.0.37.tar.gz", hash = "sha256:d2c19327175794faf50b1ee31cc966764f55b1dedefc046450bc5741a3d68352", size = 85527, upload-time = "2026-02-21T16:39:47.038Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/0d/26ec1329960ea9430131fe63f63a95ea4cb8971d49c891ff7e1f3255421c/sqlmodel-0.0.38.tar.gz", hash = "sha256:d583ec237b14103809f74e8630032bc40ab68cd6b754a610f0813c56911a547b", size = 86710, upload-time = "2026-04-02T21:03:55.571Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/e1/7c8d18e737433f3b5bbe27b56a9072a9fcb36342b48f1bef34b6da1d61f2/sqlmodel-0.0.37-py3-none-any.whl", hash = "sha256:2137a4045ef3fd66a917a7717ada959a1ceb3630d95e1f6aaab39dd2c0aef278", size = 27224, upload-time = "2026-02-21T16:39:47.781Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/10c60af0607ab6fa136264f7f39d205932218516226d38585324ffda705d/sqlmodel-0.0.38-py3-none-any.whl", hash = "sha256:84e3fa990a77395461ded72a6c73173438ce8449d5c1c4d97fbff1b1df692649", size = 27294, upload-time = "2026-04-02T21:03:56.406Z" }, ] [[package]] @@ -10303,15 +10693,15 @@ wheels = [ [[package]] name = "sse-starlette" -version = "3.3.4" +version = "3.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", 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 = "starlette", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427, upload-time = "2026-03-29T09:00:23.307Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/2b/58abc2d1fd397e7dde08e947e05c884d8ef2f78d5e2588c17a12d42d6994/sse_starlette-3.4.4.tar.gz", hash = "sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0", size = 31819, upload-time = "2026-05-12T17:37:17.019Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330, upload-time = "2026-03-29T09:00:21.846Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/805710444ea8cc75fbf70b920ed431a560c4bf9c57f7d5a3117213189399/sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973", size = 16514, upload-time = "2026-05-12T17:37:15.601Z" }, ] [[package]] @@ -10364,11 +10754,11 @@ wheels = [ [[package]] name = "structlog" -version = "25.5.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ef/52/9ba0f43b686e7f3ddfeaa78ac3af750292662284b3661e91ad5494f21dbc/structlog-25.5.0.tar.gz", hash = "sha256:098522a3bebed9153d4570c6d0288abf80a031dfdb2048d59a49e9dc2190fc98", size = 1460830, upload-time = "2025-10-27T08:28:23.028Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/89/b4a0bcfdf4f71a3dea31379f095929613d7e4528a0996bca6aa964cd0dca/structlog-26.1.0.tar.gz", hash = "sha256:f63a716cbd1b1291cf7661de7794b455acfa4c43c5bcf1630e6ad5ddc1adb3b7", size = 1459881, upload-time = "2026-06-06T07:33:39.348Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl", hash = "sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f", size = 72510, upload-time = "2025-10-27T08:28:21.535Z" }, + { url = "https://files.pythonhosted.org/packages/a9/18/489c97b834dfff9cf2fc2507cede4bcd4b11e67f84bc462acd1992496f86/structlog-26.1.0-py3-none-any.whl", hash = "sha256:e081a26d6c373e6d201eca24eede26d8ffab07f88f477822e679183428d3d91e", size = 73764, upload-time = "2026-06-06T07:33:38.046Z" }, ] [[package]] @@ -10463,45 +10853,36 @@ clickhouse = [ { name = "clickhouse-driver", 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]] -name = "threadpoolctl" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, -] - [[package]] name = "tiktoken" -version = "0.12.0" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "regex", 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 = "requests", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, - { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, - { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, - { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, - { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, - { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, - { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, - { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, - { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, - { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, - { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, - { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, - { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, - { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/91/10b9c7076bc02c246c853201fdbbe300a4b8c5ed7b84c25f7403f4e32655/tiktoken-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:91c180fe255bd5a86d8316210d2833a1d4d33d026cd86a67812f4773743c8d26", size = 984644, upload-time = "2026-05-15T04:50:23.256Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e4/fceae98015fab47fcd49b8bd7f46145bcd187a47e0add1e5378ed67ef980/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:059c8ecf554eb5b41e6e054ba467b871b03277d267dee7244380aca4359747d4", size = 1119261, upload-time = "2026-05-15T04:50:24.348Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/fe42ad00de01a8c4a49ad8649a2c8a316835a9cad5961b11d21eac0020a5/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:36217497eaffc158607a3b26f065300db2aefd43b115263f3b9688ce38146173", size = 1138253, upload-time = "2026-05-15T04:50:25.505Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/ccee1ecccca107e9a16efcecdeeb964c325305038554d466ece65b42338f/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:303f7d91b4fce3baddbcde05c139091d4caa5026ac7214c1dc7ff7a71ee429ff", size = 1185747, upload-time = "2026-05-15T04:50:27.02Z" }, + { url = "https://files.pythonhosted.org/packages/9d/03/cd0cba295522b91eb55c6b2704f1df895f8226cfe60ab10d4d51d0cc9e69/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d48843bee149630eb735a99e1f4a85b47308d21868ea63163f6e87768d3cfed", size = 1241265, upload-time = "2026-05-15T04:50:28.815Z" }, + { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970, upload-time = "2026-05-15T04:50:32.961Z" }, + { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741, upload-time = "2026-05-15T04:50:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523, upload-time = "2026-05-15T04:50:35.782Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954, upload-time = "2026-05-15T04:50:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069, upload-time = "2026-05-15T04:50:38.221Z" }, + { url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978, upload-time = "2026-05-15T04:50:42.195Z" }, + { url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355, upload-time = "2026-05-15T04:50:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772, upload-time = "2026-05-15T04:50:44.782Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415, upload-time = "2026-05-15T04:50:46.422Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879, upload-time = "2026-05-15T04:50:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516, upload-time = "2026-05-15T04:50:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518, upload-time = "2026-05-15T04:50:53.543Z" }, + { url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867, upload-time = "2026-05-15T04:50:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826, upload-time = "2026-05-15T04:50:56.296Z" }, + { url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489, upload-time = "2026-05-15T04:50:57.918Z" }, ] [[package]] @@ -10534,14 +10915,14 @@ wheels = [ [[package]] name = "tinycss2" -version = "1.4.0" +version = "1.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "webencodings", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, + { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, ] [[package]] @@ -10555,8 +10936,12 @@ 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" }, ] @@ -10586,63 +10971,47 @@ wheels = [ [[package]] name = "tomlkit" -version = "0.14.0" +version = "0.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/db/03eaf4331631ef6b27d6e3c9b68c54dc6f0d63d87201fed600cc409307fd/tomlkit-0.15.0.tar.gz", hash = "sha256:7d1a9ecba3086638211b13814ea79c90dd54dd11993564376f3aa92271f5c7a3", size = 161875, upload-time = "2026-05-10T07:38:22.245Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, + { url = "https://files.pythonhosted.org/packages/6a/43/8bd850ee71a191bf072e31302c73a66be413fecdd98fdcd111ecbcce13ca/tomlkit-0.15.0-py3-none-any.whl", hash = "sha256:4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738", size = 41328, upload-time = "2026-05-10T07:38:23.517Z" }, ] [[package]] name = "torch" -version = "2.10.0" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-bindings", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "cuda-bindings", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "filelock", 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 = "fsspec", 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 = "jinja2", 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 = "networkx", 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 = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cudnn-cu13", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparselt-cu13", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nccl-cu13", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvshmem-cu13", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "setuptools", 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 = "sympy", 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 = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "triton", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "typing-extensions", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/8b/4b61d6e13f7108f36910df9ab4b58fd389cc2520d54d81b88660804aad99/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:418997cb02d0a0f1497cf6a09f63166f9f5df9f3e16c8a716ab76a72127c714f", size = 79423467, upload-time = "2026-02-10T21:44:48.711Z" }, - { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, - { url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, - { url = "https://files.pythonhosted.org/packages/36/ab/7b562f1808d3f65414cd80a4f7d4bb00979d9355616c034c171249e1a303/torch-2.10.0-3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac5bdcbb074384c66fa160c15b1ead77839e3fe7ed117d667249afce0acabfac", size = 915518691, upload-time = "2026-03-11T14:15:43.147Z" }, - { url = "https://files.pythonhosted.org/packages/b3/7a/abada41517ce0011775f0f4eacc79659bc9bc6c361e6bfe6f7052a6b9363/torch-2.10.0-3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:98c01b8bb5e3240426dcde1446eed6f40c778091c8544767ef1168fc663a05a6", size = 915622781, upload-time = "2026-03-11T14:17:11.354Z" }, - { url = "https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:80b1b5bfe38eb0e9f5ff09f206dcac0a87aadd084230d4a36eea5ec5232c115b", size = 915627275, upload-time = "2026-03-11T14:16:11.325Z" }, - { url = "https://files.pythonhosted.org/packages/d8/f0/72bf18847f58f877a6a8acf60614b14935e2f156d942483af1ffc081aea0/torch-2.10.0-3-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:46b3574d93a2a8134b3f5475cfb98e2eb46771794c57015f6ad1fb795ec25e49", size = 915523474, upload-time = "2026-03-11T14:17:44.422Z" }, - { url = "https://files.pythonhosted.org/packages/78/89/f5554b13ebd71e05c0b002f95148033e730d3f7067f67423026cc9c69410/torch-2.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3282d9febd1e4e476630a099692b44fdc214ee9bf8ee5377732d9d9dfe5712e4", size = 145992610, upload-time = "2026-01-21T16:25:26.327Z" }, - { url = "https://files.pythonhosted.org/packages/ae/30/a3a2120621bf9c17779b169fc17e3dc29b230c29d0f8222f499f5e159aa8/torch-2.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2f9edd8dbc99f62bc4dfb78af7bf89499bca3d753423ac1b4e06592e467b763", size = 915607863, upload-time = "2026-01-21T16:25:06.696Z" }, - { url = "https://files.pythonhosted.org/packages/61/d8/15b9d9d3a6b0c01b883787bd056acbe5cc321090d4b216d3ea89a8fcfdf3/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:b7bd80f3477b830dd166c707c5b0b82a898e7b16f59a7d9d42778dd058272e8b", size = 79423461, upload-time = "2026-01-21T16:24:50.266Z" }, - { url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" }, - { url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5c/dee910b87c4d5c0fcb41b50839ae04df87c1cfc663cf1b5fca7ea565eeaa/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294", size = 79498198, upload-time = "2026-01-21T16:24:34.704Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6f/f2e91e34e3fcba2e3fc8d8f74e7d6c22e74e480bbd1db7bc8900fdf3e95c/torch-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b", size = 146004247, upload-time = "2026-01-21T16:24:29.335Z" }, - { url = "https://files.pythonhosted.org/packages/98/fb/5160261aeb5e1ee12ee95fe599d0541f7c976c3701d607d8fc29e623229f/torch-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738", size = 915716445, upload-time = "2026-01-21T16:22:45.353Z" }, - { url = "https://files.pythonhosted.org/packages/1a/0b/39929b148f4824bc3ad6f9f72a29d4ad865bcf7ebfc2fa67584773e083d2/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382", size = 79851305, upload-time = "2026-01-21T16:24:09.209Z" }, - { url = "https://files.pythonhosted.org/packages/d8/14/21fbce63bc452381ba5f74a2c0a959fdf5ad5803ccc0c654e752e0dbe91a/torch-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8", size = 146005472, upload-time = "2026-01-21T16:22:29.022Z" }, - { url = "https://files.pythonhosted.org/packages/54/fd/b207d1c525cb570ef47f3e9f836b154685011fce11a2f444ba8a4084d042/torch-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f", size = 915612644, upload-time = "2026-01-21T16:21:47.019Z" }, - { url = "https://files.pythonhosted.org/packages/0e/13/e76b4d9c160e89fff48bf16b449ea324bda84745d2ab30294c37c2434c0d/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f", size = 79498248, upload-time = "2026-01-21T16:23:09.315Z" }, + { url = "https://files.pythonhosted.org/packages/18/62/131124fb95df03811b8260d1d43dcc5ee85ea1a344b964613d7efe77fb08/torch-2.12.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:10802fd383bbfed646212e765a72c37d2185205d4f26eb197a254e8ac7ddcb25", size = 87990344, upload-time = "2026-05-13T14:55:42.154Z" }, + { url = "https://files.pythonhosted.org/packages/12/9c/dda0dbd547dc549839824135f223792fd0e725f28ed0715dda366b7acaa2/torch-2.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c12592630aef72feaf18bd3f197ef587bbfa21131b31c38b23ab2e55fce92e36", size = 426362932, upload-time = "2026-05-13T14:54:15.295Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d2/a7dd5a3f9bdaa7842124e8e2359202b317c48d47d2fc5816fafdf2049adb/torch-2.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:415c1b8d0412f67551c8e89a2daca0fb3e56694af0281ba155eaa9da481f58b4", size = 532170085, upload-time = "2026-05-13T14:55:20.788Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/285d643f254731294c9b595a007eac39db4600a98682d7bca688f42ca164/torch-2.12.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b41339df93d491435e790ff8bcbae1c0ce777175889bfd1281d119862793e6a2", size = 88010197, upload-time = "2026-05-13T14:55:35.414Z" }, + { url = "https://files.pythonhosted.org/packages/79/81/76debf1db1343bd929bbb5d74c89fb437c2ed88eb144712557e7bd3eea45/torch-2.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8fbef9f108a863e7722a73740998967e3b074742a834fc5be3a535a2befa7057", size = 426376751, upload-time = "2026-05-13T14:55:03.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/f0/80026028b603c4650ff270fc3785bdef4bd6738765a9cc5a0f5a637d65a2/torch-2.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4b4f64c2c2b11f7510d93dd6412b87025ff6eddd6bb61c3b5a3d892ea20c4756", size = 532261691, upload-time = "2026-05-13T14:52:54.453Z" }, + { url = "https://files.pythonhosted.org/packages/86/ca/01896c80ba921676aa45886b2c5b8d774912de2a1f719de48169c6f755cd/torch-2.12.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:90dd587a5f61bfe1307148b581e2084fc5bc4a06e2b90a20e9a36b81087ff16b", size = 88009511, upload-time = "2026-05-13T14:54:47.411Z" }, + { url = "https://files.pythonhosted.org/packages/a5/04/52bdaf4787eab6ac7d7f5851dff934e4def0bc8ead9c8fd2b69b3e529699/torch-2.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:864392c73b7654f4d2b3ae712f607937d0dbb1101c4555fbb41848106b297f39", size = 426383231, upload-time = "2026-05-13T14:53:32.129Z" }, + { url = "https://files.pythonhosted.org/packages/49/8a/94bdecd13f5aaa90d45920b89789d9fe7c6f4af8c3cdd7ce01fcb59908fc/torch-2.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5d6b560dfa7d56291c07d615c3bb73e8d9943d9b6d87f76cd0d9d570c4797fa6", size = 532269288, upload-time = "2026-05-13T14:53:49.423Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ad/e95e822f3538171e22640a7fbe839a1fdb666600bf6487025de2ff03b11a/torch-2.12.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:10ee1448a9f304d3b987eb4656f664ba6e4d7b410ca7a5a7c642199777a2cf88", size = 88319556, upload-time = "2026-05-13T14:54:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/b7/07/055d06d985b445d67422d25b033c11cf55bbb81785d4c4e68e28bca5820e/torch-2.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af68dbf403439cae9ceaeaaf92f8352b460787dcd27b92aa05c40dd4a19c0f1e", size = 426397656, upload-time = "2026-05-13T14:52:38.84Z" }, + { url = "https://files.pythonhosted.org/packages/43/94/b0b4fdc3014122e0a7302fb90086d352aa48f2576f0b252561ebb38c01a8/torch-2.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a6a2eebb237d3b1d9ad3b378e86d9b9e0782afdea8b1e0eba6a13646b9b49c07", size = 532183124, upload-time = "2026-05-13T14:53:16.178Z" }, ] [[package]] @@ -10656,7 +11025,7 @@ wheels = [ [[package]] name = "torchvision" -version = "0.25.0" +version = "0.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", 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')" }, @@ -10664,54 +11033,54 @@ dependencies = [ { name = "torch", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/be/c704bceaf11c4f6b19d64337a34a877fcdfe3bd68160a8c9ae9bea4a35a3/torchvision-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db74a551946b75d19f9996c419a799ffdf6a223ecf17c656f90da011f1d75b20", size = 1874923, upload-time = "2026-01-21T16:27:46.574Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e9/f143cd71232430de1f547ceab840f68c55e127d72558b1061a71d0b193cd/torchvision-0.25.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f49964f96644dbac2506dffe1a0a7ec0f2bf8cf7a588c3319fed26e6329ffdf3", size = 2344808, upload-time = "2026-01-21T16:27:43.191Z" }, - { url = "https://files.pythonhosted.org/packages/43/ae/ad5d6165797de234c9658752acb4fce65b78a6a18d82efdf8367c940d8da/torchvision-0.25.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:153c0d2cbc34b7cf2da19d73450f24ba36d2b75ec9211b9962b5022fb9e4ecee", size = 8070752, upload-time = "2026-01-21T16:27:33.748Z" }, - { url = "https://files.pythonhosted.org/packages/56/3a/6ea0d73f49a9bef38a1b3a92e8dd455cea58470985d25635beab93841748/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2abe430c90b1d5e552680037d68da4eb80a5852ebb1c811b2b89d299b10573b", size = 1874920, upload-time = "2026-01-21T16:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/51/f8/c0e1ef27c66e15406fece94930e7d6feee4cb6374bbc02d945a630d6426e/torchvision-0.25.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b75deafa2dfea3e2c2a525559b04783515e3463f6e830cb71de0fb7ea36fe233", size = 2344556, upload-time = "2026-01-21T16:27:40.125Z" }, - { url = "https://files.pythonhosted.org/packages/68/2f/f24b039169db474e8688f649377de082a965fbf85daf4e46c44412f1d15a/torchvision-0.25.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f25aa9e380865b11ea6e9d99d84df86b9cc959f1a007cd966fc6f1ab2ed0e248", size = 8072351, upload-time = "2026-01-21T16:27:21.074Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5b/1562a04a6a5a4cf8cf40016a0cdeda91ede75d6962cff7f809a85ae966a5/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:24e11199e4d84ba9c5ee7825ebdf1cd37ce8deec225117f10243cae984ced3ec", size = 1874918, upload-time = "2026-01-21T16:27:39.02Z" }, - { url = "https://files.pythonhosted.org/packages/36/b1/3d6c42f62c272ce34fcce609bb8939bdf873dab5f1b798fd4e880255f129/torchvision-0.25.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f271136d2d2c0b7a24c5671795c6e4fd8da4e0ea98aeb1041f62bc04c4370ef", size = 2309106, upload-time = "2026-01-21T16:27:30.624Z" }, - { url = "https://files.pythonhosted.org/packages/c7/60/59bb9c8b67cce356daeed4cb96a717caa4f69c9822f72e223a0eae7a9bd9/torchvision-0.25.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:855c0dc6d37f462482da7531c6788518baedca1e0847f3df42a911713acdfe52", size = 8071522, upload-time = "2026-01-21T16:27:29.392Z" }, - { url = "https://files.pythonhosted.org/packages/52/99/dca81ed21ebaeff2b67cc9f815a20fdaa418b69f5f9ea4c6ed71721470db/torchvision-0.25.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a8f8061284395ce31bcd460f2169013382ccf411148ceb2ee38e718e9860f5a7", size = 1896209, upload-time = "2026-01-21T16:27:32.159Z" }, - { url = "https://files.pythonhosted.org/packages/28/cc/2103149761fdb4eaed58a53e8437b2d716d48f05174fab1d9fcf1e2a2244/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:146d02c9876858420adf41f3189fe90e3d6a409cbfa65454c09f25fb33bf7266", size = 2310735, upload-time = "2026-01-21T16:27:22.327Z" }, - { url = "https://files.pythonhosted.org/packages/76/ad/f4c985ad52ddd3b22711c588501be1b330adaeaf6850317f66751711b78c/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c4d395cb2c4a2712f6eb93a34476cdf7aae74bb6ea2ea1917f858e96344b00aa", size = 8089557, upload-time = "2026-01-21T16:27:27.666Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d6/a7e71e981042d5c573e2e61891b9023b190c88adb75b18bed8594371250c/torchvision-0.27.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:df0c166b6bdf7c47f88e81e8b43bc085451d5c50d0c5d1691bc474c1227d6fed", size = 1758812, upload-time = "2026-05-13T14:57:16.662Z" }, + { url = "https://files.pythonhosted.org/packages/93/f9/f542fb7e4476603fb237ebdc64369a7d11f18eb5a129aa2559cbdb710aee/torchvision-0.27.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9bb9251f64b854124efed95d02953a89f7e2726c3ca662d7ea0151129157297f", size = 7831148, upload-time = "2026-05-13T14:57:08.37Z" }, + { url = "https://files.pythonhosted.org/packages/f6/61/7aa7cc2c9e8750027f6fb9ae3a7393ef43860bcdfe3966e2f71fee800e31/torchvision-0.27.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f44453f107c296d5446a79f7ac59733ad8bf5ddfa04c53805dfbae298a42a798", size = 7575519, upload-time = "2026-05-13T14:56:50.552Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c8/5cd91932f7f3671b0743dc4ae1a4c16b1d0b45bf4087976277d325bda718/torchvision-0.27.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1a6dd742a150645126df9e0b2e449874c1d635897c773b322c2e067e98382dfe", size = 1758824, upload-time = "2026-05-13T14:57:15.227Z" }, + { url = "https://files.pythonhosted.org/packages/d9/36/7fb7d19477b3d93283b52fea11fa8ee30ab9064a08c97b4a6b91445e26cb/torchvision-0.27.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65772ff3ec4f4f5d680e30019835555dd239e7fefee4b0a846375fe1cb1592ef", size = 7831034, upload-time = "2026-05-13T14:57:06.483Z" }, + { url = "https://files.pythonhosted.org/packages/62/43/dfd894c3f8b01b5b33fde990f0159c1926ebc7b6e2c4193e2efb7da3c4cb/torchvision-0.27.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7a9966a088d06b4cf6c610e03be62de469efa6f2cd2e7c7eed8e925ed6af59ac", size = 7579774, upload-time = "2026-05-13T14:56:59.337Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ae/36547812e6e047c1d80bcacd1b17a340612b08a6e876e0aabf3d0b9228b0/torchvision-0.27.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:41d6dae73e1af09fa82ded597ae57f2a2314285acde54b25890a8f8e51b999d7", size = 1758826, upload-time = "2026-05-13T14:57:05.262Z" }, + { url = "https://files.pythonhosted.org/packages/ae/30/32c4ea842738728a14e3df8c576c62dedcf5ae5cb6a5c984c6429ebe7524/torchvision-0.27.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:70f071c6f74b60d5fe8851636d8d4cd5f4fa29d57fd9348a87a6f17b990b95ba", size = 7789501, upload-time = "2026-05-13T14:56:57.786Z" }, + { url = "https://files.pythonhosted.org/packages/f6/24/4d0d48684251bd0673f87d633d5d88ab00227983b00591156eed2f86c8d5/torchvision-0.27.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:aaafa6962c9d91f42503de1957d6fa349907d028c06f335bd95da7a5bc57147d", size = 7579868, upload-time = "2026-05-13T14:56:41.618Z" }, + { url = "https://files.pythonhosted.org/packages/fa/23/95dfa40431360f42ca949bf861434bed51164adfa8fb9801e05bf3194f50/torchvision-0.27.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:c5121f1b9ab09a7f73e837871deb8321551f7eaeb19d87aa00de9191968eae44", size = 1845008, upload-time = "2026-05-13T14:57:03.768Z" }, + { url = "https://files.pythonhosted.org/packages/23/b9/9dbdf76b2b49a75ba8088df6f7c755bdb520afb6c6dbac0102b46cde5e99/torchvision-0.27.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:1c01f0d1091ae22b9dfc082b0a0fe5faaf053686a29b4fb082ba7691375c73cf", size = 7791430, upload-time = "2026-05-13T14:56:56.206Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6a/e4a16cf2f3310c2ea7760dc5d9054496844391e0f4c1fae87fefac2f3d9e/torchvision-0.27.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dadea3c5ecfd05bbb2a3312ab0374f213c58bf6459cb059122e2f4dfe13d10ed", size = 7668441, upload-time = "2026-05-13T14:57:02.127Z" }, ] [[package]] name = "tornado" -version = "6.5.5" +version = "6.5.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/24/95ec527ad67b76d59299e5465b3935d05e4294b7e0290a3924b7487df30b/tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2", size = 519252, upload-time = "2026-06-08T17:34:51.232Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, - { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, - { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, - { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, - { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163", size = 448543, upload-time = "2026-06-08T17:34:38.052Z" }, + { url = "https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972", size = 449774, upload-time = "2026-06-08T17:34:41.204Z" }, + { url = "https://files.pythonhosted.org/packages/35/37/d434c73f4c6e014b745b9b37085f34f40c022f007efff3d7fe65991899f3/tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b", size = 450745, upload-time = "2026-06-08T17:34:42.531Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/56b9aff361d7f1ab728a805ec7d7ea835f8807afa9f5cc690ea0e630efb9/tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92", size = 450578, upload-time = "2026-06-08T17:34:43.787Z" }, + { url = "https://files.pythonhosted.org/packages/02/30/a7444fb23aa76860a14198fab96ac79f1866b0a6e19e26c4381b0938e50f/tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5", size = 449985, upload-time = "2026-06-08T17:34:45.326Z" }, ] [[package]] name = "tqdm" -version = "4.67.3" +version = "4.68.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/05/0d5260f1f1ca784f4a4a0def9cbe6affe587f5b4025328d446c3d67765f4/tqdm-4.68.2.tar.gz", hash = "sha256:89c230e8dbc67c7615c142487111222f878c77427ea09549960f62389e258add", size = 171923, upload-time = "2026-06-09T13:26:42.539Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, + { url = "https://files.pythonhosted.org/packages/eb/75/1a0392bcc21c44dcdf87b3cf2d137e7829be2c083a1e38d44efca3d57a16/tqdm-4.68.2-py3-none-any.whl", hash = "sha256:d4240441fb5353290b87d6a85968c9decc131a99b8c7faa28269d829de669ede", size = 78578, upload-time = "2026-06-09T13:26:40.731Z" }, ] [[package]] name = "traitlets" -version = "5.14.3" +version = "5.15.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/a9/a2584b8313b89f94869ddb3c4074617a691de1812a614d2d50e32ca5a7a6/traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722", size = 163344, upload-time = "2026-06-03T12:26:06.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, + { url = "https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" }, ] [[package]] name = "transformers" -version = "5.5.0" +version = "5.10.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub", 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')" }, @@ -10724,67 +11093,49 @@ dependencies = [ { name = "tqdm", 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 = "typer", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/9d/fb46e729b461985f41a5740167688b924a4019141e5c164bea77548d3d9e/transformers-5.5.0.tar.gz", hash = "sha256:c8db656cf51c600cd8c75f06b20ef85c72e8b8ff9abc880c5d3e8bc70e0ddcbd", size = 8237745, upload-time = "2026-04-02T16:13:08.113Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/28/35f7411ff80a3640c1f4fc907dcbb6a65061ebb82f66950e38bfc9f7f740/transformers-5.5.0-py3-none-any.whl", hash = "sha256:821a9ff0961abbb29eb1eb686d78df1c85929fdf213a3fe49dc6bd94f9efa944", size = 10245591, upload-time = "2026-04-02T16:13:03.462Z" }, -] - -[[package]] -name = "triton" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", - "python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'", -] +sdist = { url = "https://files.pythonhosted.org/packages/8d/38/d5f978bd5091019e89aef29b9a831f5cd70f2598963a3ead8b9570cab592/transformers-5.10.2.tar.gz", hash = "sha256:f9a44b9c8ca9ab1156b467f574d832ea066284299c2fd0ed84641ccb592751fc", size = 8799687, upload-time = "2026-06-04T18:43:49.119Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, - { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, - { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/73/6f/e1564b0cc182afa05e219a8e09a8e770ffaab879b6b824b56c819bd221da/transformers-5.10.2-py3-none-any.whl", hash = "sha256:8a669db546f82c7c3618cb46ceb0f0afd89292bc70f319c058f8332ec63e268d", size = 11003830, upload-time = "2026-06-04T18:43:45.303Z" }, ] [[package]] name = "triton" version = "3.7.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", -] wheels = [ { url = "https://files.pythonhosted.org/packages/b8/c1/5d842314bb6c78442cc60437928781701c6050b8d479bc2a1aed691d37ca/triton-3.7.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9e71fc392675fac364e0ecf4ef3f76f85b7f5433a16f4c3c5fe5f05a52c85fe", size = 188480277, upload-time = "2026-05-07T19:05:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/13/31/8315ea5f8dd18e60970b3022e3a8b93fd37e0b784fbbef86e10c8e6e5ca1/triton-3.7.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22bacffce443f54593dd20f05294d5a40622e0ea9ab632816f87154504356221", size = 201415942, upload-time = "2026-05-07T18:46:06.479Z" }, { url = "https://files.pythonhosted.org/packages/f7/13/ec05adfcd87311d532ba61e3af143e8be59fcd26675884c4682841406a20/triton-3.7.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4bf49b00a7a377a68a6da603a876e797614e6455a80e9021669c476a953ad9a", size = 188505104, upload-time = "2026-05-07T19:05:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/62/7b/468a576e35beef1426e0828e28e9ba9e65f5474d496f16ee126c15646324/triton-3.7.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f111161d49bf903c0eaedde3962353a3d841c08a836839b7cc1025b8426efcf", size = 201457567, upload-time = "2026-05-07T18:46:13.505Z" }, { url = "https://files.pythonhosted.org/packages/01/e1/a59a583de59b8f62c495d67c80ee3ea97d09e91ac80c4c6e76456ed8d8ac/triton-3.7.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abdf6beaa89b1bcfb9a43cd990536ce66091a997841a4814b260b7bee4c88c3c", size = 188503209, upload-time = "2026-05-07T19:05:17.935Z" }, + { url = "https://files.pythonhosted.org/packages/30/b1/b7507bb9815d403927c8dd51d4158ed2e11751a92dbc118a044f247b6848/triton-3.7.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a35d7afe3f3f058e7ec49fcce09794049e0ffc5c59019ac25ec3413741b8c4e7", size = 201453566, upload-time = "2026-05-07T18:46:20.427Z" }, { url = "https://files.pythonhosted.org/packages/a6/8f/0bea7a6a0c989315c9135a1d7fb37e41905cfb3a17cbc1f10044ebd4cc3a/triton-3.7.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc1d61c172d257db80ddf42595131fb196ad2e9bdd751e90fe2ef13531734e8b", size = 188612899, upload-time = "2026-05-07T19:05:24.955Z" }, + { url = "https://files.pythonhosted.org/packages/e1/02/d96f57828d0912aec733b9bc7e0e7dbfd2c6f079a8fa433ac25cb93d1a30/triton-3.7.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70fb9bbdc9f400afc54bbf6eb2670af28829a6ae3996863317964783141daf56", size = 201553816, upload-time = "2026-05-07T18:46:27.49Z" }, ] [[package]] name = "trl" -version = "0.24.0" +version = "1.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accelerate", 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 = "datasets", 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 = "jinja2", 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 = "packaging", 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 = "transformers", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e8/2e/30ece0055eee5763126e2d52f6e04aec294bcae34b46d9ca16c53c4b5852/trl-0.24.0.tar.gz", hash = "sha256:eee495223725d3da0596be2607581969db89ba0f7c00b075802addc31e61eac9", size = 368447, upload-time = "2025-10-16T00:10:37.65Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/72/115c84b05d0e9b3458bb485ff3aa170cdd923fd2136e7832e41850475a8c/trl-1.5.1.tar.gz", hash = "sha256:8d73ffd9329ac21ffc47919656da2b7faaa6406fbe3574896a1923f390a9197b", size = 622292, upload-time = "2026-05-27T15:26:20.223Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/5f/c647fedde9d59ae35ee189cc49e419da5ac1d9ad9933cb69401a7eac4705/trl-0.24.0-py3-none-any.whl", hash = "sha256:a9145b7d4a4a33778de117bda48530f0cf5b2ac25acc07db80ad04836f490dfc", size = 423143, upload-time = "2025-10-16T00:10:35.809Z" }, + { url = "https://files.pythonhosted.org/packages/46/9a/646429e405d49d0db2e612c418f8278d193158781a9daf3b86356244097f/trl-1.5.1-py3-none-any.whl", hash = "sha256:502a4c71f807fcb2de9768802faf5d1e9c16e52c483f51e80630b10da3451928", size = 761076, upload-time = "2026-05-27T15:26:18.22Z" }, ] [[package]] name = "trove-classifiers" -version = "2026.1.14.14" +version = "2026.6.1.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/43/7935f8ea93fcb6680bc10a6fdbf534075c198eeead59150dd5ed68449642/trove_classifiers-2026.1.14.14.tar.gz", hash = "sha256:00492545a1402b09d4858605ba190ea33243d361e2b01c9c296ce06b5c3325f3", size = 16997, upload-time = "2026-01-14T14:54:50.526Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/e3/7ca82ee24c82d344584abd5b8637b3bd056f2900226e8d82fc22f1184b92/trove_classifiers-2026.6.1.19.tar.gz", hash = "sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745", size = 17059, upload-time = "2026-06-01T19:41:34.649Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-py3-none-any.whl", hash = "sha256:1f9553927f18d0513d8e5ff80ab8980b8202ce37ecae0e3274ed2ef11880e74d", size = 14197, upload-time = "2026-01-14T14:54:49.067Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl", hash = "sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3", size = 14211, upload-time = "2026-06-01T19:41:33.434Z" }, ] [[package]] @@ -10793,28 +11144,33 @@ version = "0.0.17" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/66/c3/41ae6346443eedb65b96761abfab890a48ce2aa5a8a27af69c5c5d99064d/ty-0.0.17.tar.gz", hash = "sha256:847ed6c120913e280bf9b54d8eaa7a1049708acb8824ad234e71498e8ad09f97", size = 5167209, upload-time = "2026-02-13T13:26:36.835Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/01/0ef15c22a1c54b0f728ceff3f62d478dbf8b0dcf8ff7b80b954f79584f3e/ty-0.0.17-py3-none-linux_armv6l.whl", hash = "sha256:64a9a16555cc8867d35c2647c2f1afbd3cae55f68fd95283a574d1bb04fe93e0", size = 10192793, upload-time = "2026-02-13T13:27:13.943Z" }, { url = "https://files.pythonhosted.org/packages/4c/a5/43746c1ff81e784f5fc303afc61fe5bcd85d0fcf3ef65cb2cef78c7486c7/ty-0.0.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f18f5fd927bc628deb9ea2df40f06b5f79c5ccf355db732025a3e8e7152801f6", size = 9564639, upload-time = "2026-02-13T13:26:42.781Z" }, { url = "https://files.pythonhosted.org/packages/d6/b8/280b04e14a9c0474af574f929fba2398b5e1c123c1e7735893b4cd73d13c/ty-0.0.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5383814d1d7a5cc53b3b07661856bab04bb2aac7a677c8d33c55169acdaa83df", size = 10061204, upload-time = "2026-02-13T13:27:00.152Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d7/493e1607d8dfe48288d8a768a2adc38ee27ef50e57f0af41ff273987cda0/ty-0.0.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c20423b8744b484f93e7bf2ef8a9724bca2657873593f9f41d08bd9f83444c9", size = 10013116, upload-time = "2026-02-13T13:26:34.543Z" }, + { url = "https://files.pythonhosted.org/packages/75/ce/744b15279a11ac7138832e3a55595706b4a8a209c9f878e3ab8e571d9032/ty-0.0.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:488bce1a9bea80b851a97cd34c4d2ffcd69593d6c3f54a72ae02e5c6e47f3d0c", size = 11069750, upload-time = "2026-02-13T13:26:48.638Z" }, + { url = "https://files.pythonhosted.org/packages/f2/be/1133c91f15a0e00d466c24f80df486d630d95d1b2af63296941f7473812f/ty-0.0.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8df66b91ec84239420985ec215e7f7549bfda2ac036a3b3c065f119d1c06825a", size = 10870862, upload-time = "2026-02-13T13:26:54.715Z" }, { url = "https://files.pythonhosted.org/packages/3e/4a/a2ed209ef215b62b2d3246e07e833081e07d913adf7e0448fc204be443d6/ty-0.0.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:002139e807c53002790dfefe6e2f45ab0e04012e76db3d7c8286f96ec121af8f", size = 10628118, upload-time = "2026-02-13T13:26:45.439Z" }, { url = "https://files.pythonhosted.org/packages/b3/0c/87476004cb5228e9719b98afffad82c3ef1f84334bde8527bcacba7b18cb/ty-0.0.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6c4e01f05ce82e5d489ab3900ca0899a56c4ccb52659453780c83e5b19e2b64c", size = 10038185, upload-time = "2026-02-13T13:27:02.693Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/98f0b3ba9aef53c1f0305519536967a4aa793a69ed72677b0a625c5313ac/ty-0.0.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2b226dd1e99c0d2152d218c7e440150d1a47ce3c431871f0efa073bbf899e881", size = 10047644, upload-time = "2026-02-13T13:27:05.474Z" }, { url = "https://files.pythonhosted.org/packages/7c/79/e2a606bd8852383ba9abfdd578f4a227bd18504145381a10a5f886b4e751/ty-0.0.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c04e196809ff570559054d3e011425fd7c04161529eb551b3625654e5f2434cb", size = 10718344, upload-time = "2026-02-13T13:26:51.66Z" }, ] [[package]] name = "typeguard" -version = "4.5.1" +version = "4.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/e8/66e25efcc18542d58706ce4e50415710593721aae26e794ab1dec34fb66f/typeguard-4.5.1.tar.gz", hash = "sha256:f6f8ecbbc819c9bc749983cc67c02391e16a9b43b8b27f15dc70ed7c4a007274", size = 80121, upload-time = "2026-02-19T16:09:03.392Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/1c/dfba5c4633cafc4c701f237d2ba63b416805047fd6d96aab4cfc40969f98/typeguard-4.5.2.tar.gz", hash = "sha256:5a16dcac23502039299c97c8941651bc33d7ea8cc4b2f7d6bbb1b528f6eea423", size = 80240, upload-time = "2026-05-14T12:59:40.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl", hash = "sha256:44d2bf329d49a244110a090b55f5f91aa82d9a9834ebfd30bcc73651e4a8cc40", size = 36745, upload-time = "2026-02-19T16:09:01.6Z" }, + { url = "https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl", hash = "sha256:fcf9de18bd945cdb4c7b996e12b4c51ce83f92f191314a6d7cf1739586ec98cf", size = 36748, upload-time = "2026-05-14T12:59:39.473Z" }, ] [[package]] name = "typer" -version = "0.24.1" +version = "0.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc", 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')" }, @@ -10822,9 +11178,9 @@ dependencies = [ { name = "rich", 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 = "shellingham", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, ] [[package]] @@ -10849,15 +11205,15 @@ s3 = [ [[package]] name = "types-aiobotocore" -version = "3.3.0" +version = "3.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore-stubs", 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 = "typing-extensions", marker = "(python_full_version < '3.12' and platform_machine == 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e7/93/e22753dc6b941093f19f0bfe87af5424e00310eaf52dd7d0d8306a6fe094/types_aiobotocore-3.3.0.tar.gz", hash = "sha256:c754c2888631d56c370cab4d2108da2bfd3afe80049303fb7132004ead3b21d6", size = 86908, upload-time = "2026-03-19T02:35:49.176Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/e8/ef1fcb876937dbdddc0f01b5df4ed53f33b166a6367d80a9014d5e5f091d/types_aiobotocore-3.7.0.tar.gz", hash = "sha256:fe35de52c12e5fdb89ca60b3989766e7fe827e3d2e95fcf4583e91581945205c", size = 87992, upload-time = "2026-05-10T03:19:32.353Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/c7/53a786a82bde6307fd79059357c1d2f510667019d78dd71d8787c49bec7f/types_aiobotocore-3.3.0-py3-none-any.whl", hash = "sha256:017e9666d5cba2c26134256ad5e4efb320a68352358b9f3257b4e2aae3fb4c18", size = 54364, upload-time = "2026-03-19T02:35:45.567Z" }, + { url = "https://files.pythonhosted.org/packages/f9/68/0cdfd7df415ee3e769c8e8f9bd8013c64c88cdd7306f72453a02123c58f9/types_aiobotocore-3.7.0-py3-none-any.whl", hash = "sha256:ff4139b3eae22d242b6b39ba56048344b2b86f67daeeca4680da1a6e191681fd", size = 54804, upload-time = "2026-05-10T03:19:29.487Z" }, ] [[package]] @@ -10874,49 +11230,49 @@ wheels = [ [[package]] name = "types-awscrt" -version = "0.31.3" +version = "0.34.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/26/0aa563e229c269c528a3b8c709fc671ac2a5c564732fab0852ac6ee006cf/types_awscrt-0.31.3.tar.gz", hash = "sha256:09d3eaf00231e0f47e101bd9867e430873bc57040050e2a3bd8305cb4fc30865", size = 18178, upload-time = "2026-03-08T02:31:14.569Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/59/44409a8fc06b444ab1a6f71dcb29d49a6e17e02424345eb51b051bebb345/types_awscrt-0.34.1.tar.gz", hash = "sha256:559aa04250f6a419a617dfb788f3e10903aaf74700ef23e521b64a411b83b803", size = 19062, upload-time = "2026-06-05T04:40:10.689Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/e5/47a573bbbd0a790f8f9fe452f7188ea72b212d21c9be57d5fc0cbc442075/types_awscrt-0.31.3-py3-none-any.whl", hash = "sha256:e5ce65a00a2ab4f35eacc1e3d700d792338d56e4823ee7b4dbe017f94cfc4458", size = 43340, upload-time = "2026-03-08T02:31:13.38Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b1/214b12162b452ed6acd230065e6c587cde6b96871e3ce6d653f40888f8df/types_awscrt-0.34.1-py3-none-any.whl", hash = "sha256:20c752b6031544d8f694803c35174aee129f1be5ddf886ae46d22f7ffd9b7d75", size = 45688, upload-time = "2026-06-05T04:40:09.198Z" }, ] [[package]] name = "types-docker" -version = "7.1.0.20260328" +version = "7.1.0.20260518" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-paramiko", 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 = "types-requests", 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 = "urllib3", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/de/5f13b22eca604e58af908fbef60f4704a28bcc87ca1d9241d5b8bd433c85/types_docker-7.1.0.20260328.tar.gz", hash = "sha256:6e1a614685bc494580226891da1d01e0cfca6bd00b2761d8239ef09806b2154b", size = 32930, upload-time = "2026-03-28T04:08:02.78Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/71/ee4bd2b713f0b3b0cecc48fd13952b244f187c3b68cfa0408360f102fde8/types_docker-7.1.0.20260518.tar.gz", hash = "sha256:d194c4b82a4110fc58c84af05a5d9de6d3e54f94357ff745d01b5dca12c8eaaa", size = 33868, upload-time = "2026-05-18T06:08:19.78Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/64/cb764366d6d76495a0ffbda4bdedc5663d585215d0dd8ac808ed6484b48e/types_docker-7.1.0.20260328-py3-none-any.whl", hash = "sha256:3fb95d3ad63fae06d9d5cb18a0394252453b7c06d843bed567d634aa8ede3ebc", size = 47466, upload-time = "2026-03-28T04:08:01.519Z" }, + { url = "https://files.pythonhosted.org/packages/1f/1d/1d174b6f94afe0de50189de0a9dfd4531d2f8170e6dd9ef68ab616aa770b/types_docker-7.1.0.20260518-py3-none-any.whl", hash = "sha256:59893937816bfe40eb2915c84b31c3131107537500287c47d4a10dd7a9d59deb", size = 48183, upload-time = "2026-05-18T06:08:18.89Z" }, ] [[package]] name = "types-paramiko" -version = "4.0.0.20260322" +version = "4.0.0.20260518" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/cc/b83f1c085cc2c4d85f4ba2f799d1b18840b768d7b1a7dfb7d5cc5470fbc9/types_paramiko-4.0.0.20260322.tar.gz", hash = "sha256:dfcb13d8cf52499a198ced552b78fa685369a376b143abfb90cd49f465e383a0", size = 29040, upload-time = "2026-03-22T04:08:47.815Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/b6/4cdb11fb6006be6309844dc5f88b6efef3f5ea5352ade42a1e9308570e28/types_paramiko-4.0.0.20260518.tar.gz", hash = "sha256:286f6830945cba63797eedf375ed87138d93198121253afe66c5d6dbcf91318d", size = 29193, upload-time = "2026-05-18T06:06:36.776Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/92/10415430e8035fe0155582757d9829784202bb198ed84fe9afa5e59d5263/types_paramiko-4.0.0.20260322-py3-none-any.whl", hash = "sha256:c585bcf81b5d2fc722279763d50eca8095777ee949af8706900e9f8411af979b", size = 38809, upload-time = "2026-03-22T04:08:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/44/a2/1a54b77758c9c175526bd0448de353f0563e71ba1ddc8bd4ac0c835deafd/types_paramiko-4.0.0.20260518-py3-none-any.whl", hash = "sha256:0ffaf1a6eb796833a49653cba4c7be13af51c8269d75234972d6239763dda270", size = 38791, upload-time = "2026-05-18T06:06:35.771Z" }, ] [[package]] name = "types-requests" -version = "2.33.0.20260327" +version = "2.33.0.20260518" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/5f/2e3dbae6e21be6ae026563bad96cbf76602d73aa85ea09f13419ddbdabb4/types_requests-2.33.0.20260327.tar.gz", hash = "sha256:f4f74f0b44f059e3db420ff17bd1966e3587cdd34062fe38a23cda97868f8dd8", size = 23804, upload-time = "2026-03-27T04:23:38.737Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/01/c5a19253fe1ac159159ddf9a3a07cec8bb5e486ec4d9002ad2821da0e5d2/types_requests-2.33.0.20260518.tar.gz", hash = "sha256:df7bd3bfe0ca8402dfb841e7d9be714bb5578203283d66d7dc4ef69343449a5e", size = 24752, upload-time = "2026-05-18T06:07:37.966Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/55/951e733616c92cb96b57554746d2f65f4464d080cc2cc093605f897aba89/types_requests-2.33.0.20260327-py3-none-any.whl", hash = "sha256:fde0712be6d7c9a4d490042d6323115baf872d9a71a22900809d0432de15776e", size = 20737, upload-time = "2026-03-27T04:23:37.813Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl", hash = "sha256:626d697d1adaaff76e2044dc8c5c051d8f21abc157bdfe204a75558076fe0bf0", size = 21391, upload-time = "2026-05-18T06:07:37.044Z" }, ] [[package]] @@ -10978,11 +11334,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2025.3" +version = "2026.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, ] [[package]] @@ -10996,16 +11352,16 @@ wheels = [ [[package]] name = "uncalled-for" -version = "0.2.0" +version = "0.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/7c/b5b7d8136f872e3f13b0584e576886de0489d7213a12de6bebf29ff6ebfc/uncalled_for-0.2.0.tar.gz", hash = "sha256:b4f8fdbcec328c5a113807d653e041c5094473dd4afa7c34599ace69ccb7e69f", size = 49488, upload-time = "2026-02-27T17:40:58.137Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/82/345cc927f7fbdae6065e7768759932fcc827fc20b29b45dfbafa2f1f7da4/uncalled_for-0.3.2.tar.gz", hash = "sha256:89f5dbcd71e2b8f47c030b1fa302e6cce2ec795d1ac565eeb6525c5fe55cb8a2", size = 50032, upload-time = "2026-05-06T13:38:25.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/7f/4320d9ce3be404e6310b915c3629fe27bf1e2f438a1a7a3cb0396e32e9a9/uncalled_for-0.2.0-py3-none-any.whl", hash = "sha256:2c0bd338faff5f930918f79e7eb9ff48290df2cb05fcc0b40a7f334e55d4d85f", size = 11351, upload-time = "2026-02-27T17:40:56.804Z" }, + { url = "https://files.pythonhosted.org/packages/3b/25/2c87754f3a9e692315f7b811244090e68f362979fc8886b3fbd2985a1d8c/uncalled_for-0.3.2-py3-none-any.whl", hash = "sha256:0ff60b142c7d1f8070bde9d42afaa70aedc77dcc10998c227687e9c15713418e", size = 11444, upload-time = "2026-05-06T13:38:24.025Z" }, ] [[package]] name = "unsloth" -version = "2026.6.1" +version = "2025.9.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accelerate", 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')" }, @@ -11014,31 +11370,26 @@ dependencies = [ { name = "diffusers", 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 = "hf-transfer", 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 = "huggingface-hub", 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 = "nest-asyncio", 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 = "numpy", 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 = "packaging", 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 = "peft", 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 = "psutil", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "pydantic", extra = ["email"], marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "pyyaml", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "sentencepiece", 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 = "torch", 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 = "torchvision", 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 = "tqdm", 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 = "transformers", 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 = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and 'linux' in sys_platform" }, - { name = "triton", version = "3.7.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'arm64' and sys_platform == 'darwin' and 'linux' in sys_platform) or (platform_machine == 'aarch64' and sys_platform == 'linux' and 'linux' in sys_platform)" }, + { name = "triton", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "trl", 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 = "typer", 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 = "tyro", 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 = "unsloth-zoo", 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 = "wheel", 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 = "xformers", marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and 'linux' in sys_platform" }, + { name = "xformers", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/a9/08c3ad96aaf2d29c3d011f0824b7f1617a4ea0afae815bd0295bc8ecf0ec/unsloth-2026.6.1.tar.gz", hash = "sha256:f4ce75af8349500d9927f28ac5872d9a627e718b65604646c41c1717107bb81d", size = 76174449, upload-time = "2026-06-03T14:33:35.197Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/b5/5eb5da36873df1544eaf38522f078db6d1ae1c763f5c2231a006decaf897/unsloth-2025.9.5.tar.gz", hash = "sha256:7863edb453f265ebaa7a0ee7750a6540dec1eaeea87025eabba97d6138ea14df", size = 269444, upload-time = "2025-09-15T11:07:50.953Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/14/00e0318306ed536ce915f8b895fd6206a3a2029e8749b6d85b7fccbf0201/unsloth-2026.6.1-py3-none-any.whl", hash = "sha256:5084fff4571d527f4a61ded79e628243c13234e5da6d76705fcc54cd9e78c654", size = 71613685, upload-time = "2026-06-03T14:33:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/5fa313bae3ca270f784b46ecff9feec8fceb074bd2664c60c76bd647fb87/unsloth-2025.9.5-py3-none-any.whl", hash = "sha256:7d920353ed1eed28c2ca0676091b9e21d562c06cae758b304e285be97d463e37", size = 309994, upload-time = "2025-09-15T11:07:47.846Z" }, ] [package.optional-dependencies] @@ -11048,21 +11399,16 @@ huggingface = [ { name = "diffusers", 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 = "hf-transfer", 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 = "huggingface-hub", 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 = "nest-asyncio", 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 = "numpy", 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 = "packaging", 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 = "peft", 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 = "psutil", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "pydantic", extra = ["email"], marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "pyyaml", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "sentence-transformers", 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 = "sentencepiece", 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 = "torchvision", 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 = "tqdm", 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 = "transformers", 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 = "trl", 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 = "typer", 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 = "tyro", 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 = "unsloth-zoo", 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 = "wheel", 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')" }, @@ -11070,41 +11416,36 @@ huggingface = [ [[package]] name = "unsloth-zoo" -version = "2026.6.1" +version = "2025.9.12" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "accelerate", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "cut-cross-entropy", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "accelerate", 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 = "cut-cross-entropy", 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 = "datasets", 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 = "filelock", 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 = "hf-transfer", 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 = "huggingface-hub", 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 = "mlx", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "mlx-lm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, - { name = "mlx-vlm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, { name = "msgspec", 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 = "numpy", 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 = "packaging", 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 = "peft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "peft", 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 = "pillow", 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 = "psutil", 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 = "regex", 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 = "sentencepiece", 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 = "torch", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "torchao", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "torch", 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 = "torchao", 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 = "tqdm", 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 = "transformers", 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 = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and 'linux' in sys_platform" }, - { name = "triton", version = "3.7.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'arm64' and sys_platform == 'darwin' and 'linux' in sys_platform) or (platform_machine == 'aarch64' and sys_platform == 'linux' and 'linux' in sys_platform)" }, - { name = "trl", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "triton", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "trl", 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 = "typing-extensions", 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 = "tyro", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "tyro", 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 = "wheel", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/83/c22583e8d9f1c88c0cb33542c48c0a364ba50e64891a5df7788f09b42742/unsloth_zoo-2026.6.1.tar.gz", hash = "sha256:6feaeb6cac35cd56db2c35b0d36eb9594efd12ffaae8c2287c2415677af66711", size = 846182, upload-time = "2026-06-03T13:34:40.035Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/a5/3c2f8cd8ade5d6c1ad3c686bf0be109904e45e701e0ab7561c20ac713826/unsloth_zoo-2025.9.12.tar.gz", hash = "sha256:9a9ca709c739d998cb2b79a2dee92b169375ee232ab0cfe5ed47c8d531e04d9a", size = 227231, upload-time = "2025-09-26T15:24:04.67Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/11/4306117c203a74ab5dcb33eba33d6b6ba327e4712c2a9ed5935461d6c862/unsloth_zoo-2026.6.1-py3-none-any.whl", hash = "sha256:0ac07a2ab9aba8fb360dd477074cfd810009c7b52f2c78b69cf92fdc7a69aa8c", size = 924415, upload-time = "2026-06-03T13:34:38.212Z" }, + { url = "https://files.pythonhosted.org/packages/a2/37/ca3503aa67effb62f4ab85c7b5574f3414a6909d5b8bc1ff31b1d3801f1a/unsloth_zoo-2025.9.12-py3-none-any.whl", hash = "sha256:cc611b20bb29ea81312dd45e07a537fa2099b0b18a20492d9666641d60833fab", size = 247744, upload-time = "2025-09-26T15:24:02.52Z" }, ] [[package]] @@ -11127,31 +11468,61 @@ wheels = [ [[package]] name = "uuid-utils" -version = "0.14.1" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" } -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/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/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" }, - { url = "https://files.pythonhosted.org/packages/91/f9/6c64bdbf71f58ccde7919e00491812556f446a5291573af92c49a5e9aaef/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b197cd5424cf89fb019ca7f53641d05bfe34b1879614bed111c9c313b5574cd8", size = 591617, upload-time = "2026-02-20T22:50:24.532Z" }, - { url = "https://files.pythonhosted.org/packages/85/89/d91862b544c695cd58855efe3201f83894ed82fffe34500774238ab8eba7/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b5d2ad28063d422ccc2c28d46471d47b61a58de885d35113a8f18cb547e25bf", size = 337678, upload-time = "2026-02-20T22:50:39.768Z" }, - { url = "https://files.pythonhosted.org/packages/77/a1/0857f64d53a90321e6a46a3d4cc394f50e1366132dcd2ae147f9326ca98b/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dbe718765f70f5b7f9b7f66b6a937802941b1cc56bcf642ce0274169741e01", size = 338902, upload-time = "2026-02-20T22:50:33.927Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/01/a1/822ceef22d1c139cffebe4b1b660cfaa10253d5c770aa2598dc8e9497593/uuid_utils-0.16.0.tar.gz", hash = "sha256:d6902d4375dfba4c9902c736bb82d3c040417b67f7d0fa48910ddfdb1ac95de7", size = 42596, upload-time = "2026-05-19T07:44:23.28Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/24/0e18177e2fbb0b9f54f90fd48fe3302dfda731e22ad650d6e6f8f4b3d3d3/uuid_utils-0.16.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:04af9966ecd82b78eeba5725e29aa1e86fb8eb84b5443dd6a9935f9fadb6678e", size = 565929, upload-time = "2026-05-19T07:44:06.496Z" }, + { url = "https://files.pythonhosted.org/packages/69/2a/47ee18b294af59754ef5acfa96eb027137c98cef7521199b6f70be705de4/uuid_utils-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f504efeb20ffd9571621658f7c8093c646d33150406d5742e49ff7cd861615", size = 328059, upload-time = "2026-05-19T07:45:30.533Z" }, + { url = "https://files.pythonhosted.org/packages/89/7c/ed6d8bb48eeecaed6722af1187d722c5243334be750419d10d5f05dffeb2/uuid_utils-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57d85f48535dc541060f6b82f277cbcd12b78c04008ccc1039546cfcec027327", size = 334759, upload-time = "2026-05-19T07:45:07.715Z" }, + { url = "https://files.pythonhosted.org/packages/ff/33/371bddf9fd47e045c375df9668eea0d96ce9201ab6a03985b0155498e376/uuid_utils-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39453f1ebf4398fbeb71607f3437e2ac469c9e38b5921755c1e17ad0158a8907", size = 448927, upload-time = "2026-05-19T07:45:11.464Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f1/b201d5ee005d4987fc072714fcb9f6e75303520cf19d4deec0b4df44bf40/uuid_utils-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50361aca5c2a770728a6343df85109fe57f89ac026827f34fe0153563cdc9ce7", size = 327178, upload-time = "2026-05-19T07:44:02.255Z" }, + { url = "https://files.pythonhosted.org/packages/2c/19/25db019727d14630c75c2a75a8ea66dd712bb468adcf410bac8d01ff19fd/uuid_utils-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ceef237cf8467fddbf6d8466cc1f6e2c04605ec919046ef5eba10a895b559fcf", size = 504686, upload-time = "2026-05-19T07:43:46.43Z" }, + { url = "https://files.pythonhosted.org/packages/5d/93/c000cd42ebfdd37cc74981ed31c979a1270156572bdebab8b5d61460e750/uuid_utils-0.16.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:24e6fa0d0ade7a9ad60a3c296022474983243df5b4e863babb4828a85ef2e52c", size = 610102, upload-time = "2026-05-19T07:45:53.765Z" }, + { url = "https://files.pythonhosted.org/packages/f1/49/b6a688648368a9cc0137e183657956853a91dc06ef73deda27290d586155/uuid_utils-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2e2f369dd734050fe96ae4905c58779b09276d47d5e9a0e5cd33ec7982784341", size = 532255, upload-time = "2026-05-19T07:45:16.936Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4c/b4cf43a5d22bcdb91727acdf54be0d78e83e595b73c5a9a8a4291875f059/uuid_utils-0.16.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:727fae3f0682191ec9c8ce1cd0f71e81b471a2e26b7c5fd66712fc0f11640aa0", size = 562183, upload-time = "2026-05-19T07:45:02.683Z" }, + { url = "https://files.pythonhosted.org/packages/de/43/2dc6c7401c8fab86e46b0b33ada6dcfde949b2fd48877ba6f880862be80e/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9152bff801ec2ccf630df06d67389090a2c612dea87fbf9a887ab4b222929f6f", size = 326171, upload-time = "2026-05-19T07:45:25.186Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f5/48f11fb91f36453611ca148bc441436f279870b1ec6b576dc5167fb6e680/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:06fc7db470c37e5c1ab3fd2cd159697d6f8b279d7d23b5b96bd418b115f8caa9", size = 332222, upload-time = "2026-05-19T07:45:09.036Z" }, + { url = "https://files.pythonhosted.org/packages/30/cb/b2b49528521e4a097f129e8bf7850a26f00af46afba778832cf3458a5c00/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e1a1f57fe3631e164dad27b24aa81267810e20575f705af3b0fa734f3a21247", size = 444801, upload-time = "2026-05-19T07:45:37.517Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b3/a28d9c6f7c701dfe01c8020b30e33899a28eb9e4d056b07e7388f50ebf67/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ee392fe59808a731b7b6bf4d453fb6e833774921331cceae5f254d1e9c5b97d", size = 325594, upload-time = "2026-05-19T07:44:44.682Z" }, + { url = "https://files.pythonhosted.org/packages/ed/57/fb19b7951f66a46e03bd1943a61ee9d59c83e994e56e8c97d79aff1f0e47/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbb92feb4db08cd76e27b4d3b1a82bfde708447317150c614eb9f761a43b387e", size = 502115, upload-time = "2026-05-19T07:43:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/2f/8e/9a129c469b7b77afb62da5c6b7e92591073b845bd0c3108c0d0aa65389fb/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c3c5afaaa68b1d6393d653e9fc93a2fde9da1681da01f74b4593f41d31fb5f1", size = 607433, upload-time = "2026-05-19T07:44:11.675Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/68e60ea053ca30f35df877b96001331398140d5c4983561affa1350331b1/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41a67e546d9adf11c4e4cb5c8e81f000f8b1f000c17912ced089b499855719a5", size = 530645, upload-time = "2026-05-19T07:45:49.278Z" }, + { url = "https://files.pythonhosted.org/packages/60/9b/74c1f47a9b4f138a254e51528e5ffaeba6bf99ecead9f0c4b6fccccfbfcb/uuid_utils-0.16.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d34cf9681e8892fad2a63e393068e544505408748cd8bf0c3517d753a01528d4", size = 563166, upload-time = "2026-05-19T07:44:10.494Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5e/e0323d54321166639eb2be5e8a464f5cb0fc04d72d91f3e78944bb6a1da8/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed45fb8732d216426227096b55accbb87cba57febc86a044d90780b090eb99d0", size = 326328, upload-time = "2026-05-19T07:45:31.901Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a3/046f6cb958467c3bf4a163a8a53b178b64a62e21ed8ad5b2c1dacb3a2cfc/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b617a334bb01ef2ff8c22900f5a14125eb9063f602131494cc9dc59519beaa5b", size = 332322, upload-time = "2026-05-19T07:43:41.284Z" }, + { url = "https://files.pythonhosted.org/packages/67/80/01914e3949744db7acd0006885e5542fbebb6e39114857d007d29b3265c2/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a750d8aeb8ae880aa9a2529606bde0e994bcc7448730c953107f357a28e6102e", size = 445787, upload-time = "2026-05-19T07:45:36.102Z" }, + { url = "https://files.pythonhosted.org/packages/14/ef/f6908f41279f205d70c8a0d5dcb25dd6802741d7f88e3f0123453c3584d3/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a250e111903c4368745fce5ac2aa607bd477c62d3307e45347338fdb64b38e0", size = 324678, upload-time = "2026-05-19T07:45:12.77Z" }, + { url = "https://files.pythonhosted.org/packages/e6/31/3b5c60172b8c57bf4ca485484b8e4edef550ca324f9287f1183be97422e2/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:420aa3ca403cedb73490b6ea3aeefeea7e0455f5ce60bbf856390ee872ae3306", size = 502456, upload-time = "2026-05-19T07:45:00.821Z" }, + { url = "https://files.pythonhosted.org/packages/88/bf/3da8d497af80fd51d8bf85551c77ede67f07825924ec5987bf9b6031014a/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b8a9a7b1065a12d40f2cc25b7d705ab34954cc57095034367bca39ebcf4a876b", size = 607727, upload-time = "2026-05-19T07:44:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5f/af955feae69cce7fd2121ca3f790ff4b85ad2e17b2149546f50753e1a047/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c8083284488b84ad178e74add64cfd1e74e8be5e30821e5acbc5019281c658b0", size = 529986, upload-time = "2026-05-19T07:45:57.85Z" }, + { url = "https://files.pythonhosted.org/packages/10/cf/3fec757e51bef10eb41ae8075f5442c60e85ff456b42d16a3063f5dc6c80/uuid_utils-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:27a071a899ba46a551d6524dbbc5a98b88be176d0f55ddf72cf71c005326ac10", size = 98683, upload-time = "2026-05-19T07:44:16.369Z" }, + { url = "https://files.pythonhosted.org/packages/21/05/ca6d60705e71fdeaa3431dad94e279a8213c5573cb2925e1aabf3dc0330a/uuid_utils-0.16.0-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73486b6aa3f755a6c97000f5ea67e7ac78d6df89bf22980789a1e943e24b74f0", size = 564408, upload-time = "2026-05-19T07:44:38.351Z" }, + { url = "https://files.pythonhosted.org/packages/f2/33/a53afeef1a56051551a0f5a801e4bce411dd73c6a8c99bad16902651256d/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9346ce6eb1fbd8b03a6b331d66016afcb4edcdff6eac708e21391600529a016a", size = 325762, upload-time = "2026-05-19T07:45:18.261Z" }, + { url = "https://files.pythonhosted.org/packages/72/ca/4462a4f36365d7ee72d41e05e6bcfe127e861b073ab37c25b2c8a518317c/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a0fc6eb3fd821466fbab69cf356c6ec2b7327266bbbc740a2eb57c77c4bef965", size = 332359, upload-time = "2026-05-19T07:45:34.886Z" }, + { url = "https://files.pythonhosted.org/packages/c5/67/9d3373fa7c5a746fdecc64e30caf915c29eb632203508d87676f9243ed03/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13a797e5e8f0dadc18351a5aa013815ddac25dce6864072a539d510910c95f71", size = 445483, upload-time = "2026-05-19T07:44:49.598Z" }, + { url = "https://files.pythonhosted.org/packages/57/08/ce01aa6d897fc7f875844fe58cad0a542c8ebf089d9242b654b56260ecb8/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57c3583b1f1c00a94f59726a5e2b988fa209221143919a1af5c2fc24e318fc98", size = 326281, upload-time = "2026-05-19T07:44:59.677Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9b/c1ed447328b32229cca38ac4c62d309eab006e5e9c4020e2056a175bc607/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:91db59bad97ed2b9d2c6ed25082fe9762b2c422e694fe06786b28cf4e776ac4c", size = 502088, upload-time = "2026-05-19T07:44:09.208Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e0/8442f4efe7bde72f0b4ae5f675d0c7fbe209ad0b54718b8ddf43c46c6fae/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:41985e342a30e76366a8becc60bbdb07d72cd1b86ec657b1f31654e9fb1baada", size = 607631, upload-time = "2026-05-19T07:44:19.384Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/1bcfdb9d539bd42736dd6076470a42fbb5db23f79712c0a06aa0a3752f7b/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:26fe23ab60f05de4ad70aaa5b6a4c2a7bbd43055e3dd6f6b31efba0532ac9c71", size = 530971, upload-time = "2026-05-19T07:45:06.348Z" }, + { url = "https://files.pythonhosted.org/packages/d3/89/655408a5485c56bf2c4561eb85f5bca119b1f4020370b4daaeb8d13e46fb/uuid_utils-0.16.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4e35e9a986e86806a61288fac3afbb51317f2580929feefd1661891ffd7b8c24", size = 569295, upload-time = "2026-05-19T07:45:22.325Z" }, + { url = "https://files.pythonhosted.org/packages/dd/75/4267ab8baa1e6a8ad7c262e204484b44df0fde0920025ea9b43c2b869726/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4fd5c7936a876ba2606ba124603b559a5c2cea458c59b9c31677e6acc3c53cc", size = 329619, upload-time = "2026-05-19T07:44:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/15/77/c794102831e331564f651099cac55006694677938d70f1033b35da451a89/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:130f7452c1b87b7c16d0bdc1f32a1de531ae4cc4220ed4e691402bbcfc39e0a9", size = 335121, upload-time = "2026-05-19T07:45:47.974Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3e/458a0a2da75c596b151182a6c7550c6c3d30f479e14e40f69c0336579e59/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5ee0bbbd4ca3968422cd8308f0072520bc73dc760cb26c6fa75ca1aca14d210", size = 449631, upload-time = "2026-05-19T07:45:50.645Z" }, + { url = "https://files.pythonhosted.org/packages/ed/15/dd1fab6f7fcd15f2c331d0c1f0f516bb1113a640216460f82be53db3dcf8/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc0824a31898ef46a9d84d748c3abe27cdb615ac3773c53cc1f84fc8e66dc7c4", size = 328418, upload-time = "2026-05-19T07:44:52.38Z" }, ] [[package]] name = "uvicorn" -version = "0.42.0" +version = "0.49.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", 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 = "h11", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" }, ] [package.optional-dependencies] @@ -11198,7 +11569,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.2.0" +version = "21.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib", 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')" }, @@ -11206,23 +11577,23 @@ dependencies = [ { name = "platformdirs", 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-discovery", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/0d/4e93c8e6d1001a75763f87d8f5ecda8ebc7f4aa2153dddfaf4ae8892821a/virtualenv-21.4.2.tar.gz", hash = "sha256:38e6ee0a555615c0ea9da2ac7e9998fe8dc3b911dd33ad8eaad2020957653b0c", size = 7613326, upload-time = "2026-05-31T17:01:22.827Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/557dc082be035381b85fdb2b74e21d3d21b57750b74f2b47a32f3a639ff9/virtualenv-21.4.2-py3-none-any.whl", hash = "sha256:854210ca524a1a4d0d744734f4acbc721c3ffe163b85bbf5d56d14d5ae2f0fae", size = 7594079, upload-time = "2026-05-31T17:01:20.735Z" }, ] [[package]] name = "wasmtime" -version = "43.0.0" +version = "45.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/0e/967542865d59d9529bab604b9b88f09a92636e69cc4b1d30c5013e854493/wasmtime-43.0.0.tar.gz", hash = "sha256:eb98b8e2bc35d03dd69c9dd095a388044323622526fc94a9406b8efc48ddc259", size = 117449, upload-time = "2026-03-31T19:26:23.663Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/ff/db9cfc61d988bc15303134bb174176a29839976876dfd18c3a12548ad291/wasmtime-45.0.0.tar.gz", hash = "sha256:2ad4bf7ca286ceea35c1e420d10b368d7f83faf9a5ffde87b4ee334a9b7f55f3", size = 128297, upload-time = "2026-05-26T17:57:39.131Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/ca/67db17c3f098894be798457ce261816fb67c0c1b80c1a53ed1dfa8ed4ff1/wasmtime-43.0.0-py3-none-any.whl", hash = "sha256:9441349d9346230420ed24d357d6f8330fe7251ac5938bb892147728bbe731d7", size = 6472597, upload-time = "2026-03-31T19:26:06.61Z" }, - { url = "https://files.pythonhosted.org/packages/08/42/d9588fa6dad9a609e5acaa72d1d5b346b2913f87c2e95d0c7ddadf5e919b/wasmtime-43.0.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a03c7aa03519df58fed5115ad8093d6deac46386115add715e725448e89ab25", size = 6615055, upload-time = "2026-03-31T19:26:10.506Z" }, - { url = "https://files.pythonhosted.org/packages/48/a9/25b27545ad916a169583dbea41a6a03c58fe04c1d05fa39797dc43bd50b9/wasmtime-43.0.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:341542e87caf1f2ef7ff648a78827fcef5751e3e9be2ee07a1fcf3a04413c213", size = 7819110, upload-time = "2026-03-31T19:26:12.335Z" }, - { url = "https://files.pythonhosted.org/packages/d8/9a/4d8760f827931b5b265b83e52316d40b8e0eb999bb8e2d457c2ae172d5cc/wasmtime-43.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:30b042fd4a05d0f8a320baed53fcb971aff8a3789ed6967f4521f87931ace717", size = 6910375, upload-time = "2026-03-31T19:26:14.207Z" }, - { url = "https://files.pythonhosted.org/packages/ce/19/81c748c089a693b102f9a6239f2558a0ffd55fc721fcdd139361aaede1a1/wasmtime-43.0.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:34ff18384ad62625cb1438fd0266f6c74b4a72ddcb8ba30c60a66be3632db44b", size = 6938286, upload-time = "2026-03-31T19:26:15.898Z" }, - { url = "https://files.pythonhosted.org/packages/0f/fa/c37e77c907567a8802696f9ab839b719ea811cf3d59ffc815cc95d894339/wasmtime-43.0.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c7025d477d807df30dad07c9318ea747c6cfc99764c7cb2a8e44e75b8c43e3be", size = 7852033, upload-time = "2026-03-31T19:26:17.915Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c7/7594da7fa8a3bc5e765733ad57aac9b7b27262c4afa47521bd500e4a4574/wasmtime-45.0.0-py3-none-any.whl", hash = "sha256:6251ee5074a8b8bfaa98e6e99cb5d49d6d0f2320b3265d5aa6c2ee5df5fb4519", size = 8019034, upload-time = "2026-05-26T17:57:20.138Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0b/a81b5daf5adea482ecb68d9615f6a348486ab4d8e980a915d4420e57ee4d/wasmtime-45.0.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:31d10f25c330cebcfb364e9a357123deeec96c41725ff2bba91b705587f38a93", size = 8255954, upload-time = "2026-05-26T17:57:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8c/e9019a28e908214031310aefd78e4755221d02303190b54b2c85cb69573e/wasmtime-45.0.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:5d1416ec6da8cd87c29e2e9eb074358c91839c2fff971fe428c8921eaae68e73", size = 9681185, upload-time = "2026-05-26T17:57:26.641Z" }, + { url = "https://files.pythonhosted.org/packages/42/56/ed5f492bd553a31c8e28d621f8256f2c7b1a133b28f73525d96ca355891a/wasmtime-45.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:a499f6ab0eebb70dca83d6a4904b743cd122f322af3abe86af08ad753533d946", size = 8582001, upload-time = "2026-05-26T17:57:28.883Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/9b41740da83f51014b88181c9086de0ed75d736a5329baff7323c4fb6eff/wasmtime-45.0.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bef65282b7de744106a91da43e4d06ba19d2d587bc54abb83b3e757f0c4fc030", size = 8633462, upload-time = "2026-05-26T17:57:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/ea/63/49d8317706a108d9ed1d4166d0fc710796da1b20e591a98a96575dec367a/wasmtime-45.0.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a0b6ca14b4628a5d1ffa91ccf2c0f2c58fa171f126ec085d564b09d5795395dd", size = 9712524, upload-time = "2026-05-26T17:57:33.839Z" }, ] [[package]] @@ -11238,50 +11609,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, ] [[package]] name = "watchfiles" -version = "1.1.1" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, - { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, - { 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/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/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/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" }, - { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, - { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5", size = 394562, upload-time = "2026-05-18T04:30:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a", size = 456611, upload-time = "2026-05-18T04:30:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1", size = 461379, upload-time = "2026-05-18T04:31:29.292Z" }, + { url = "https://files.pythonhosted.org/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b", size = 575255, upload-time = "2026-05-18T04:30:40.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5", size = 467052, upload-time = "2026-05-18T04:31:17.942Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e", size = 456858, upload-time = "2026-05-18T04:30:30.182Z" }, + { url = "https://files.pythonhosted.org/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165", size = 467579, upload-time = "2026-05-18T04:32:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6", size = 633253, upload-time = "2026-05-18T04:31:37.123Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5", size = 660713, upload-time = "2026-05-18T04:31:24.62Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, ] [[package]] name = "wcwidth" -version = "0.6.0" +version = "0.8.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/b4/51fe890511f0f242d07cb1ebe6a5b6db417262b9d2568b460347c57d95cc/wcwidth-0.8.1.tar.gz", hash = "sha256:faf5b4a5366a72dc49cad48cdf21f52bdf63bdda995178e483ba247ff79089b9", size = 1466072, upload-time = "2026-06-08T05:57:23.146Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl", hash = "sha256:f453740b1e4a4f3291faa37944c555d71056c4da08d59809b307ef4feba695c8", size = 323092, upload-time = "2026-06-08T05:57:21.413Z" }, ] [[package]] @@ -11313,44 +11704,41 @@ wheels = [ [[package]] name = "websockets" -version = "16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, - { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, - { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, - { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, - { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, - { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, - { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, - { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, - { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] [[package]] name = "werkzeug" -version = "3.1.7" +version = "3.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/43/76ded108b296a49f52de6bac5192ca1c4be84e886f9b5c9ba8427d9694fd/werkzeug-3.1.7.tar.gz", hash = "sha256:fb8c01fe6ab13b9b7cdb46892b99b1d66754e1d7ab8e542e865ec13f526b5351", size = 875700, upload-time = "2026-03-24T01:08:07.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/b2/0bba9bbb4596d2d2f285a16c2ab04118f6b957d8441566e1abb892e6a6b2/werkzeug-3.1.7-py3-none-any.whl", hash = "sha256:4b314d81163a3e1a169b6a0be2a000a0e204e8873c5de6586f453c55688d422f", size = 226295, upload-time = "2026-03-24T01:08:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, ] [[package]] @@ -11416,8 +11804,8 @@ name = "xformers" version = "0.0.35" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "torch", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "numpy", 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 = "torch", 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')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/5a/6e27734bd793adc44d0b8d294e67cfacf4ec590572c1aef51d683fc7a791/xformers-0.0.35.tar.gz", hash = "sha256:f7fc183a58e4bf0e2ae339a18fb1b1d4a37854c0f2545b4f360fef001646ab76", size = 4258182, upload-time = "2026-02-20T20:33:05.417Z" } wheels = [ @@ -11426,32 +11814,68 @@ wheels = [ [[package]] name = "xxhash" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, - { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, - { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, - { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, - { 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/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/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/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/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/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/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" }, - { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, - { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/2f/e183a1b407002f5af81822bee18b61cdb94b8670208ef34734d8d2b8ebe9/xxhash-3.7.0.tar.gz", hash = "sha256:6cc4eefbb542a5d6ffd6d70ea9c502957c925e800f998c5630ecc809d6702bae", size = 82022, upload-time = "2026-04-25T11:10:32.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/26/4e00c88a6a2c8a759cfb77d2a9a405f901e8aa66e60ef1fd0aeb35edda48/xxhash-3.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea6daa712f4e094a30830cf01e9b47d03b24d05cc9dab8609f0d9a9db8454712", size = 30857, upload-time = "2026-04-25T11:05:49.189Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/96f132c08b1e5951c68691d3b9ec351ec2edc028f6a01fcd294f46b9d9f0/xxhash-3.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:363c139bf15e1ac5f136b981d3c077eb551299b1effede7f12faa010b8590a60", size = 213613, upload-time = "2026-04-25T11:05:52.571Z" }, + { url = "https://files.pythonhosted.org/packages/82/89/d4e92b796c5ed052d29ed324dbfc1dc1188e0c4bf64bebbf0f8fc20698df/xxhash-3.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a778b25874cb0f862eaab5986bff4ca49ffb0def7c0a34c237b948b3c6c775b2", size = 236726, upload-time = "2026-04-25T11:05:54.395Z" }, + { url = "https://files.pythonhosted.org/packages/40/f1/81fc4361921dc6e557a9c60cb3712f36d244d06eeeb71cd2f4252ac42678/xxhash-3.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e1860f1e43d40e9d904cf22d93e587ea42e010ebce4160877e46bcab4bc232a", size = 212443, upload-time = "2026-04-25T11:05:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/afeddd4cff50a332f50d4b8a2e8857673153ab0564ef472fcdeb0b5430df/xxhash-3.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9122ad6f867c4a0f5e655f5c3bdf89103852009dbb442a3d23e688b9e699e800", size = 445793, upload-time = "2026-04-25T11:05:58.953Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d0/3c91e4e6a05ca4d7df8e39ec3a75b713609258ec84705ab34be6430826a1/xxhash-3.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d9110d0c3fb02679972837a033251fd186c529aa62f19c132fc909c74052b8", size = 193937, upload-time = "2026-04-25T11:06:00.546Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3a/a6b0772d9801dd4bea4ca4fd34734d6e9b51a711c8a611a24a79de26a878/xxhash-3.7.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:347a93f2b4ce67ce61959665e32a7447c380f8347e55e100daa23766baacf0e5", size = 285188, upload-time = "2026-04-25T11:06:01.96Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f8/cf8e31fd7282230fe7367cd501a2e75b4b67b222bfc7eacccfc20d2652cb/xxhash-3.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:acbb48679ddf3852c45280c10ff10d52ca2cd1da2e552fb81db1ff786c75d0e4", size = 210966, upload-time = "2026-04-25T11:06:03.453Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f0/fd36cc4a81bf52ee5633275daae2b93dd958aace67fd4f5d466ec83b5f35/xxhash-3.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:fe14c356f8b23ad811dc026077a6d4abccdaa7bce5ca98579605550657b6fcfb", size = 241994, upload-time = "2026-04-25T11:06:05.264Z" }, + { url = "https://files.pythonhosted.org/packages/50/17/a4c865ca22d2da6b1bc7d739bf88cab209533cf52ba06ca9da27c3039bee/xxhash-3.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:693d02c6dc7d1aa0a45921d54cd8c1ff629e09dfdc2238471507af1f7a1c6f04", size = 210917, upload-time = "2026-04-25T11:06:08.853Z" }, + { url = "https://files.pythonhosted.org/packages/49/8b/453b35810d697abac3c96bde3528bece685869227da274eb80a4a4d4a119/xxhash-3.7.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:14bf7a54e43825ec131ee7fe3c60e142e7c2c1e676ad0f93fc893432d15414af", size = 275772, upload-time = "2026-04-25T11:06:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ad/4eed7eab07fd3ee6678f416190f0413d097ab5d7c1278906bf1e9549d789/xxhash-3.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ae3a39a4d96bdb6f8d154fd7f490c4ad06f0532fcd2bb656052a9a7762cf5d31", size = 414068, upload-time = "2026-04-25T11:06:12.511Z" }, + { url = "https://files.pythonhosted.org/packages/d3/4e/fd6f8a680ba248fdb83054fa71a8bfa3891225200de1708b888ef2c49829/xxhash-3.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1cc07c639e3a77ef1d32987464d3e408565b8a3be57b545d3542b191054d9923", size = 191459, upload-time = "2026-04-25T11:06:14.07Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1b/0c2c933809421ffd9bf42b59315552c143c755db5d9a816b2f1ae273e884/xxhash-3.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5e7ce913b61f35b0c1c839a49ac9c8e75dd8d860150688aed353b0ce1bf409d8", size = 30869, upload-time = "2026-04-25T11:06:21.989Z" }, + { url = "https://files.pythonhosted.org/packages/87/ee/2f9f2ed993e77206d1e66991290a1ebe22e843351ca3ebec8e49e01ba186/xxhash-3.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3e7b689c3bce16699efcf736066f5c6cc4472c3840fe4b22bd8279daf4abdac", size = 212977, upload-time = "2026-04-25T11:06:25.019Z" }, + { url = "https://files.pythonhosted.org/packages/de/60/5a91644615a9e9d4e42c2e9925f1908e3a24e4e691d9de7340d565bea024/xxhash-3.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a6545e6b409e3d5cbafc850fb84c55a1ca26ed15a6b11e3bf07a0e0cd84517c8", size = 236373, upload-time = "2026-04-25T11:06:26.482Z" }, + { url = "https://files.pythonhosted.org/packages/22/c0/f3a9384eaaed9d14d4d062a5d953aa0da489bfe9747877aa994caa87cd0b/xxhash-3.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:31ab1461c77a11461d703c88eb949e132a1c6515933cf675d97ec680f4bd18de", size = 212229, upload-time = "2026-04-25T11:06:28.065Z" }, + { url = "https://files.pythonhosted.org/packages/2e/67/02f07a9fd79726804190f2172c4894c3ed9a4ebccaca05653c84beb58025/xxhash-3.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c4d596b7676f811172687ec567cbafb9e4dea2f9be1bbb4f622410cb7f40f40", size = 445462, upload-time = "2026-04-25T11:06:30.048Z" }, + { url = "https://files.pythonhosted.org/packages/40/37/558f5a90c0672fc9b4402dc25d87ac5b7406616e8969430c9ca4e52ee74d/xxhash-3.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13805f0461cba0a857924e70ff91ae6d52d2598f79a884e788db80532614a4a1", size = 193932, upload-time = "2026-04-25T11:06:31.857Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/aaa09cd58661d32044dbbad7df55bbe22a623032b810e7ed3b8c569a2a6f/xxhash-3.7.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d398f372496152f1c6933a33566373f8d1b37b98b8c9d608fa6edc0976f23b2", size = 284807, upload-time = "2026-04-25T11:06:33.697Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f3/53df3719ab127a02c174f0c1c74924fcd110866e89c966bc7909cfa8fa84/xxhash-3.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d610aa62cdb7d4d497740741772a24a794903bf3e79eaa51d2e800082abe11e5", size = 210445, upload-time = "2026-04-25T11:06:35.488Z" }, + { url = "https://files.pythonhosted.org/packages/72/33/d219975c0e8b6fa2eb9ccd486fe47e21bf1847985b878dd2fbc3126e0d5c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:073c23900a9fbf3d26616c17c830db28af9803677cd5b33aea3224d824111514", size = 241273, upload-time = "2026-04-25T11:06:37.24Z" }, + { url = "https://files.pythonhosted.org/packages/c6/75/5f42a1a4c78717d906a4b6a140c6dbf837ab1f547a54d23c4e2903310936/xxhash-3.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:03f8ff4474ee61c845758ce00711d7087a770d77efb36f7e74a6e867301000b8", size = 210709, upload-time = "2026-04-25T11:06:40.958Z" }, + { url = "https://files.pythonhosted.org/packages/8a/85/237e446c25abced71e9c53d269f2cef5bab8a82b3f88a12e00c5368e7368/xxhash-3.7.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:44fba4a5f1d179b7ddc7b3dc40f56f9209046421679b57025d4d8821b376fd8d", size = 275345, upload-time = "2026-04-25T11:06:42.525Z" }, + { url = "https://files.pythonhosted.org/packages/62/34/c2c26c0a6a9cc739bc2a5f0ae03ba8b87deb12b8bce35f7ac495e790dc6d/xxhash-3.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31e3516a0f829d06ded4a2c0f3c7c5561993256bfa1c493975fb9dc7bfa828a1", size = 414056, upload-time = "2026-04-25T11:06:44.343Z" }, + { url = "https://files.pythonhosted.org/packages/a0/aa/5c58e9bc8071b8afd8dcf297ff362f723c4892168faba149f19904132bf4/xxhash-3.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b59ee2ac81de57771a09ecad09191e840a1d2fae1ef684208320591055768f83", size = 191485, upload-time = "2026-04-25T11:06:46.262Z" }, + { url = "https://files.pythonhosted.org/packages/bf/11/4cc834eb3d79f2f2b3a6ef7324195208bcdfbdcf7534d2b17267aa5f3a8f/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:fddbbb69a6fff4f421e7a0d1fa28f894b20112e9e3fab306af451e2dfd0e459b", size = 29624, upload-time = "2026-04-25T11:06:54.311Z" }, + { url = "https://files.pythonhosted.org/packages/23/83/e97d3e7b635fe73a1dfb1e91f805324dd6d930bb42041cbf18f183bc0b6d/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:54876a4e45101cec2bf8f31a973cda073a23e2e108538dad224ba07f85f22487", size = 30638, upload-time = "2026-04-25T11:06:55.864Z" }, + { url = "https://files.pythonhosted.org/packages/f4/40/d84951d80c35db1f4c40a29a64a8520eea5d56e764c603906b4fe763580f/xxhash-3.7.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:0c72fe9c7e3d6dfd7f1e21e224a877917fa09c465694ba4e06464b9511b65544", size = 33323, upload-time = "2026-04-25T11:06:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6e/46b84017b1301d54091430353d4ad5901654a3e0871649877a416f7f1644/xxhash-3.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c3b07cf3362086d8f126c6aecd8e5e9396ad8b2f2219ea7e49a8250c318acd", size = 30874, upload-time = "2026-04-25T11:06:59.834Z" }, + { url = "https://files.pythonhosted.org/packages/f3/29/a804ded9f5d3d3758292678d23e7528b08fda7b7e750688d08b052322475/xxhash-3.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:921c14e93817842dd0dd9f372890a0f0c72e534650b6ab13c5be5cd0db11d47e", size = 213033, upload-time = "2026-04-25T11:07:03.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/91/1ce5a7d2fdc975267320e2c78fc1cecfe7ab735ccbcf6993ec5dd541cb2c/xxhash-3.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e64a7c9d7dfca3e0fafcbc5e455519090706a3e36e95d655cec3e04e79f95aaa", size = 236140, upload-time = "2026-04-25T11:07:05.396Z" }, + { url = "https://files.pythonhosted.org/packages/34/04/fd595a4fd8617b05fa27bd9b684ecb4985bfed27917848eea85d54036d06/xxhash-3.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2220af08163baf5fa36c2b8af079dc2cbe6e66ae061385267f9472362dfd53c6", size = 212291, upload-time = "2026-04-25T11:07:06.966Z" }, + { url = "https://files.pythonhosted.org/packages/03/fb/f1a379cbc372ae5b9f4ab36154c48a849ca6ebe3ac477067a57865bf3bc6/xxhash-3.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f14bb8b22a4a91325813e3d553b8963c10cf8c756cff65ee50c194431296c655", size = 445532, upload-time = "2026-04-25T11:07:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/65/59/172424b79f8cfd4b6d8a122b2193e6b8ad4b11f7159bb3b6f9b3191329bb/xxhash-3.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:496736f86a9bedaf64b0dc70e3539d0766df01c71ea22032698e88f3f04a1ce9", size = 193990, upload-time = "2026-04-25T11:07:10.315Z" }, + { url = "https://files.pythonhosted.org/packages/b9/19/aeac22161d953f139f07ba5586cb4a17c5b7b6dff985122803bb12933500/xxhash-3.7.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0ff71596bd79816975b3de7130ab1ff4541410285a3c084584eeb1c8239996fd", size = 284876, upload-time = "2026-04-25T11:07:12.15Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/4fd0b59e7a02242953da05ff679fbb961b0a4368eac97a217e11dae110c1/xxhash-3.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1ad86695c19b1d46fe106925db3c7a37f16be37669dcf58dcc70a9dd6e324676", size = 210495, upload-time = "2026-04-25T11:07:13.952Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fb/976a3165c728c7faf74aa1b5ab3cf6a85e6d731612894741840524c7d28c/xxhash-3.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:970f9f8c50961d639cbd0d988c96f80ddf66006de93641719282c4fe7a87c5e6", size = 241331, upload-time = "2026-04-25T11:07:15.557Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/876e722d533833f5f9a83473e6ba993e48745701096944e77bbecf29b2c3/xxhash-3.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6e934bbae1e0ec74e27d5f0d7f37ef547ce5ff9f0a7e63fb39e559fc99526734", size = 210744, upload-time = "2026-04-25T11:07:19.055Z" }, + { url = "https://files.pythonhosted.org/packages/21/e6/d7e7baef7ce24166b4668d3c48557bb35a23b92ecadcac7e7718d099ab69/xxhash-3.7.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3b6b3d28228af044ebcded71c4a3dd86e1dbd7e2f4645bf40f7b5da65bb5fb5a", size = 275406, upload-time = "2026-04-25T11:07:20.908Z" }, + { url = "https://files.pythonhosted.org/packages/92/fe/198b3763b2e01ca908f2154969a2352ec99bda892b574a11a9a151c5ede4/xxhash-3.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:6be4d70d9ab76c9f324ead9c01af6ff52c324745ea0c3731682a0cf99720f1fe", size = 414125, upload-time = "2026-04-25T11:07:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/3a/6d/019a11affd5a5499137cacca53808659964785439855b5aa40dfd3412916/xxhash-3.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:151d7520838d4465461a0b7f4ae488b3b00de16183dd3214c1a6b14bf89d7fb6", size = 191555, upload-time = "2026-04-25T11:07:24.991Z" }, + { url = "https://files.pythonhosted.org/packages/07/f2/36d3310161db7f72efb4562aadde0ed429f1d0531782dd6345b12d2da527/xxhash-3.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8f4608a06e4d61b7a3425665a46d00e0579122e1a2fae97a0c52953a3aad9aa3", size = 31123, upload-time = "2026-04-25T11:07:31.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/f10d7ff8c7a733d4403a43b9de18c8fabc005f98cec054644f04418659ee/xxhash-3.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc026e3b89d98e30a8288c95cb696e77d150b3f0fb7a51f73dcd49ee6b5577fa", size = 215793, upload-time = "2026-04-25T11:07:34.919Z" }, + { url = "https://files.pythonhosted.org/packages/8b/fd/778f60aa295f58907938f030a8b514611f391405614a525cccd2ffc00eb5/xxhash-3.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c9b31ab1f28b078a6a1ac1a54eb35e7d5390deddd56870d0be3a0a733d1c321c", size = 237993, upload-time = "2026-04-25T11:07:36.638Z" }, + { url = "https://files.pythonhosted.org/packages/70/f5/736db5de387b4a540e37a05b84b40dc58a1ce974bfd2b4e5754ce29b68c3/xxhash-3.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3bb5fd680c038fd5229e44e9c493782f90df9bef632fd0499d442374688ff70b", size = 214887, upload-time = "2026-04-25T11:07:38.564Z" }, + { url = "https://files.pythonhosted.org/packages/4d/aa/09a095f22fdb9a27fbb716841fbff52119721f9ca4261952d07a912f7839/xxhash-3.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:030c0fd688fce3569fbb49a2feefd4110cbb0b650186fb4610759ecfac677548", size = 448407, upload-time = "2026-04-25T11:07:40.552Z" }, + { url = "https://files.pythonhosted.org/packages/74/8a/b745efeeca9e34a91c26fdc97ad8514c43d5a81ac78565cba80a1353870a/xxhash-3.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b1bde10324f4c31812ae0d0502e92d916ae8917cad7209353f122b8b8f610c3", size = 196119, upload-time = "2026-04-25T11:07:42.101Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5c/0cfceb024af90c191f665c7933b1f318ee234f4797858383bebd1881d52f/xxhash-3.7.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:503722d52a615f2604f5e7611de7d43878df010dc0053094ef91cb9a9ac3d987", size = 286751, upload-time = "2026-04-25T11:07:43.568Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0a/0793e405dc3cf8f4ebe2c1acec1e4e4608cd9e7e50ea691dabbc2a95ccbb/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c72500a3b6d6c30ebfc135035bcace9eb5884f2dc220804efcaaba43e9f611dd", size = 212961, upload-time = "2026-04-25T11:07:45.388Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7e/721118ffc63bfff94aa565bcf2555a820f9f4bdb0f001e0d609bdfad70de/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:43475925a766d01ca8cd9a857fd87f3d50406983c8506a4c07c4df12adcc867f", size = 243703, upload-time = "2026-04-25T11:07:47.053Z" }, + { url = "https://files.pythonhosted.org/packages/2d/94/80ba841287fd97e3e9cac1d228788c8ef623746f570404961eec748ecb5c/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c50269d0055ac1faecfd559886d2cbe4b730de236585aba0e873f9d9dadbe585", size = 213357, upload-time = "2026-04-25T11:07:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/106d4067130c59f1e18a55ffadcd876d8c68534883a1e02685b29d3d8153/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1910df4756a5ab58cfad8744fc2d0f23926e3efcc346ee76e87b974abab922f4", size = 277600, upload-time = "2026-04-25T11:07:51.745Z" }, + { url = "https://files.pythonhosted.org/packages/c5/86/a081dd30da71d720b2612a792bfd55e45fa9a07ac76a0507f60487473c25/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d006faf3b491957efcb433489be3c149efe4787b7063d5cddb8ddaefdc60e0c1", size = 416980, upload-time = "2026-04-25T11:07:53.504Z" }, + { url = "https://files.pythonhosted.org/packages/35/29/1a95221a029a3c1293773869e1ab47b07cbbdd82444a42809e8c60156626/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:abb65b4e947e958f7b3b0d71db3ce447d1bc5f37f5eab871ce7223bda8768a04", size = 193840, upload-time = "2026-04-25T11:07:55.103Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4e/075559bd712bc62e84915ea46bbee859f935d285659082c129bdbff679dd/xxhash-3.7.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5de686e73690cdaf72b96d4fa083c230ec9020bcc2627ce6316138e2cf2fe2d1", size = 28553, upload-time = "2026-04-25T11:10:23.1Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b1/dfe2629f7c77eb2fa234c72ff537cdd64939763df704e256446ed364a16d/xxhash-3.7.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48b542c347c2089f43dc5a6db31d2a6f3cdb04ee33505ec6e9f653834dbb0bde", size = 36307, upload-time = "2026-04-25T11:10:26.949Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f7/5a484afce0f48dd8083208b42e4911f290a82c7b52458ef2927e4d421a45/xxhash-3.7.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a169a036bed0995e090d1493b283cc2cc8a6f5046821086b843abefff80643bc", size = 32534, upload-time = "2026-04-25T11:10:29.01Z" }, ] [[package]] @@ -11474,49 +11898,67 @@ wheels = [ [[package]] name = "yarl" -version = "1.23.0" +version = "1.24.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna", 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 = "multidict", 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 = "propcache", 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, - { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, - { 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/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/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/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/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/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/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/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/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/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" }, +sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/c5/1ce244152ff2839645e7cae92f90e7bafcb2c52bea7ff586ac714f14f5df/yarl-1.24.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:36348bebb147b83818b9d7e673ea4debc75970afc6ffdc7e3975ad05ce5a58c1", size = 128971, upload-time = "2026-05-19T21:28:20.543Z" }, + { url = "https://files.pythonhosted.org/packages/31/d0/1fb0c1cd27288f39f6974da4318c32768d72c9890984541fdf1e2e32a51d/yarl-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d027d56f1035e339d1001ac33eceab5b2ec8e42e449787bb75e289fb9a5cd1d", size = 91343, upload-time = "2026-05-19T21:28:24.092Z" }, + { url = "https://files.pythonhosted.org/packages/03/ce/d4a646508bed2f8dec6435b40166fe9308dd191262033d3f307b2bbcaecd/yarl-1.24.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a6377060e7927187a42b7eb202090cbe2b34933a4eeaf90e3bd9e33432e5cae", size = 105704, upload-time = "2026-05-19T21:28:25.872Z" }, + { url = "https://files.pythonhosted.org/packages/4b/07/b3278e82d8bc41485bcf6d856cd0433262593de615b1d3dc43bd3f5bead4/yarl-1.24.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:17076578bce0049a5ce57d14ad1bded391b68a3b213e9b81b0097b090244999a", size = 97281, upload-time = "2026-05-19T21:28:27.352Z" }, + { url = "https://files.pythonhosted.org/packages/17/5b/4cee6e7c92e487bebe7afc797da0aa54a248ab4e776a68fe369ec29665a5/yarl-1.24.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:50713f1d4d6be6375bb178bb43d140ee1acb8abe589cd723320b7925a275be1e", size = 114020, upload-time = "2026-05-19T21:28:29.458Z" }, + { url = "https://files.pythonhosted.org/packages/5c/82/111076571545a7d4f9cca3fbd5c6f40615af58642be09f12328f48022468/yarl-1.24.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:34263e2fa8fb5bb63a0d97706cda38edbad62fddb58c7f12d6acbc092812aa50", size = 111450, upload-time = "2026-05-19T21:28:31.262Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ec/08f671f69a444d704aeecebf92af659b67b97a869942411d0a578b08c334/yarl-1.24.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49016d82f032b1bd1e10b01078a7d29ae71bf468eeae0ea22df8bab691e60003", size = 106384, upload-time = "2026-05-19T21:28:32.856Z" }, + { url = "https://files.pythonhosted.org/packages/e5/86/ce41e7a7a199340b2330d52b60f25c4074b6636dd0e60b1a80d31a9db042/yarl-1.24.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3f6d2c216318f8f32038ca3f72501ba08536f0fd18a36e858836b121b2deed9f", size = 106153, upload-time = "2026-05-19T21:28:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5d/31be8a729531ab3e55ac3e7e5c800be8c89ea98947f418b2f6ea259fb6ee/yarl-1.24.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:08d3a33218e0c64393e7610284e770409a9c31c429b078bcb24096ed0a783b8f", size = 105322, upload-time = "2026-05-19T21:28:36.642Z" }, + { url = "https://files.pythonhosted.org/packages/47/9b/b57afb22b386ae87ac9940f09878b98d8c333f89113e6fc96fcf4ca9eb64/yarl-1.24.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5d699376c4ca3cba49bbfae3a05b5b70ded572937171ce1e0b8d87118e2ba294", size = 99057, upload-time = "2026-05-19T21:28:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4f/06348c27c8389256c313e8a57d796808fc0264c915dd5e7cfd3c0e314dc7/yarl-1.24.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a1cab588b4fa14bea2e55ebea27478adfb05372f47573738e1acc4a36c0b05d2", size = 113502, upload-time = "2026-05-19T21:28:40.091Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1c/284f307b298e4a17b7943b07d9d7ecc4151537f8d137ba51f3bb6c31ca20/yarl-1.24.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ec87ccc31bd21db7ad009d8572c127c1000f268517618a4cc09adba3c2a7f21c", size = 105253, upload-time = "2026-05-19T21:28:41.987Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bf/0de123bec8619e45c80cbded9085f61b5b4a9eddb8abe6d25d28ee1ec866/yarl-1.24.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d1dd47a22843b212baa8d74f37796815d43bd046b42a0f41e9da433386c3136b", size = 111345, upload-time = "2026-05-19T21:28:43.93Z" }, + { url = "https://files.pythonhosted.org/packages/90/af/0248eb065e51129d2a9b2436cd1b5c772c19a6b04e5b6a186955671e3319/yarl-1.24.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7b54b9c67c2b06bd7b9a77253d242124b9c95d2c02def5a1144001ee547dd9d5", size = 106558, upload-time = "2026-05-19T21:28:45.806Z" }, + { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" }, + { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" }, + { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" }, + { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" }, + { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" }, + { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" }, + { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" }, + { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" }, + { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" }, + { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" }, + { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" }, + { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, ] [[package]] name = "zipp" -version = "3.23.0" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, ] [[package]] @@ -11527,23 +11969,35 @@ sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529 wheels = [ { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, { url = "https://files.pythonhosted.org/packages/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" }, ] From 8cad0d860fcd3fd0e47af5dd2dd9ce17501c8b33 Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Tue, 9 Jun 2026 11:24:04 -0600 Subject: [PATCH 05/26] fix accidental changes Signed-off-by: Sam Oluwalana --- packages/nemo_platform/pyproject.toml | 9 +++++++++ pyproject.toml | 1 - uv.lock | 6 ------ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/nemo_platform/pyproject.toml b/packages/nemo_platform/pyproject.toml index 325834f7fe..311dfb1497 100644 --- a/packages/nemo_platform/pyproject.toml +++ b/packages/nemo_platform/pyproject.toml @@ -433,6 +433,13 @@ nmp-common = [ "pyjwt[crypto]>=2.12.0", ] +# Generated from [tool.bundle-package]; do not edit by hand. +platform-seed-service = [ + "nmp-common", + "nmp-auth", + "nmp-guardrails", +] + # Generated from [tool.bundle-package]; do not edit by hand. plugins = [ "nemo-platform[nemo-agents-example-calculator]", @@ -461,6 +468,7 @@ services = [ "nmp-common", "pyleak>=0.1.0", "rich>=14.1.0", + "nemo-platform[platform-seed-service]", "nemo-platform[core-service]", "nemo-platform[studio-service]", "nemo-platform[intake-service]", @@ -642,6 +650,7 @@ nmp-inference-gateway = { source = "../../services/core/inference-gateway/src/nm nmp-guardrails = { source = "../../services/guardrails/src/nmp/guardrails", module = "nmp/guardrails", deps_group = "guardrails-service" } nmp-customizer = { source = "../../services/customizer/src/nmp/customizer", module = "nmp/customizer", deps_group = "customizer-service" } nmp-evaluator = { source = "../../services/evaluator/src/nmp/evaluator", module = "nmp/evaluator", deps_group = "evaluator-service" } +nmp-platform-seed = { source = "../../services/platform-seed/src/nmp/platform_seed", module = "nmp/platform_seed", deps_group = "platform-seed-service" } nmp-hello-world = { source = "../../services/hello-world/src/nmp/hello_world", module = "nmp/hello_world", deps_group = "hello-world-service" } nmp-intake = { source = "../../services/intake/src/nmp/intake", module = "nmp/intake", deps_group = "intake-service" } diff --git a/pyproject.toml b/pyproject.toml index 25b7239906..0ed9235e59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -179,7 +179,6 @@ enabled-plugins = [ "nemo-customizer-plugin", "nemo-automodel-plugin", "nemo-unsloth-plugin", - "nmp-unsloth", ] # Legacy runtime needed specifically for task images that still invoke diff --git a/uv.lock b/uv.lock index 051a2daef1..a4ed194f13 100644 --- a/uv.lock +++ b/uv.lock @@ -6350,7 +6350,6 @@ core-services = [ { name = "nmp-models", 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 = "nmp-platform", 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 = "nmp-secrets", 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 = "nmp-unsloth", 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')" }, ] cpu-tasks = [ { name = "nemo-anonymizer-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')" }, @@ -6438,7 +6437,6 @@ enabled-plugins = [ { name = "nemo-safe-synthesizer-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-switchyard", 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-unsloth-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nmp-unsloth", 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')" }, ] functional-services = [ { name = "nemo-agents-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')" }, @@ -6470,7 +6468,6 @@ functional-services = [ { name = "nmp-platform-seed", 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 = "nmp-secrets", 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 = "nmp-studio", 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 = "nmp-unsloth", 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')" }, ] gpu-tasks = [ { name = "nemo-platform", 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')" }, @@ -6559,7 +6556,6 @@ core-services = [ { name = "nmp-models", editable = "services/core/models" }, { name = "nmp-platform", editable = "packages/nmp_platform" }, { name = "nmp-secrets", editable = "services/core/secrets" }, - { name = "nmp-unsloth", editable = "services/unsloth" }, ] cpu-tasks = [ { name = "nemo-anonymizer-plugin", editable = "plugins/nemo-anonymizer" }, @@ -6649,7 +6645,6 @@ enabled-plugins = [ { name = "nemo-safe-synthesizer-plugin", editable = "plugins/nemo-safe-synthesizer" }, { name = "nemo-switchyard", editable = "plugins/nemo-switchyard" }, { name = "nemo-unsloth-plugin", editable = "plugins/nemo-unsloth" }, - { name = "nmp-unsloth", editable = "services/unsloth" }, ] functional-services = [ { name = "nemo-agents-plugin", editable = "plugins/nemo-agents" }, @@ -6682,7 +6677,6 @@ functional-services = [ { name = "nmp-platform-seed", editable = "services/platform-seed" }, { name = "nmp-secrets", editable = "services/core/secrets" }, { name = "nmp-studio", editable = "services/studio" }, - { name = "nmp-unsloth", editable = "services/unsloth" }, ] gpu-tasks = [ { name = "nemo-platform", editable = "packages/nemo_platform" }, From 6e85594acfe775ff9d1572e88b0ed65ade50876d Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Tue, 9 Jun 2026 11:32:19 -0600 Subject: [PATCH 06/26] lint fix Signed-off-by: Sam Oluwalana --- e2e/conftest.py | 1 + uv.lock | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/e2e/conftest.py b/e2e/conftest.py index 6b285be5a5..5259d192d3 100644 --- a/e2e/conftest.py +++ b/e2e/conftest.py @@ -23,6 +23,7 @@ import socket import subprocess import sys +import tempfile import time import uuid from collections.abc import Iterator diff --git a/uv.lock b/uv.lock index a4ed194f13..4438c2c8f0 100644 --- a/uv.lock +++ b/uv.lock @@ -4617,7 +4617,9 @@ all = [ { name = "nemo-safe-synthesizer", 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 = "nemoguardrails", extra = ["tracing"], 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 = "ngcsdk", 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 = "nmp-auth", 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 = "nmp-common", 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 = "nmp-guardrails", 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 = "nvidia-nat-config-optimizer", 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 = "nvidia-nat-core", 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 = "nvidia-nat-langchain", 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')" }, @@ -5008,6 +5010,11 @@ nmp-common = [ { name = "sqlalchemy", 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 = "structlog", 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')" }, ] +platform-seed-service = [ + { name = "nmp-auth", 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 = "nmp-common", 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 = "nmp-guardrails", 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')" }, +] plugins = [ { name = "anthropic", 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 = "boto3", 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')" }, @@ -5093,7 +5100,9 @@ services = [ { name = "nemo-safe-synthesizer", 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 = "nemoguardrails", extra = ["tracing"], 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 = "ngcsdk", 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 = "nmp-auth", 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 = "nmp-common", 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 = "nmp-guardrails", 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 = "nvidia-nat-config-optimizer", 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 = "nvidia-nat-core", 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 = "nvidia-nat-langchain", 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')" }, @@ -5430,6 +5439,9 @@ requires-dist = [ { name = "ngcsdk", marker = "extra == 'files-service'", specifier = ">=4.9.10" }, { name = "ngcsdk", marker = "extra == 'nemo-platform-sdk'", specifier = ">=4.8.2" }, { name = "ngcsdk", marker = "extra == 'services'", specifier = ">=4.9.10" }, + { name = "nmp-auth", marker = "extra == 'all'", editable = "services/core/auth" }, + { name = "nmp-auth", marker = "extra == 'platform-seed-service'", editable = "services/core/auth" }, + { name = "nmp-auth", marker = "extra == 'services'", editable = "services/core/auth" }, { name = "nmp-common", editable = "packages/nmp_common" }, { name = "nmp-common", marker = "extra == 'all'", editable = "packages/nmp_common" }, { name = "nmp-common", marker = "extra == 'auth-service'", editable = "packages/nmp_common" }, @@ -5449,10 +5461,14 @@ requires-dist = [ { name = "nmp-common", marker = "extra == 'nemo-data-designer-plugin'", editable = "packages/nmp_common" }, { name = "nmp-common", marker = "extra == 'nemo-evaluator-plugin'", editable = "packages/nmp_common" }, { name = "nmp-common", marker = "extra == 'nemo-safe-synthesizer-plugin'", editable = "packages/nmp_common" }, + { name = "nmp-common", marker = "extra == 'platform-seed-service'", editable = "packages/nmp_common" }, { name = "nmp-common", marker = "extra == 'plugins'", editable = "packages/nmp_common" }, { name = "nmp-common", marker = "extra == 'secrets-service'", editable = "packages/nmp_common" }, { name = "nmp-common", marker = "extra == 'services'", editable = "packages/nmp_common" }, { name = "nmp-common", marker = "extra == 'studio-service'", editable = "packages/nmp_common" }, + { name = "nmp-guardrails", marker = "extra == 'all'", editable = "services/guardrails" }, + { name = "nmp-guardrails", marker = "extra == 'platform-seed-service'", editable = "services/guardrails" }, + { name = "nmp-guardrails", marker = "extra == 'services'", editable = "services/guardrails" }, { name = "nvidia-ml-py", marker = "extra == 'nemo-platform-sdk'", specifier = ">=13.0.0" }, { name = "nvidia-ml-py", marker = "extra == 'nmp-common'", specifier = ">=13.0.0" }, { name = "nvidia-nat-config-optimizer", marker = "extra == 'all'", specifier = ">=1.7.0,<1.8" }, @@ -5744,7 +5760,7 @@ requires-dist = [ { name = "yara-python", marker = "extra == 'guardrails-service'", specifier = "==4.5.1" }, { name = "yara-python", marker = "extra == 'services'", specifier = "==4.5.1" }, ] -provides-extras = ["aiohttp", "all", "auditor-service", "auth-service", "core-service", "customizer-service", "data-designer-nemo", "entities-service", "evaluator-service", "files-service", "guardrails-service", "hello-world-service", "inference-gateway-service", "intake-service", "jobs-service", "models-service", "nemo-agents-example-calculator", "nemo-agents-plugin", "nemo-anonymizer-plugin", "nemo-auditor-plugin", "nemo-data-designer-plugin", "nemo-evaluator-plugin", "nemo-evaluator-sdk", "nemo-guardrails-plugin", "nemo-platform-plugin", "nemo-platform-sdk", "nemo-safe-synthesizer-plugin", "nemo-switchyard", "nmp-common", "plugins", "secrets-service", "services", "studio-service", "switchyard"] +provides-extras = ["aiohttp", "all", "auditor-service", "auth-service", "core-service", "customizer-service", "data-designer-nemo", "entities-service", "evaluator-service", "files-service", "guardrails-service", "hello-world-service", "inference-gateway-service", "intake-service", "jobs-service", "models-service", "nemo-agents-example-calculator", "nemo-agents-plugin", "nemo-anonymizer-plugin", "nemo-auditor-plugin", "nemo-data-designer-plugin", "nemo-evaluator-plugin", "nemo-evaluator-sdk", "nemo-guardrails-plugin", "nemo-platform-plugin", "nemo-platform-sdk", "nemo-safe-synthesizer-plugin", "nemo-switchyard", "nmp-common", "platform-seed-service", "plugins", "secrets-service", "services", "studio-service", "switchyard"] [[package]] name = "nemo-platform-ext" From 1a61dd56bc67c42bcebb280aa37fab8394a95615 Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Tue, 9 Jun 2026 11:38:21 -0600 Subject: [PATCH 07/26] get_qualified_image moved Signed-off-by: Sam Oluwalana --- services/automodel/src/nmp/automodel/images.py | 2 +- services/unsloth/src/nmp/unsloth/images.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/automodel/src/nmp/automodel/images.py b/services/automodel/src/nmp/automodel/images.py index 9f782b283c..ef99c28ab1 100644 --- a/services/automodel/src/nmp/automodel/images.py +++ b/services/automodel/src/nmp/automodel/images.py @@ -5,8 +5,8 @@ from __future__ import annotations +from nemo_platform_plugin.jobs.image import get_qualified_image from nmp.automodel.config import config -from nmp.common.jobs.image import get_qualified_image # Default NGC dev registry for platform-built automodel images (flat repo names for NVCR). DEFAULT_AUTOMODEL_IMAGE_REGISTRY = "nvcr.io/0921617854601259/nemo-platform-dev" diff --git a/services/unsloth/src/nmp/unsloth/images.py b/services/unsloth/src/nmp/unsloth/images.py index e35e52bf6b..43cacd6de0 100644 --- a/services/unsloth/src/nmp/unsloth/images.py +++ b/services/unsloth/src/nmp/unsloth/images.py @@ -15,7 +15,7 @@ from __future__ import annotations -from nmp.common.jobs.image import get_qualified_image +from nemo_platform_plugin.jobs.image import get_qualified_image from nmp.unsloth.config import config DEFAULT_UNSLOTH_IMAGE_REGISTRY = "nvcr.io/0921617854601259/nemo-platform-dev" From 44291785cf95bd82c09631854a5464db4330bbfc Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Tue, 9 Jun 2026 11:56:24 -0600 Subject: [PATCH 08/26] Fix startup for e2e tests Signed-off-by: Sam Oluwalana --- e2e/conftest.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/e2e/conftest.py b/e2e/conftest.py index 5259d192d3..3d0583650f 100644 --- a/e2e/conftest.py +++ b/e2e/conftest.py @@ -256,7 +256,19 @@ def _services(services_log_path: Path) -> Iterator[str]: url = f"http://127.0.0.1:{port}" nemo_bin = str(Path(sys.executable).parent / "nemo") - args = [nemo_bin, "services", "run", "--service-group", "all", "--port", str(port)] + # --service-group alone does not start controllers (see resolve_run_configuration); + # jobs e2e tests need the jobs controller for scheduling. + args = [ + nemo_bin, + "services", + "run", + "--service-group", + "all", + "--controller-group", + "all", + "--port", + str(port), + ] env = os.environ.copy() env["NMP_SEED_ON_STARTUP"] = "true" if not _e2e_auth_enabled(): From 9a1eae6804efe76e929893b0f7e78616df9d06bc Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Tue, 9 Jun 2026 12:07:16 -0600 Subject: [PATCH 09/26] Don't leak devops configurations Signed-off-by: Sam Oluwalana --- docker-bake.hcl | 10 +++++----- e2e/conftest.py | 2 -- .../nemo-customizer/references/troubleshooting.md | 2 +- services/automodel/README.md | 2 +- .../docker/Dockerfile.nmp-automodel-tasks | 2 +- .../docker/Dockerfile.nmp-automodel-training | 2 +- services/automodel/docker/README.md | 14 +++++++------- services/automodel/src/nmp/automodel/config.py | 2 +- services/automodel/src/nmp/automodel/images.py | 2 +- .../nmp/automodel/tasks/docker/docker-compose.yaml | 2 +- services/automodel/tests/test_images.py | 12 ++++++------ services/unsloth/docker/README.md | 10 +++++----- services/unsloth/src/nmp/unsloth/config.py | 2 +- services/unsloth/src/nmp/unsloth/images.py | 2 +- .../agentic-use/customizer-lora-job-cli/README.md | 2 +- .../environment/docker-compose.yaml | 2 +- .../environment/image-env.sh | 4 ++-- .../customizer-lora-job-cli/instruction.md | 2 +- 18 files changed, 37 insertions(+), 39 deletions(-) diff --git a/docker-bake.hcl b/docker-bake.hcl index 8c7a166c5e..c4969b2a7d 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -9,7 +9,7 @@ # docker buildx bake --print -f docker-bake.hcl nmp-automodel-gpu-wheels # # Automodel — build and push wheels: -# export WHEELS_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev +# export WHEELS_REGISTRY=my-registry/nemo-platform-dev # export WHEELS_TAG=$(git rev-parse --short HEAD) # docker buildx bake -f docker-bake.hcl nmp-automodel-gpu-wheels --push # @@ -21,7 +21,7 @@ # --set "*.platform=linux/amd64" # # Unsloth — push to registry: -# export IMAGE_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev +# export IMAGE_REGISTRY=my-registry/nemo-platform-dev # export BAKE_TAG=$(git rev-parse --short HEAD) # docker buildx bake -f docker-bake.hcl nmp-unsloth-training --push \ # --set "*.platform=linux/amd64" @@ -35,15 +35,15 @@ # --------------------------------------------------------------------------- variable "IMAGE_REGISTRY" { - default = "nvcr.io/0921617854601259/nemo-platform-dev" + default = "my-registry/nemo-platform-dev" } variable "BASE_REGISTRY" { - default = "nvcr.io/0921617854601259/nemo-platform-dev" + default = "my-registry/nemo-platform-dev" } variable "WHEELS_REGISTRY" { - default = "nvcr.io/0921617854601259/nemo-platform-dev" + default = "my-registry/nemo-platform-dev" } variable "BAKE_TAG" { diff --git a/e2e/conftest.py b/e2e/conftest.py index 3d0583650f..0eda34608d 100644 --- a/e2e/conftest.py +++ b/e2e/conftest.py @@ -256,8 +256,6 @@ def _services(services_log_path: Path) -> Iterator[str]: url = f"http://127.0.0.1:{port}" nemo_bin = str(Path(sys.executable).parent / "nemo") - # --service-group alone does not start controllers (see resolve_run_configuration); - # jobs e2e tests need the jobs controller for scheduling. args = [ nemo_bin, "services", diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/troubleshooting.md b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/troubleshooting.md index 0587097a39..6cc4c87d75 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/troubleshooting.md +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/troubleshooting.md @@ -130,7 +130,7 @@ docker buildx bake \ --load \ --set "*.platform=linux/amd64" -export NMP_UNSLOTH_TRAINING_IMAGE="${IMAGE_REGISTRY:-nvcr.io/0921617854601259/nemo-platform-dev}/nmp-unsloth-training:${BAKE_TAG:-local}" +export NMP_UNSLOTH_TRAINING_IMAGE="${IMAGE_REGISTRY:-my-registry/nemo-platform-dev}/nmp-unsloth-training:${BAKE_TAG:-local}" # Restart platform so the env var is picked up nemo services restart ``` diff --git a/services/automodel/README.md b/services/automodel/README.md index 0b6f872234..10a2edc2f0 100644 --- a/services/automodel/README.md +++ b/services/automodel/README.md @@ -1,5 +1,5 @@ # nmp-automodel -Compiler and task entrypoints for NeMo Automodel training jobs on the platform. **No HTTP server** — consumed by `nemo-automodel-plugin` and Jobs task images (`nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-tasks`, `.../nmp-automodel-training`). +Compiler and task entrypoints for NeMo Automodel training jobs on the platform. **No HTTP server** — consumed by `nemo-automodel-plugin` and Jobs task images (`my-registry/nemo-platform-dev/nmp-automodel-tasks`, `.../nmp-automodel-training`). Runtime exceptions from `nemo_automodel` are mapped to user-facing error types via `src/nmp/automodel/tasks/training/errors/error_rules.yaml`. See [docs/automodel_errors.md](docs/automodel_errors.md) for the full catalog and validation status of each Automodel error. diff --git a/services/automodel/docker/Dockerfile.nmp-automodel-tasks b/services/automodel/docker/Dockerfile.nmp-automodel-tasks index 65c2814a6e..416547451f 100644 --- a/services/automodel/docker/Dockerfile.nmp-automodel-tasks +++ b/services/automodel/docker/Dockerfile.nmp-automodel-tasks @@ -3,7 +3,7 @@ # Built on nmp-automodel-base (GPU-capable; runs on CPU or GPU nodes). ARG BASE_TAG_AUTOMODEL=local -ARG BASE_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev +ARG BASE_REGISTRY=my-registry/nemo-platform-dev ARG SMOKE_MARKER=smoke_nmp_automodel_tasks FROM ${BASE_REGISTRY}/nmp-automodel-base:${BASE_TAG_AUTOMODEL} AS nmp-automodel-base diff --git a/services/automodel/docker/Dockerfile.nmp-automodel-training b/services/automodel/docker/Dockerfile.nmp-automodel-training index 429b38548d..660f17af0a 100644 --- a/services/automodel/docker/Dockerfile.nmp-automodel-training +++ b/services/automodel/docker/Dockerfile.nmp-automodel-training @@ -3,7 +3,7 @@ # Same platform glue as tasks; separate image tag for the compiler training step. ARG BASE_TAG_AUTOMODEL=local -ARG BASE_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev +ARG BASE_REGISTRY=my-registry/nemo-platform-dev ARG SMOKE_MARKER=smoke_nmp_automodel_training FROM ${BASE_REGISTRY}/nmp-automodel-base:${BASE_TAG_AUTOMODEL} AS nmp-automodel-base diff --git a/services/automodel/docker/README.md b/services/automodel/docker/README.md index 31268b0160..f8409a98fb 100644 --- a/services/automodel/docker/README.md +++ b/services/automodel/docker/README.md @@ -1,6 +1,6 @@ # nmp-automodel container images -Three images derived from the legacy `nmp` **customizer-automodel** base builder (not the full `customizer-automodel` HTTP service image). Published as flat NVCR repo names under **`nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-*`** (no nested `nmp/...` path — NVCR rejects that on push). +Three images derived from the legacy `nmp` **customizer-automodel** base builder (not the full `customizer-automodel` HTTP service image). Published as flat repo names under **`my-registry/nemo-platform-dev/nmp-automodel-*`** (no nested `nmp/...` path — some registries reject that on push). | Image | Dockerfile | Role | |-------|------------|------| @@ -10,9 +10,9 @@ Three images derived from the legacy `nmp` **customizer-automodel** base builder Full references (default tag `local`): -- `nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-base:local` -- `nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-tasks:local` -- `nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-training:local` +- `my-registry/nemo-platform-dev/nmp-automodel-base:local` +- `my-registry/nemo-platform-dev/nmp-automodel-tasks:local` +- `my-registry/nemo-platform-dev/nmp-automodel-training:local` Bake file: **`docker-bake.hcl`** at the Platform repo root (`context = "."`). Run all commands from the Platform repo root. @@ -36,8 +36,8 @@ docker login nvcr.io export WHEELS_TAG="$(git rev-parse --short HEAD)" # Bake variables (WHEELS_REGISTRY, WHEELS_TAG, IMAGE_REGISTRY) are overridden via env, not --set. # Example: -# export WHEELS_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev -# export IMAGE_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev +# export WHEELS_REGISTRY=my-registry/nemo-platform-dev +# export IMAGE_REGISTRY=my-registry/nemo-platform-dev docker buildx bake --print -f docker-bake.hcl nmp-automodel-gpu-wheels @@ -95,4 +95,4 @@ docker run --rm $NMP_AUTOMODEL_TASKS_IMAGE docker run --rm $NMP_AUTOMODEL_TASKS_IMAGE -m nmp.automodel.tasks --list ``` -The job compiler resolves `nmp-automodel-tasks` and `nmp-automodel-training` under `NMP_AUTOMODEL_IMAGE_REGISTRY` (default `nvcr.io/0921617854601259/nemo-platform-dev`). See `nmp.automodel.images`. +The job compiler resolves `nmp-automodel-tasks` and `nmp-automodel-training` under `NMP_AUTOMODEL_IMAGE_REGISTRY` (default `my-registry/nemo-platform-dev`). See `nmp.automodel.images`. diff --git a/services/automodel/src/nmp/automodel/config.py b/services/automodel/src/nmp/automodel/config.py index 4a698993cf..c35c85a7b5 100644 --- a/services/automodel/src/nmp/automodel/config.py +++ b/services/automodel/src/nmp/automodel/config.py @@ -11,7 +11,7 @@ class AutomodelConfig(create_service_config_class("automodel")): # type: ignore """Environment variables use the NMP_AUTOMODEL_ prefix.""" image_registry: str = Field( - default="nvcr.io/0921617854601259/nemo-platform-dev", + default="my-registry/nemo-platform-dev", description=( "Registry host/path prefix for nmp-automodel-tasks and nmp-automodel-training. " "Override via NMP_AUTOMODEL_IMAGE_REGISTRY for other environments." diff --git a/services/automodel/src/nmp/automodel/images.py b/services/automodel/src/nmp/automodel/images.py index ef99c28ab1..5943ace1b3 100644 --- a/services/automodel/src/nmp/automodel/images.py +++ b/services/automodel/src/nmp/automodel/images.py @@ -9,7 +9,7 @@ from nmp.automodel.config import config # Default NGC dev registry for platform-built automodel images (flat repo names for NVCR). -DEFAULT_AUTOMODEL_IMAGE_REGISTRY = "nvcr.io/0921617854601259/nemo-platform-dev" +DEFAULT_AUTOMODEL_IMAGE_REGISTRY = "my-registry/nemo-platform-dev" BASE_IMAGE_NAME = "nmp-automodel-base" TASKS_IMAGE_NAME = "nmp-automodel-tasks" diff --git a/services/automodel/src/nmp/automodel/tasks/docker/docker-compose.yaml b/services/automodel/src/nmp/automodel/tasks/docker/docker-compose.yaml index 0eaa2f56ae..a367ae44a3 100644 --- a/services/automodel/src/nmp/automodel/tasks/docker/docker-compose.yaml +++ b/services/automodel/src/nmp/automodel/tasks/docker/docker-compose.yaml @@ -15,7 +15,7 @@ services: file-io: - image: ${FILE_IO_IMAGE:-nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-tasks:local} + image: ${FILE_IO_IMAGE:-my-registry/nemo-platform-dev/nmp-automodel-tasks:local} container_name: file-io-task # Mount config file and storage directory diff --git a/services/automodel/tests/test_images.py b/services/automodel/tests/test_images.py index 4a633491ec..1636657990 100644 --- a/services/automodel/tests/test_images.py +++ b/services/automodel/tests/test_images.py @@ -12,7 +12,7 @@ ) -def test_default_automodel_images_use_nvcr_dev_registry(monkeypatch): +def test_default_automodel_images_use_platform_dev_registry(monkeypatch): monkeypatch.setattr("nmp.automodel.images.config", AutomodelConfig()) tasks = get_tasks_image() @@ -20,18 +20,18 @@ def test_default_automodel_images_use_nvcr_dev_registry(monkeypatch): assert tasks == f"{DEFAULT_AUTOMODEL_IMAGE_REGISTRY}/{TASKS_IMAGE_NAME}:local" assert training == f"{DEFAULT_AUTOMODEL_IMAGE_REGISTRY}/{TRAINING_IMAGE_NAME}:local" - assert TASKS_IMAGE_NAME.count("/") == 0 # NVCR: single repo segment, no nested paths + assert TASKS_IMAGE_NAME.count("/") == 0 # single repo segment, no nested paths def test_automodel_image_registry_override(monkeypatch): monkeypatch.setattr( "nmp.automodel.images.config", - AutomodelConfig(image_registry="nvcr.io/0921617854601259/other-registry"), + AutomodelConfig(image_registry="my-registry/other-registry"), ) assert ( get_automodel_qualified_image(TASKS_IMAGE_NAME) - == "nvcr.io/0921617854601259/other-registry/nmp-automodel-tasks:local" + == "my-registry/other-registry/nmp-automodel-tasks:local" ) @@ -39,8 +39,8 @@ def test_automodel_full_image_override(monkeypatch): monkeypatch.setattr( "nmp.automodel.images.config", AutomodelConfig( - tasks_image="nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-tasks:dev", + tasks_image="my-registry/nemo-platform-dev/nmp-automodel-tasks:dev", ), ) - assert get_tasks_image() == "nvcr.io/0921617854601259/nemo-platform-dev/nmp-automodel-tasks:dev" + assert get_tasks_image() == "my-registry/nemo-platform-dev/nmp-automodel-tasks:dev" diff --git a/services/unsloth/docker/README.md b/services/unsloth/docker/README.md index 571a1d726c..f7b6645f3a 100644 --- a/services/unsloth/docker/README.md +++ b/services/unsloth/docker/README.md @@ -36,7 +36,7 @@ docker buildx bake \ # --- Option B: push to a registry --- docker login nvcr.io -export IMAGE_REGISTRY="nvcr.io/0921617854601259/nemo-platform-dev" +export IMAGE_REGISTRY="my-registry/nemo-platform-dev" export BAKE_TAG="$(git rev-parse --short HEAD)" docker buildx bake \ -f docker-bake.hcl \ @@ -124,7 +124,7 @@ docker buildx bake \ **B) Push to a registry the GPU host will pull from**: ```bash -export IMAGE_REGISTRY="nvcr.io/0921617854601259/nemo-platform-dev" +export IMAGE_REGISTRY="my-registry/nemo-platform-dev" export BAKE_TAG="$(git rev-parse --short HEAD)" docker buildx bake \ @@ -138,7 +138,7 @@ docker buildx bake \ **C) Air-gapped GPU host** (save + `scp` + `docker load`): ```bash -export IMAGE_REGISTRY="nvcr.io/0921617854601259/nemo-platform-dev" +export IMAGE_REGISTRY="my-registry/nemo-platform-dev" export BAKE_TAG="$(git rev-parse --short HEAD)" docker buildx build \ -f services/unsloth/docker/Dockerfile.nmp-unsloth-training \ @@ -159,7 +159,7 @@ build this is just the bare name: ```bash # Local bake (--load; matches docker-bake.hcl defaults): -export NMP_UNSLOTH_TRAINING_IMAGE="${IMAGE_REGISTRY:-nvcr.io/0921617854601259/nemo-platform-dev}/nmp-unsloth-training:${BAKE_TAG:-local}" +export NMP_UNSLOTH_TRAINING_IMAGE="${IMAGE_REGISTRY:-my-registry/nemo-platform-dev}/nmp-unsloth-training:${BAKE_TAG:-local}" # Or, when you pushed (Option B above): export NMP_UNSLOTH_TRAINING_IMAGE="${IMAGE_REGISTRY}/nmp-unsloth-training:${BAKE_TAG}" @@ -172,7 +172,7 @@ Or persist in `~/.nemo/config.yaml`: ```yaml unsloth: - training_image: nvcr.io/0921617854601259/nemo-platform-dev/nmp-unsloth-training:local + training_image: my-registry/nemo-platform-dev/nmp-unsloth-training:local ``` ### 3. Prepare model + dataset filesets diff --git a/services/unsloth/src/nmp/unsloth/config.py b/services/unsloth/src/nmp/unsloth/config.py index d839c989a8..6981ba2d05 100644 --- a/services/unsloth/src/nmp/unsloth/config.py +++ b/services/unsloth/src/nmp/unsloth/config.py @@ -17,7 +17,7 @@ class UnslothConfig(create_service_config_class("unsloth")): # type: ignore[mis """Environment variables use the ``NMP_UNSLOTH_`` prefix.""" image_registry: str = Field( - default="nvcr.io/0921617854601259/nemo-platform-dev", + default="my-registry/nemo-platform-dev", description=( "Registry host/path prefix for nmp-unsloth-tasks and nmp-unsloth-training. " "Override via NMP_UNSLOTH_IMAGE_REGISTRY for other environments." diff --git a/services/unsloth/src/nmp/unsloth/images.py b/services/unsloth/src/nmp/unsloth/images.py index 43cacd6de0..b28d2c8800 100644 --- a/services/unsloth/src/nmp/unsloth/images.py +++ b/services/unsloth/src/nmp/unsloth/images.py @@ -18,7 +18,7 @@ from nemo_platform_plugin.jobs.image import get_qualified_image from nmp.unsloth.config import config -DEFAULT_UNSLOTH_IMAGE_REGISTRY = "nvcr.io/0921617854601259/nemo-platform-dev" +DEFAULT_UNSLOTH_IMAGE_REGISTRY = "my-registry/nemo-platform-dev" BASE_IMAGE_NAME = "nmp-unsloth-base" TASKS_IMAGE_NAME = "nmp-unsloth-tasks" diff --git a/tests/agentic-use/customizer-lora-job-cli/README.md b/tests/agentic-use/customizer-lora-job-cli/README.md index 6d8e4bb726..c3a77a858d 100644 --- a/tests/agentic-use/customizer-lora-job-cli/README.md +++ b/tests/agentic-use/customizer-lora-job-cli/README.md @@ -17,7 +17,7 @@ Build and push platform/automodel images before running this eval. From the repo export BAKE_TAG=$(git rev-parse --short HEAD) export BASE_TAG_AUTOMODEL=$BAKE_TAG export NMP_IMAGE_TAG=$BAKE_TAG -export NMP_IMAGE_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev +export NMP_IMAGE_REGISTRY=my-registry/nemo-platform-dev echo "$NMP_IMAGE_TAG" # Bake/push nmp-automodel images (and platform task images as needed) diff --git a/tests/agentic-use/customizer-lora-job-cli/environment/docker-compose.yaml b/tests/agentic-use/customizer-lora-job-cli/environment/docker-compose.yaml index c2d8d29b7a..3582623dd8 100644 --- a/tests/agentic-use/customizer-lora-job-cli/environment/docker-compose.yaml +++ b/tests/agentic-use/customizer-lora-job-cli/environment/docker-compose.yaml @@ -4,7 +4,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock environment: - DOCKER_HOST=unix:///var/run/docker.sock - - NMP_IMAGE_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev + - NMP_IMAGE_REGISTRY=my-registry/nemo-platform-dev # BAKE_TAG / NMP_IMAGE_TAG default to `git rev-parse --short HEAD` inside the container # when unset. Set both on the host before `docker compose up` to pin a baked tag: # export BAKE_TAG=$(git rev-parse --short HEAD) diff --git a/tests/agentic-use/customizer-lora-job-cli/environment/image-env.sh b/tests/agentic-use/customizer-lora-job-cli/environment/image-env.sh index 80f32e03ce..4087aa3fd8 100644 --- a/tests/agentic-use/customizer-lora-job-cli/environment/image-env.sh +++ b/tests/agentic-use/customizer-lora-job-cli/environment/image-env.sh @@ -5,7 +5,7 @@ # export BAKE_TAG=$(git rev-parse --short HEAD) # export BASE_TAG_AUTOMODEL=$BAKE_TAG # export NMP_IMAGE_TAG=$BAKE_TAG -# export NMP_IMAGE_REGISTRY=nvcr.io/0921617854601259/nemo-platform-dev +# export NMP_IMAGE_REGISTRY=my-registry/nemo-platform-dev # # Override any variable before starting the eval container when testing a # different tag or registry. @@ -21,5 +21,5 @@ fi export BAKE_TAG export BASE_TAG_AUTOMODEL="${BASE_TAG_AUTOMODEL:-$BAKE_TAG}" export NMP_IMAGE_TAG="${NMP_IMAGE_TAG:-$BAKE_TAG}" -export NMP_IMAGE_REGISTRY="${NMP_IMAGE_REGISTRY:-nvcr.io/0921617854601259/nemo-platform-dev}" +export NMP_IMAGE_REGISTRY="${NMP_IMAGE_REGISTRY:-my-registry/nemo-platform-dev}" export NMP_AUTOMODEL_IMAGE_REGISTRY="${NMP_AUTOMODEL_IMAGE_REGISTRY:-$NMP_IMAGE_REGISTRY}" diff --git a/tests/agentic-use/customizer-lora-job-cli/instruction.md b/tests/agentic-use/customizer-lora-job-cli/instruction.md index 117e21cfd6..85942dc4da 100644 --- a/tests/agentic-use/customizer-lora-job-cli/instruction.md +++ b/tests/agentic-use/customizer-lora-job-cli/instruction.md @@ -9,7 +9,7 @@ The CLIs are available at `/app/.venv/bin/nemo` and `/app/.venv/bin/nmp`. The pl ## Context - The NeMo Platform API server is running with the jobs controller and customization plugin enabled -- Platform image registry/tag are configured for `nvcr.io/0921617854601259/nemo-platform-dev` (see environment setup) +- Platform image registry/tag are configured for `my-registry/nemo-platform-dev` (see environment setup) - The Docker backend is configured for GPU job execution; the Docker socket is mounted - A workspace `lora-training-workspace` has been pre-created - A model entity `smollm-135m` (HF weights fileset `smollm-135m-weights`) has been registered in the workspace From 4a5b4b327ae093539444d0628969a51d45e9871e Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Tue, 9 Jun 2026 12:16:27 -0600 Subject: [PATCH 10/26] everything is fine Signed-off-by: Sam Oluwalana --- services/automodel/tests/test_images.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/services/automodel/tests/test_images.py b/services/automodel/tests/test_images.py index 1636657990..68901a7f56 100644 --- a/services/automodel/tests/test_images.py +++ b/services/automodel/tests/test_images.py @@ -29,10 +29,7 @@ def test_automodel_image_registry_override(monkeypatch): AutomodelConfig(image_registry="my-registry/other-registry"), ) - assert ( - get_automodel_qualified_image(TASKS_IMAGE_NAME) - == "my-registry/other-registry/nmp-automodel-tasks:local" - ) + assert get_automodel_qualified_image(TASKS_IMAGE_NAME) == "my-registry/other-registry/nmp-automodel-tasks:local" def test_automodel_full_image_override(monkeypatch): From aa78f7acef5c94dd8921be79e8b7357a89719b7a Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Tue, 9 Jun 2026 13:03:04 -0600 Subject: [PATCH 11/26] wait_for_model_entity() is conflating IGW model entity cache and virutal model in igw cache, separate them fix the issue with AUTOMODEL and UNSLOTH image selection, use platform image_registry unless overridden Signed-off-by: Sam Oluwalana --- packages/nmp_testing/src/nmp/testing/utils.py | 70 ++++++++++++++++--- .../automodel/src/nmp/automodel/config.py | 10 +-- .../automodel/src/nmp/automodel/images.py | 8 +-- services/automodel/tests/test_images.py | 37 +++++++--- services/unsloth/src/nmp/unsloth/config.py | 10 +-- services/unsloth/src/nmp/unsloth/images.py | 7 +- services/unsloth/tests/test_images.py | 61 ++++++++++++++++ 7 files changed, 167 insertions(+), 36 deletions(-) create mode 100644 services/unsloth/tests/test_images.py diff --git a/packages/nmp_testing/src/nmp/testing/utils.py b/packages/nmp_testing/src/nmp/testing/utils.py index bc11b68a55..f5e094edd4 100644 --- a/packages/nmp_testing/src/nmp/testing/utils.py +++ b/packages/nmp_testing/src/nmp/testing/utils.py @@ -101,6 +101,12 @@ def wait_for_model_entity( ) -> None: """Poll until a model entity is available in IGW's model cache. + Uses the OpenAI ``GET /v1/models/{name}`` route, which reads + :attr:`~nmp.core.inference_gateway.api.model_cache.ModelCache.model_entity_info_map` + and does **not** require a VirtualModel. Do not poll the model-entity proxy route + here — that route resolves a VirtualModel first and will 404 until IGW's separate + VirtualModel cache refreshes, even when the model entity is already served. + Useful in E2E tests that create a mock provider to wait for the model cache to refresh. Args: @@ -118,8 +124,7 @@ def wait_for_model_entity( while time.time() - start < timeout: try: - sdk.inference.gateway.model.get( - "v1/models", + sdk.inference.gateway.openai.v1.models.get( name=model_name, workspace=workspace, ) @@ -144,13 +149,12 @@ def wait_for_virtual_model( timeout: float = 20, poll_interval: float = 0.5, ) -> None: - """Poll until a VirtualModel is available via the platform SDK. + """Poll until a VirtualModel exists in the entity store (platform SDK). - Companion to :func:`wait_for_model_entity`. The IGW now requires every - inference request to resolve to a VirtualModel, and the production provider - reconciler creates one autoprovisioned VM per served entity asynchronously. - E2E tests that create a provider and immediately fire inference requests - can race the controller; this helper bounds that race. + This confirms the VirtualModel document was persisted. It does **not** mean + IGW's in-process VirtualModel cache has refreshed yet — use + :func:`wait_for_igw_virtual_model` before hitting model-entity or OpenAI + inference proxy routes. Args: sdk: The NeMoPlatform SDK client. @@ -180,6 +184,53 @@ def wait_for_virtual_model( raise TimeoutError(f"VirtualModel {workspace}/{name} not available after {timeout}s. Last error: {last_error}") +def wait_for_igw_virtual_model( + sdk: NeMoPlatform, + workspace: str, + name: str, + timeout: float = 20, + poll_interval: float = 0.5, +) -> None: + """Poll until a VirtualModel is visible in IGW's in-process cache. + + Model-entity and OpenAI inference proxy routes resolve the VirtualModel from + IGW's cache (refreshed on a background interval, default 3s) before proxying. + E2E tests that create a provider and immediately fire inference requests can + race that refresh; this helper bounds the race. + + Args: + sdk: The NeMoPlatform SDK client. + workspace: The workspace containing the VirtualModel. + name: The VirtualModel name (without workspace prefix). + timeout: Maximum time to wait in seconds (default: 20). + poll_interval: Time between polls in seconds (default: 0.5). + + Raises: + TimeoutError: If the VirtualModel is not available in IGW within the timeout. + """ + start = time.time() + last_error: Exception | None = None + + while time.time() - start < timeout: + try: + sdk.inference.gateway.model.get( + "v1/models", + name=name, + workspace=workspace, + ) + return + except NotFoundError as e: + last_error = e + time.sleep(poll_interval) + continue + except Exception: + raise + + raise TimeoutError( + f"VirtualModel {workspace}/{name} not available in IGW after {timeout}s. Last error: {last_error}" + ) + + def short_unique_name(prefix: str, max_length: int = 32) -> str: """Generate a short unique name with a prefix and random suffix. @@ -571,7 +622,8 @@ def add_mock_provider( # (We've also created VMs via the SDK above, but the container's cache is # decoupled from this process so it still needs to refresh and pick them up.) for entity_name in served_models.keys(): - wait_for_model_entity(sdk, workspace, entity_name) wait_for_virtual_model(sdk, workspace, entity_name) + wait_for_model_entity(sdk, workspace, entity_name) + wait_for_igw_virtual_model(sdk, workspace, entity_name) return provider diff --git a/services/automodel/src/nmp/automodel/config.py b/services/automodel/src/nmp/automodel/config.py index c35c85a7b5..2c189ab9c2 100644 --- a/services/automodel/src/nmp/automodel/config.py +++ b/services/automodel/src/nmp/automodel/config.py @@ -10,20 +10,20 @@ class AutomodelConfig(create_service_config_class("automodel")): # type: ignore """Environment variables use the NMP_AUTOMODEL_ prefix.""" - image_registry: str = Field( - default="my-registry/nemo-platform-dev", + image_registry: str | None = Field( + default=None, description=( "Registry host/path prefix for nmp-automodel-tasks and nmp-automodel-training. " - "Override via NMP_AUTOMODEL_IMAGE_REGISTRY for other environments." + "Override via NMP_AUTOMODEL_IMAGE_REGISTRY for other environments, defaults to the platform's image registry." ), ) training_image: str | None = Field( default=None, - description="Override GPU training image (default: nmp-automodel-training under image_registry).", + description="Override entire GPU training image (registry/name:tag).", ) tasks_image: str | None = Field( default=None, - description="Override CPU tasks image (default: nmp-automodel-tasks under image_registry).", + description="Override entire CPU tasks image (registry/name:tag).", ) default_job_resource_cpu_request: str = Field(default="1") diff --git a/services/automodel/src/nmp/automodel/images.py b/services/automodel/src/nmp/automodel/images.py index 5943ace1b3..efb29034ca 100644 --- a/services/automodel/src/nmp/automodel/images.py +++ b/services/automodel/src/nmp/automodel/images.py @@ -5,12 +5,10 @@ from __future__ import annotations +from nemo_platform_plugin.config import get_platform_config from nemo_platform_plugin.jobs.image import get_qualified_image from nmp.automodel.config import config -# Default NGC dev registry for platform-built automodel images (flat repo names for NVCR). -DEFAULT_AUTOMODEL_IMAGE_REGISTRY = "my-registry/nemo-platform-dev" - BASE_IMAGE_NAME = "nmp-automodel-base" TASKS_IMAGE_NAME = "nmp-automodel-tasks" TRAINING_IMAGE_NAME = "nmp-automodel-training" @@ -33,7 +31,9 @@ def get_automodel_qualified_image(name: str, override: str | None = None) -> str """ if override: return override - registry = config.image_registry or DEFAULT_AUTOMODEL_IMAGE_REGISTRY + + platform_config = get_platform_config() + registry = config.image_registry or platform_config.image_registry return get_qualified_image(name, registry=registry) diff --git a/services/automodel/tests/test_images.py b/services/automodel/tests/test_images.py index 68901a7f56..64e05d4147 100644 --- a/services/automodel/tests/test_images.py +++ b/services/automodel/tests/test_images.py @@ -1,9 +1,13 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +from types import SimpleNamespace + +import nemo_platform_plugin.jobs.image as platform_image +import nmp.automodel.images as automodel_images +import pytest from nmp.automodel.config import AutomodelConfig from nmp.automodel.images import ( - DEFAULT_AUTOMODEL_IMAGE_REGISTRY, TASKS_IMAGE_NAME, TRAINING_IMAGE_NAME, get_automodel_qualified_image, @@ -12,29 +16,42 @@ ) -def test_default_automodel_images_use_platform_dev_registry(monkeypatch): - monkeypatch.setattr("nmp.automodel.images.config", AutomodelConfig()) +@pytest.fixture +def platform_config(monkeypatch: pytest.MonkeyPatch) -> SimpleNamespace: + config = SimpleNamespace(image_registry="registry.example.com/nemo", image_tag="test-tag") + monkeypatch.setattr(automodel_images, "get_platform_config", lambda: config) + monkeypatch.setattr(platform_image, "get_platform_config", lambda: config) + return config + + +def test_default_automodel_images_use_platform_registry(monkeypatch, platform_config): + monkeypatch.setattr(automodel_images, "config", AutomodelConfig()) tasks = get_tasks_image() training = get_training_image() - assert tasks == f"{DEFAULT_AUTOMODEL_IMAGE_REGISTRY}/{TASKS_IMAGE_NAME}:local" - assert training == f"{DEFAULT_AUTOMODEL_IMAGE_REGISTRY}/{TRAINING_IMAGE_NAME}:local" + assert tasks == f"{platform_config.image_registry}/{TASKS_IMAGE_NAME}:{platform_config.image_tag}" + assert training == f"{platform_config.image_registry}/{TRAINING_IMAGE_NAME}:{platform_config.image_tag}" assert TASKS_IMAGE_NAME.count("/") == 0 # single repo segment, no nested paths -def test_automodel_image_registry_override(monkeypatch): +def test_automodel_image_registry_override(monkeypatch, platform_config): monkeypatch.setattr( - "nmp.automodel.images.config", + automodel_images, + "config", AutomodelConfig(image_registry="my-registry/other-registry"), ) - assert get_automodel_qualified_image(TASKS_IMAGE_NAME) == "my-registry/other-registry/nmp-automodel-tasks:local" + assert ( + get_automodel_qualified_image(TASKS_IMAGE_NAME) + == f"my-registry/other-registry/{TASKS_IMAGE_NAME}:{platform_config.image_tag}" + ) -def test_automodel_full_image_override(monkeypatch): +def test_automodel_full_image_override(monkeypatch, platform_config): monkeypatch.setattr( - "nmp.automodel.images.config", + automodel_images, + "config", AutomodelConfig( tasks_image="my-registry/nemo-platform-dev/nmp-automodel-tasks:dev", ), diff --git a/services/unsloth/src/nmp/unsloth/config.py b/services/unsloth/src/nmp/unsloth/config.py index 6981ba2d05..ec41665979 100644 --- a/services/unsloth/src/nmp/unsloth/config.py +++ b/services/unsloth/src/nmp/unsloth/config.py @@ -16,20 +16,20 @@ class UnslothConfig(create_service_config_class("unsloth")): # type: ignore[misc] """Environment variables use the ``NMP_UNSLOTH_`` prefix.""" - image_registry: str = Field( - default="my-registry/nemo-platform-dev", + image_registry: str | None = Field( + default=None, description=( "Registry host/path prefix for nmp-unsloth-tasks and nmp-unsloth-training. " - "Override via NMP_UNSLOTH_IMAGE_REGISTRY for other environments." + "Override via NMP_UNSLOTH_IMAGE_REGISTRY for other environments, defaults to the platform's image registry." ), ) training_image: str | None = Field( default=None, - description="Override GPU training image (default: nmp-unsloth-training under image_registry).", + description="Override entire GPU training image (registry/name:tag).", ) tasks_image: str | None = Field( default=None, - description="Override CPU tasks image (default: nmp-unsloth-tasks under image_registry).", + description="Override entire CPU tasks image (registry/name:tag).", ) default_job_resource_cpu_request: str = Field(default="1") diff --git a/services/unsloth/src/nmp/unsloth/images.py b/services/unsloth/src/nmp/unsloth/images.py index b28d2c8800..137ec231fd 100644 --- a/services/unsloth/src/nmp/unsloth/images.py +++ b/services/unsloth/src/nmp/unsloth/images.py @@ -15,11 +15,10 @@ from __future__ import annotations +from nemo_platform_plugin.config import get_platform_config from nemo_platform_plugin.jobs.image import get_qualified_image from nmp.unsloth.config import config -DEFAULT_UNSLOTH_IMAGE_REGISTRY = "my-registry/nemo-platform-dev" - BASE_IMAGE_NAME = "nmp-unsloth-base" TASKS_IMAGE_NAME = "nmp-unsloth-tasks" TRAINING_IMAGE_NAME = "nmp-unsloth-training" @@ -43,7 +42,9 @@ def get_unsloth_qualified_image(name: str, override: str | None = None) -> str: """ if override: return override - registry = config.image_registry or DEFAULT_UNSLOTH_IMAGE_REGISTRY + + platform_config = get_platform_config() + registry = config.image_registry or platform_config.image_registry return get_qualified_image(name, registry=registry) diff --git a/services/unsloth/tests/test_images.py b/services/unsloth/tests/test_images.py new file mode 100644 index 0000000000..0b2c84d68c --- /dev/null +++ b/services/unsloth/tests/test_images.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from types import SimpleNamespace + +import nemo_platform_plugin.jobs.image as platform_image +import nmp.unsloth.images as unsloth_images +import pytest +from nmp.unsloth.config import UnslothConfig +from nmp.unsloth.images import ( + TASKS_IMAGE_NAME, + TRAINING_IMAGE_NAME, + get_tasks_image, + get_training_image, + get_unsloth_qualified_image, +) + + +@pytest.fixture +def platform_config(monkeypatch: pytest.MonkeyPatch) -> SimpleNamespace: + config = SimpleNamespace(image_registry="registry.example.com/nemo", image_tag="test-tag") + monkeypatch.setattr(unsloth_images, "get_platform_config", lambda: config) + monkeypatch.setattr(platform_image, "get_platform_config", lambda: config) + return config + + +def test_default_unsloth_images_use_platform_registry(monkeypatch, platform_config): + monkeypatch.setattr(unsloth_images, "config", UnslothConfig()) + + training = get_training_image() + tasks = get_tasks_image() + + expected_training = f"{platform_config.image_registry}/{TRAINING_IMAGE_NAME}:{platform_config.image_tag}" + assert training == expected_training + assert tasks == expected_training # tasks falls back to training when tasks_image is unset + assert TASKS_IMAGE_NAME.count("/") == 0 # single repo segment, no nested paths + + +def test_unsloth_image_registry_override(monkeypatch, platform_config): + monkeypatch.setattr( + unsloth_images, + "config", + UnslothConfig(image_registry="my-registry/other-registry"), + ) + + assert ( + get_unsloth_qualified_image(TRAINING_IMAGE_NAME) + == f"my-registry/other-registry/{TRAINING_IMAGE_NAME}:{platform_config.image_tag}" + ) + + +def test_unsloth_full_image_override(monkeypatch, platform_config): + monkeypatch.setattr( + unsloth_images, + "config", + UnslothConfig( + tasks_image="my-registry/nemo-platform-dev/nmp-unsloth-tasks:dev", + ), + ) + + assert get_tasks_image() == "my-registry/nemo-platform-dev/nmp-unsloth-tasks:dev" From f97e64a4ceb64bf936fd07ea630b795797b9b9b3 Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Tue, 9 Jun 2026 13:32:10 -0600 Subject: [PATCH 12/26] Resolve discrepancy between unsloth and automodel metric reporting Signed-off-by: Sam Oluwalana --- packages/nmp_testing/src/nmp/testing/utils.py | 14 +++--- .../skills/nemo-customizer/SKILL.md | 7 +++ .../references/hyperparameters.md | 2 +- .../automodel/app/jobs/training/compiler.py | 2 +- services/automodel/tests/test_compiler.py | 14 +++--- .../tasks/training/backends/unsloth_sft.py | 36 +++++++++++++++- services/unsloth/tests/test_eval_steps.py | 43 +++++++++++++++++++ 7 files changed, 100 insertions(+), 18 deletions(-) create mode 100644 services/unsloth/tests/test_eval_steps.py diff --git a/packages/nmp_testing/src/nmp/testing/utils.py b/packages/nmp_testing/src/nmp/testing/utils.py index f5e094edd4..2711a67222 100644 --- a/packages/nmp_testing/src/nmp/testing/utils.py +++ b/packages/nmp_testing/src/nmp/testing/utils.py @@ -23,6 +23,8 @@ NemoRun = Callable[..., subprocess.CompletedProcess[str]] +_E2E_IGW_WAIT_TIMEOUT_SEC = 60 + _ENTITY_NAME_PATTERN = re.compile(NAME_PATTERN) @@ -96,7 +98,7 @@ def wait_for_model_entity( sdk: NeMoPlatform, workspace: str, model_name: str, - timeout: float = 20, + timeout: float = _E2E_IGW_WAIT_TIMEOUT_SEC, poll_interval: float = 0.5, ) -> None: """Poll until a model entity is available in IGW's model cache. @@ -113,7 +115,7 @@ def wait_for_model_entity( sdk: The NeMoPlatform SDK client. workspace: The workspace containing the model entity. model_name: The model entity name (without workspace prefix). - timeout: Maximum time to wait in seconds (default: 20). + timeout: Maximum time to wait in seconds (default: 60). poll_interval: Time between polls in seconds (default: 0.5). Raises: @@ -146,7 +148,7 @@ def wait_for_virtual_model( sdk: NeMoPlatform, workspace: str, name: str, - timeout: float = 20, + timeout: float = _E2E_IGW_WAIT_TIMEOUT_SEC, poll_interval: float = 0.5, ) -> None: """Poll until a VirtualModel exists in the entity store (platform SDK). @@ -161,7 +163,7 @@ def wait_for_virtual_model( workspace: The workspace containing the VirtualModel. name: The VirtualModel name (without workspace prefix). For an autoprovisioned passthrough VM, this is the served model entity name. - timeout: Maximum time to wait in seconds (default: 20). + timeout: Maximum time to wait in seconds (default: 60). poll_interval: Time between polls in seconds (default: 0.5). Raises: @@ -188,7 +190,7 @@ def wait_for_igw_virtual_model( sdk: NeMoPlatform, workspace: str, name: str, - timeout: float = 20, + timeout: float = _E2E_IGW_WAIT_TIMEOUT_SEC, poll_interval: float = 0.5, ) -> None: """Poll until a VirtualModel is visible in IGW's in-process cache. @@ -202,7 +204,7 @@ def wait_for_igw_virtual_model( sdk: The NeMoPlatform SDK client. workspace: The workspace containing the VirtualModel. name: The VirtualModel name (without workspace prefix). - timeout: Maximum time to wait in seconds (default: 20). + timeout: Maximum time to wait in seconds (default: 60). poll_interval: Time between polls in seconds (default: 0.5). Raises: diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/SKILL.md b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/SKILL.md index 82ee24129a..859e96d0bf 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/SKILL.md +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/SKILL.md @@ -87,6 +87,7 @@ Training never runs inside the `nemo` CLI process. After `submit`, the platform' - User asks to tune **batch or parallelism** (automodel) → **Batch sizing** / **Multi-GPU** below. Other fields (LR, epochs, LoRA rank, distillation) → `references/hyperparameters.md`. For unsloth, see **Batch sizing — unsloth** and the `Unsloth job JSON` section in `references/hyperparameters.md`. Run `nemo customization explain` for the live schema. - Skill **defaults** (`micro_batch_size` 1, `global_batch_size` 4) are safe on unknown VRAM. When the user has **≥48 GB** on one GPU, use **Batch sizing** instead of defaults. Unsloth's analogues are `batch.per_device_train_batch_size` and `batch.gradient_accumulation_steps` (effective batch = product). - **Unsloth training is single-GPU per job** (inside the container). `hardware.gpus` sets `CUDA_VISIBLE_DEVICES` before `import torch` — **selection, not reservation**. No `parallelism`/TP/PP block in job JSON. Multi-GPU sharding → use automodel. Pass `--profile ` on `unsloth submit` when the default `gpu` profile is wrong (automodel sets `training.execution_profile` in JSON instead). +- **Unsloth validation defaults** — when `dataset.validation_path` is set and `schedule.eval_steps` is omitted, the trainer runs validation once per effective epoch automatically. Report final `metrics.val_loss` from job status (see **Report to user**). Set `eval_steps` explicitly to override cadence. - **Do not use local `docker info`** to pick automodel vs unsloth. After auth, run `uv run nemo jobs list-execution-profiles -f json` against the user's platform (see `references/troubleshooting.md`). Default output is a table — **`-f json` is required** for scripting; parse **stdout only** (do not pipe `2>&1` into `json.load`). - **Do not merge stderr into stdout when parsing JSON** — `submit`, `explain`, and `-f json` commands write **JSON on stdout**; harmless warnings like `Configuration file not found, using defaults` go to **stderr**. Piping with **`2>&1`** before `json.load` raises `JSONDecodeError` even when submit **succeeded** — a common cause of **duplicate jobs** when the agent re-submits after a parse error. Parse stdout only; redirect stderr if needed (`2>/dev/null`). See `references/troubleshooting.md` § **Parsing CLI JSON**. - For submit/image/plugin errors (both backends), read `references/troubleshooting.md`. Unsloth needs the `nmp-unsloth-training` container image on the **platform host's** Docker daemon (see `services/unsloth/docker/README.md`). @@ -434,6 +435,8 @@ After polling reaches a **terminal** status (`completed`, `error`, or `cancelled - **Dataset fileset:** default/ - **Output adapter fileset:** - **Status:** +- **Final train loss:** +- **Final validation loss:** - **Notes:** ``` @@ -447,8 +450,12 @@ After polling reaches a **terminal** status (`completed`, `error`, or `cancelled | **Dataset fileset** | automodel: `dataset.training`; unsloth: `dataset.path` | | **Output adapter fileset** | `output.name` from job JSON. Label **Output adapter fileset (planned):** when status is `error` or `cancelled` and no output was registered | | **Status** | Top-level `status` from `nemo jobs get-status` — not step-level status | +| **Final train loss** | Last entry in `status_details.metrics.train_loss` (or nested under a step's `status_details.metrics`). Use the **last** `value` in the list — not `status_details.train_loss` alone (that is the most recent logged step, which may differ from epoch-average loss on some backends). Round to 3 decimal places. | +| **Final validation loss** | Last entry in `status_details.metrics.val_loss`. If the list is empty, report `n/a (no validation run)` and note whether validation data was configured. Automodel validates once per epoch by default. Unsloth validates once per epoch when `dataset.validation_path` is set and `schedule.eval_steps` is omitted (platform default: `max(1, effective_steps - 1)`). | | **Notes** | See **Notes by status** below | +**Metrics extraction** — after polling, always run `uv run nemo jobs get-status ` and read `status_details.metrics` (both backends accumulate `train_loss` and `val_loss` time series there). Include both final losses in the report even when status is `error` if training completed before the failure (e.g. entity registration failed after upload). + **Notes by status** | Status | Notes | diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md index 4caa6299e7..8f54b4a25e 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/hyperparameters.md @@ -469,7 +469,7 @@ See `references/dataset-formats.md` § Unsloth for row-shape rules. | `lr_scheduler_type` | `"linear"` | `"linear"`, `"cosine"`, `"constant"`, `"constant_with_warmup"`, `"cosine_with_restarts"`. | | `logging_steps` | `1` | Loss-log cadence. | | `save_steps` | `null` | If set, save checkpoint every N steps. | -| `eval_steps` | `null` | If set with `validation_path`, eval every N steps. | +| `eval_steps` | `null` | If set with `validation_path`, eval every N steps. When `null` and `validation_path` is set, the training driver defaults to **one validation pass per effective epoch** at `max(1, effective_steps - 1)` (same effective-step cap as automodel's default `val_check_interval`). | | `seed` | `3407` | Trainer seed (`TrainingArguments.seed`). | **Hard mutex enforced by the schema:** `epochs` xor `max_steps`; `warmup_steps` xor `warmup_ratio`. Validation errors surface at submit time. diff --git a/services/automodel/src/nmp/automodel/app/jobs/training/compiler.py b/services/automodel/src/nmp/automodel/app/jobs/training/compiler.py index e25e63e894..5533baae6e 100644 --- a/services/automodel/src/nmp/automodel/app/jobs/training/compiler.py +++ b/services/automodel/src/nmp/automodel/app/jobs/training/compiler.py @@ -217,7 +217,7 @@ def compile_training_step( secret_envs = _collect_integration_secret_envs(job_spec) return PlatformJobStep( - name="customization-training-job", + name="training", executor=executor, environment=[*base_env, *secret_envs, EnvironmentVariable(name="HF_DATASETS_OFFLINE", value="1")], config=training_config.model_dump(mode="json"), diff --git a/services/automodel/tests/test_compiler.py b/services/automodel/tests/test_compiler.py index 306cfbfcb8..f0d4f0b4b0 100644 --- a/services/automodel/tests/test_compiler.py +++ b/services/automodel/tests/test_compiler.py @@ -15,7 +15,7 @@ from nmp.automodel.api.v2.jobs.schemas import CustomizationJobOutput, LoRAParams, OutputResponse, SFTTraining from nmp.automodel.app.jobs.compiler import _build_file_download_config from nmp.automodel.compile import platform_job_config_compiler -from nmp.automodel.images import DEFAULT_AUTOMODEL_IMAGE_REGISTRY, TASKS_IMAGE_NAME, TRAINING_IMAGE_NAME +from nmp.automodel.images import get_tasks_image, get_training_image from nmp.common.entities.utils import get_random_id from nmp.common.jobs.exceptions import PlatformJobCompilationError @@ -118,7 +118,7 @@ async def test_platform_job_config_compiler_sft_lora(mock_sdk, monkeypatch): assert len(steps) == 4 training_step = steps[1] training_name = training_step.name if hasattr(training_step, "name") else training_step["name"] - assert training_name == "customization-training-job" + assert training_name == "training" training_cmd = ( training_step.executor.container.command if hasattr(training_step, "executor") @@ -143,9 +143,7 @@ def _step_image(step) -> str: return step.executor.container.image return step["executor"]["container"]["image"] - tasks_image = f"{DEFAULT_AUTOMODEL_IMAGE_REGISTRY}/{TASKS_IMAGE_NAME}" - training_image = f"{DEFAULT_AUTOMODEL_IMAGE_REGISTRY}/{TRAINING_IMAGE_NAME}" - assert _step_image(steps[0]).startswith(tasks_image) - assert _step_image(steps[1]).startswith(training_image) - assert _step_image(steps[2]).startswith(tasks_image) - assert _step_image(steps[3]).startswith(tasks_image) + assert _step_image(steps[0]) == get_tasks_image() + assert _step_image(steps[1]) == get_training_image() + assert _step_image(steps[2]) == get_tasks_image() + assert _step_image(steps[3]) == get_tasks_image() diff --git a/services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py b/services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py index 6c65f9022f..a8d379cf9a 100644 --- a/services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py +++ b/services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py @@ -30,6 +30,26 @@ logger = logging.getLogger(__name__) +def compute_default_eval_steps( + *, + num_train_samples: int, + per_device_train_batch_size: int, + gradient_accumulation_steps: int, + max_steps: int | None = None, +) -> int: + """Default ``eval_steps`` when validation data is present but ``eval_steps`` is unset. + + Runs one validation pass per effective epoch at + ``max(1, effective_steps - 1)``, where ``effective_steps`` is + ``min(steps_per_epoch, max_steps)`` when ``max_steps`` is set (same + cap automodel uses in ``compute_val_check_interval``). + """ + effective_batch = per_device_train_batch_size * gradient_accumulation_steps + steps_per_epoch = max(1, (num_train_samples + effective_batch - 1) // effective_batch) + effective_steps = min(steps_per_epoch, max_steps) if max_steps is not None else steps_per_epoch + return max(1, effective_steps - 1) + + def train_sft( spec: "UnslothJobOutput", ctx: "JobContext", @@ -215,8 +235,20 @@ def train_sft( args_kwargs["save_strategy"] = "steps" else: args_kwargs["save_strategy"] = "epoch" - if spec.schedule.eval_steps is not None: - args_kwargs["eval_steps"] = spec.schedule.eval_steps + eval_steps = spec.schedule.eval_steps + if eval_ds is not None and eval_steps is None: + eval_steps = compute_default_eval_steps( + num_train_samples=len(train_ds), + per_device_train_batch_size=spec.batch.per_device_train_batch_size, + gradient_accumulation_steps=spec.batch.gradient_accumulation_steps, + max_steps=spec.schedule.max_steps, + ) + logger.info( + "Default eval_steps=%s (validation data present, schedule.eval_steps unset)", + eval_steps, + ) + if eval_ds is not None and eval_steps is not None: + args_kwargs["eval_steps"] = eval_steps args_kwargs["eval_strategy"] = "steps" # Wandb run-name: surfaces in W&B when WANDB_API_KEY is set in the diff --git a/services/unsloth/tests/test_eval_steps.py b/services/unsloth/tests/test_eval_steps.py new file mode 100644 index 0000000000..c043ac1931 --- /dev/null +++ b/services/unsloth/tests/test_eval_steps.py @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for default Unsloth validation cadence.""" + +from __future__ import annotations + +from nmp.unsloth.tasks.training.backends.unsloth_sft import compute_default_eval_steps + + +def test_default_eval_steps_once_per_epoch() -> None: + # commonsense_qa-scale: 9741 samples, effective batch 128 → 77 steps/epoch + assert ( + compute_default_eval_steps( + num_train_samples=9741, + per_device_train_batch_size=8, + gradient_accumulation_steps=16, + ) + == 76 + ) + + +def test_default_eval_steps_respects_max_steps_cap() -> None: + assert ( + compute_default_eval_steps( + num_train_samples=9741, + per_device_train_batch_size=8, + gradient_accumulation_steps=16, + max_steps=50, + ) + == 49 + ) + + +def test_default_eval_steps_minimum_one() -> None: + assert ( + compute_default_eval_steps( + num_train_samples=1, + per_device_train_batch_size=1, + gradient_accumulation_steps=1, + ) + == 1 + ) From 38c6c569918ece0f78afb12970e09ced07190811 Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Tue, 9 Jun 2026 14:07:40 -0600 Subject: [PATCH 13/26] test fixes Signed-off-by: Sam Oluwalana --- e2e/conftest.py | 40 +++++++++++++++---- .../test_model_entity_service_integration.py | 3 +- .../unit/test_model_entity_service_unit.py | 2 +- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/e2e/conftest.py b/e2e/conftest.py index 0eda34608d..10f397de37 100644 --- a/e2e/conftest.py +++ b/e2e/conftest.py @@ -89,6 +89,38 @@ def services_log_path(request: pytest.FixtureRequest, tmp_path_factory: pytest.T return path +_E2E_REPO_ROOT = Path(__file__).resolve().parents[1] +_E2E_PLATFORM_CONFIG = _E2E_REPO_ROOT / "packages/nmp_platform/config/local.yaml" + + +def _e2e_services_env() -> dict[str, str]: + """Environment for the ``nemo services run`` child process. + + ``pytest_configure`` sets ``NMP_INFERENCE_GATEWAY_MOCK_PROVIDER_PREFIX`` on the + pytest process so ``add_mock_provider()`` can build providers, but the IGW + must see the same value in *its* process or mock routing and cache refresh + behave differently from the test client. Mirror the Docker E2E backend + (``nmp.testing.e2e.docker``) by setting inference env vars explicitly here + rather than relying on inherited shell state. + + Use ``packages/nmp_platform/config/local.yaml`` (``inference_gateway: {}``) + so IGW polls the Models service on the background refresh interval instead + of the dev-only ``debug_model_providers`` block in + ``services/core/inference-gateway/config/local.yaml``, which disables that + loop. + """ + env = os.environ.copy() + env["NMP_SEED_ON_STARTUP"] = "true" + env["NMP_INFERENCE_GATEWAY_MOCK_PROVIDER_PREFIX"] = "igw-mock-" + env["NMP_CONFIG_FILE_PATH"] = str(_E2E_PLATFORM_CONFIG) + env["NMP_CONFIG_WARNINGS_DISABLED"] = "1" + if not _e2e_auth_enabled(): + env["NMP_AUTH_ENABLED"] = "false" + elif "NMP_AUTH_ENABLED" not in env: + env["NMP_AUTH_ENABLED"] = "true" + return env + + def _e2e_auth_enabled() -> bool: """Return whether the e2e harness should run with authorization enabled. @@ -267,13 +299,7 @@ def _services(services_log_path: Path) -> Iterator[str]: "--port", str(port), ] - env = os.environ.copy() - env["NMP_SEED_ON_STARTUP"] = "true" - if not _e2e_auth_enabled(): - # Bundled local.yaml enables auth; disable it for the default e2e harness. - env["NMP_AUTH_ENABLED"] = "false" - elif "NMP_AUTH_ENABLED" not in env: - env["NMP_AUTH_ENABLED"] = "true" + env = _e2e_services_env() logger.info("Starting nemo services on port %d", port) diff --git a/services/core/models/tests/integration/test_model_entity_service_integration.py b/services/core/models/tests/integration/test_model_entity_service_integration.py index fc218c8803..80ea73fdb7 100644 --- a/services/core/models/tests/integration/test_model_entity_service_integration.py +++ b/services/core/models/tests/integration/test_model_entity_service_integration.py @@ -8,8 +8,7 @@ import pytest from nemo_platform import AsyncNeMoPlatform from nemo_platform.filesets import ListFilesResponse -from nemo_platform.types import FilesetMetadata -from nemo_platform.types.files import Fileset, FilesetFile, LocalStorageConfig +from nemo_platform.types.files import Fileset, FilesetFile, FilesetMetadata, LocalStorageConfig from nmp.common.api.filter import ComparisonOperation, FilterOperator, LogicalOperation from nmp.common.api.parsed_filter import ParsedFilter from nmp.common.entities.client import EntityClient diff --git a/services/core/models/tests/unit/test_model_entity_service_unit.py b/services/core/models/tests/unit/test_model_entity_service_unit.py index b30bb23104..e0b4c157f2 100644 --- a/services/core/models/tests/unit/test_model_entity_service_unit.py +++ b/services/core/models/tests/unit/test_model_entity_service_unit.py @@ -11,10 +11,10 @@ import pytest from nemo_platform import AsyncNeMoPlatform from nemo_platform.filesets import ListFilesResponse -from nemo_platform.types import FilesetMetadata from nemo_platform.types.files import ( Fileset, FilesetFile, + FilesetMetadata, HuggingfaceStorageConfig, LocalStorageConfig, NGCStorageConfig, From 666c1d9969d0c402ef22bbf450908751f78bcb84 Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Tue, 9 Jun 2026 15:15:16 -0600 Subject: [PATCH 14/26] PR Comment fixes Signed-off-by: Sam Oluwalana --- .../customization_contributor.py | 4 +++ .../src/nemo_platform_plugin/discovery.py | 29 +++++++++--------- .../tests/test_discovery.py | 30 ++++++++++++++++--- .../src/nemo_automodel_plugin/cli/inputs.py | 2 -- .../src/nemo_customizer/router.py | 3 +- .../src/nemo_unsloth_plugin/__init__.py | 2 +- .../src/nemo_unsloth_plugin/cli/inputs.py | 3 -- .../src/nemo_unsloth_plugin/config.py | 11 ++++--- .../nemo_unsloth_plugin/sdk/job_resources.py | 7 +---- .../src/nemo_unsloth_plugin/sdk/resources.py | 25 ++-------------- .../src/nemo_unsloth_plugin/transform.py | 10 ++++--- services/unsloth/pyproject.toml | 7 ++--- .../unsloth/src/nmp/unsloth/app/constants.py | 10 +++---- .../nmp/unsloth/app/jobs/training/compiler.py | 10 +++---- services/unsloth/src/nmp/unsloth/config.py | 7 ++--- services/unsloth/src/nmp/unsloth/schemas.py | 6 ++-- services/unsloth/tests/test_compile.py | 7 ++--- services/unsloth/tests/test_main.py | 6 ++-- 18 files changed, 85 insertions(+), 94 deletions(-) diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/customization_contributor.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/customization_contributor.py index 401468fa6a..f6e63436a8 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/customization_contributor.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/customization_contributor.py @@ -12,6 +12,10 @@ from nemo_platform_plugin.service import RouterSpec +class CustomizationContributorDiscoveryError(RuntimeError): + """Raised when customization contributor discovery fails.""" + + @runtime_checkable class CustomizationContributor(Protocol): """One training backend mounted under ``/apis/customization``.""" diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py index d9fb4e810f..e5bf6a4b25 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/discovery.py @@ -49,7 +49,10 @@ from nemo_platform_plugin.cli import NemoCLI from nemo_platform_plugin.controller import NemoController -from nemo_platform_plugin.customization_contributor import CustomizationContributor +from nemo_platform_plugin.customization_contributor import ( + CustomizationContributor, + CustomizationContributorDiscoveryError, +) from nemo_platform_plugin.function import NemoFunction from nemo_platform_plugin.inference_middleware import NemoInferenceMiddleware from nemo_platform_plugin.interface import PluginManifest @@ -496,8 +499,8 @@ def discover_customization_contributors() -> dict[str, CustomizationContributor] Returns a dict keyed by entry-point key (e.g. ``"automodel"``) mapping to a :class:`~nemo_platform_plugin.customization_contributor.CustomizationContributor` instance. Entry points may register a class (instantiated here) or a pre-built - instance. Broken contributors are skipped with a warning (same fault isolation as - :func:`discover`). + instance. Broken or misconfigured contributors raise + :class:`~nemo_platform_plugin.customization_contributor.CustomizationContributorDiscoveryError`. """ result: dict[str, CustomizationContributor] = {} @@ -508,10 +511,9 @@ def discover_customization_contributors() -> dict[str, CustomizationContributor] contributor = _instantiate_customization_contributor(loaded) key = getattr(type(contributor), "name", None) or ep.name if key != ep.name: - logger.warning( - "Contributor entry-point key %r differs from class name %r; using entry-point key", - ep.name, - key, + raise CustomizationContributorDiscoveryError( + f"Contributor entry-point key {ep.name!r} differs from class name {key!r}; " + "entry-point key and contributor class `name` must match.", ) result[ep.name] = contributor logger.debug( @@ -519,13 +521,12 @@ def discover_customization_contributors() -> dict[str, CustomizationContributor] ep.name, ep.value, ) - except Exception: - logger.warning( - "Failed to load customization contributor %r (%s) — skipping", - ep.name, - ep.value, - exc_info=True, - ) + except CustomizationContributorDiscoveryError: + raise + except Exception as exc: + raise CustomizationContributorDiscoveryError( + f"Failed to load customization contributor {ep.name!r} ({ep.value})", + ) from exc return result diff --git a/packages/nemo_platform_plugin/tests/test_discovery.py b/packages/nemo_platform_plugin/tests/test_discovery.py index ac629d00a3..39e8f560aa 100644 --- a/packages/nemo_platform_plugin/tests/test_discovery.py +++ b/packages/nemo_platform_plugin/tests/test_discovery.py @@ -597,7 +597,9 @@ def get_authz_contribution(self): result = discover_customization_contributors() assert isinstance(result["fake"], _Contributor) - def test_failing_contributor_is_skipped(self) -> None: + def test_failing_contributor_raises(self) -> None: + from nemo_platform_plugin.customization_contributor import CustomizationContributorDiscoveryError + bad = _make_ep("bad", None) bad.load.side_effect = RuntimeError("broken") @@ -616,6 +618,26 @@ def get_authz_contribution(self): good = _make_ep("good", _Contributor) with patch("nemo_platform_plugin.discovery.entry_points", return_value=[bad, good]): - result = discover_customization_contributors() - assert "bad" not in result - assert "good" in result + with pytest.raises(CustomizationContributorDiscoveryError, match="Failed to load"): + discover_customization_contributors() + + def test_name_mismatch_raises(self) -> None: + from nemo_platform_plugin.customization_contributor import CustomizationContributorDiscoveryError + + class _Contributor: + name = "wrong" + dependencies = ["jobs"] + + def get_routers(self) -> list[RouterSpec]: + return [] + + def get_cli(self) -> None: + return None + + def get_authz_contribution(self): + return None + + ep = _make_ep("expected", _Contributor) + with patch("nemo_platform_plugin.discovery.entry_points", return_value=[ep]): + with pytest.raises(CustomizationContributorDiscoveryError, match="differs from class name"): + discover_customization_contributors() diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/inputs.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/inputs.py index cf401be67a..9a78f4450d 100644 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/inputs.py +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/inputs.py @@ -3,8 +3,6 @@ """CLI overrides: submit/run accept a job JSON file instead of ``--spec``.""" -from __future__ import annotations - import json from collections.abc import Callable from pathlib import Path diff --git a/plugins/nemo-customizer/src/nemo_customizer/router.py b/plugins/nemo-customizer/src/nemo_customizer/router.py index b885978e4b..2c471e381e 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/router.py +++ b/plugins/nemo-customizer/src/nemo_customizer/router.py @@ -9,6 +9,7 @@ from fastapi import APIRouter from nemo_platform_plugin.authz import AuthzContribution, AuthzEndpointMethod, combine_authz_contributions +from nemo_platform_plugin.customization_contributor import CustomizationContributorDiscoveryError from nemo_platform_plugin.discovery import ( CUSTOMIZATION_CONTRIBUTORS_GROUP, discover_customization_contributors, @@ -16,7 +17,7 @@ from nemo_platform_plugin.service import NemoService, RouterSpec -class CustomizationRouterError(RuntimeError): +class CustomizationRouterError(CustomizationContributorDiscoveryError): """Raised when the customization router cannot start.""" diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/__init__.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/__init__.py index d90bb76e4b..539a87421d 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/__init__.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/__init__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""NeMo Unsloth customization contributor — local fine-tuning via submit.""" +"""NeMo Unsloth customization contributor — container-submit GPU fine-tuning.""" diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/inputs.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/inputs.py index 8181a08e3e..20904723d0 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/inputs.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/inputs.py @@ -18,8 +18,6 @@ - ``explain`` → unchanged (the original schema dump is useful as-is). """ -from __future__ import annotations - import json from collections.abc import Callable from pathlib import Path @@ -92,7 +90,6 @@ def run( def _replace_job_submit(group: typer.Typer) -> None: """Replace ``submit`` with a ``JOB_JSON`` positional + standard submit flags.""" original = _pluck_callback(group, "submit") - _drop_command(group, "submit") @group.command("submit") def submit( diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/config.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/config.py index eac7b6c4a3..6ae3f12e58 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/config.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/config.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Plugin configuration for Unsloth local training.""" +"""Plugin configuration for Unsloth container-submit training.""" from __future__ import annotations @@ -13,11 +13,10 @@ class UnslothPluginConfig(BaseSettings): """Environment-driven Unsloth plugin settings. - All fields are optional; defaults match the in-process / BYO-venv - posture documented in the plugin README. The only knob the contributor - actually consumes today is ``default_training_execution_profile`` — - forwarded into ``add_job_routes`` so the platform's job collection - routes have a sensible profile when the submitter omits one. + All fields are optional. The only knob the contributor actually + consumes today is ``default_training_execution_profile`` — forwarded + into ``add_job_routes`` so the platform's job collection routes have + a sensible profile when the submitter omits one. """ model_config = SettingsConfigDict(env_prefix="NMP_UNSLOTH_", extra="ignore") diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/job_resources.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/job_resources.py index 6bdbd8d050..699b14b4cd 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/job_resources.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/job_resources.py @@ -1,12 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Unsloth job resources for status polling via the customization plugin API. - -Local-only training executes inside the user's venv, but we still expose -an SDK so callers can ``get_status`` against any record the -customization router persisted. -""" +"""Unsloth job resources for status polling via the customization plugin API.""" from __future__ import annotations diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/resources.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/resources.py index 836c8215e0..8e1bc1e045 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/resources.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/resources.py @@ -1,20 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Unsloth contributor SDK resources (composed by ``nemo-customizer-plugin``). - -A job-collection resource exposing ``plugin_status``, ``create``, and -``get_job_resource``, plus a ``UnslothCustomization`` namespace that -the hub mounts under ``client.customization.unsloth``. - -Even though Unsloth runs locally, we keep the SDK because the -customization router still persists job records (the jobs collection -route is exposed). ``create`` will succeed at the HTTP layer up to the -point ``compile()`` runs; production deployments should prefer the -local CLI path. -""" - -from __future__ import annotations +"""Unsloth contributor SDK resources (composed by ``nemo-customizer-plugin``).""" from typing import Any @@ -58,15 +45,7 @@ def create( workspace: str | None = None, name: str | None = None, ) -> UnslothJobResource: - """Submit an Unsloth training job record. - - Note: - Unsloth jobs execute *locally* — the platform-side compile - path raises ``NotImplementedError`` if the request lands - against an environment that tries to run the canonical job - container. Use ``nemo customization unsloth run`` for the - common case. - """ + """Submit an Unsloth training job to the platform GPU cluster.""" body: dict[str, Any] = http_utils.create_job_payload(spec) if name is not None: body["name"] = name diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py index 8ec7819721..24815f278b 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py @@ -5,12 +5,14 @@ Mirrors the automodel pattern: validates the platform refs (model entity + dataset fileset) against the live SDK, then resolves output -naming and the fileset name. The plugin's local ``run`` orchestrates -the actual download → train → upload → model_entity steps later. +naming and the fileset name. :meth:`~nemo_unsloth_plugin.jobs.jobs.UnslothJob.compile` +turns the canonical spec into a 4-step container job that performs +download → train → upload → model_entity on the platform cluster. Only platform refs are accepted today (per the strict-refs design -choice). Bare HF ids and arbitrary local paths are rejected before the -job runs because the run path expects a real fileset to download from. +choice). Bare HF ids and arbitrary local paths are rejected before +submit because the container pipeline expects a real fileset to +download from. """ from __future__ import annotations diff --git a/services/unsloth/pyproject.toml b/services/unsloth/pyproject.toml index 5aa2aca095..1ff9ae9e8d 100644 --- a/services/unsloth/pyproject.toml +++ b/services/unsloth/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "nmp-unsloth" version = "0.1.0" -description = "NeMo Unsloth task package — local SFT driver and container compile glue. No HTTP server." +description = "NeMo Unsloth task package — container SFT driver and compile glue. No HTTP server." readme = "README.md" requires-python = ">=3.11,<3.14" dependencies = [ @@ -15,9 +15,8 @@ dependencies = [ # Heavy ML deps live behind an optional extra. The package itself is # importable without these (e.g. compile.py + schemas), so the API -# process / plugin discovery doesn't pay the install cost. BYO-venv -# users install nmp-unsloth[unsloth] (or nemo-unsloth-plugin[unsloth] -# which delegates here) into a separate, GPU-capable venv. +# process / plugin discovery doesn't pay the install cost. The extra is +# for the training container image and local development against train_sft. [project.optional-dependencies] # We deliberately don't pin transformers / trl / peft / accelerate / bitsandbytes # here. Unsloth's own pyproject.toml has *precise* constraints for these diff --git a/services/unsloth/src/nmp/unsloth/app/constants.py b/services/unsloth/src/nmp/unsloth/app/constants.py index 32e8c97041..ecb3cfa3da 100644 --- a/services/unsloth/src/nmp/unsloth/app/constants.py +++ b/services/unsloth/src/nmp/unsloth/app/constants.py @@ -1,11 +1,10 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Shared constants for the unsloth service. +"""Shared constants for the unsloth container job pipeline. These mirror the ``services/automodel`` constants so the unsloth service -exposes the same path layout to a future container submit pipeline (and to -the plugin's local ``run`` orchestration today). +exposes the same path layout to the 4-step container submit pipeline. """ from nmp.common.jobs.constants import DEFAULT_JOB_STORAGE_PATH @@ -18,9 +17,8 @@ DEFAULT_VALIDATION_DATASET_OUTPUT_DIR_NAME = "validation_dataset" DEFAULT_OUTPUT_MODEL_DIR_NAME = "output_model" -# Absolute paths used by the compiler when wiring step-to-step file sharing. -# The plugin's local ``run`` re-derives equivalents under -# ``ctx.storage.persistent`` so it does not depend on these values. +# Absolute paths used by the compiler when wiring step-to-step file sharing +# inside the platform Jobs runner's mounted storage layout. DEFAULT_MODEL_PATH = f"{DEFAULT_JOB_STORAGE_PATH}/{DEFAULT_MODEL_OUTPUT_DIR_NAME}" DEFAULT_DATASET_PATH = f"{DEFAULT_JOB_STORAGE_PATH}/{DEFAULT_DATASET_OUTPUT_DIR_NAME}" DEFAULT_VALIDATION_DATASET_PATH = f"{DEFAULT_JOB_STORAGE_PATH}/{DEFAULT_VALIDATION_DATASET_OUTPUT_DIR_NAME}" diff --git a/services/unsloth/src/nmp/unsloth/app/jobs/training/compiler.py b/services/unsloth/src/nmp/unsloth/app/jobs/training/compiler.py index 8321fb63de..9439c175f9 100644 --- a/services/unsloth/src/nmp/unsloth/app/jobs/training/compiler.py +++ b/services/unsloth/src/nmp/unsloth/app/jobs/training/compiler.py @@ -1,13 +1,11 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Compile the training step of an unsloth job. +"""Compile the GPU training step of an unsloth container job. -This is the GPU step that container submit will execute. The plugin's -local ``run`` does **not** call this code path — it invokes -:func:`~nmp.unsloth.tasks.training.backends.unsloth_sft.train_sft` -directly inside the BYO-venv. Wiring the compiler step here today keeps -a future submit one-file-per-step away. +This is the second step in the 4-step ``PlatformJobSpec`` built by +:func:`~nmp.unsloth.compile.platform_job_config_compiler`. The container +entrypoint is ``python -m nmp.unsloth.tasks.training``. """ from __future__ import annotations diff --git a/services/unsloth/src/nmp/unsloth/config.py b/services/unsloth/src/nmp/unsloth/config.py index ec41665979..88761f17f8 100644 --- a/services/unsloth/src/nmp/unsloth/config.py +++ b/services/unsloth/src/nmp/unsloth/config.py @@ -1,12 +1,11 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Configuration for the nmp-unsloth compiler and tasks. +"""Configuration for the nmp-unsloth compiler and container tasks. Modeled after :mod:`nmp.automodel.config`. Environment variables use -the ``NMP_UNSLOTH_`` prefix. None of these settings are consumed by the -plugin's local ``run`` path today — they exist so a future container -submit (4-step PlatformJobSpec) lands without restructuring config. +the ``NMP_UNSLOTH_`` prefix and drive image resolution for the 4-step +container ``PlatformJobSpec`` the plugin's :meth:`~nemo_unsloth_plugin.jobs.jobs.UnslothJob.compile` builds. """ from nmp.common.config import create_service_config_class, get_platform_config, get_service_config diff --git a/services/unsloth/src/nmp/unsloth/schemas.py b/services/unsloth/src/nmp/unsloth/schemas.py index 2b165e32dd..33afbed22d 100644 --- a/services/unsloth/src/nmp/unsloth/schemas.py +++ b/services/unsloth/src/nmp/unsloth/schemas.py @@ -201,8 +201,8 @@ class WandbIntegration(BaseModel): enabled: bool = False project: str | None = None run_name: str | None = None - # No api_key_secret for now — users export WANDB_API_KEY in the venv - # environment themselves. Matches the BYO-venv stance. + # No api_key_secret for now — users export WANDB_API_KEY in the + # training container environment themselves. class IntegrationsSpec(BaseModel): @@ -215,7 +215,7 @@ class IntegrationsSpec(BaseModel): class OutputResponse(BaseModel): - """Stored on the canonical UnslothJobOutput. Path is resolved at run() time. + """Stored on the canonical UnslothJobOutput. Output naming is resolved during ``to_spec``. ``type`` is the high-level shape (``adapter`` for a saved LoRA, ``model`` for a merged checkpoint). ``save_method`` keeps the original Unsloth diff --git a/services/unsloth/tests/test_compile.py b/services/unsloth/tests/test_compile.py index 560f01c0d8..27cd9556b7 100644 --- a/services/unsloth/tests/test_compile.py +++ b/services/unsloth/tests/test_compile.py @@ -3,10 +3,9 @@ """Tests for the compile-side entry. -The plugin's local ``run`` does not invoke this code path — it lives -here so a future container submit drops in. We only check the -delegation contract: ``compile.platform_job_config_compiler`` forwards -to :mod:`nmp.unsloth.app.jobs.compiler` with the args the platform +We check the delegation contract: +``compile.platform_job_config_compiler`` forwards to +:mod:`nmp.unsloth.app.jobs.compiler` with the args the platform schedule passes through. """ diff --git a/services/unsloth/tests/test_main.py b/services/unsloth/tests/test_main.py index 2b901acc97..442fb3bae0 100644 --- a/services/unsloth/tests/test_main.py +++ b/services/unsloth/tests/test_main.py @@ -8,8 +8,8 @@ ``NEMO_JOB_STEP_CONFIG_FILE_PATH`` and then invokes ``train_sft`` against the paths the file_io step downloaded to. -These tests cover the failure path (no env var → exit 2 with a hint -back to the local BYO-venv flow). The happy path requires the +These tests cover the failure path (no env var → exit 2 with a hint to +submit via the customization CLI). The happy path requires the ``[unsloth]`` extra in the test env, so we don't exercise it here. """ @@ -103,7 +103,7 @@ def test_returns_2_when_step_config_env_missing( rc = main() assert rc == 2 err = capsys.readouterr().err - # Friendly hint redirects the user to the local CLI path. + # Friendly hint redirects the user to the submit CLI path. assert "nemo customization unsloth submit" in err def test_module_invocation_exits_2(self, monkeypatch: pytest.MonkeyPatch) -> None: From 246d5371f0f9748946467a7074528aa4c88719d6 Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Tue, 9 Jun 2026 15:53:10 -0600 Subject: [PATCH 15/26] Try to resolve flaky IGW test only occurring under xdist Signed-off-by: Sam Oluwalana --- packages/nmp_testing/src/nmp/testing/utils.py | 38 +++++-------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/packages/nmp_testing/src/nmp/testing/utils.py b/packages/nmp_testing/src/nmp/testing/utils.py index 2711a67222..83072abc23 100644 --- a/packages/nmp_testing/src/nmp/testing/utils.py +++ b/packages/nmp_testing/src/nmp/testing/utils.py @@ -480,6 +480,10 @@ def add_mock_provider( # reconciler calls GET /v1/models on each provider, and MOCK_SERVED_MODELS_HEADER tells # the mock which IDs to return. Without it, the mock returns the generic "mock-model" # default, causing the reconciler to overwrite our update_status served_models mapping. + # Use model *entity* names (the served_models keys), not served_model_name values: + # discovery builds model_entity_id from the mock's /v1/models ids, and passthrough + # VirtualModels are named after that entity. Advertising served names here makes the + # reconciler delete VMs created for the entity and recreate them under the wrong name. if served_models is None: if mock_response_body_by_model: served_models = { @@ -500,7 +504,7 @@ def add_mock_provider( if mock_status is not None: default_extra_headers[MOCK_STATUS_HEADER] = str(mock_status) - default_extra_headers[MOCK_SERVED_MODELS_HEADER] = json.dumps(list(served_models.values())) + default_extra_headers[MOCK_SERVED_MODELS_HEADER] = json.dumps(list(served_models.keys())) # Create the provider via SDK API (served_models not supported in SDK API). # If a provider with the same name already exists (ex. in a shared workspace), @@ -564,24 +568,6 @@ def add_mock_provider( ], ) - # Create a passthrough VirtualModel via the SDK for every served entity, mirroring - # the production provider reconciler's _ensure_passthrough_virtual_model behavior. - # The IGW now requires every inference request to resolve to a VirtualModel, and - # the IGW's background cache refresher rebuilds the VM map from the SDK list every - # few seconds — so any local-only seed would be wiped out at the next refresh tick. - # Going through the SDK ensures the VM exists in the entity store and survives refreshes. - # 409 ConflictError is treated as idempotent (matches the production reconciler). - for entity_name in served_models: - try: - sdk.inference.virtual_models.create( - workspace=workspace, - name=entity_name, - default_model_entity=f"{workspace}/{entity_name}", - autoprovisioned=True, - ) - except ConflictError: - pass - try: # From integration tests, we can directly update the local model cache to speed up subsequent requests model_cache = global_model_cache() @@ -591,8 +577,7 @@ def add_mock_provider( # Also seed the local VirtualModel cache so requests fired immediately after # this call hit the right cache state without waiting for the IGW's next - # background refresh tick. The SDK create above is what makes this survive - # subsequent refreshes; this in-place seed is purely a latency optimization. + # background refresh tick. This in-place seed is purely a latency optimization. from datetime import datetime as _datetime from nemo_platform.types.inference.virtual_model import VirtualModel as _SDKVirtualModel @@ -615,14 +600,11 @@ def add_mock_provider( updated_at=now_iso, ) except RuntimeError: - # From E2E tests, the local cache is not available (app runs in container). + # From E2E tests, the local cache is not available (app runs in a separate process). # Wait for model entities AND their autoprovisioned VirtualModels to become - # available in the container's caches. The container's caches pick up served_models - # from the update_status above; the production provider reconciler then creates - # the passthrough VirtualModel asynchronously. Inference requests now require - # a VirtualModel, so both must be present before the test fires requests. - # (We've also created VMs via the SDK above, but the container's cache is - # decoupled from this process so it still needs to refresh and pick them up.) + # available in the remote IGW caches. The provider reconciler creates passthrough + # VirtualModels asynchronously after update_status; inference routes require both + # the model entity and its VirtualModel to be visible before requests succeed. for entity_name in served_models.keys(): wait_for_virtual_model(sdk, workspace, entity_name) wait_for_model_entity(sdk, workspace, entity_name) From 697777c0ca3bdc781daa2ca0adb50f66e5e24bbe Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Tue, 9 Jun 2026 18:23:03 -0600 Subject: [PATCH 16/26] Fix tests Signed-off-by: Sam Oluwalana --- .../tests/integration/test_model_entity_service_integration.py | 3 ++- .../core/models/tests/unit/test_model_entity_service_unit.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/services/core/models/tests/integration/test_model_entity_service_integration.py b/services/core/models/tests/integration/test_model_entity_service_integration.py index 80ea73fdb7..609fe138bd 100644 --- a/services/core/models/tests/integration/test_model_entity_service_integration.py +++ b/services/core/models/tests/integration/test_model_entity_service_integration.py @@ -8,7 +8,8 @@ import pytest from nemo_platform import AsyncNeMoPlatform from nemo_platform.filesets import ListFilesResponse -from nemo_platform.types.files import Fileset, FilesetFile, FilesetMetadata, LocalStorageConfig +from nemo_platform.types.files import Fileset, FilesetFile, LocalStorageConfig +from nemo_platform.types.shared import FilesetMetadata from nmp.common.api.filter import ComparisonOperation, FilterOperator, LogicalOperation from nmp.common.api.parsed_filter import ParsedFilter from nmp.common.entities.client import EntityClient diff --git a/services/core/models/tests/unit/test_model_entity_service_unit.py b/services/core/models/tests/unit/test_model_entity_service_unit.py index e0b4c157f2..303ec80126 100644 --- a/services/core/models/tests/unit/test_model_entity_service_unit.py +++ b/services/core/models/tests/unit/test_model_entity_service_unit.py @@ -14,11 +14,11 @@ from nemo_platform.types.files import ( Fileset, FilesetFile, - FilesetMetadata, HuggingfaceStorageConfig, LocalStorageConfig, NGCStorageConfig, ) +from nemo_platform.types.shared import FilesetMetadata from nmp.common.api.common import Page, PaginationData from nmp.common.api.filter import ComparisonOperation, FilterOperator, LogicalOperation from nmp.common.api.parsed_filter import ParsedFilter From bcf7131f85fcc8cc5070edb040a6202ee473134a Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Tue, 9 Jun 2026 18:29:07 -0600 Subject: [PATCH 17/26] Fix tests Signed-off-by: Sam Oluwalana --- .../tests/integration/test_mock_provider_mode.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/services/core/inference-gateway/tests/integration/test_mock_provider_mode.py b/services/core/inference-gateway/tests/integration/test_mock_provider_mode.py index af21a9cca5..398aa453e3 100644 --- a/services/core/inference-gateway/tests/integration/test_mock_provider_mode.py +++ b/services/core/inference-gateway/tests/integration/test_mock_provider_mode.py @@ -719,8 +719,8 @@ def test_provider_route_smart_default_models( provider_in_cache: tuple[str, str, str], endpoint: str, ): - """Test provider route returns the configured served model IDs for the models endpoint.""" - provider_name, _, served_model_name = provider_in_cache + """Test provider route returns configured model entity IDs for the models endpoint.""" + provider_name, model_entity_name, _ = provider_in_cache client = mock_provider_test_clients.test_client response = client.get(_provider_route(DEFAULT_WORKSPACE, provider_name, endpoint)) @@ -729,7 +729,7 @@ def test_provider_route_smart_default_models( data = response.json() assert data["object"] == "list" assert len(data["data"]) > 0 - assert data["data"][0]["id"] == served_model_name + assert data["data"][0]["id"] == model_entity_name # ============================================================================= @@ -877,8 +877,8 @@ def test_model_route_smart_default_models( provider_in_cache: tuple[str, str, str], endpoint: str, ): - """Test model entity route returns the configured served model IDs for the models endpoint.""" - _, model_entity_name, served_model_name = provider_in_cache + """Test model entity route returns configured model entity IDs for the models endpoint.""" + _, model_entity_name, _ = provider_in_cache client = mock_provider_test_clients.test_client response = client.get(_model_route(DEFAULT_WORKSPACE, model_entity_name, endpoint)) @@ -886,7 +886,7 @@ def test_model_route_smart_default_models( assert response.status_code == 200 data = response.json() assert data["object"] == "list" - assert data["data"][0]["id"] == served_model_name + assert data["data"][0]["id"] == model_entity_name # ============================================================================= From 2324ada25f335f3862c7b71430c2f56c6c4e7a6b Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Wed, 10 Jun 2026 11:12:53 -0600 Subject: [PATCH 18/26] Remove unused bake variable, pin unsloth Signed-off-by: Sam Oluwalana --- docker-bake.hcl | 8 -------- services/unsloth/docker/Dockerfile.nmp-unsloth-training | 3 ++- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/docker-bake.hcl b/docker-bake.hcl index c4969b2a7d..9fdf9ecad4 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -79,14 +79,6 @@ variable "BUILD_PLATFORMS" { default = ["linux/amd64", "linux/arm64"] } -# --------------------------------------------------------------------------- -# Unsloth variables -# --------------------------------------------------------------------------- - -variable "BUILD_PLATFORM" { - default = "linux/amd64" -} - # --------------------------------------------------------------------------- # Automodel helpers # --------------------------------------------------------------------------- diff --git a/services/unsloth/docker/Dockerfile.nmp-unsloth-training b/services/unsloth/docker/Dockerfile.nmp-unsloth-training index 4858f7bc68..7e74010e03 100644 --- a/services/unsloth/docker/Dockerfile.nmp-unsloth-training +++ b/services/unsloth/docker/Dockerfile.nmp-unsloth-training @@ -53,6 +53,7 @@ FROM base AS runtime ARG USERNAME=ubuntu ARG USER_UID=1000 ARG USER_GID=1000 +ARG UNSLOTH_VERSION=2026.6.1 WORKDIR /app @@ -65,7 +66,7 @@ RUN mkdir -p /home/${USERNAME}/.cache && \ RUN --mount=type=cache,target=/root/.cache/uv \ uv pip install --python ${VIRTUAL_ENV}/bin/python --no-cache \ --torch-backend=auto \ - unsloth + unsloth==${UNSLOTH_VERSION} # TODO: Step 1b: Flash Attention 2 — compiled from source against the NGC 26.02 torch. # /usr/local/cuda symlinks to an older toolkit; use /usr/local/cuda-13.1 instead. From f162ed1ebf58d6f2fdc75a3fb7b85134ee25e6e8 Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Wed, 10 Jun 2026 12:08:54 -0600 Subject: [PATCH 19/26] Fix missing mkdocs.yaml in dockerfile Signed-off-by: Sam Oluwalana --- services/automodel/docker/Dockerfile.platform-workspace | 1 - services/unsloth/docker/Dockerfile.platform-workspace | 1 - 2 files changed, 2 deletions(-) diff --git a/services/automodel/docker/Dockerfile.platform-workspace b/services/automodel/docker/Dockerfile.platform-workspace index 70aeae3f3c..ff2e8dc1da 100644 --- a/services/automodel/docker/Dockerfile.platform-workspace +++ b/services/automodel/docker/Dockerfile.platform-workspace @@ -10,7 +10,6 @@ COPY services/automodel/docker/pyproject.workspace.toml pyproject.toml # docs/api/openapi.yaml is a symlink to ../../openapi/openapi.yaml — copy both. COPY docs docs COPY openapi openapi -COPY mkdocs.yml mkdocs.yml COPY packages/nmp_build_tools packages/nmp_build_tools COPY packages/models packages/models COPY packages/nmp_common packages/nmp_common diff --git a/services/unsloth/docker/Dockerfile.platform-workspace b/services/unsloth/docker/Dockerfile.platform-workspace index 96bc0181dd..fa9f22dc80 100644 --- a/services/unsloth/docker/Dockerfile.platform-workspace +++ b/services/unsloth/docker/Dockerfile.platform-workspace @@ -12,7 +12,6 @@ COPY services/unsloth/docker/pyproject.workspace.toml pyproject.toml # trees so the symlink resolves at build time. COPY docs docs COPY openapi openapi -COPY mkdocs.yml mkdocs.yml COPY packages/nmp_build_tools packages/nmp_build_tools COPY packages/nmp_common packages/nmp_common COPY packages/nemo_platform_plugin packages/nemo_platform_plugin From bc100dfc90e5dab0e1244cba6586e229e6fe3b36 Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Wed, 10 Jun 2026 12:37:15 -0600 Subject: [PATCH 20/26] update platform-workspace comments after Fern docs migration Signed-off-by: Sam Oluwalana --- services/automodel/docker/Dockerfile.platform-workspace | 4 ++-- services/unsloth/docker/Dockerfile.platform-workspace | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/automodel/docker/Dockerfile.platform-workspace b/services/automodel/docker/Dockerfile.platform-workspace index ff2e8dc1da..b8b77ae190 100644 --- a/services/automodel/docker/Dockerfile.platform-workspace +++ b/services/automodel/docker/Dockerfile.platform-workspace @@ -6,8 +6,8 @@ FROM scratch AS platform-workspace # Do not copy repo-root pyproject.toml/uv.lock — they reference the full monorepo workspace. COPY services/automodel/docker/pyproject.workspace.toml pyproject.toml -# nemo-platform-sdk hatch build force-includes docs/ and mkdocs.yml from repo root. -# docs/api/openapi.yaml is a symlink to ../../openapi/openapi.yaml — copy both. +# nemo-platform-sdk hatch build force-includes docs/ from repo root. +# docs/fern/openapi/openapi.yaml is a symlink to ../../../openapi/openapi.yaml — copy both. COPY docs docs COPY openapi openapi COPY packages/nmp_build_tools packages/nmp_build_tools diff --git a/services/unsloth/docker/Dockerfile.platform-workspace b/services/unsloth/docker/Dockerfile.platform-workspace index fa9f22dc80..6d4d46c55d 100644 --- a/services/unsloth/docker/Dockerfile.platform-workspace +++ b/services/unsloth/docker/Dockerfile.platform-workspace @@ -6,9 +6,9 @@ FROM scratch AS platform-workspace # Do not copy repo-root pyproject.toml/uv.lock — they reference the full monorepo workspace. COPY services/unsloth/docker/pyproject.workspace.toml pyproject.toml -# nemo-platform-sdk's hatch build force-includes docs/ and mkdocs.yml from the -# repo root (see sdk/python/nemo-platform/pyproject.toml [tool.hatch.build.targets.wheel.force-include]). -# docs/api/openapi.yaml is a symlink to ../../openapi/openapi.yaml — copy both +# nemo-platform-sdk's hatch build force-includes docs/ from the repo root +# (see sdk/python/nemo-platform/pyproject.toml [tool.hatch.build.targets.wheel.force-include]). +# docs/fern/openapi/openapi.yaml is a symlink to ../../../openapi/openapi.yaml — copy both # trees so the symlink resolves at build time. COPY docs docs COPY openapi openapi From 252098da8eb1a3d38dc8352b0cc397a8f09920a7 Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Wed, 10 Jun 2026 12:54:05 -0600 Subject: [PATCH 21/26] Resolve python type merge conflict with VLLM PR Signed-off-by: Sam Oluwalana --- .../nmp/automodel/tasks/model_entity/run.py | 23 ++++++++++++------- .../src/nmp/unsloth/tasks/model_entity/run.py | 23 ++++++++++++------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/services/automodel/src/nmp/automodel/tasks/model_entity/run.py b/services/automodel/src/nmp/automodel/tasks/model_entity/run.py index d1a6263d83..8009c979f7 100644 --- a/services/automodel/src/nmp/automodel/tasks/model_entity/run.py +++ b/services/automodel/src/nmp/automodel/tasks/model_entity/run.py @@ -28,10 +28,11 @@ NotFoundError, ) from nemo_platform.types.inference import ( + ContainerExecutorConfigParam, ModelDeploymentConfig, ModelDeploymentConfigFilterParam, + ModelDeploymentConfigModelSpecParam, ModelDeploymentFilterParam, - NIMDeploymentParam, ) from nemo_platform.types.models import LoraParam, ModelEntity from nemo_platform.types.shared_params.tool_call_config import ToolCallConfig as ToolCallConfigParam @@ -335,18 +336,20 @@ def _resolve_config_ref(self, config_ref: str, me_workspace: str) -> ModelDeploy def _create_deployment_config(self, deploy_params: DeploymentParameters, me: ModelEntity) -> ModelDeploymentConfig: """Create (or update) a ModelDeploymentConfig from inline parameters.""" - nim_deployment = NIMDeploymentParam( + model_spec = ModelDeploymentConfigModelSpecParam( + model_name=me.name, + model_namespace=me.workspace, + lora_enabled=deploy_params.lora_enabled, + ) + executor_config = ContainerExecutorConfigParam( image_name=deploy_params.image_name, image_tag=deploy_params.image_tag, gpu=deploy_params.gpu, - model_name=me.name, - model_namespace=me.workspace, additional_envs=deploy_params.additional_envs, - lora_enabled=deploy_params.lora_enabled, ) if deploy_params.tool_call_config: - nim_deployment["tool_call_config"] = ToolCallConfigParam( + model_spec["tool_call_config"] = ToolCallConfigParam( **deploy_params.tool_call_config.model_dump(exclude_none=True) ) @@ -355,14 +358,18 @@ def _create_deployment_config(self, deploy_params: DeploymentParameters, me: Mod return self.sdk.inference.deployment_configs.create( workspace=me.workspace, name=deployment_cfg_name, - nim_deployment=nim_deployment, + engine="nim", + model_spec=model_spec, + executor_config=executor_config, ) except ConflictError: logger.info(f"Deployment config {me.workspace}/{deployment_cfg_name} already exists, updating") return self.sdk.inference.deployment_configs.update( workspace=me.workspace, name=deployment_cfg_name, - nim_deployment=nim_deployment, + engine="nim", + model_spec=model_spec, + executor_config=executor_config, ) def _create_deployment(self, deployment_config: ModelDeploymentConfig, me: ModelEntity) -> None: diff --git a/services/unsloth/src/nmp/unsloth/tasks/model_entity/run.py b/services/unsloth/src/nmp/unsloth/tasks/model_entity/run.py index 603549287a..c3fe0f54eb 100644 --- a/services/unsloth/src/nmp/unsloth/tasks/model_entity/run.py +++ b/services/unsloth/src/nmp/unsloth/tasks/model_entity/run.py @@ -29,10 +29,11 @@ NotFoundError, ) from nemo_platform.types.inference import ( + ContainerExecutorConfigParam, ModelDeploymentConfig, ModelDeploymentConfigFilterParam, + ModelDeploymentConfigModelSpecParam, ModelDeploymentFilterParam, - NIMDeploymentParam, ) from nemo_platform.types.models import LoraParam, ModelEntity from nemo_platform.types.shared_params.tool_call_config import ToolCallConfig as ToolCallConfigParam @@ -336,18 +337,20 @@ def _resolve_config_ref(self, config_ref: str, me_workspace: str) -> ModelDeploy def _create_deployment_config(self, deploy_params: DeploymentParameters, me: ModelEntity) -> ModelDeploymentConfig: """Create (or update) a ``ModelDeploymentConfig`` from inline parameters.""" - nim_deployment = NIMDeploymentParam( + model_spec = ModelDeploymentConfigModelSpecParam( + model_name=me.name, + model_namespace=me.workspace, + lora_enabled=deploy_params.lora_enabled, + ) + executor_config = ContainerExecutorConfigParam( image_name=deploy_params.image_name, image_tag=deploy_params.image_tag, gpu=deploy_params.gpu, - model_name=me.name, - model_namespace=me.workspace, additional_envs=deploy_params.additional_envs, - lora_enabled=deploy_params.lora_enabled, ) if deploy_params.tool_call_config: - nim_deployment["tool_call_config"] = ToolCallConfigParam( + model_spec["tool_call_config"] = ToolCallConfigParam( **deploy_params.tool_call_config.model_dump(exclude_none=True) ) @@ -356,14 +359,18 @@ def _create_deployment_config(self, deploy_params: DeploymentParameters, me: Mod return self.sdk.inference.deployment_configs.create( workspace=me.workspace, name=deployment_cfg_name, - nim_deployment=nim_deployment, + engine="nim", + model_spec=model_spec, + executor_config=executor_config, ) except ConflictError: logger.info(f"Deployment config {me.workspace}/{deployment_cfg_name} already exists, updating") return self.sdk.inference.deployment_configs.update( workspace=me.workspace, name=deployment_cfg_name, - nim_deployment=nim_deployment, + engine="nim", + model_spec=model_spec, + executor_config=executor_config, ) def _create_deployment(self, deployment_config: ModelDeploymentConfig, me: ModelEntity) -> None: From f8d2616b1421a4c6db86905cf951ed87171cb24d Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Wed, 10 Jun 2026 13:14:23 -0600 Subject: [PATCH 22/26] Allow lint-fix to report on whether the fix was successful afterwards, fix 503s from storage when using HF repos Signed-off-by: Sam Oluwalana --- Makefile | 6 ++- .../core/models/tests/integration/conftest.py | 16 ++++++ .../integration/test_models_with_auth.py | 1 + tools/lint/lint-all.sh | 5 +- tools/lint/lint-fix.sh | 54 +++++++++++++++++-- 5 files changed, 74 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index cf6921c3ec..5ad9be7e17 100644 --- a/Makefile +++ b/Makefile @@ -228,9 +228,11 @@ check-copyright-headers: lint: ## Run all linters (licenses, openapi, config docs, python style/types/sdk, vendored SDK, CLI, auth config) bash tools/lint/lint-all.sh +LINT_FIX_VERIFY ?= 0 + .PHONY: lint-fix -lint-fix: ## Auto-fix lint issues in dependency order (openapi → stainless → style → cli → vendor → licenses → config-docs) - bash tools/lint/lint-fix.sh +lint-fix: ## Auto-fix lint issues (set LINT_FIX_VERIFY=1 to also run CI lint checks) + LINT_FIX_VERIFY=$(LINT_FIX_VERIFY) bash tools/lint/lint-fix.sh .PHONY: vendor vendor: ## Vendor packages into the SDK and generate wrapper metadata diff --git a/services/core/models/tests/integration/conftest.py b/services/core/models/tests/integration/conftest.py index 61bd002a9b..cd5833f926 100644 --- a/services/core/models/tests/integration/conftest.py +++ b/services/core/models/tests/integration/conftest.py @@ -18,6 +18,7 @@ from nmp.common.secrets.encryption import get_base64_encoded_random_bytes from nmp.core.models.controllers.backends.backends import DeploymentStatusUpdate, ServiceBackend from nmp.core.models.controllers.backends.registry import BackendRegistry +from nmp.core.files.app.backends.huggingface import HuggingfaceStorageImpl from nmp.core.models.controllers.models_controller import ModelsController from nmp.core.models.service import ModelsService from nmp.core.secrets.config import SecretsServiceConfig @@ -36,6 +37,21 @@ blockbuster = blockbuster_fixture(autouse=True) + +@pytest.fixture +def no_hf_network(monkeypatch): + """Disable live HuggingFace API calls; keep fileset create/update paths local.""" + + async def _validate_noop(self): + return None + + async def _resolve_passthrough(self): + return self.config + + monkeypatch.setattr(HuggingfaceStorageImpl, "validate_storage", _validate_noop) + monkeypatch.setattr(HuggingfaceStorageImpl, "resolve_config", _resolve_passthrough) + + # ============================================================================= # Constants # ============================================================================= diff --git a/services/core/models/tests/integration/test_models_with_auth.py b/services/core/models/tests/integration/test_models_with_auth.py index ab63ab746b..6cf7e6ad46 100644 --- a/services/core/models/tests/integration/test_models_with_auth.py +++ b/services/core/models/tests/integration/test_models_with_auth.py @@ -1324,6 +1324,7 @@ def test_custom_role_denied_create_model_with_fileset_without_fileset_read(self, @pytest.mark.integration +@pytest.mark.usefixtures("no_hf_network") class TestTrustRemoteCodePermission: """Test trust_remote_code permission (models.trust-remote-code.set) at the API layer. diff --git a/tools/lint/lint-all.sh b/tools/lint/lint-all.sh index 8524b03d60..681cc712b4 100755 --- a/tools/lint/lint-all.sh +++ b/tools/lint/lint-all.sh @@ -72,10 +72,13 @@ if [[ ${#failed[@]} -gt 0 ]]; then break fi done - if [[ "${has_fix}" == "true" ]]; then + if [[ "${has_fix}" == "true" && "${LINT_AFTER_FIX:-}" != "1" ]]; then echo "To fix auto-fixable issues, run:" echo " make lint-fix" echo "" + elif [[ "${LINT_AFTER_FIX:-}" == "1" ]]; then + echo "Auto-fix completed; remaining issues require manual fixes (see output above)." + echo "" fi for name in "${failed[@]}"; do if is_no_fix_lint "${name}"; then diff --git a/tools/lint/lint-fix.sh b/tools/lint/lint-fix.sh index 776c1d2cf2..f5b8a0fbf8 100755 --- a/tools/lint/lint-fix.sh +++ b/tools/lint/lint-fix.sh @@ -7,9 +7,12 @@ set -euo pipefail # 4. Python style (ruff; run before vendoring so generated files aren't re-linted) # 5. CLI command generation (the vendoring and docs are handled by the next step) # 6. Vendor all packages (covers nemo_platform_ext too) + CLI reference docs -# 7. License update (may change after vendoring) -# 8. Config reference docs (independent, but run after structural changes) -# 9. Auth docs (regenerate permissions reference from static-authz.yaml) +# 7. Copyright headers (after generated files are in place) +# 8. License update (may change after vendoring) +# 9. Config reference docs (independent, but run after structural changes) +# 10. Auth docs (regenerate permissions reference from static-authz.yaml) +# 11. Verification (optional) — run the same checks as CI (tools/lint/lint-all.sh) +# Enable with LINT_FIX_VERIFY=1. # # Note: update-sdk = build-policy + refresh-openapi + stainless + update-cli, so we use # stainless directly here to avoid re-running refresh-openapi and update-cli redundantly. @@ -20,6 +23,11 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="${CI_PROJECT_DIR:-$(cd "${SCRIPT_DIR}/../.." && pwd)}" cd "${PROJECT_ROOT}" || exit 1 +verify=false +if [[ "${LINT_FIX_VERIFY:-}" == "1" ]]; then + verify=true +fi + declare -a steps=( "refresh-openapi:make refresh-openapi" "web-sdk:bash tools/lint/lint-fix-web-sdk.sh" @@ -27,6 +35,7 @@ declare -a steps=( "python-style:uv run ruff format && uv run ruff check --fix" "generate-cli-commands:make generate-cli-commands" "vendor+cli-reference-docs:make vendor && make generate-cli-reference-docs" + "copyright-headers:make update-copyright-headers" "update-licenses:bash tools/lint/lint-fix-licenses.sh" "auth-config:uv run python services/core/auth/scripts/auth-tools.py update" "generate-config-docs:uv run generate-config-docs" @@ -63,7 +72,42 @@ for row in "${timing_rows[@]}"; do done if [[ ${#failed[@]} -gt 0 ]]; then echo "" - echo "Failed steps: ${failed[*]}" + echo "Failed fix steps: ${failed[*]}" +fi + +if [[ "${verify}" == "true" ]]; then + echo "" + echo ">>> verification: bash tools/lint/lint-all.sh" + verify_start=$(date +%s) + if LINT_AFTER_FIX=1 bash tools/lint/lint-all.sh; then + verify_result="PASS" + else + verify_result="FAIL" + failed+=("verification") + fi + verify_elapsed=$(( $(date +%s) - verify_start )) + printf ' %-40s %s\n' "verification (lint-all)" "${verify_result} ${verify_elapsed}s" +fi + +echo "" +if [[ "${verify}" == "true" ]]; then + echo "--- Overall summary ---" + if [[ ${#failed[@]} -eq 0 ]]; then + echo "All fix steps and CI lint checks passed." + exit 0 + fi + if [[ ${#failed[@]} -eq 1 && " ${failed[*]} " == *" verification "* ]]; then + echo "Auto-fix completed; remaining lint issues require manual fixes (see output above)." + else + echo "Some fix steps and/or lint checks failed: ${failed[*]}" + fi exit 1 fi -exit 0 + +if [[ ${#failed[@]} -eq 0 ]]; then + echo "All fix steps completed." + exit 0 +fi +echo "Some fix steps failed: ${failed[*]}" +echo "Run with LINT_FIX_VERIFY=1 to check CI lint after fixing." +exit 1 From 8f8cc09e4e25eea9ef7b331ce9c27269ecd395c4 Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Wed, 10 Jun 2026 13:53:25 -0600 Subject: [PATCH 23/26] lint fix Signed-off-by: Sam Oluwalana --- services/core/models/tests/integration/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/core/models/tests/integration/conftest.py b/services/core/models/tests/integration/conftest.py index cd5833f926..8c3cdacb1a 100644 --- a/services/core/models/tests/integration/conftest.py +++ b/services/core/models/tests/integration/conftest.py @@ -16,9 +16,9 @@ from nemo_platform.types.inference.model_deployment_config import ModelDeploymentConfig from nemo_platform.types.models.model_entity import ModelEntity from nmp.common.secrets.encryption import get_base64_encoded_random_bytes +from nmp.core.files.app.backends.huggingface import HuggingfaceStorageImpl from nmp.core.models.controllers.backends.backends import DeploymentStatusUpdate, ServiceBackend from nmp.core.models.controllers.backends.registry import BackendRegistry -from nmp.core.files.app.backends.huggingface import HuggingfaceStorageImpl from nmp.core.models.controllers.models_controller import ModelsController from nmp.core.models.service import ModelsService from nmp.core.secrets.config import SecretsServiceConfig From cd32dfefa61e0c02debadda8e458406e51e05a0a Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Wed, 10 Jun 2026 14:50:35 -0600 Subject: [PATCH 24/26] Remove type hint fixes Signed-off-by: Sam Oluwalana --- conftest.py | 2 +- .../src/nemo_platform_plugin/commands.py | 20 ++++++------- .../src/nemo_platform_plugin/config.py | 4 +-- .../src/nemo_platform_plugin/entities.py | 2 +- .../jobs/result_manager.py | 6 ++-- .../tests/test_cli_progress.py | 2 +- .../tests/test_dispatcher.py | 2 +- .../src/calculator_agent/register.py | 8 +++--- .../nemo_agents_plugin/jobs/analyze_batch.py | 2 +- .../nemo_agents_plugin/jobs/evaluate_agent.py | 2 +- .../jobs/optimize_skills.py | 2 +- .../telemetry/files_service_exporter.py | 14 +++++----- .../tests/unit/test_runner_controller.py | 2 +- plugins/nemo-agents/tests/unit/test_utils.py | 28 +++++++++---------- .../nemo-anonymizer/tests/unit/test_input.py | 6 ++-- .../tests/unit/test_preview_function.py | 2 +- .../tests/unit/test_routing.py | 2 +- .../cli/renderers.py | 4 +-- .../tests/unit/test_middleware.py | 2 +- .../tasks/training/backends/finetune.py | 6 ++-- 20 files changed, 59 insertions(+), 59 deletions(-) diff --git a/conftest.py b/conftest.py index 193651143e..e231124c72 100644 --- a/conftest.py +++ b/conftest.py @@ -305,4 +305,4 @@ def _patched_reschedule(self, node): _original_reschedule(self, node) -LoadScopeScheduling._reschedule = _patched_reschedule +LoadScopeScheduling._reschedule = _patched_reschedule # type: ignore[invalid-assignment] diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/commands.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/commands.py index c80b964a6a..0e9550e1e7 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/commands.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/commands.py @@ -441,7 +441,7 @@ def _do_run() -> Any: renderer.on_complete(ctx=rctx) help_text = f"Run {job_cls.name} locally, in-process." - _run.__signature__ = _build_job_run_signature(leaves) + _run.__signature__ = _build_job_run_signature(leaves) # type: ignore[attr-defined] group.command(name="run", help=help_text)(_run) @@ -618,7 +618,7 @@ def _do_submit() -> Any: renderer.on_complete(ctx=rctx) help_text = f"Submit {job_cls.name} to a cluster." - _submit.__signature__ = _build_job_submit_signature(leaves) + _submit.__signature__ = _build_job_submit_signature(leaves) # type: ignore[attr-defined] group.command(name="submit", help=help_text)(_submit) @@ -960,7 +960,7 @@ def _run(typer_ctx: typer.Context, **kwargs: object) -> None: help_text = f"Run {fn_cls.name} locally, in-process." epilog = build_epilog(schema=fn_cls.spec_schema, leaves=leaves, kind="Function") - _run.__signature__ = _build_function_run_signature(leaves) + _run.__signature__ = _build_function_run_signature(leaves) # type: ignore[attr-defined] group.command(name="run", help=help_text, epilog=epilog)(_run) @@ -1113,12 +1113,12 @@ def _add_function_submit_command( def _submit(typer_ctx: typer.Context, **kwargs: object) -> None: original_kwargs = dict(kwargs) - spec_str: str = kwargs.pop("spec", "{}") - spec_file: Path | None = kwargs.pop("spec_file", None) - cluster: str | None = kwargs.pop("cluster", None) - base_url: str | None = kwargs.pop("base_url", None) - workspace: str = kwargs.pop("workspace", "default") - request_id: str | None = kwargs.pop("request_id", None) + spec_str: str = kwargs.pop("spec", "{}") # type: ignore[assignment] + spec_file: Path | None = kwargs.pop("spec_file", None) # type: ignore[assignment] + cluster: str | None = kwargs.pop("cluster", None) # type: ignore[assignment] + base_url: str | None = kwargs.pop("base_url", None) # type: ignore[assignment] + workspace: str = kwargs.pop("workspace", "default") # type: ignore[assignment] + request_id: str | None = kwargs.pop("request_id", None) # type: ignore[assignment] base = _load_spec(spec_str, spec_file) overlay = build_overlay(leaves, kwargs, unset_sentinel=UNSET) @@ -1164,7 +1164,7 @@ def _submit(typer_ctx: typer.Context, **kwargs: object) -> None: help_text = f"Submit {fn_cls.name} over HTTP." epilog = build_epilog(schema=fn_cls.spec_schema, leaves=leaves, kind="Function") - _submit.__signature__ = _build_function_submit_signature(leaves) + _submit.__signature__ = _build_function_submit_signature(leaves) # type: ignore[attr-defined] group.command(name="submit", help=help_text, epilog=epilog)(_submit) diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/config.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/config.py index 8e401f5f8d..0184bdc9a0 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/config.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/config.py @@ -283,7 +283,7 @@ def _get_cached_config(service_config: Type[_T_config]) -> _T_config: @classmethod def get_service_config(cls, service_config: Type[_T_config]) -> _T_config: if service_config in cls._overrides: - return cls._overrides[service_config] + return cls._overrides[service_config] # type: ignore[return-value] return cls._get_cached_config(service_config) @staticmethod @@ -637,7 +637,7 @@ def get_nemo_platform_config() -> NemoPlatformConfig: # Default implementation — subclasses (nmp-common) override this to return their extended PlatformConfig. -Configuration.get_platform_config = classmethod(lambda cls: cls.get_service_config(NemoPlatformConfig)) +Configuration.get_platform_config = classmethod(lambda cls: cls.get_service_config(NemoPlatformConfig)) # type: ignore[attr-defined] # Aliases — nmp-common and services use PlatformConfig / get_platform_config PlatformConfig = NemoPlatformConfig diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/entities.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/entities.py index fdbbb27cf2..56dde5bd12 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/entities.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/entities.py @@ -105,7 +105,7 @@ class EntityBase(BaseModel): "_db_version", } - __entity_type__: ClassVar[str] = EntityTypeDefault() + __entity_type__: ClassVar[str] = EntityTypeDefault() # type: ignore[assignment] model_config = {"populate_by_name": True} diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/jobs/result_manager.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/jobs/result_manager.py index 95bcfccb5f..b65a31de3f 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/jobs/result_manager.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/jobs/result_manager.py @@ -241,9 +241,9 @@ def result_manager_factory( job_name=job_name, workspace=workspace, attempt_id=attempt_id, - file_manager_cls=file_manager_cls, - files_sdk=files_sdk, - jobs_sdk=jobs_sdk, + file_manager_cls=file_manager_cls, # type: ignore + files_sdk=files_sdk, # type: ignore + jobs_sdk=jobs_sdk, # type: ignore ) diff --git a/packages/nemo_platform_plugin/tests/test_cli_progress.py b/packages/nemo_platform_plugin/tests/test_cli_progress.py index 23834abd01..25bb3aa67a 100644 --- a/packages/nemo_platform_plugin/tests/test_cli_progress.py +++ b/packages/nemo_platform_plugin/tests/test_cli_progress.py @@ -93,5 +93,5 @@ def __init__(self, elapsed: float | None) -> None: self.finished = False self.finished_time = None - rendered = _MinutesSecondsElapsedColumn().render(_StubTask(elapsed)) + rendered = _MinutesSecondsElapsedColumn().render(_StubTask(elapsed)) # type: ignore[arg-type] assert rendered.plain == expected diff --git a/packages/nemo_platform_plugin/tests/test_dispatcher.py b/packages/nemo_platform_plugin/tests/test_dispatcher.py index 38a4e2e75f..c8d0c68d47 100644 --- a/packages/nemo_platform_plugin/tests/test_dispatcher.py +++ b/packages/nemo_platform_plugin/tests/test_dispatcher.py @@ -156,7 +156,7 @@ class _Job(NemoJob): name = "non-dict" def run(self, config: dict) -> dict: - return "hello" + return "hello" # type: ignore[return-value] rc = run_task(_Job, sdk=_DEFAULT_SDK) diff --git a/plugins/nemo-agents/examples/calculator-agent/src/calculator_agent/register.py b/plugins/nemo-agents/examples/calculator-agent/src/calculator_agent/register.py index d7cfdf0d56..1435c56c76 100644 --- a/plugins/nemo-agents/examples/calculator-agent/src/calculator_agent/register.py +++ b/plugins/nemo-agents/examples/calculator-agent/src/calculator_agent/register.py @@ -8,10 +8,10 @@ from collections.abc import AsyncGenerator -from nat.builder.builder import Builder -from nat.builder.function import FunctionGroup -from nat.cli.register_workflow import register_function_group -from nat.data_models.function import FunctionGroupBaseConfig +from nat.builder.builder import Builder # type: ignore +from nat.builder.function import FunctionGroup # type: ignore +from nat.cli.register_workflow import register_function_group # type: ignore +from nat.data_models.function import FunctionGroupBaseConfig # type: ignore from pydantic import Field diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/jobs/analyze_batch.py b/plugins/nemo-agents/src/nemo_agents_plugin/jobs/analyze_batch.py index eca016d19c..36f55842f6 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/jobs/analyze_batch.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/jobs/analyze_batch.py @@ -155,7 +155,7 @@ def run(self, config: dict, *, ctx: JobContext | None = None) -> dict: ga = asyncio.run(generate_gap_analysis(batch=batch, parser=parser, baselines=baselines)) if cfg.format == "json": - return _serialize(ga) + return _serialize(ga) # type: ignore[return-value] # markdown from nemo_agents_plugin.improvement._analysis_reporting import generate_gap_report diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/jobs/evaluate_agent.py b/plugins/nemo-agents/src/nemo_agents_plugin/jobs/evaluate_agent.py index 29bd3c4aa2..2eeda79456 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/jobs/evaluate_agent.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/jobs/evaluate_agent.py @@ -150,7 +150,7 @@ class EvaluateAgentJob(NemoJob): spec_schema: ClassVar[type[BaseModel]] = EvaluateAgentSpec @classmethod - async def compile( + async def compile( # type: ignore[override] cls, *, workspace: str, diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/jobs/optimize_skills.py b/plugins/nemo-agents/src/nemo_agents_plugin/jobs/optimize_skills.py index 3327668d0d..6993b491bc 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/jobs/optimize_skills.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/jobs/optimize_skills.py @@ -182,7 +182,7 @@ def run(self, config: dict, *, ctx: JobContext | None = None) -> dict: trace_parser=cfg.trace_parser, ) ) - return _serialize(state) # type: ignore[return-value] + return _serialize(state) # type: ignore[return-value] # type: ignore[return-value] # Preflight: fail fast before any slow work preflight.check_evals_dir(evals_dir) diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/telemetry/files_service_exporter.py b/plugins/nemo-agents/src/nemo_agents_plugin/telemetry/files_service_exporter.py index a922679ed2..10d77c1bbe 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/telemetry/files_service_exporter.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/telemetry/files_service_exporter.py @@ -31,13 +31,13 @@ import time import uuid -from nat.builder.builder import Builder -from nat.cli.register_workflow import register_telemetry_exporter -from nat.data_models.intermediate_step import IntermediateStep -from nat.data_models.telemetry_exporter import TelemetryExporterBaseConfig -from nat.observability.exporter.base_exporter import IsolatedAttribute -from nat.observability.exporter.raw_exporter import RawExporter -from nat.observability.processor.intermediate_step_serializer import ( +from nat.builder.builder import Builder # type: ignore[unresolved-import] +from nat.cli.register_workflow import register_telemetry_exporter # type: ignore[unresolved-import] +from nat.data_models.intermediate_step import IntermediateStep # type: ignore[unresolved-import] +from nat.data_models.telemetry_exporter import TelemetryExporterBaseConfig # type: ignore[unresolved-import] +from nat.observability.exporter.base_exporter import IsolatedAttribute # type: ignore[unresolved-import] +from nat.observability.exporter.raw_exporter import RawExporter # type: ignore[unresolved-import] +from nat.observability.processor.intermediate_step_serializer import ( # type: ignore[unresolved-import] IntermediateStepSerializer, ) from nemo_agents_plugin.utils import get_base_url diff --git a/plugins/nemo-agents/tests/unit/test_runner_controller.py b/plugins/nemo-agents/tests/unit/test_runner_controller.py index 5699b78ac0..4b222036fc 100644 --- a/plugins/nemo-agents/tests/unit/test_runner_controller.py +++ b/plugins/nemo-agents/tests/unit/test_runner_controller.py @@ -45,7 +45,7 @@ def _make_controller() -> tuple[AgentDeploymentController, Any]: ctrl._backend = backend ctrl._entities = MagicMock() ctrl._controller_config = ControllerConfig(health_check_timeout_seconds=120) - ctrl._save = AsyncMock() + ctrl._save = AsyncMock() # type: ignore[method-assign] return ctrl, cast(Any, backend) diff --git a/plugins/nemo-agents/tests/unit/test_utils.py b/plugins/nemo-agents/tests/unit/test_utils.py index 111bd6b14e..75f3c7a100 100644 --- a/plugins/nemo-agents/tests/unit/test_utils.py +++ b/plugins/nemo-agents/tests/unit/test_utils.py @@ -567,7 +567,7 @@ def fake_upload( with job._resolve_output( FilesetRef("eval-results"), workspace="default", - sdk=sentinel_sdk, + sdk=sentinel_sdk, # type: ignore[arg-type] ctx=ctx, ) as base: (base / "summary.json").write_text("{}") @@ -599,7 +599,7 @@ def fake_upload( with job._resolve_output( FilesetRef("prod/eval-results"), workspace="default", - sdk=object(), + sdk=object(), # type: ignore[arg-type] # type: ignore[arg-type] ctx=ctx, ) as _: pass @@ -629,7 +629,7 @@ def fake_upload( with job._resolve_output( FilesetRef("eval-results"), workspace="default", - sdk=object(), + sdk=object(), # type: ignore[arg-type] ctx=ctx, ) as base: (base / "partial.json").write_text("{}") @@ -698,7 +698,7 @@ def __init__(self) -> None: Path("/tmp/eval-out"), fileset="eval-results", workspace="prod", - sdk=sdk, + sdk=sdk, # type: ignore[arg-type] # type: ignore[arg-type] ) assert sdk.files.calls == [ @@ -1024,7 +1024,7 @@ def __init__(self) -> None: agent_config, endpoint = OptimizeAgentJob._resolve_agent( ref, workspace="default", - sdk=_StubSDK(), + sdk=_StubSDK(), # type: ignore[arg-type] ) assert captured == {"name": "react-agent", "workspace": "default"} @@ -1047,7 +1047,7 @@ def __init__(self) -> None: self.agents = _StubAgents() ref = AgentRef("prod/react-agent") - OptimizeAgentJob._resolve_agent(ref, workspace="default", sdk=_StubSDK()) + OptimizeAgentJob._resolve_agent(ref, workspace="default", sdk=_StubSDK()) # type: ignore[arg-type] assert captured == {"name": "react-agent", "workspace": "prod"} @@ -1065,7 +1065,7 @@ def __init__(self) -> None: ref = AgentRef("react-agent") with pytest.raises(RuntimeError, match="empty or invalid stored config"): - OptimizeAgentJob._resolve_agent(ref, workspace="default", sdk=_StubSDK()) + OptimizeAgentJob._resolve_agent(ref, workspace="default", sdk=_StubSDK()) # type: ignore[arg-type] class TestRebaseOptimizeOutputs: @@ -1333,7 +1333,7 @@ class _StubResponse: headers: dict[str, str] = {} request = None - return NotFoundError(message=message, response=_StubResponse(), body={"detail": message}) + return NotFoundError(message=message, response=_StubResponse(), body={"detail": message}) # type: ignore[arg-type] class _RecordingVirtualModels: @@ -1386,7 +1386,7 @@ def test_happy_path_dedupes_repeated_model_names(self) -> None: vms = _RecordingVirtualModels() sdk = _StubSDKWithVirtualModels(vms) - validate_llm_models(config, workspace="default", sdk=sdk) + validate_llm_models(config, workspace="default", sdk=sdk) # type: ignore[arg-type] assert vms.calls == [{"name": "shared-model", "workspace": "default"}] @@ -1400,7 +1400,7 @@ def test_distinct_model_names_each_get_one_call(self) -> None: vms = _RecordingVirtualModels() sdk = _StubSDKWithVirtualModels(vms) - validate_llm_models(config, workspace="ws", sdk=sdk) + validate_llm_models(config, workspace="ws", sdk=sdk) # type: ignore[arg-type] # type: ignore[arg-type] # type: ignore[arg-type] # type: ignore[arg-type] # type: ignore[arg-type] # type: ignore[arg-type] # type: ignore[arg-type] # type: ignore[arg-type] names = sorted(call["name"] for call in vms.calls) assert names == ["model-a", "model-b"] @@ -1416,7 +1416,7 @@ def test_missing_model_raises_value_error_with_actionable_message(self) -> None: sdk = _StubSDKWithVirtualModels(vms) with pytest.raises(ValueError) as exc_info: - validate_llm_models(config, workspace="default", sdk=sdk) + validate_llm_models(config, workspace="default", sdk=sdk) # type: ignore[arg-type] # type: ignore[arg-type] message = str(exc_info.value) # Names the missing model + the YAML key + the workspace, and points @@ -1542,7 +1542,7 @@ def test_soft_fails_on_non_not_found_exception(self, caplog: pytest.LogCaptureFi with caplog.at_level("WARNING"): # Must not raise — the underlying eval/optimize call will surface # the real error if the model truly isn't reachable. - validate_llm_models(config, workspace="ws", sdk=sdk) + validate_llm_models(config, workspace="ws", sdk=sdk) # type: ignore[arg-type] assert any("Could not validate LLM" in record.message for record in caplog.records) @@ -1584,7 +1584,7 @@ def test_happy_path_loads_yaml_and_validates(self, tmp_path: Path) -> None: vms = _RecordingVirtualModels() sdk = _StubSDKWithVirtualModels(vms) - preflight_validate_llm_models(config_path, workspace="ws", sdk=sdk) + preflight_validate_llm_models(config_path, workspace="ws", sdk=sdk) # type: ignore[arg-type] # type: ignore[arg-type] assert vms.calls == [{"name": "real-model", "workspace": "ws"}] @@ -1651,7 +1651,7 @@ def test_missing_model_propagates_validate_llm_models_error(self, tmp_path: Path sdk = _StubSDKWithVirtualModels(vms) with pytest.raises(ValueError) as exc_info: - preflight_validate_llm_models(config_path, workspace="default", sdk=sdk) + preflight_validate_llm_models(config_path, workspace="default", sdk=sdk) # type: ignore[arg-type] # Sanity-check the message shape; full message coverage lives in # TestValidateLLMModels. diff --git a/plugins/nemo-anonymizer/tests/unit/test_input.py b/plugins/nemo-anonymizer/tests/unit/test_input.py index 28b77b461f..041d3cee62 100644 --- a/plugins/nemo-anonymizer/tests/unit/test_input.py +++ b/plugins/nemo-anonymizer/tests/unit/test_input.py @@ -193,7 +193,7 @@ def raise_bad_input(*args: object, **kwargs: object) -> object: with pytest.raises(AnonymizerInvalidConfigError, match="bad input"): prepare_anonymizer_input( AnonymizerInputSpec(source="fs#input.csv"), - sdk=object(), + sdk=object(), # type: ignore[arg-type] workspace="team-a", allow_local_paths=False, ) @@ -222,7 +222,7 @@ def fake_prepare_anonymizer_input( captured["sdk"] = sdk captured["workspace"] = workspace captured["allow_local_paths"] = allow_local_paths - return input_module.PreparedAnonymizerInput(input=sentinel) + return input_module.PreparedAnonymizerInput(input=sentinel) # type: ignore[arg-type] sdk = FakeSyncPlatform() monkeypatch.setattr(input_module, "NeMoPlatform", FakeSyncPlatform) @@ -230,7 +230,7 @@ def fake_prepare_anonymizer_input( prepared = await prepare_anonymizer_input_async( AnonymizerInputSpec(source="fs#input.csv"), - sdk=sdk, + sdk=sdk, # type: ignore[arg-type] workspace="team-a", allow_local_paths=False, ) diff --git a/plugins/nemo-anonymizer/tests/unit/test_preview_function.py b/plugins/nemo-anonymizer/tests/unit/test_preview_function.py index 551814df73..e5641939f2 100644 --- a/plugins/nemo-anonymizer/tests/unit/test_preview_function.py +++ b/plugins/nemo-anonymizer/tests/unit/test_preview_function.py @@ -56,7 +56,7 @@ def preview(self, **kwargs: object) -> FakeResult: worker_module._make_preview( frames.append, _preview_spec(tmp_path), - data=object(), + data=object(), # type: ignore[arg-type] model_configs_yaml="", dd_providers=None, num_records=1, diff --git a/plugins/nemo-anonymizer/tests/unit/test_routing.py b/plugins/nemo-anonymizer/tests/unit/test_routing.py index 5ce4932eb0..dbf3c0748d 100644 --- a/plugins/nemo-anonymizer/tests/unit/test_routing.py +++ b/plugins/nemo-anonymizer/tests/unit/test_routing.py @@ -50,7 +50,7 @@ def get(self, url: str, **kwargs: object) -> Response: default_headers={}, _client=Client(), ) - resource = AnonymizerResource(platform) + resource = AnonymizerResource(platform) # type: ignore[arg-type] request = AnonymizerRequest( config=AnonymizerConfig(replace=Redact()), data=AnonymizerInputSpec(source="inputs#records.csv"), diff --git a/plugins/nemo-data-designer/src/nemo_data_designer_plugin/cli/renderers.py b/plugins/nemo-data-designer/src/nemo_data_designer_plugin/cli/renderers.py index ff8c1f0f88..1f76ce76cc 100644 --- a/plugins/nemo-data-designer/src/nemo_data_designer_plugin/cli/renderers.py +++ b/plugins/nemo-data-designer/src/nemo_data_designer_plugin/cli/renderers.py @@ -141,8 +141,8 @@ def _build_preview_results(self, df: pd.DataFrame) -> PreviewResults | None: return PreviewResults( config_builder=builder, dataset=df, - dataset_metadata=self._dataset_metadata, - analysis=self._analysis, + dataset_metadata=self._dataset_metadata, # type: ignore[arg-type] + analysis=self._analysis, # type: ignore[arg-type] processor_artifacts=self._processor_outputs or None, ) except Exception as exc: # pragma: no cover - defensive diff --git a/plugins/nemo-guardrails/tests/unit/test_middleware.py b/plugins/nemo-guardrails/tests/unit/test_middleware.py index 4408c97675..65db5bd28a 100644 --- a/plugins/nemo-guardrails/tests/unit/test_middleware.py +++ b/plugins/nemo-guardrails/tests/unit/test_middleware.py @@ -681,7 +681,7 @@ async def test_blocked_rail_returns_immediate_response(self, middleware: Guardra assert isinstance(result, ImmediateResponse) assert not isinstance(result.data, AsyncIterator) - data: dict[str, Any] = result.data + data: dict[str, Any] = result.data # type: ignore[assignment] assert data["id"].startswith("chatcmpl-") assert data["model"] == "ws/llama" assert data["choices"] == [ diff --git a/services/automodel/src/nmp/automodel/tasks/training/backends/finetune.py b/services/automodel/src/nmp/automodel/tasks/training/backends/finetune.py index 291f36d8ce..abaf469ba1 100644 --- a/services/automodel/src/nmp/automodel/tasks/training/backends/finetune.py +++ b/services/automodel/src/nmp/automodel/tasks/training/backends/finetune.py @@ -102,9 +102,9 @@ def __init__(self, recipe: AutomodelRecipe, job_ctx: NMPJobContext | None = None self._original_save_checkpoint = recipe.save_checkpoint # Monkey-patch the recipe's methods to add our callbacks - recipe.log_train_metrics = self._log_train_metrics - recipe.log_val_metrics = self._log_val_metrics - recipe.save_checkpoint = self._save_checkpoint + recipe.log_train_metrics = self._log_train_metrics # type: ignore[method-assign] + recipe.log_val_metrics = self._log_val_metrics # type: ignore[method-assign] + recipe.save_checkpoint = self._save_checkpoint # type: ignore[method-assign] @property def recipe(self) -> AutomodelRecipe: From 6a9b1ca8b4bbc8b43f321a72320aabc42047e73f Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Wed, 10 Jun 2026 15:45:31 -0600 Subject: [PATCH 25/26] Address Aaron's comments: - Fix discovery of the SDK from contributors instead of hardcoding the SDK contributions in the router - delete services/customizer deadcode To fix in the refactor: - Opinionated shared parameters - Integrations object - Epoch behavior - Steps behavior - All Weights / Full training type discrepancies Signed-off-by: Sam Oluwalana --- .dockerignore | 1 - .gitattributes | 44 - .pre-commit-config.yaml | 1 - Makefile | 8 +- conftest.py | 2 - docs/set-up/config-reference.mdx | 48 +- openapi/README.md | 2 +- packages/nemo_platform/pyproject.toml | 11 - .../nemo_platform_ext/cli/core/lazy_load.py | 4 + .../nemo_platform_ext/tests/cli/test_app.py | 17 + .../customization_contributor.py | 26 +- .../nmp_common/src/nmp/common/mcp/README.md | 2 +- .../nmp_common/src/nmp/common/model_utils.py | 2 +- packages/nmp_platform/README.md | 6 +- .../src/nemo_automodel_plugin/cli/inputs.py | 2 +- .../src/nemo_automodel_plugin/config.py | 2 - .../src/nemo_automodel_plugin/contributor.py | 9 + .../nemo-automodel/tests/test_contributor.py | 9 + plugins/nemo-customizer/docs/CUSTOMIZATION.md | 2 +- .../src/nemo_customizer/cli.py | 31 +- .../src/nemo_customizer/sdk/resources.py | 68 +- plugins/nemo-customizer/tests/test_cli.py | 46 + plugins/nemo-customizer/tests/test_sdk.py | 31 +- .../src/nemo_unsloth_plugin/contributor.py | 13 +- .../nemo-unsloth/tests/test_contributor.py | 10 + pyproject.toml | 9 - pytest.ini | 3 - script/generate_config_docs.py | 6 +- script/generate_openapi_spec.py | 24 - script/openapi_helper/customizer_generator.py | 110 -- .../src/nemo_platform/cli/core/lazy_load.py | 4 + .../nemo_platform_ext/cli/test_app.py | 17 + .../automodel/docker/Dockerfile.mamba-wheel | 8 +- .../docker/Dockerfile.nmp-automodel-base | 2 +- services/automodel/docker/README.md | 6 +- .../src/nmp/automodel/tasks/docker/README.md | 13 +- .../tasks/docker/docker-compose.yaml | 8 +- .../data/files_to_upload/nested_1/file1.txt | 1 + .../data/files_to_upload/nested_2/file2.txt | 1 + .../tasks/file_io/data/sample_config.json | 14 + .../files_to_upload/nested_1/file1.txt | 0 .../files_to_upload/nested_2/__0_0.distcp | Bin 0 -> 19565704 bytes .../files_to_upload/nested_2/file2.txt | 0 .../tests/tasks/file_io}/sample_config.json | 0 services/core/mcp/README.md | 2 +- services/customizer/README.md | 470 ----- services/customizer/docs/megatron_bridge.md | 204 -- services/customizer/docs/rl.md | 288 --- services/customizer/pyproject.toml | 59 - .../customizer/src/nmp/customizer/__init__.py | 4 - .../src/nmp/customizer/api/__init__.py | 4 - .../src/nmp/customizer/api/v2/__init__.py | 4 - .../nmp/customizer/api/v2/jobs/__init__.py | 4 - .../nmp/customizer/api/v2/jobs/endpoints.py | 33 - .../src/nmp/customizer/api/v2/jobs/schemas.py | 639 ------- .../src/nmp/customizer/app/__init__.py | 4 - .../src/nmp/customizer/app/constants.py | 173 -- .../src/nmp/customizer/app/jobs/__init__.py | 4 - .../src/nmp/customizer/app/jobs/compiler.py | 506 ----- .../src/nmp/customizer/app/jobs/context.py | 82 - .../customizer/app/jobs/file_io/schemas.py | 181 -- .../app/jobs/model_entity/__init__.py | 11 - .../app/jobs/model_entity/schemas.py | 106 -- .../customizer/app/jobs/training/compiler.py | 432 ----- .../customizer/app/jobs/training/schemas.py | 345 ---- .../customizer/src/nmp/customizer/config.py | 53 - .../src/nmp/customizer/entities/__init__.py | 29 - .../src/nmp/customizer/entities/validators.py | 64 - .../src/nmp/customizer/entities/values.py | 96 - .../customizer/src/nmp/customizer/main.py | 9 - .../src/nmp/customizer/platform_client.py | 56 - .../customizer/src/nmp/customizer/service.py | 38 - .../src/nmp/customizer/tasks/__init__.py | 4 - .../nmp/customizer/tasks/file_io/__init__.py | 8 - .../nmp/customizer/tasks/file_io/__main__.py | 11 - .../nmp/customizer/tasks/file_io/callbacks.py | 783 -------- .../customizer/tasks/file_io/docker/README.md | 74 - .../tasks/file_io/docker/docker-compose.yaml | 52 - .../tasks/file_io/progress_reporter.py | 105 - .../src/nmp/customizer/tasks/file_io/run.py | 560 ------ .../src/nmp/customizer/tasks/file_io/utils.py | 184 -- .../customizer/tasks/model_entity/__init__.py | 8 - .../customizer/tasks/model_entity/__main__.py | 15 - .../nmp/customizer/tasks/model_entity/run.py | 443 ----- .../nmp/customizer/tasks/training/README.md | 990 ---------- .../nmp/customizer/tasks/training/__init__.py | 0 .../nmp/customizer/tasks/training/__main__.py | 44 - .../training/backends/megatron_bridge/TODO | 0 .../training/backends/nemo_rl/backend.py | 278 --- .../training/backends/nemo_rl/callbacks.py | 95 - .../training/backends/nemo_rl/checkpoints.py | 86 - .../training/backends/nemo_rl/dpo_config.py | 432 ----- .../training/backends/nemo_rl/dpo_driver.py | 300 --- .../training/backends/nemo_rl/grpo_config.py | 47 - .../training/backends/nemo_rl/grpo_driver.py | 108 -- .../backends/nemo_rl/nemo_rl_logger.py | 193 -- .../nemo_rl/no_override_requirements.txt | 8 - .../nemo_rl/preference_datasets/__init__.py | 91 - .../nemo_rl/preference_datasets/helpsteer3.py | 71 - .../nemo_rl/preference_datasets/tulu3.py | 73 - .../backends/nemo_rl/ray_bootstrap.py | 798 -------- .../tasks/training/chat_templates.py | 196 -- .../tasks/training/datasets/preparation.py | 514 ----- .../tasks/training/datasets/schemas.py | 430 ----- .../tasks/training/datasets/validation.py | 394 ---- .../customizer/tasks/training/distributed.py | 245 --- .../tasks/training/errors/converter.py | 108 -- .../tasks/training/errors/error_rules.yaml | 643 ------- .../tasks/training/errors/exceptions.py | 431 ----- .../tasks/training/errors/parser.py | 255 --- .../customizer/tasks/training/integrations.py | 168 -- .../tasks/training/model_utils/constants.py | 4 - .../tasks/training/model_utils/file_utils.py | 172 -- .../nmp/customizer/tasks/training/progress.py | 173 -- .../nmp/customizer/tasks/training/protocol.py | 133 -- .../nmp/customizer/tasks/training/runner.py | 323 ---- .../nmp/customizer/tasks/training/schemas.py | 51 - .../tasks/training/sequence_packing.py | 353 ---- .../templates/llama-3.1-instruct.jinja | 61 - .../templates/llama-3.2-instruct.jinja | 61 - .../templates/llama-3.3-instruct.jinja | 61 - .../training/templates/nemotron-3.1.jinja | 51 - .../training/templates/nemotron-3.3.jinja | 21 - .../templates/nemotron-super-3.3.jinja | 82 - .../tasks/training/templates/phi-4.jinja | 15 - .../nmp/customizer/tasks/training/utils.py | 93 - .../customizer/src/nmp/customizer/utils.py | 127 -- services/customizer/tests/conftest.py | 41 - services/customizer/tests/constants.py | 7 - .../tests/integration/test_customizer.py | 142 -- .../tests/integration/test_file_io_task.py | 399 ---- .../integration/test_model_entity_task.py | 435 ----- .../files_to_upload/nested_2/__0_0.distcp | 3 - .../tests/tasks/file_io/test_callbacks.py | 868 --------- .../tests/tasks/file_io/test_file_io.py | 1690 ----------------- .../tasks/file_io/test_progress_reporter.py | 408 ---- .../tasks/model_entity/test_model_entity.py | 1302 ------------- .../testdata/dpo/dpo-convo/training.jsonl | 100 - .../testdata/dpo/dpo-convo/validation.jsonl | 20 - .../convert_to_extra_id_template | 37 - .../convert_to_llama_template.py | 48 - .../download_helpsteer_3.py | 57 - .../dpo/dpo-demo/customizer_dpo_demo.ipynb | 310 --- .../dpo-demo/data0/test_example_data.jsonl | 50 - .../dpo-demo/data0/train_example_data.jsonl | 100 - .../dpo/dpo-demo/data0/val_example_data.jsonl | 50 - .../testdata/dpo/expected_dpo_config.yaml | 103 - .../dpo/help_steer_small/training.jsonl | 100 - .../dpo/help_steer_small/validation.jsonl | 20 - .../tasks/testdata/dpo/tulu3/training.jsonl | 100 - .../tasks/testdata/dpo/tulu3/validation.jsonl | 20 - .../email-composition-small/training.jsonl | 103 - .../email-composition-small/validation.jsonl | 32 - .../testdata/sft/sft-chat/training.jsonl | 4 - .../testdata/sft/sft-chat/validation.jsonl | 2 - .../customizer/tests/tasks/training/README.md | 149 -- .../backends/nemo_rl/test_dpo_config.py | 1001 ---------- .../backends/nemo_rl/test_nemo_rl_logger.py | 495 ----- .../backends/nemo_rl/test_ray_bootstrap.py | 592 ------ .../tasks/training/datasets/test_schemas.py | 880 --------- .../training/datasets/test_validation.py | 701 ------- .../tests/tasks/training/test_datasets.py | 457 ----- .../tests/tasks/training/test_distributed.py | 273 --- .../tests/tasks/training/test_errors.py | 997 ---------- .../tests/tasks/training/test_integrations.py | 316 --- .../tasks/training/test_mlflow_api_surface.py | 134 -- .../tests/tasks/training/test_progress.py | 107 -- .../tests/tasks/training/test_runner.py | 546 ------ services/customizer/tests/test_compiler.py | 1627 ---------------- services/customizer/tests/test_job_context.py | 169 -- services/customizer/tests/test_job_schemas.py | 695 ------- .../customizer/tests/test_platform_client.py | 118 -- services/customizer/tests/test_service.py | 81 - services/customizer/tests/test_validators.py | 213 --- tests/smoke_gpu/conftest.py | 2 - .../smoke_gpu/test_customizer_entry_points.py | 50 - tests/smoke_gpu/test_customizer_rl.py | 33 - tests/smoke_gpu/test_customizer_tasks.py | 41 - tests/smoke_gpu/test_gpu_tasks.py | 4 - tests/smoke_gpu/test_ray_cli_compat.py | 45 - third_party/requirements-main.txt | 7 - uv.lock | 81 +- 182 files changed, 313 insertions(+), 31601 deletions(-) create mode 100644 plugins/nemo-customizer/tests/test_cli.py delete mode 100644 script/openapi_helper/customizer_generator.py create mode 100644 services/automodel/tests/tasks/file_io/data/files_to_upload/nested_1/file1.txt create mode 100644 services/automodel/tests/tasks/file_io/data/files_to_upload/nested_2/file2.txt create mode 100644 services/automodel/tests/tasks/file_io/data/sample_config.json rename services/{customizer/tests/tasks/file_io/data => automodel/tests/tasks/file_io}/files_to_upload/nested_1/file1.txt (100%) create mode 100644 services/automodel/tests/tasks/file_io/files_to_upload/nested_2/__0_0.distcp rename services/{customizer/tests/tasks/file_io/data => automodel/tests/tasks/file_io}/files_to_upload/nested_2/file2.txt (100%) rename services/{customizer/tests/tasks/file_io/data => automodel/tests/tasks/file_io}/sample_config.json (100%) delete mode 100644 services/customizer/README.md delete mode 100644 services/customizer/docs/megatron_bridge.md delete mode 100644 services/customizer/docs/rl.md delete mode 100644 services/customizer/pyproject.toml delete mode 100644 services/customizer/src/nmp/customizer/__init__.py delete mode 100644 services/customizer/src/nmp/customizer/api/__init__.py delete mode 100644 services/customizer/src/nmp/customizer/api/v2/__init__.py delete mode 100644 services/customizer/src/nmp/customizer/api/v2/jobs/__init__.py delete mode 100644 services/customizer/src/nmp/customizer/api/v2/jobs/endpoints.py delete mode 100644 services/customizer/src/nmp/customizer/api/v2/jobs/schemas.py delete mode 100644 services/customizer/src/nmp/customizer/app/__init__.py delete mode 100644 services/customizer/src/nmp/customizer/app/constants.py delete mode 100644 services/customizer/src/nmp/customizer/app/jobs/__init__.py delete mode 100644 services/customizer/src/nmp/customizer/app/jobs/compiler.py delete mode 100644 services/customizer/src/nmp/customizer/app/jobs/context.py delete mode 100644 services/customizer/src/nmp/customizer/app/jobs/file_io/schemas.py delete mode 100644 services/customizer/src/nmp/customizer/app/jobs/model_entity/__init__.py delete mode 100644 services/customizer/src/nmp/customizer/app/jobs/model_entity/schemas.py delete mode 100644 services/customizer/src/nmp/customizer/app/jobs/training/compiler.py delete mode 100644 services/customizer/src/nmp/customizer/app/jobs/training/schemas.py delete mode 100644 services/customizer/src/nmp/customizer/config.py delete mode 100644 services/customizer/src/nmp/customizer/entities/__init__.py delete mode 100644 services/customizer/src/nmp/customizer/entities/validators.py delete mode 100644 services/customizer/src/nmp/customizer/entities/values.py delete mode 100644 services/customizer/src/nmp/customizer/main.py delete mode 100644 services/customizer/src/nmp/customizer/platform_client.py delete mode 100644 services/customizer/src/nmp/customizer/service.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/__init__.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/file_io/__init__.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/file_io/__main__.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/file_io/callbacks.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/file_io/docker/README.md delete mode 100644 services/customizer/src/nmp/customizer/tasks/file_io/docker/docker-compose.yaml delete mode 100644 services/customizer/src/nmp/customizer/tasks/file_io/progress_reporter.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/file_io/run.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/file_io/utils.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/model_entity/__init__.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/model_entity/__main__.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/model_entity/run.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/README.md delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/__init__.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/__main__.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/megatron_bridge/TODO delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/nemo_rl/backend.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/nemo_rl/callbacks.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/nemo_rl/checkpoints.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/nemo_rl/dpo_config.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/nemo_rl/dpo_driver.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/nemo_rl/grpo_config.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/nemo_rl/grpo_driver.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/nemo_rl/nemo_rl_logger.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/nemo_rl/no_override_requirements.txt delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/nemo_rl/preference_datasets/__init__.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/nemo_rl/preference_datasets/helpsteer3.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/nemo_rl/preference_datasets/tulu3.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/backends/nemo_rl/ray_bootstrap.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/chat_templates.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/datasets/preparation.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/datasets/schemas.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/datasets/validation.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/distributed.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/errors/converter.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/errors/error_rules.yaml delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/errors/exceptions.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/errors/parser.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/integrations.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/model_utils/constants.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/model_utils/file_utils.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/progress.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/protocol.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/runner.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/schemas.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/sequence_packing.py delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/templates/llama-3.1-instruct.jinja delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/templates/llama-3.2-instruct.jinja delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/templates/llama-3.3-instruct.jinja delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/templates/nemotron-3.1.jinja delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/templates/nemotron-3.3.jinja delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/templates/nemotron-super-3.3.jinja delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/templates/phi-4.jinja delete mode 100644 services/customizer/src/nmp/customizer/tasks/training/utils.py delete mode 100644 services/customizer/src/nmp/customizer/utils.py delete mode 100644 services/customizer/tests/conftest.py delete mode 100644 services/customizer/tests/constants.py delete mode 100644 services/customizer/tests/integration/test_customizer.py delete mode 100644 services/customizer/tests/integration/test_file_io_task.py delete mode 100644 services/customizer/tests/integration/test_model_entity_task.py delete mode 100644 services/customizer/tests/tasks/file_io/data/files_to_upload/nested_2/__0_0.distcp delete mode 100644 services/customizer/tests/tasks/file_io/test_callbacks.py delete mode 100644 services/customizer/tests/tasks/file_io/test_file_io.py delete mode 100644 services/customizer/tests/tasks/file_io/test_progress_reporter.py delete mode 100644 services/customizer/tests/tasks/model_entity/test_model_entity.py delete mode 100644 services/customizer/tests/tasks/testdata/dpo/dpo-convo/training.jsonl delete mode 100644 services/customizer/tests/tasks/testdata/dpo/dpo-convo/validation.jsonl delete mode 100644 services/customizer/tests/tasks/testdata/dpo/dpo-data-conversion/convert_to_extra_id_template delete mode 100644 services/customizer/tests/tasks/testdata/dpo/dpo-data-conversion/convert_to_llama_template.py delete mode 100644 services/customizer/tests/tasks/testdata/dpo/dpo-data-conversion/download_helpsteer_3.py delete mode 100644 services/customizer/tests/tasks/testdata/dpo/dpo-demo/customizer_dpo_demo.ipynb delete mode 100644 services/customizer/tests/tasks/testdata/dpo/dpo-demo/data0/test_example_data.jsonl delete mode 100644 services/customizer/tests/tasks/testdata/dpo/dpo-demo/data0/train_example_data.jsonl delete mode 100644 services/customizer/tests/tasks/testdata/dpo/dpo-demo/data0/val_example_data.jsonl delete mode 100644 services/customizer/tests/tasks/testdata/dpo/expected_dpo_config.yaml delete mode 100644 services/customizer/tests/tasks/testdata/dpo/help_steer_small/training.jsonl delete mode 100644 services/customizer/tests/tasks/testdata/dpo/help_steer_small/validation.jsonl delete mode 100644 services/customizer/tests/tasks/testdata/dpo/tulu3/training.jsonl delete mode 100644 services/customizer/tests/tasks/testdata/dpo/tulu3/validation.jsonl delete mode 100644 services/customizer/tests/tasks/testdata/sft/email-composition-small/training.jsonl delete mode 100644 services/customizer/tests/tasks/testdata/sft/email-composition-small/validation.jsonl delete mode 100644 services/customizer/tests/tasks/testdata/sft/sft-chat/training.jsonl delete mode 100644 services/customizer/tests/tasks/testdata/sft/sft-chat/validation.jsonl delete mode 100644 services/customizer/tests/tasks/training/README.md delete mode 100644 services/customizer/tests/tasks/training/backends/nemo_rl/test_dpo_config.py delete mode 100644 services/customizer/tests/tasks/training/backends/nemo_rl/test_nemo_rl_logger.py delete mode 100644 services/customizer/tests/tasks/training/backends/nemo_rl/test_ray_bootstrap.py delete mode 100644 services/customizer/tests/tasks/training/datasets/test_schemas.py delete mode 100644 services/customizer/tests/tasks/training/datasets/test_validation.py delete mode 100644 services/customizer/tests/tasks/training/test_datasets.py delete mode 100644 services/customizer/tests/tasks/training/test_distributed.py delete mode 100644 services/customizer/tests/tasks/training/test_errors.py delete mode 100644 services/customizer/tests/tasks/training/test_integrations.py delete mode 100644 services/customizer/tests/tasks/training/test_mlflow_api_surface.py delete mode 100644 services/customizer/tests/tasks/training/test_progress.py delete mode 100644 services/customizer/tests/tasks/training/test_runner.py delete mode 100644 services/customizer/tests/test_compiler.py delete mode 100644 services/customizer/tests/test_job_context.py delete mode 100644 services/customizer/tests/test_job_schemas.py delete mode 100644 services/customizer/tests/test_platform_client.py delete mode 100644 services/customizer/tests/test_service.py delete mode 100644 services/customizer/tests/test_validators.py delete mode 100644 tests/smoke_gpu/test_customizer_entry_points.py delete mode 100644 tests/smoke_gpu/test_customizer_rl.py delete mode 100644 tests/smoke_gpu/test_customizer_tasks.py delete mode 100644 tests/smoke_gpu/test_ray_cli_compat.py diff --git a/.dockerignore b/.dockerignore index 7a3d0e0b56..7a386ea546 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,7 +2,6 @@ docker-bake.hcl .venv .ruff_cache Dockerfile.bake -services/customizer/tests **/Dockerfile* .dockerignore **/__pycache__ diff --git a/.gitattributes b/.gitattributes index eb25b94d50..38aa5831a0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -36,50 +36,6 @@ third_party/requirements*.txt linguist-generated docker/locks/**/uv.lock linguist-generated documentation/docs/audit/_snippets/output/*.jsonl filter=lfs diff=lfs merge=lfs -text documentation/docs/generate-synthetic-data/images/* filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/grpo/workbench/training.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/grpo/comp_coding/training.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/grpo/instruction_following/training.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/grpo/multineedle/validation.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/grpo/python_math_exec/training.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/grpo/mcqa/training.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/grpo/comp_coding/validation.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/grpo/google_search/training.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/grpo/google_search/validation.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/grpo/instruction_following/validation.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/grpo/library_judge_math/training.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/grpo/library_judge_math/validation.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/grpo/multiverse_math_hard/training.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/grpo/workbench/validation.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/grpo/mcqa/validation.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/grpo/multineedle/training.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/grpo/multiverse_math_hard/validation.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/grpo/python_math_exec/validation.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/tasks/file_io/data/files_to_upload/nested_2/__0_0.distcp filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/python/training/nemo/data/mp_rank_00_customization.nemo filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/python/training/nemo/data/customization.nemo filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/python/training/nemo/data/gpt2b_tp1_lora.nemo filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/python/training/nemo/data/gpt8b_tp4_lora.nemo filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/python/training/nemo/data/expected_llmservice_peft_lora/model_weights.ckpt filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/python/training/nemo/data/expected_llmservice_peft_lora_tp4/model_weights.ckpt filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/python/data/gpt_126m.nemo filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/e2e-eval/email-composition-train/training/training_file.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/e2e-eval/email-composition-train/validation/validation_file.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/e2e-eval/email-composition-eval/email_eval_ms_test.json filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/e2e-eval/email-composition-eval/email_eval_ms_test_sft.json filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/gpt-sft-chat-dataset/e21a501b3cc14174835d787ced1583e2_tokenizer.model filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/gpt-sft-chat-dataset/llama2_tokenizer.model filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/gpt-sft-chat-dataset/merges.txt filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/gpt-sft-chat-dataset/tokenizer.model filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/gpt-sft-chat-dataset/vocab.json filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/e2e-eval/email-composition-convo/training/training_file.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/e2e-eval/email-composition-convo/validation/validation_file.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/tool-calling/xlam_openai_format.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/tool-calling/training.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/tool-calling/validation.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/tool-calling/testing.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/embedding/training/training.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/embedding/testing.jsonl filter=lfs diff=lfs merge=lfs -text -services/customizer/tests/testdata/embedding/validation/validation.jsonl filter=lfs diff=lfs merge=lfs -text # Files maintained by external garak project packages/garak_api/garakapi/_config.py linguist-generated packages/garak_api/garakapi/_plugins.py linguist-generated diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8cfd4a0a7c..6db33207d9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,6 @@ repos: packages/nmp_common/src/nmp_common/api/.*| # Individual microservices - services/customizer/src/customizer/api/v1/.*| services/evaluator/src/evaluator/api/.*| services/guardrails/src/guardrails/api/.*| services/core/infrastructure/jobs/src/jobs/api/.*| diff --git a/Makefile b/Makefile index 5ad9be7e17..574057e2c0 100644 --- a/Makefile +++ b/Makefile @@ -479,10 +479,10 @@ test-e2e-kubernetes-gpu: ## Run GPU e2e tests against Kubernetes (requires GPU n @echo "Running GPU e2e tests with Kubernetes with feature gpu enabled..." uv run --frozen pytest e2e --kubernetes --feature gpu -v --junitxml=report-kubernetes-gpu.xml -.PHONY: test-e2e-kubernetes-gpu-customizer -test-e2e-kubernetes-gpu-customizer: ## Run GPU customizer e2e tests against Kubernetes (requires GPU nodes; set NMP_E2E_CLUSTER_URL) - @echo "Running GPU customizer e2e tests with Kubernetes..." - uv run --frozen pytest e2e/test_customizer.py --kubernetes --feature gpu --feature customizer --log-cli-level=INFO -v --junitxml=report-kubernetes-gpu-customizer.xml +.PHONY: test-e2e-kubernetes-gpu-automodel +test-e2e-kubernetes-gpu-automodel: ## Run GPU automodel customization e2e tests against Kubernetes (requires GPU nodes; set NMP_E2E_CLUSTER_URL) + @echo "Running GPU automodel customization e2e tests with Kubernetes..." + uv run --frozen pytest tests/agentic-use/customizer-lora-job-cli/tests/test_outputs.py --kubernetes --feature gpu --log-cli-level=INFO -v --junitxml=report-kubernetes-gpu-automodel.xml .PHONY: benchmark-guardrails benchmark-guardrails: ## Run nemo-guardrails IGW benchmark sweep (set BENCHMARK_ARGS for extra flags) diff --git a/conftest.py b/conftest.py index e231124c72..dc4a33c51a 100644 --- a/conftest.py +++ b/conftest.py @@ -212,8 +212,6 @@ def pytest_collection_modifyitems(config, items): "unit", "e2e", "smoke_gpu_tasks", - "smoke_customizer_tasks", - "smoke_customizer_rl", "smoke_nmp_automodel_tasks", "smoke_nmp_automodel_training", "integration", diff --git a/docs/set-up/config-reference.mdx b/docs/set-up/config-reference.mdx index 0f555ba6e3..fc716d2c59 100644 --- a/docs/set-up/config-reference.mdx +++ b/docs/set-up/config-reference.mdx @@ -763,18 +763,18 @@ secrets: token: ``` -### `customizer` +### `automodel` -Configuration for the Customizer service. +Environment variables use the NMP_AUTOMODEL_ prefix. ```yaml -customizer: - # Port to run the service on | default: 8000 - port: 8000 - # Enable debug mode | default: False - debug: false - # Override container image for DPO training. If not set, uses platform defaults. - training_rl_image: +automodel: + # Registry host/path prefix for nmp-automodel-tasks and nmp-automodel-training. Override via NMP_AUTOMODEL_IMAGE_REGISTRY for other environments, defaults to the platform's image registry. + image_registry: + # Override entire GPU training image (registry/name:tag). + training_image: + # Override entire CPU tasks image (registry/name:tag). + tasks_image: # default: '1' default_job_resource_cpu_request: '1' # default: '8Gi' @@ -783,10 +783,34 @@ customizer: default_job_resource_cpu_limit: '4' # default: '16Gi' default_job_resource_memory_limit: 16Gi - # Terminate a training step if no task reports progress within this many seconds. 0 disables the check. | default: 3600 + # Terminate training if no task progress within this many seconds (0 disables). | default: 3600 training_staleness_timeout_seconds: 3600 - # Default execution profile for GPU training steps. Used for all training jobs unless the user specifies one explicitly. | default: 'default' - default_training_execution_profile: default + # Default GPU execution profile when the job spec omits training.execution_profile. | default: 'gpu' + default_training_execution_profile: gpu +``` + +### `unsloth` + +Environment variables use the ``NMP_UNSLOTH_`` prefix. + +```yaml +unsloth: + # Registry host/path prefix for nmp-unsloth-tasks and nmp-unsloth-training. Override via NMP_UNSLOTH_IMAGE_REGISTRY for other environments, defaults to the platform's image registry. + image_registry: + # Override entire GPU training image (registry/name:tag). + training_image: + # Override entire CPU tasks image (registry/name:tag). + tasks_image: + # default: '1' + default_job_resource_cpu_request: '1' + # default: '8Gi' + default_job_resource_memory_request: 8Gi + # default: '4' + default_job_resource_cpu_limit: '4' + # default: '16Gi' + default_job_resource_memory_limit: 16Gi + # Default GPU execution profile when the job spec omits training.execution_profile. | default: 'gpu' + default_training_execution_profile: gpu ``` ### `evaluator` diff --git a/openapi/README.md b/openapi/README.md index 57168a9489..acd1619433 100644 --- a/openapi/README.md +++ b/openapi/README.md @@ -19,7 +19,7 @@ The following table lists all the OpenAPI specifications that are merged into th | Entity Store | Generated from `entity_store.server:app` | `entity-store.openapi.yaml` | | Evaluator | Generated from `evaluator.server:app` | `evaluator.openapi.yaml` | | Guardrails | Generated from `guardrails.app:app` | `guardrails.openapi.yaml` | -| Customizer | Generated via `customizer/openapi/generate_openapi_spec.py` | `customizer.openapi.yaml` | +| Customization | Generated from `nemo-customizer-plugin` contributor routes | `customization.openapi.yaml` | | Deployment Management | Direct copy from `deployment.openapi.yaml` | `deployment-management.openapi.yaml` | | Jobs | Generated from `jobs.api.server:app` | `jobs.openapi.yaml` | | Data Designer | Generated from `data_designer.api.server:app` | `data-designer.openapi.yaml` | diff --git a/packages/nemo_platform/pyproject.toml b/packages/nemo_platform/pyproject.toml index ff7dbde3ee..00c779afec 100644 --- a/packages/nemo_platform/pyproject.toml +++ b/packages/nemo_platform/pyproject.toml @@ -84,15 +84,6 @@ core-service = [ "nemo-platform[secrets-service]", ] -# Generated from [tool.bundle-package]; do not edit by hand. -customizer-service = [ - "fastapi[standard]>=0.115.4", - "uvicorn[standard]>=0.12.0", - "pydantic>=2.10.3", - "pydantic-settings>=2.6.1", - "nmp-common", -] - # Generated from [tool.bundle-package]; do not edit by hand. data-designer-nemo = [ "data-designer==0.6.1", @@ -470,7 +461,6 @@ services = [ "nemo-platform[hello-world-service]", "nemo-platform[guardrails-service]", "nemo-platform[evaluator-service]", - "nemo-platform[customizer-service]", "nemo-platform[plugins]", ] @@ -647,7 +637,6 @@ nmp-inference-gateway = { source = "../../services/core/inference-gateway/src/nm # Non-core services nmp-guardrails = { source = "../../services/guardrails/src/nmp/guardrails", module = "nmp/guardrails", deps_group = "guardrails-service" } -nmp-customizer = { source = "../../services/customizer/src/nmp/customizer", module = "nmp/customizer", deps_group = "customizer-service" } nmp-evaluator = { source = "../../services/evaluator/src/nmp/evaluator", module = "nmp/evaluator", deps_group = "evaluator-service" } nmp-platform-seed = { source = "../../services/platform-seed/src/nmp/platform_seed", module = "nmp/platform_seed", deps_group = "platform-seed-service" } nmp-hello-world = { source = "../../services/hello-world/src/nmp/hello_world", module = "nmp/hello_world", deps_group = "hello-world-service" } diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/core/lazy_load.py b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/core/lazy_load.py index 7a60ba0bf3..8f090a9ed2 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/core/lazy_load.py +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/core/lazy_load.py @@ -91,9 +91,13 @@ def _load_plugin_cli() -> click.Command: ) try: + from nemo_platform_plugin.customization_contributor import CustomizationContributorDiscoveryError + cli_cls = resolve_name(import_path) cli_obj = cli_cls() plugin_app = cli_obj.get_cli() + except CustomizationContributorDiscoveryError as exc: + raise click.ClickException(str(exc)) from exc except Exception: return _plugin_placeholder_command(f"Plugin commands for {plugin_name} are unavailable.") diff --git a/packages/nemo_platform_ext/tests/cli/test_app.py b/packages/nemo_platform_ext/tests/cli/test_app.py index 7e1dfe70f9..abf4f64e74 100644 --- a/packages/nemo_platform_ext/tests/cli/test_app.py +++ b/packages/nemo_platform_ext/tests/cli/test_app.py @@ -507,6 +507,23 @@ def test_plugin_loader_returns_placeholder_help_for_broken_cli(): assert loaded.help == "Plugin commands for example are unavailable." +def test_plugin_loader_surfaces_customization_contributor_discovery_error(): + from nemo_platform_plugin.customization_contributor import CustomizationContributorDiscoveryError + + class _BrokenCustomizationCLI(NemoCLI): + name = "customization" + + def __init__(self) -> None: + raise CustomizationContributorDiscoveryError("no contributors were discovered") + + def get_cli(self) -> typer.Typer: + return typer.Typer() + + with patch("nemo_platform_ext.cli.core.lazy_load.resolve_name", return_value=_BrokenCustomizationCLI): + with pytest.raises(click.ClickException, match="no contributors were discovered"): + lazy_plugin_loader("customization", "fake.module:BrokenCustomizationCLI")() + + def test_token_refresh_skipped_when_quickstart_auth_disabled(): """Token refresh should not run when the quickstart config has auth disabled.""" runner = CliRunner() diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/customization_contributor.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/customization_contributor.py index f6e63436a8..e235039d7e 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/customization_contributor.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/customization_contributor.py @@ -5,7 +5,8 @@ from __future__ import annotations -from typing import ClassVar, Protocol, runtime_checkable +from dataclasses import dataclass +from typing import Any, ClassVar, Protocol, runtime_checkable import typer from nemo_platform_plugin.authz import AuthzContribution @@ -16,6 +17,18 @@ class CustomizationContributorDiscoveryError(RuntimeError): """Raised when customization contributor discovery fails.""" +@dataclass(frozen=True, slots=True) +class CustomizationContributorSDKResources: + """Sync/async resource classes mounted under ``client.customization.``.""" + + sync_resource: type[Any] | None = None + async_resource: type[Any] | None = None + + def __post_init__(self) -> None: + if self.sync_resource is None and self.async_resource is None: + raise ValueError("At least one of sync_resource or async_resource must be provided") + + @runtime_checkable class CustomizationContributor(Protocol): """One training backend mounted under ``/apis/customization``.""" @@ -38,3 +51,14 @@ def get_authz_contribution(self) -> AuthzContribution | None: ``nemo.authz`` entry point for customization backends. """ ... + + def get_sdk_resources(self) -> CustomizationContributorSDKResources | None: + """Return SDK resource classes for ``client.customization.``. + + Return :class:`CustomizationContributorSDKResources` with sync and/or async + resource classes (each accepts a :class:`~nemo_platform.NeMoPlatform` or + :class:`~nemo_platform.AsyncNeMoPlatform` in ``__init__``). Return ``None`` + when the backend has no Python SDK surface. Do not register a separate + ``nemo.sdk`` entry point — the customization hub composes contributors. + """ + ... diff --git a/packages/nmp_common/src/nmp/common/mcp/README.md b/packages/nmp_common/src/nmp/common/mcp/README.md index bad2d48761..0a02e6d2bb 100644 --- a/packages/nmp_common/src/nmp/common/mcp/README.md +++ b/packages/nmp_common/src/nmp/common/mcp/README.md @@ -14,7 +14,7 @@ When multiple MCP servers exist across the platform: services/core/mcp/ # Core infrastructure tools services/guardrails/mcp/ # Guardrails-specific tools services/evaluator/mcp/ # Evaluation-specific tools -services/customizer/mcp/ # Customization-specific tools +plugins/nemo-customizer/ # Customization plugin (router + contributor discovery) ``` These shared utilities ensure: diff --git a/packages/nmp_common/src/nmp/common/model_utils.py b/packages/nmp_common/src/nmp/common/model_utils.py index 79b0bb8590..0a317de31b 100644 --- a/packages/nmp_common/src/nmp/common/model_utils.py +++ b/packages/nmp_common/src/nmp/common/model_utils.py @@ -6,7 +6,7 @@ # A draft for a better version of this function is in the nmp/services/core/models/src/nmp/core/models/tasks/model_spec/utils.py > is_embedding_model_v2 # Use it instead of this function in services/core/models/src/nmp/core/models/tasks/model_spec/run.py. -# services/customizer/src/nmp/customizer/app/jobs/compiler.py > _resolve_is_embedding_model uses this function as a fallback. +# nemo-automodel / nemo-unsloth job compilers use this function as a fallback for embedding model detection. def is_embedding_model(model_name: str | None) -> bool: """Return True when model identifier strongly suggests embedding usage.""" if model_name is None: diff --git a/packages/nmp_platform/README.md b/packages/nmp_platform/README.md index c819acd790..0fb9083927 100644 --- a/packages/nmp_platform/README.md +++ b/packages/nmp_platform/README.md @@ -24,9 +24,9 @@ that points callers at `nemo services run`. A handful of task container images and seed jobs invoke `nemo-platform run task` as their entrypoint: -- `nmp-cpu-tasks` — used by the file_io task - (`services/customizer/src/nmp/customizer/tasks/file_io/docker/docker-compose.yaml` - sets it as the image `ENTRYPOINT`). +- `nmp-automodel-tasks` — used by the automodel file_io task + (`services/automodel/src/nmp/automodel/tasks/docker/docker-compose.yaml` + runs `nmp.automodel.tasks.file_io`). - `services/platform-seed` — recommended invocation in its README is `nemo-platform run task --task nmp.platform_seed`. diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/inputs.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/inputs.py index 9a78f4450d..9229fea42c 100644 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/inputs.py +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/inputs.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""CLI overrides: submit/run accept a job JSON file instead of ``--spec``.""" +"""CLI overrides: submit accepts a job JSON file instead of ``--spec``.""" import json from collections.abc import Callable diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/config.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/config.py index 7ac7a09e58..c54a2a1dcd 100644 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/config.py +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/config.py @@ -14,8 +14,6 @@ class AutomodelPluginConfig(BaseSettings): model_config = SettingsConfigDict(env_prefix="NMP_AUTOMODEL_", extra="ignore") default_training_execution_profile: str = "gpu" - training_image: str = "nmp-automodel-training" - tasks_image: str = "nmp-automodel-tasks" def get_config() -> AutomodelPluginConfig: diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/contributor.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/contributor.py index b6b7b8ce92..ebe0b5c299 100644 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/contributor.py +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/contributor.py @@ -10,6 +10,7 @@ import typer from fastapi import APIRouter from nemo_platform_plugin.authz import AuthzContribution, authz_for_workspace_job_collection +from nemo_platform_plugin.customization_contributor import CustomizationContributorSDKResources from nemo_platform_plugin.jobs.api_factory import JobRouteOption from nemo_platform_plugin.jobs.routes import add_job_routes from nemo_platform_plugin.service import RouterSpec @@ -86,3 +87,11 @@ def get_authz_contribution(self) -> AuthzContribution: include_healthz=True, healthz_suffix="/automodel/healthz", ) + + def get_sdk_resources(self) -> CustomizationContributorSDKResources: + from nemo_automodel_plugin.sdk.resources import AsyncAutomodelCustomization, AutomodelCustomization + + return CustomizationContributorSDKResources( + sync_resource=AutomodelCustomization, + async_resource=AsyncAutomodelCustomization, + ) diff --git a/plugins/nemo-automodel/tests/test_contributor.py b/plugins/nemo-automodel/tests/test_contributor.py index 6c1e540cd3..b4df0030a7 100644 --- a/plugins/nemo-automodel/tests/test_contributor.py +++ b/plugins/nemo-automodel/tests/test_contributor.py @@ -26,3 +26,12 @@ def test_contributor_get_cli_exposes_flat_verbs() -> None: assert cli.info.name == "automodel" assert not any(g.name == "jobs" for g in cli.registered_groups) assert {cmd.name for cmd in cli.registered_commands} >= {"run", "submit", "explain"} + + +def test_contributor_exposes_sdk_resources() -> None: + from nemo_automodel_plugin.sdk.resources import AsyncAutomodelCustomization, AutomodelCustomization + + sdk = AutomodelContributor().get_sdk_resources() + assert sdk is not None + assert sdk.sync_resource is AutomodelCustomization + assert sdk.async_resource is AsyncAutomodelCustomization diff --git a/plugins/nemo-customizer/docs/CUSTOMIZATION.md b/plugins/nemo-customizer/docs/CUSTOMIZATION.md index dfb63576da..abcfbb7c54 100644 --- a/plugins/nemo-customizer/docs/CUSTOMIZATION.md +++ b/plugins/nemo-customizer/docs/CUSTOMIZATION.md @@ -9,7 +9,7 @@ Implement `CustomizationContributor`: - `name` — must match the entry-point key (e.g. `automodel`) - `get_routers()` — `RouterSpec` list with a **unique** prefix under `v2/workspaces/{workspace}//` - `get_cli()` — optional `typer.Typer` mounted at `nemo customization ` -- SDK: contributors implement HTTP/CLI only; **`nemo-customizer-plugin`** owns `nemo.sdk` → `customization` and composes backends (e.g. `client.customization.automodel.jobs` from `nemo-automodel-plugin`) +- `get_sdk_resources()` — optional sync/async resource classes for `client.customization.` (do not register a separate `nemo.sdk` entry point; **`nemo-customizer-plugin`** owns `nemo.sdk` → `customization` and composes backends) ## pyproject.toml diff --git a/plugins/nemo-customizer/src/nemo_customizer/cli.py b/plugins/nemo-customizer/src/nemo_customizer/cli.py index 73948e1dc0..96b72bef92 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/cli.py +++ b/plugins/nemo-customizer/src/nemo_customizer/cli.py @@ -9,7 +9,15 @@ import typer from nemo_platform_plugin.cli import NemoCLI -from nemo_platform_plugin.discovery import discover_customization_contributors +from nemo_platform_plugin.customization_contributor import CustomizationContributorDiscoveryError +from nemo_platform_plugin.discovery import ( + CUSTOMIZATION_CONTRIBUTORS_GROUP, + discover_customization_contributors, +) + + +class CustomizationCLIError(CustomizationContributorDiscoveryError): + """Raised when the customization CLI cannot start.""" class CustomizationCLI(NemoCLI): @@ -18,6 +26,15 @@ class CustomizationCLI(NemoCLI): name: ClassVar[str] = "customization" description: ClassVar[str] = "Customization training backends (Automodel, …)." + def __init__(self) -> None: + self._contributors = discover_customization_contributors() + if not self._contributors: + raise CustomizationCLIError( + "Customization CLI is enabled but no contributors were discovered. " + "Install a backend plugin (e.g. nemo-automodel) and ensure " + f"'{CUSTOMIZATION_CONTRIBUTORS_GROUP}' entry points are registered.", + ) + def get_cli(self) -> typer.Typer: app = typer.Typer( name=self.name, @@ -25,16 +42,8 @@ def get_cli(self) -> typer.Typer: no_args_is_help=True, ) - contributors = discover_customization_contributors() - if not contributors: - typer.echo( - "No customization contributors installed. Add nemo-automodel (or another backend) to enabled-plugins.", - err=True, - ) - return app - - for key in sorted(contributors.keys()): - contributor = contributors[key] + for key in sorted(self._contributors.keys()): + contributor = self._contributors[key] subgroup = contributor.get_cli() if subgroup is not None: app.add_typer(subgroup, name=key) diff --git a/plugins/nemo-customizer/src/nemo_customizer/sdk/resources.py b/plugins/nemo-customizer/src/nemo_customizer/sdk/resources.py index d769619728..f14c0b7f91 100644 --- a/plugins/nemo-customizer/src/nemo_customizer/sdk/resources.py +++ b/plugins/nemo-customizer/src/nemo_customizer/sdk/resources.py @@ -5,34 +5,38 @@ from __future__ import annotations -import importlib import logging -from typing import Any from nemo_platform import AsyncNeMoPlatform, NeMoPlatform +from nemo_platform_plugin.customization_contributor import CustomizationContributor from nemo_platform_plugin.discovery import discover_customization_contributors from nemo_platform_plugin.sdk import NemoPluginSDKResources logger = logging.getLogger(__name__) -# Contributor entry-point key → (module, sync class, async class) -_CONTRIBUTOR_SDK: dict[str, tuple[str, str, str]] = { - "automodel": ( - "nemo_automodel_plugin.sdk.resources", - "AutomodelCustomization", - "AsyncAutomodelCustomization", - ), - "unsloth": ( - "nemo_unsloth_plugin.sdk.resources", - "UnslothCustomization", - "AsyncUnslothCustomization", - ), -} - -def _load_contributor_sdk_class(module_path: str, class_name: str) -> type[Any]: - module = importlib.import_module(module_path) - return getattr(module, class_name) +def _mount_contributor_sdk_resources( + target: object, + platform: NeMoPlatform | AsyncNeMoPlatform, + contributors: dict[str, CustomizationContributor], + *, + async_: bool, +) -> None: + for key in sorted(contributors.keys()): + contributor = contributors[key] + sdk_resources = contributor.get_sdk_resources() + if sdk_resources is None: + continue + resource_cls = sdk_resources.async_resource if async_ else sdk_resources.sync_resource + if resource_cls is None: + continue + try: + setattr(target, key, resource_cls(platform)) + except ImportError: + logger.warning( + "Customization contributor %r is installed but SDK resources are unavailable", + key, + ) class Customization: @@ -40,18 +44,7 @@ class Customization: def __init__(self, platform: NeMoPlatform) -> None: contributors = discover_customization_contributors() - for key, (module_path, sync_cls, _async_cls) in _CONTRIBUTOR_SDK.items(): - if key not in contributors: - continue - try: - cls = _load_contributor_sdk_class(module_path, sync_cls) - setattr(self, key, cls(platform)) - except ImportError: - logger.warning( - "Customization contributor %r is installed but SDK module %s is missing", - key, - module_path, - ) + _mount_contributor_sdk_resources(self, platform, contributors, async_=False) class AsyncCustomization: @@ -59,18 +52,7 @@ class AsyncCustomization: def __init__(self, platform: AsyncNeMoPlatform) -> None: contributors = discover_customization_contributors() - for key, (module_path, _sync_cls, async_cls) in _CONTRIBUTOR_SDK.items(): - if key not in contributors: - continue - try: - cls = _load_contributor_sdk_class(module_path, async_cls) - setattr(self, key, cls(platform)) - except ImportError: - logger.warning( - "Customization contributor %r is installed but SDK module %s is missing", - key, - module_path, - ) + _mount_contributor_sdk_resources(self, platform, contributors, async_=True) customization_sdk_resources = NemoPluginSDKResources( diff --git a/plugins/nemo-customizer/tests/test_cli.py b/plugins/nemo-customizer/tests/test_cli.py new file mode 100644 index 0000000000..d467ed2a23 --- /dev/null +++ b/plugins/nemo-customizer/tests/test_cli.py @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import ClassVar + +import pytest +import typer +from nemo_customizer.cli import CustomizationCLI, CustomizationCLIError +from nemo_platform_plugin.service import RouterSpec + + +class _FakeContributor: + name: ClassVar[str] = "fake" + + def get_routers(self) -> list[RouterSpec]: + return [] + + def get_cli(self) -> typer.Typer: + app = typer.Typer() + + @app.command("info") + def info() -> None: + typer.echo("fake") + + return app + + +def test_cli_raises_without_contributors(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + "nemo_customizer.cli.discover_customization_contributors", + lambda: {}, + ) + with pytest.raises(CustomizationCLIError, match="no contributors"): + CustomizationCLI() + + +def test_cli_mounts_contributor_subgroups(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + "nemo_customizer.cli.discover_customization_contributors", + lambda: {"fake": _FakeContributor()}, + ) + cli = CustomizationCLI() + app = cli.get_cli() + assert "fake" in {group.name for group in app.registered_groups} diff --git a/plugins/nemo-customizer/tests/test_sdk.py b/plugins/nemo-customizer/tests/test_sdk.py index a430935d48..2120303311 100644 --- a/plugins/nemo-customizer/tests/test_sdk.py +++ b/plugins/nemo-customizer/tests/test_sdk.py @@ -5,14 +5,26 @@ from unittest.mock import MagicMock, patch +from nemo_automodel_plugin.sdk.resources import AutomodelCustomization from nemo_customizer.sdk.resources import ( AsyncCustomization, Customization, customization_sdk_resources, ) +from nemo_platform_plugin.customization_contributor import CustomizationContributorSDKResources from nemo_platform_plugin.sdk import NemoPluginSDKResources +class _AutomodelContributorStub: + def get_sdk_resources(self) -> CustomizationContributorSDKResources: + return CustomizationContributorSDKResources(sync_resource=AutomodelCustomization) + + +class _ContributorWithoutSdk: + def get_sdk_resources(self) -> None: + return None + + def test_customization_sdk_resources_entry_point_shape() -> None: assert isinstance(customization_sdk_resources, NemoPluginSDKResources) assert customization_sdk_resources.sync_resource is Customization @@ -26,12 +38,27 @@ def test_customization_composes_automodel_when_contributor_present() -> None: platform.base_url = "http://localhost:8000" platform.default_headers = {} - fake_contributor = object() with patch( "nemo_customizer.sdk.resources.discover_customization_contributors", - return_value={"automodel": fake_contributor}, + return_value={"automodel": _AutomodelContributorStub()}, ): customization = Customization(platform) assert hasattr(customization, "automodel") assert hasattr(customization.automodel, "jobs") + + +def test_customization_skips_contributors_without_sdk() -> None: + platform = MagicMock() + platform._client = MagicMock() + platform.workspace = "default" + platform.base_url = "http://localhost:8000" + platform.default_headers = {} + + with patch( + "nemo_customizer.sdk.resources.discover_customization_contributors", + return_value={"noop": _ContributorWithoutSdk()}, + ): + customization = Customization(platform) + + assert not hasattr(customization, "noop") diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/contributor.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/contributor.py index 2a23b7f4d4..8cc7d47483 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/contributor.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/contributor.py @@ -10,9 +10,7 @@ class at startup and: - merges :meth:`get_routers` into ``/apis/customization/...`` - adds :meth:`get_cli` under ``nemo customization unsloth`` - merges :meth:`get_authz_contribution` into the platform authz policy -- composes ``nemo-unsloth-plugin.sdk.resources.UnslothCustomization`` - under ``client.customization.unsloth`` (via the hub's - ``_CONTRIBUTOR_SDK`` map) +- composes :meth:`get_sdk_resources` under ``client.customization.unsloth`` """ from __future__ import annotations @@ -22,6 +20,7 @@ class at startup and: import typer from fastapi import APIRouter from nemo_platform_plugin.authz import AuthzContribution, authz_for_workspace_job_collection +from nemo_platform_plugin.customization_contributor import CustomizationContributorSDKResources from nemo_platform_plugin.jobs.api_factory import JobRouteOption from nemo_platform_plugin.jobs.routes import add_job_routes from nemo_platform_plugin.service import RouterSpec @@ -113,3 +112,11 @@ def get_authz_contribution(self) -> AuthzContribution: include_healthz=True, healthz_suffix="/unsloth/healthz", ) + + def get_sdk_resources(self) -> CustomizationContributorSDKResources: + from nemo_unsloth_plugin.sdk.resources import AsyncUnslothCustomization, UnslothCustomization + + return CustomizationContributorSDKResources( + sync_resource=UnslothCustomization, + async_resource=AsyncUnslothCustomization, + ) diff --git a/plugins/nemo-unsloth/tests/test_contributor.py b/plugins/nemo-unsloth/tests/test_contributor.py index 12df2de8b6..006f8ba560 100644 --- a/plugins/nemo-unsloth/tests/test_contributor.py +++ b/plugins/nemo-unsloth/tests/test_contributor.py @@ -107,3 +107,13 @@ def test_submit_help_shows_job_json_positional(self, contributor: object) -> Non assert "--workspace" in plain or "-w" in plain assert "--profile" in plain assert "--base-url" in plain + + +class TestSDK: + def test_exposes_sdk_resources(self, contributor: object) -> None: + from nemo_unsloth_plugin.sdk.resources import AsyncUnslothCustomization, UnslothCustomization + + sdk = contributor.get_sdk_resources() + assert sdk is not None + assert sdk.sync_resource is UnslothCustomization + assert sdk.async_resource is AsyncUnslothCustomization diff --git a/pyproject.toml b/pyproject.toml index 0ed9235e59..3702b119aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,6 @@ dependencies = [ "mypy-extensions==1.0.0", "tornado>=6.5.5", # pinned for CVE GHSA-7cx3-6m66-7c5m (>=6.5.0) + GHSA-qjxf-f2mg-c6mc (<=6.5.4, fixed in 6.5.5) "tomlkit>=0.13.3", - "nmp-customizer", "nmp-evaluator", "nmp-guardrails", "nmp-hello-world", @@ -121,7 +120,6 @@ dev = [ "sphinx-design>=0.6.0", "python-dotenv>=1.2.2", # Service packages for local development (not in root deps to allow --only-group in containers) - "nmp-customizer", "nemo-anonymizer-plugin", "nemo-data-designer-plugin", "nmp-evaluator", @@ -207,7 +205,6 @@ functional-services = [ "nmp-studio", "nmp-evaluator", "nmp-guardrails", - "nmp-customizer", "nemo-data-designer-plugin", "nemo-anonymizer-plugin", "nmp-intake", @@ -221,7 +218,6 @@ cpu-tasks = [ { include-group = "nmp-base" }, { include-group = "nmp-task-runtime" }, "nmp-evaluator", - "nmp-customizer", "nemo-anonymizer-plugin", "nemo-data-designer-plugin", "nmp-hello-world", @@ -233,7 +229,6 @@ gpu-tasks = [ { include-group = "nmp-base" }, { include-group = "nmp-task-runtime" }, "nmp-models", - "nmp-customizer", ] @@ -361,7 +356,6 @@ nmp-platform-runner = { workspace = true } nmp-platform-seed = { workspace = true } nmp-hello-world = { workspace = true } nmp-studio = { workspace = true } -nmp-customizer = { workspace = true } nmp-secrets = { workspace = true } nmp-dev-mcp = { workspace = true } nmp-testing = { workspace = true } @@ -399,7 +393,6 @@ members = [ "services/studio", "services/guardrails", "services/evaluator", - "services/customizer", "services/intake", "plugins/nemo-anonymizer", "plugins/nemo-data-designer", @@ -568,8 +561,6 @@ exclude = [ "packages/garak_api/", - "./services/customizer/", - # GPU-container training drivers import torch/peft/nemo_automodel/unsloth at runtime only. "services/automodel/src/nmp/automodel/tasks/training/backends/", "services/automodel/src/nmp/automodel/tasks/training/utils.py", diff --git a/pytest.ini b/pytest.ini index 266cf09b7d..3b977e1c3e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -40,7 +40,6 @@ testpaths = services/core/models/tests services/core/secrets/tests services/core/tests - services/customizer/tests services/data-designer/tests services/evaluator/tests services/guardrails/tests @@ -57,8 +56,6 @@ markers = integration: Service integration tests - test individual service interfaces and interactions (uses ASGI, mocks external services via SDK) gpu_integration: GPU integration tests - test individual services that utilize AI dependencies/ a GPU smoke_gpu_tasks: Import smoke tests for the nmp-gpu-tasks image - smoke_customizer_tasks: Import smoke tests for the customizer-tasks image - smoke_customizer_rl: Import smoke tests for the customizer-rl image smoke_nmp_automodel_tasks: Import smoke tests for the nmp-automodel-tasks image smoke_nmp_automodel_training: Import smoke tests for the nmp-automodel-training image e2e: End-to-end tests - test complete customer workflows on deployed infrastructure (Helm/Docker Compose) diff --git a/script/generate_config_docs.py b/script/generate_config_docs.py index b621cfe8b8..3ed49f9407 100644 --- a/script/generate_config_docs.py +++ b/script/generate_config_docs.py @@ -40,6 +40,7 @@ import yaml from nemo_safe_synthesizer_plugin.config import SafeSynthesizerConfig +from nmp.automodel.config import AutomodelConfig from nmp.common.config.base import CommonServiceConfig, PlatformConfig from nmp.core.auth.config import AuthServiceConfig from nmp.core.entities.config import EntitiesConfig @@ -48,9 +49,9 @@ from nmp.core.jobs.config import JobsServiceConfig from nmp.core.models.config import ModelsConfig from nmp.core.secrets.config import SecretsServiceConfig -from nmp.customizer.config import CustomizerConfig from nmp.evaluator.config import EvaluatorSettings from nmp.studio.config import StudioConfig +from nmp.unsloth.config import UnslothConfig from ruamel.yaml import YAML from ruamel.yaml.comments import CommentedMap @@ -69,7 +70,8 @@ JobsServiceConfig, ModelsConfig, SecretsServiceConfig, - CustomizerConfig, + AutomodelConfig, + UnslothConfig, EvaluatorSettings, SafeSynthesizerConfig, StudioConfig, diff --git a/script/generate_openapi_spec.py b/script/generate_openapi_spec.py index 1034fd51f5..85628283cc 100644 --- a/script/generate_openapi_spec.py +++ b/script/generate_openapi_spec.py @@ -20,7 +20,6 @@ from nmp.common.version import platform_api_version from uvicorn.importer import import_from_string -from .openapi_helper.customizer_generator import generate_customizer_openapi from .openapi_helper.openapi_tools import ( copy_tags, fix_openai_streaming_endpoints, @@ -87,7 +86,6 @@ class ServiceConfig: output_file: str app_dir: Optional[str] = None env_vars: Optional[Dict[str, str]] = None - custom_generation: Optional[str] = None # For special cases like customizer copy_from: Optional[str] = None # For deployment-management spec_type: SpecType = SpecType.GA @@ -155,28 +153,6 @@ def extract_openapi_spec(service: ServiceConfig) -> tuple[str, bool, str]: shutil.copy(service.copy_from, temp_path) return service.name, True, "" - # Handle special cases - if service.custom_generation: - if service.custom_generation == "customizer": - # Generate the customizer OpenAPI spec - generated_file = generate_customizer_openapi(output_dir="openapi") - - if os.path.exists(generated_file): - temp_path = service.temp_output_path() - - # Ensure directories exist - os.makedirs(os.path.dirname(temp_path), exist_ok=True) - - shutil.move(generated_file, temp_path) - else: - return ( - service.name, - False, - f"Generated file {generated_file} not found", - ) - - return service.name, True, "" - # Set environment variables if specified old_env = {} if service.env_vars: diff --git a/script/openapi_helper/customizer_generator.py b/script/openapi_helper/customizer_generator.py deleted file mode 100644 index d1f2c87327..0000000000 --- a/script/openapi_helper/customizer_generator.py +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -""" -Customizer OpenAPI spec generator. -Moved from services/customizer/openapi/generate_openapi_spec.py for easier integration. -""" - -import os -import pathlib -import sys -from typing import Dict - -import yaml -from fastapi.testclient import TestClient - - -def merge_openapi_schemas(main_schema: Dict, mounted_schema: Dict) -> Dict: - """ - Merge two OpenAPI schemas with the following conditions: - 1. Prefix paths from the mounted schema with '/v1'. - 2. Do not merge tags from the mounted schema to the main schema. - """ - # Merge paths with '/v1' prefix for the mounted schema - main_paths = main_schema.get("paths", {}) - mounted_paths = mounted_schema.get("paths", {}) - for path, methods in mounted_paths.items(): - # Prepend '/v1' to the path - main_paths[f"/v1{path}"] = methods - main_schema["paths"] = main_paths - - # Merge components, if they exist - main_components = main_schema.get("components", {}) - mounted_components = mounted_schema.get("components", {}) - for component_type in [ - "schemas", - "responses", - "parameters", - "examples", - "requestBodies", - "headers", - "securitySchemes", - "links", - "callbacks", - ]: - main_component_type = main_components.get(component_type, {}) - mounted_component_type = mounted_components.get(component_type, {}) - main_component_type.update(mounted_component_type) - main_components[component_type] = main_component_type - main_schema["components"] = main_components - return main_schema - - -def generate_customizer_openapi(output_dir: str = "openapi") -> str: - """ - Generate the customizer OpenAPI specification. - - Args: - output_dir: Directory where to write the generated file - - Returns: - Path to the generated file - """ - # Add customizer to path for imports - customizer_path = "services/customizer/src" - if customizer_path not in sys.path: - sys.path.insert(0, customizer_path) - - try: - # Import customizer apps - from customizer.main import app - from nmp.customizer.api.v1.main import app as app_v1 - - # Generate OpenAPI specs - client = TestClient(app) - response = client.get("/openapi.json") - main_json = response.json() - - client = TestClient(app_v1) - response = client.get("/openapi.json") - v1_json = response.json() - - # Prepare output - output_path_obj = pathlib.Path(output_dir) - output_path_obj.mkdir(parents=True, exist_ok=True) - - output_file = output_path_obj / "customizer.generated.openapi.yaml" - with output_file.open("w", encoding="utf-8") as fp: - yaml.dump( - merge_openapi_schemas(main_json, v1_json), - fp, - sort_keys=False, - allow_unicode=True, - ) - - return str(output_file) - - except ImportError as e: - raise ImportError( - f"Failed to import customizer modules: {e}. Make sure you're running from the project root." - ) from e - - -# For backward compatibility with the original click-based interface -def main(output: str = None): - """Main function for backward compatibility.""" - if output is None: - output = os.getcwd() - return generate_customizer_openapi(output) diff --git a/sdk/python/nemo-platform/src/nemo_platform/cli/core/lazy_load.py b/sdk/python/nemo-platform/src/nemo_platform/cli/core/lazy_load.py index 848a0aa145..b3dc66c86a 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/cli/core/lazy_load.py +++ b/sdk/python/nemo-platform/src/nemo_platform/cli/core/lazy_load.py @@ -91,9 +91,13 @@ def _load_plugin_cli() -> click.Command: ) try: + from nemo_platform_plugin.customization_contributor import CustomizationContributorDiscoveryError + cli_cls = resolve_name(import_path) cli_obj = cli_cls() plugin_app = cli_obj.get_cli() + except CustomizationContributorDiscoveryError as exc: + raise click.ClickException(str(exc)) from exc except Exception: return _plugin_placeholder_command(f"Plugin commands for {plugin_name} are unavailable.") diff --git a/sdk/python/nemo-platform/tests/vendored/nemo_platform_ext/cli/test_app.py b/sdk/python/nemo-platform/tests/vendored/nemo_platform_ext/cli/test_app.py index 69f59cb4c3..8a9234e00d 100644 --- a/sdk/python/nemo-platform/tests/vendored/nemo_platform_ext/cli/test_app.py +++ b/sdk/python/nemo-platform/tests/vendored/nemo_platform_ext/cli/test_app.py @@ -507,6 +507,23 @@ def test_plugin_loader_returns_placeholder_help_for_broken_cli(): assert loaded.help == "Plugin commands for example are unavailable." +def test_plugin_loader_surfaces_customization_contributor_discovery_error(): + from nemo_platform_plugin.customization_contributor import CustomizationContributorDiscoveryError + + class _BrokenCustomizationCLI(NemoCLI): + name = "customization" + + def __init__(self) -> None: + raise CustomizationContributorDiscoveryError("no contributors were discovered") + + def get_cli(self) -> typer.Typer: + return typer.Typer() + + with patch("nemo_platform.cli.core.lazy_load.resolve_name", return_value=_BrokenCustomizationCLI): + with pytest.raises(click.ClickException, match="no contributors were discovered"): + lazy_plugin_loader("customization", "fake.module:BrokenCustomizationCLI")() + + def test_token_refresh_skipped_when_quickstart_auth_disabled(): """Token refresh should not run when the quickstart config has auth disabled.""" runner = CliRunner() diff --git a/services/automodel/docker/Dockerfile.mamba-wheel b/services/automodel/docker/Dockerfile.mamba-wheel index c4b62e79d5..0d0cee1cca 100644 --- a/services/automodel/docker/Dockerfile.mamba-wheel +++ b/services/automodel/docker/Dockerfile.mamba-wheel @@ -97,7 +97,7 @@ COPY --from=ghcr.io/astral-sh/uv:0.9.14 /uv /usr/local/bin/uv # ============================================================================= -# causal-conv1d wheel — Python 3.11 (for nmp-gpu-tasks and nmp-customizer-tasks) +# causal-conv1d wheel — Python 3.11 (for nmp-gpu-tasks and nmp-automodel-tasks) # ============================================================================= FROM mamba-wheel-base AS causal-conv1d-wheel-builder @@ -139,7 +139,7 @@ RUN mkdir -p /wheels && \ rm -rf /src/causal-conv1d # The final causal-conv1d-wheel image contains: -# - causal_conv1d-*-cp311-*.whl (for Python 3.11 consumers: nmp-gpu-tasks, nmp-customizer-tasks) +# - causal_conv1d-*-cp311-*.whl (for Python 3.11 consumers: nmp-gpu-tasks, nmp-automodel-tasks) # - causal_conv1d-*-cp312-*.whl (for Python 3.12 consumers) # Consumers must pin the Python tag glob (e.g. causal_conv1d-*cp311*.whl) to select the right one. FROM scratch AS causal-conv1d-wheel @@ -182,7 +182,7 @@ RUN mkdir -p /wheels && \ rm -rf /src/mamba # ============================================================================= -# mamba-ssm 2.3.0 wheel — Python 3.11 (for nmp-customizer-tasks) +# mamba-ssm 2.3.0 wheel — Python 3.11 (for nmp-automodel-tasks) # ============================================================================= FROM mamba-wheel-base AS mamba-ssm-23-wheel-builder @@ -234,7 +234,7 @@ RUN mkdir -p /wheels && \ # The final mamba-ssm-wheel image contains four versions: # - mamba_ssm-2.2.5-cp311-*.whl (from MAMBA_22_COMMIT=6b32be06, for nmp-gpu-tasks / Python 3.11) # - mamba_ssm-2.2.5-cp312-*.whl (from MAMBA_22_COMMIT=6b32be06, for Python 3.12 consumers, e.g. automodel) -# - mamba_ssm-2.3.0-cp311-*.whl (from v2.3.0, for nmp-customizer-tasks / Python 3.11) +# - mamba_ssm-2.3.0-cp311-*.whl (from v2.3.0, for nmp-automodel-tasks / Python 3.11) # - mamba_ssm-2.3.0-cp312-*.whl (from v2.3.0, for Python 3.12 consumers) # Consumers must pin both version AND Python tag glob to select the correct wheel. FROM scratch AS mamba-ssm-wheel diff --git a/services/automodel/docker/Dockerfile.nmp-automodel-base b/services/automodel/docker/Dockerfile.nmp-automodel-base index 5103bf6361..6ffc87f694 100644 --- a/services/automodel/docker/Dockerfile.nmp-automodel-base +++ b/services/automodel/docker/Dockerfile.nmp-automodel-base @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1 # nmp-automodel base - PyTorch NGC image + Automodel + CUDA extension wheels. # -# Mirrors nmp/docker/Dockerfile.nmp-customizer customizer-automodel-base-builder. +# PyTorch NGC base + Automodel + CUDA extension wheels for nmp-automodel images. # Publish target: nmp-automodel-base (slim image; builder is build-only). ARG CAUSAL_CONV1D_WHEEL_IMAGE=local diff --git a/services/automodel/docker/README.md b/services/automodel/docker/README.md index f8409a98fb..32150ec1ef 100644 --- a/services/automodel/docker/README.md +++ b/services/automodel/docker/README.md @@ -1,6 +1,6 @@ # nmp-automodel container images -Three images derived from the legacy `nmp` **customizer-automodel** base builder (not the full `customizer-automodel` HTTP service image). Published as flat repo names under **`my-registry/nemo-platform-dev/nmp-automodel-*`** (no nested `nmp/...` path — some registries reject that on push). +Three images for the **nmp-automodel** customization backend. Published as flat repo names under **`my-registry/nemo-platform-dev/nmp-automodel-*`** (no nested `nmp/...` path — some registries reject that on push). | Image | Dockerfile | Role | |-------|------------|------| @@ -77,11 +77,11 @@ Override registry: `export WHEELS_REGISTRY=...` and `export IMAGE_REGISTRY=...` ## Tasks / training runtime (platform glue) -**Base (`nmp-automodel-base`):** Same as `customizer-automodel-base-builder` — NGC PyTorch 26.02, Automodel `uv sync --locked`, pinned `transformers`/`torch`. +**Base (`nmp-automodel-base`):** NGC PyTorch 26.02, Automodel `uv sync --locked`, pinned `transformers`/`torch`. **Tasks image:** `uv sync --package nmp-automodel --no-dev --inexact` from the minimal workspace. CPU steps only need platform SDK glue; upgrading ancillary packages here does not affect training. -**Training image:** Do **not** use `uv sync` — it upgrades `transformers` and breaks `PreTrainedModel`. Use **`uv pip install -e`** with **`--overrides no_override_requirements.txt`** (customizer pattern), then `uv pip install --no-deps -e /opt/Automodel` to re-pin `nemo_automodel` from the base clone (not PyPI). +**Training image:** Do **not** use `uv sync` — it upgrades `transformers` and breaks `PreTrainedModel`. Use **`uv pip install -e`** with **`--overrides no_override_requirements.txt`**, then `uv pip install --no-deps -e /opt/Automodel` to re-pin `nemo_automodel` from the base clone (not PyPI). ## Runtime diff --git a/services/automodel/src/nmp/automodel/tasks/docker/README.md b/services/automodel/src/nmp/automodel/tasks/docker/README.md index b57974656d..bf41efc344 100644 --- a/services/automodel/src/nmp/automodel/tasks/docker/README.md +++ b/services/automodel/src/nmp/automodel/tasks/docker/README.md @@ -5,11 +5,10 @@ Scripts for running the file_io task container locally. ## Prerequisites 1. **Build the Docker image** from the repository root: -This will build `my-registry/nmp-cpu-tasks:local` image that will be used for this task. ```bash - cd /path/to/nmp - make docker/nmp-cpu-tasks + cd /path/to/nemo-platform + docker buildx bake -f docker-bake.hcl nmp-automodel-tasks-docker ``` 2. **Have NeMo Platform running** (files service) at `http://localhost:8080` @@ -19,16 +18,16 @@ This will build `my-registry/nmp-cpu-tasks:local` image that will be used for th ### Run with Docker Compose ```bash -cd services/customizer/src/nmp/customizer/tasks/file_io/docker +cd services/automodel/src/nmp/automodel/tasks/docker # Run the task docker compose up # Run with custom image -FILE_IO_IMAGE=my-registry/nmp-cpu-tasks:dev docker compose up +FILE_IO_IMAGE=my-registry/nemo-platform-dev/nmp-automodel-tasks:dev docker compose up # Run interactively -docker compose run --rm file-io run task --task nmp.customizer.tasks.file_io +docker compose run --rm file-io run task --task nmp.automodel.tasks.file_io ``` ## Configuration @@ -45,7 +44,7 @@ docker compose run --rm file-io run task --task nmp.customizer.tasks.file_io | `NEMO_JOB_TASK` | Task identifier | `file-io-task` | | `NEMO_JOB_WORKSPACE` | Workspace name | `default` | | `LOG_LEVEL` | Logging level | `INFO` | -| `FILE_IO_IMAGE` | Docker image to use | `my-registry/nmp-cpu-tasks:local` | +| `FILE_IO_IMAGE` | Docker image to use | `my-registry/nemo-platform-dev/nmp-automodel-tasks:local` | ### Config File Format diff --git a/services/automodel/src/nmp/automodel/tasks/docker/docker-compose.yaml b/services/automodel/src/nmp/automodel/tasks/docker/docker-compose.yaml index a367ae44a3..75b6b8b31b 100644 --- a/services/automodel/src/nmp/automodel/tasks/docker/docker-compose.yaml +++ b/services/automodel/src/nmp/automodel/tasks/docker/docker-compose.yaml @@ -5,7 +5,7 @@ # docker compose up # # # Run with custom command -# docker compose run --rm file-io run task --task nmp.customizer.tasks.file_io +# docker compose run --rm file-io run task --task nmp.automodel.tasks.file_io # # Prerequisites: # - Build the image first (from Platform repo root): @@ -19,10 +19,10 @@ services: container_name: file-io-task # Mount config file and storage directory - # Using test data from services/customizer/tests/tasks/file_io/data/ - # files will be downloaded under services/customizer/tests/tasks/file_io/data/temp which is in .gitignore + # Using test data from services/automodel/tests/tasks/file_io/data/ + # files will be downloaded under services/automodel/tests/tasks/file_io/data/temp which is in .gitignore volumes: - - ../../../../../../tests/tasks/file_io/data:/var/run/scratch + - ../../../../../tests/tasks/file_io/data:/var/run/scratch environment: # NeMo Platform URLs - use host.docker.internal to reach host services diff --git a/services/automodel/tests/tasks/file_io/data/files_to_upload/nested_1/file1.txt b/services/automodel/tests/tasks/file_io/data/files_to_upload/nested_1/file1.txt new file mode 100644 index 0000000000..d9039017ab --- /dev/null +++ b/services/automodel/tests/tasks/file_io/data/files_to_upload/nested_1/file1.txt @@ -0,0 +1 @@ +file1 content diff --git a/services/automodel/tests/tasks/file_io/data/files_to_upload/nested_2/file2.txt b/services/automodel/tests/tasks/file_io/data/files_to_upload/nested_2/file2.txt new file mode 100644 index 0000000000..f3c77b12c6 --- /dev/null +++ b/services/automodel/tests/tasks/file_io/data/files_to_upload/nested_2/file2.txt @@ -0,0 +1 @@ +file2 content diff --git a/services/automodel/tests/tasks/file_io/data/sample_config.json b/services/automodel/tests/tasks/file_io/data/sample_config.json new file mode 100644 index 0000000000..6b97c24a45 --- /dev/null +++ b/services/automodel/tests/tasks/file_io/data/sample_config.json @@ -0,0 +1,14 @@ +{ + "upload": [ + { + "src": "files_to_upload", + "dest": "default/test-fileset" + } + ], + "download": [ + { + "src": "default/test-fileset", + "dest": "temp/downloaded_files" + } + ] +} diff --git a/services/customizer/tests/tasks/file_io/data/files_to_upload/nested_1/file1.txt b/services/automodel/tests/tasks/file_io/files_to_upload/nested_1/file1.txt similarity index 100% rename from services/customizer/tests/tasks/file_io/data/files_to_upload/nested_1/file1.txt rename to services/automodel/tests/tasks/file_io/files_to_upload/nested_1/file1.txt diff --git a/services/automodel/tests/tasks/file_io/files_to_upload/nested_2/__0_0.distcp b/services/automodel/tests/tasks/file_io/files_to_upload/nested_2/__0_0.distcp new file mode 100644 index 0000000000000000000000000000000000000000..112f2cdf7de34dd561c202a162cdbd420cd2f579 GIT binary patch literal 19565704 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 z z&q~w4z)GbzJJoV5ZmJ$wEQarPgT+pEMNSwvjglQaJtXD>9nfT;WSwc#S&d^S_%qQw zKLbw1SV1lWZ?o+5arn@xtAvsKN3owST>*R>z$aP#+NkZo<`5$_A4k+yU4 zth4wpvw9@yE+~hr9IDjd+XGUUgW*wjlozpErqQ4N2G$$paD?6WUlH@7j=>ib(D(pc zd#0v^sfovE`!rxaB9x$Wv&64M|8C->ZPn|;)2!b-G@j>Xp}a|ai3eCExo`04_t?={ z+?v)%0iV=>qxISh4ZN?}^0Z$emuWRT_%VNN(iax2j$dz~k@xkTHcm^bxo zcc0WSw{hh&66ORw%H^t63%eDa;|(xiojzv*%NxaO1_jN-a1#0JD0u&T0ef(PkoT|~FYd90= zbq}UTZlbr^6nQ#|4By52s`XVqFO&dDrL3a}ICR1RA2{>x zj)LzIjdGU9cq;H`l}N2hB*wq}DSVTpZ!&rfIKN4j$`Y(N^r{RK(<0CNx0t;Odi^oB z$s(wc2p06_DDa{{v{)kjsd^SzYbFlmWF+usSp2Fb3p(kR4RV86PDCRsAd3%LKhldC zTKJxfXdY4{5o&4IbD%iuaS{XHo^#~XDp$M)y2MlM5KDdv8va9mJ~&yhJw$azAlF^Yw1(QQVG3wL}IL-v>V*>P->jvKwe#E9PJBlUlkmIIN&p(sVi0HHGgb z25czLY=-{q&!Fr=19*lfsLca?Psqoitx)$Y^JMFNymccQbRQO!K&%oDwtaSP=VY7Y zq7x=d6ra?2YQ1@@{amAOdh0SYgBAUc0Tn3K>hb2VhBPt$yI4<_DQM61bR;9Eka5+l zDq~{<1Hag{gjhgUa0rdaej{oPipDmI?6xelm)Rd)aOCAdziT!Uvt{^xgx1Us%P+dl4K)Gy3s2N!Kq5M zTZ6t5$KBiNght6u-3)EqhE(bHk3ubz@?CheQ#LbhJ$oo(94sj8;z%qa^IP>IvYK=f z8vK@g7Y!<`#G0TLE@C%yVAY9$4s}F&W@)@0!%b?H?g>#hgDmK$p@YiMZm@r)E_12q z10`rJMXd8KU^?YL0wPUN--wt!q($f^21i)`12VtaHQgiOQ^T4?(RRb7CcUS%g2+tt zFC?|qgIxvgv#?S%$m5LKfxPO6D!bV~eS9@jCh@h!AiEw=^Cu@NS(wZ;?px6dj^l=R zwJYR2ZIGQ{#dPTHnxX$2L0tz}kOV9$Dbn==OLLc^Hwpe8p z0egQ6tj5+;ZV!;Ekr$xYG59ZwvFpfnFVI<}^OW>U79*QR)1v!?8>>!X->`epL15X< zUhxF7TJk2?$2~IUt+Ei>hI|j}K_)xj#ybH!-y-(`#b+cN+3F)YLvhUhI%hJvs5?u{ z?~N1)^`oCw%Z13T2`%E?HmnWZy3;QW)w}!Un?fZMjf7+4Sq-&2Tmxr$C%dVaJ0%tT z%*UQ<9w1_-kp>%lvvi_!Kcj;x-~$^cQUEV9xLyfWD*K=(46@N;dCk&4qnt3{d|D0N+Bz^->5jXt1_)j0GZY^O8C)R7H6xqPd zQI82FvEQmAe~2-acA%9c77O!+ju>@=QLA0?JaZFI4m~z7s^tjJq6K0f)X$NzpG$>4 z2E;!H`#)7GtLZTN{3)=s9&{E%f0!rZB$g-nVP5KpsZ}^N%TBqP-|7T8L2tuPQN$_K zqt-!u6i99Y`*wF+rwx+qCy<#;;HOnmp3^CpD@cpwey6C0PO)f z*Jvg1HSfkE?GySU0Lf1BRJw>D2y=t`P|9T(Tv15^uB8$c?{cC$%WU)VW1j=I_Tdd5 zmDi+Ny5X%6Ar^wQm`^Gi`ne_iLpZGhtc{>0^m9ricw&Ay^DDta7dp)?QldX|8-*GK zx|zFKFSaN=DOmIB!BuP+tBz=>lXLxi|0(+l^PFYATE!kBVI@aefMYT^{Gq#1%;VV1 z?d2yf67Ogd|F4u(Jp-y7kQj87d`=gt?y<(N{4AKe(jG_ zi&wPT7Uqk|k%zzz@mj>s+~jMum*|*DB@=}2=j#Xh1oCVG&f`wTTUAg^EBr%DljIs! z!F)EK*FkNw`VM>NL{+O0c|dpYOaU0R@xlzVgm!1COQq8-~+y&?t56?~s7ANpzk_Zi$y8wB4{&k_RjbuVE(4@Q9b<0-P zH0VBL-3P$`rypR7dMl?3JJM^>hOE9~E$cL2Az9q=bKlF@+=}^rSjTi3 ze}~1S(962c@AYTPatGe2;I3xC;cx6Yux3msmaMpL88i+5l3zQ+J@X zQdwB%L4V&u)BlXMe-q0|FZR%|)JilXu0a3Y=PG2IZt^uiAy$6j_JaRJydU&>`j_j;mVI302s<8%KTpXjcZ_0a6|P^#5I`O3vBnkbYp1cWAK1c=S@W4T$T z|HZ?1b@0YPVBCr3RL$(%f05=Byl*~CvxMw*2eklde_g7;(;;{+NoM)qYv3B+KJlzJZCnXUGDA>@MN{)<6z2B#(#eR1pGrE9!lb)wjp-5I5Eq1rJsDx?orm~Qg znRRbU;(%n8;3L;-e0jJ?b7h`91a1x@9Xho@4+vf<&4Qno$(?#M+{>Ml_w4>q3(Z@F zZmT)kg#Bv@3!8aitk!}>1AuFGOn)R9VwP#;X2lL`;Q9jniPqw4IK&zbbJh<6durNn z%OF-yiLPaJ)Q1#uTaW_>iQQVK+^>+(d)dXXVBv6H{u?@M!LHCDUxs3!oII`8M7@KR zmH0YGF15QH9P~<=jH^Y|)tbeHv2S8k2@s)gIR*+)Gq&A*>>8`|DY*k@u#Avn% z8O4nY=7mrg04et8?TrAF)w+9g2s8lWRA7tx_r|`*iIj+9_6=^i5~650`5fqtVF$y4xDU8mf~#u^ouC+27o~ zn_2rV&fpP#3$x!>+37AQ>QZmkOCs};JPk;(DNZza1j&2@=MX&!;C*U12Q#`>1zzcq zYTo>gl!*BdJAn_DIcO`!S=HXZhr;mEsMauY0#irQDOaKkavPK9X5MLkDVQ(BdJe_hRgPrLGeAJfU^+MW{>@fL|_FhU0ua^>pLa z>gqQ$(yP2dhNUh*B2{L?oF z=5MkL@@dZ63Mg$@79p8-Gr|pw@CN>QWtKV_LB{vXN6a*aJPm6XJD}>Olo>T3i6`87 zVm7E0Aa)XzMfO&(FgVZc!E#-OW?;QVN14TJ(N?9e2xuO|&Z(+God=7u8**drX}ufH zJ{cJqttH6yG+=rvXW13djn&`0k`+dW=tqc-x>Cbx)b|7QikFCQxte{JtMwxMz}DbQqdLXRNI$f(X;#5qS|^Ay@6;Q}U$NY{exB`y+Ge`5$&<__+o+dULJy$?xQCqreB!HHduqt+R}*25gd+dvY7+0r@h5)JImf$p}893V3F#?gFC|@(=gh!0g{~ z(yR1x0Fsef3;#^<-Ixnx1gTZ0cGg(U$Z=-;RKG3eKPY4@gX$SNpxH==cUeV~#w(Gf zx`=gHb%q}3gPPBJvmREfp?QdR$ofK_%rZ-}npB+yF8?nmkPObl3bpRQ0n*`Vi3iF7 z*5!F&tgKFfK%6Ehw$6g)PruPlqe^+jN{9~V?qpDw`QWT1U#6bCM47dp=Z}2Mx4q2D?jQ_ZIavtgC^` zM!^P#gnCe|XHFiL>~45=ls#E4V3nXvvA#C3+Pf8=K5bGSYT&mtpm!Lk9)>F?iMX{M zF4JmS!BS_pFlvS>=L|5hy9piKd8$jcpilJb$I=B&^a2yh7V_GGzlJ*q`|i~Hfuli% zn#@71bU+(v+Nqym;j&tlkLb_5<+=1i8Ps)wi=Aoy zL?!FJP+kH!vB)lqUbhTH^7yojch2G*s0Ox{qcEi}1@-zPEY{KT9JkXgE*1_X8rdzE z1ixB|zTV!^^p>9b) zi~bq*nF*kcH;*5yyhg^dm`#qDduWNsb|D%a_*n%5JG0CmS^+j*0A^jXO8RwyH!X%7 zJfPmlUaR%ikkv6iN^WAj_rXqQ#8vJk_A$g59l-QsZO2kDz}hVrah%;J1IHVny|eL< zo44Rv>=@>S9?=GHn-4#95@k@X-TXNVihKrXZZvfgp3N7)*5nb~xE|KfywKf)qa}Q-AKAvXlH~GlVmE?gdf)OjQ@`t{d}?fh0V}CXFQx; z38yCTB4Hw8r=h} zShd!t(Z=RiO8xnHYbunksDrkETe~3~MVkD$d5*fpCPnYR_)?nTl^kPlnyJ2MB zByTF~y_>cC)#vKh4nKet`-h;#$E9BFj=VT*=UYK23J(uGj5I~|bEC@CD>xCTkfmvy zbdMt2(m4H|hANHgj@0Z&!hHnxO$Q!T>iL10VL1cdex3CNjMXBRg=|*V8Dt0Xg5*bT zXMF5g@DN#z{8+rK*&(eS!wfrJi}W}YiWP1X{ExcHtYU({*Qr@@2DKDvyi|{);${nP zz9eV4leGkG!(w#Fk9W~x9^b$FOWgkh^Uct8Co8b}ijCYFCs3iii~x|VVa`$3yiLrHYLS=J9n=%JpIhw#I>y5zRz2=UR+Y;+a=@(sw$spQxzb@< z?Zh}NmKDk^g`H+A^;p0%-9gp7Xcp0BSwnV*Ba#<Hy)}y^2YAXz=l)+m2fdaREU>E1J z70W-g5r}dp?^3&w?1lDr_)%znTFd?4gBYwxec*9id!U1EIWcUN4Dbr*7(8csRlk-X zlkB}IG?`7>$KD@j1(u^*qf2-fA2xJB^)u~~YV-)ZW6!_`7Jp_i{#g3-y3onscT_&W z*AT`^JO!TU$Hoc^;7EMW%+kxa-_`Y!&JITfxapw2tWD5yf-B|wIr#qD?l?t55wEG? zA9J&Q6fHW2z085rgG#4r83qd1a-Xc${|Qa$B)<({4Oi&Z^x#*3_q5u*;3QVpiJY{` z1`D91o$9ekHtup2Eb_v~f_<~37DII zaZc~hvS19}AXbrYEvkYlRE&rh0*N^TG@arlV7^A_kY}~PnO6CN-@{45S?-R75_(-D zcFR=8CZkKJ_2ZK@BAnsAug_^6w(wU&mB^m&;8h=!E}f7Sp_NhyExaPL%t_r=<9uLP z%ZQfuI3pkX3xW1EGA0|q{*;@<9%P>UJN*LY`+GzKp0_lfUXR3ULnbfe%)3&1u~j8; z4poHyxv(b2K24rw*}v8zDC6igp#Hxr5$_Z!_ z!(ur^WY}@CtQJw?%H=YjLwxuFH;-G#SD_4iiYaajezy3(3g?MJ;|BkEZ;eSAmw1qftQHaVByOx+>1BS?B;?nmohW$Jp0^ zzW`pWv^Swj%h)0Z9BDFu1!h9-2)=Su_2=eSz?!TBQ9YQj+~x*qsQ=vE7{u{OCzzS? z1Hk1vAm1lnVcE2sU}p z^$Yo}RH-xzwuVC@Nj^Cu;0~6_`aRIQkdf&0Agzoh?5@J+fZ+)wTb0)8EcAAf(8Uqh zpCntL>n0xwtgUa{H`Vl#LNeAY+T0?yIY+QkZ*q3~nC6sQ&vtga9WEj>dw6s8<<7+u4|zavqqZl(ef@v(E^h+ zKn7F+`#-pP?kFE)xv`GemOH)~j_%MT$8FkGQ{N>LtcSJU#dCk&4wT|GUh}BF{p)Z% zaBbxtmySfIiyrVL&)@fl0#5TsSaBY@X&<+sT6he(1|6J?W!mJP-Pwoq_wHFqa8Y6u zQOEl5UO*{HQZ4h5Jzx4+&5$&y^!t%b35-zct;@i;J69_?+0W)_v#w@JA39SHTFo3g z;uZkz=p4(4R?nkb=Ojl5#Bx|CB^7DZE*6O#uVheis?-1_Ze8*daBV(ntblwnDt7>B ztocXq^_s@>KQCiSO#^0oITQ{3;b(%rtz+ofO8gXS8RPvnG)c===L~`-_sicL{Z=@A z)@uWB+6JGYXDi)W+0izA2>sk@m7Iy(>(eXnhIgv!43=5zJ(?%iBT4B>8WbX%Z)Gpu z7x6CHM!J@hbp!KRH0c`sKpw_MhxgDmLJcd3v!F&{XcCC_vMTFndI$2PmTxB5IaU%@ z*W#a2`f)?^P15DkfqN0sW?V-74pv}!cXoTU2*s*k46Ah+DXK`R+vvhGIP^t+5Q8XlzobBsI_RD;2BDEm0%#t2Wm>x*?j zZ z5p0qP#JKTN#fTl!AF_H&3D6e4WY(4DqUW*8X0mOJi((wYhUJY!C6$){?l1?ZozzeYknj&{etuEyS z0TnX!RKBr(ma$Nu$?UhqVgM|AI0bG`lckPYocwQGmWJ$xoXAcJ!EGP-D*>0o&~H7n zPM~4jqvV<)iSn?XbM`>HW;GyoAMfNAo*2?`oq#{P@l}>;9kLkx(vd+6)^ef6F|=%M zm+=2NSgHd3f5CUeqP9o$O!nN#?ZrA(hk;Z#{=UnZB?}6wL7%CTd(cD8))phj`bT_) zppD9Ye%|zbK?OYb2=6_}PLh2*yCDmk)AmW|%Cd&GYm{ya*0Fmsv9t(!-lyM3ay<-2 zqI4_w#fwyR2fM!4SGf_WUts49S1Y|)f#ZHS)1nly1GsY--|kHr zioIOw@LX6NVwTpq2X(i$Qu%H~s80zLtY>XC@K9(CSxiJ!SO)Q!nh*X>C0hu|M+GKN zW(ztv_XG46%f$}lHL4F(>b?F%Bw7r(PnJvE2Bgb&e?1Ven$3gSMJ28M_&;vdN2!OF z!h5fC`hDU0;i_2q2Z|ZojxO{qJmqzAUT_(w_05`sFMT<9PUl9OgLbfq6M-ln)>?uF z^&(uG26b)XlT7yBOs>XSXnYK~G(q>55~r2UUW!=xyl^2H{JUQj8iY^Up&t540t;$~ zs`V2-#PbJqnx|*wHnn;md*Rt0{hgna@5wG9Qe4Y`bi5f`HPYpy*)4KuP#CpL)mB^wcboy&gr!@LXt%6fot+^>LAmpg z9Ym#4OW;|F6+C&8%Lw;}(%{T~bcS-)KI+e7<$JMD8qHYkq(LB3%pc2ev7RpGMGW&z zT!7CQYh5u!rpf%4jXe0=N6VC&Rv1Q#m>h2|FdnJ?z7h zB_V&VUgl<)$?BoZLcbPoNM%DHy2h0&jErxb9&5sgJ{pROk}7T}U%J))6*wbXnlw&E zf!HSZuTZCwjmsK7)uPZ4G~bEOE&;4Xa;u&PSB-;T(-W=6SE0V8xnTAbpfRTZ^cEjA zCK)n`#%B>pT{Qcrk9W7~CeEOIycFM1>pV;sCzr~;O)Kq0`W17cH|V=Sm?(%~7pD&L5P0_L zHhnA@mM+Z>(lno%GnNxIjHK*imNa=$scQf~Y+zq=jIoz9$-J{&YSDd-TEHHN34spC zU*%lg#wUa9#cr9ylED5ZrBNr4BNh{%!C8o(L(yu1&Jz7)P@&O4eNLlfRxB#zG2m); zp?YqXeaM6v&XG1Sy#O2Xw?j*r*Wxv=pkiW9aJiI0Tg#y$t0lcxEEo3?@OwTZo(5e` za)O+UyRIBY)F@Q>!+IIvAJ8WN$r@+E{xJ>zW1@iGf9=H9|F}xgO8X ztDx6meVg}3kawQvS{Sok3%E^usN>|oyhQ{{zZ^p(3-xZbMowe(eL$ufEredRj5H}$ zot=nGyu*#$(U&P1;>caoCQCI@2H{QW2y*f^VM#iOr0xRx$HN;|*W2PrHt8-_KwJmk z&`Bg+-Rx?L-^Y2oN+LP! z{_VWZ1!Hi5MSD@rK)S&ib$Nhe5Bz<*Q~#NkNBm98T=}fAB4DA<-%oyRj zhnRl?iEB5%erTE6tWrvEtbTepPmA4N%|2_9l~Y(=rqR~PujCEuHXP`u;M+WBCvZ{~ zLo4k_Q>uZnBm87)Q3128!g6k>*wq@nmQQE=ln^PWDj%?G)l6!9(Dhu#fNin#=@|G; z;kgpl0u^c^ly}x`R&ino$!4(xL_qS?MY@%BSv8A&oR4X88PI9f50Q@M0eMhz)bgJu zq*ear1LmhDwQL4b)<3I3>4d_c>JZT;=s?8#0L5~>&|Ray30F6=&<1(oV+-~yY zeA>bO6R{i&^VB}Q3HbYHpE)`vKD$H>H{I9Eo+Sjdgata8BweEat#pM3`CK&?7m6SBDJ4${V6a+ zW)zbAQSF9SYJ_-F*~ptFX)RO9dPn>rG>y$N7plJm$#Yn5k&p~R`J>v&w`aK=AbXdT z%k|v7SBmB7aQ~6v;J`XW zfCQOZI?Nqpmh*ZXYjzVDGuhq^-}d5vuv7T~H|4OUhUdiM9nd4uv59E{Q+LQPd0k=n zt5aS8Pt&z`N<5IUx*11fx1h=g9KP7)>GuWu4*tSSsCB(q24xGl!Rn^k8L$D#W}1DKe|nvUSa}*e@1)h7TXf*}{99ST;^#TS9%19MflZ(2KjV^HjRtNE2H!hry z5((yBJu)VC8$2%4ayQhJ>?b)<+N42V!I$_Um?sWKo3N~34aQ=X*bbyLHEdm@5FzZ& zQYv2sXNh@IYRLB-Mb=FNBcuX%94wq*p%V@zN`+PA>Nt^($H5y9L6zt`#QiPfTmjqeM!pbt#&F-pV-z zu(UHf{wn{e_K0;hdN7ouW5k`jubjWS63D&}d~=Y~gF1zEcn-*s{mh?IcxN&4&1~JX z@&>WOZa9qdoBZSbB8?s42ghTs_?{fmW?A_4|A_#(C1rpKfjpVId^5DcC$bK+ zzMA%DUivnmJVW<7d}?4EX~vsSKNQ~zO`d4j)lsamIX+)XWJHQX7E4Vh7T#6nuZ23l zaqExJ@@847RgttFR74Krd!1qZZ$p821%bgs+6Ij2ldQ9NC})9eHX6_rYckumokG)M zU0tlZ$*@3;K&OQnI-#-|v_I>yL)|1swn|NF;TkfCkv68)MX5tNWD5Q3uvFdWEekMO zpJwHYk$bght5hc^D)nMljvaU#v=R^p_pXw)?0(}LT`4z|MyO;hYfO|esbD41{QIa{ zZ6fpXhuQO`lFz?Ap#k12gvLtPX#h@&^;p-)?IKw|a`T~C)$Q7fea#|mCwy!Oj}NOz z0m3&UQ}5v9C#F}<_Bp_?ket0Y*<})%Mm=)ASUTXI1EzP&F1Uv7C1T$2dd(5e1*mRT z_ke+Me%8y!{I(EC{*|%Hc%xgFi$w{JVrOlXe!T!#F99!o?CT8pzLI-u0iO?1`|UmU zumh}C!J}9w#OzQn=rcqO^#Q9)ndY{9jr4+7)6YIMENZ7zb8`IBTUV%G;AMB-I*sp< z%aNy82RUC(r1#OBey)w$2A+N@hw%zTfyp){SCH?{VZ~;J{;BrEN7jkVJhVr;ghp5e z=TvBaf+xy=nMIGU12R^*Zk*rnu&enJEk?b`{g=0#qDL6ndPFaB=HHERJGERd@w$(YkYQiRV%P=U*OA#UrK^oh_`Ucs8%bA zSQ_-;C2kKC^P+nenCDRYn-h$4GDDwct)`t7>Pm2NzfS0WsOK;z&|WB{6c}gA7NBw> z=cG|xiIU?4M93gg({YVH3z78obZeIhv1+Wvf<)15J~g9ra2g%wA{_pG8pBc5)Y%_={x7?SQ`4>#w|d3I^PE zq#HI7Y2akSR*03bN{`iM{EF7S>hJK?7EM4ZcQDRBe42YplesYy^Di5MmGI=boQem$ zMU`51-<1A?+exD$bJ)Wq7I=#xBEnmVrBdrB(&J7ilK%~)j8!DO0n2lXH?Q#!@fGz_ zCML~?iP+qVmNtOZu2SEDV!QP$O^{mXcSe_TyYA$cVs$d&r5#kVZEBWftE3VMeO5zv#7p6~ zw%VdoYE&{t1kkit2c}6a26mIYQwRN8PHs++C3G}~(vzI^h$&HO*rGFmX=&HbeX^`z zWi@cvUZ@k#r&u+yQvWiTtkd_7V01vEh-NriE%wzos-0&RpnXltYw`|jve?BIt$`|u z)c3@RpfyktlU3(w3*R44o4vpF@?x=));ip4V zS+Dpx_R+^@Rv}|fl6Z4apNDF>1%bbMzni&|@SvB6$YEGqq)*y#N>& z<{ELLC2x^cZIG5S0!pnD1S$YeD=uG^igu~-`dW>WM* zDp3pGQit72m{#eX>c)#quku{{oiDtA&n+ z+)n5s3kZ?ht7Lp4ZI0&M8pavml*y)dG?jg26bsmI-T%0k{kvc)F?0&ii|e4mmn0cz zCF4Ecg$zLxgD0&Ud8#JrzkD53o5!AOfDC!mL9|klTEB4h@>{?e_Ou_J;9xLOrADy0~20^uH@HK41Z z*qy-2?!|VKpxPm}66&_H>a*n$EJB-D=QTbI7Ht zNroKd%qM0wjlK!&C(r{%uy^-^Ve3I){mCBD9%%Jf?&m=Xv*!}M^PfzoL#*c`IN$2{ zSIQoVfl_P51=iQQQvU3BV0k+?G#HT%AM~RW$4C_T$MO@ZkX@Xi8SZ>v$;o9gX|H${ zi`Y03d{>aaxgXhUJ+*)D(%F9(D~p##?SiM?hVnOqZ>s~82fQP(DEu6I-;Lz;S3%d4 z&h$UaF*bkqUZ`&x{r9_201+*6CY*q#E<<9uB~gscsm_c^L>$Ztkw##WT2idaY7OLt4>+Ad=^x&Z=qHs8?QsB5|XuB(z5Hh)-t_HXcZ?15T0@o4d zwLW)OaK9R2hqLl5_e=Us!E-;-{p_wAh%H3Fs|OQ3vVeCi&fF|~cc6is;PC|tIi5m2 zDme(wOuF;|mredAmxg4s*v&rnjeSL`k&Ywyb8^)5^3&PrlX5g##BsCd+fub`$9}YrA>ABdJugwCYbTp?j-2x1|S{=XJf=S<{>iz zY|@3?U;2@UyIA1@{~3`9R`orK-+t{jEB%Vt&9~6F=?eo^de!1G>fzB;Z`ESUiErwc zRccu+=!J}4qCLoF(=^RTjwP1wYQg0MZ(YHPtV&`6t3J(V3U_F^$FIeUmQJ)%86NgR z{@&`NLTUIso=4bqk;;YQRE^tHjAgJo%bLKm$~>Vu!>c zNf+27_lUi}t_^Va50pHOP&3+08d^u0INpiD4^iZa%9TkyHdMvfrBp<+)36HqGXI`s zS(LI$%POy7EgSS3dS}Fo+`HayO8?&?$0`K82Ri4sBC!U8@C; zFM8Nr>}VeFONZO%AuDf?6(71^kY$l-QiNBg|`1#T}qtEY8+!^Plh} zb;cx8TeMJbW#yK|GJwU1Q`O@=MlxYt;H^mhZ~eLPtNS8qk&WmvP|1H|LoiPRo^GJE zi1|lkqYU!gxV#;-f`eKp+$?V6a-mC=QDD@spK>EG>kFAtoG>3F_u~Y63c5$0?BKRY z&3V0A(S6xVD%?b+L-I5dx$BaF10R%11v`5hU()4J{XJlEyUs_(oFrZR{~b809DDf# zjtE%U7(BsDKk$t}Z4pnu$4V>?Jt2e*2>!Kd{@vWH%Goy=smxU+R6USaxa&0n^FUhk z8Yz-0ZzuX2aAm)A$ZFpxo84=!m|b@vFPpTV{OC3rfX)YGia5x6r1J~fFFQ07td|47 z6!>Rclho52vlS|$6A1UICH`@sXA)qU>_&EutNDE_YNwL>rfI>{z|#_?Qw=A{G(7q* zPsE5$NS&U^Xckl5iAOh)Ijq9QO8#vF-WT~Y=#(1x+6J9(WnT?=^-7_mRg9NZ+pTGg zVNv+R=0Nx3P(T<;!D7k{$jh`?KP;;cGY8Eb(B2>{4N%%i?3L=+ay&6!-B8AMbPpmk zLZ6}aP!kOZG_umi8P{UmS8*;46Qy81gLYDde$E?)iC92_(IekvzYhpEBXf6ZnQY~D-lXUH5^chE zog&Z6U)*(Y_)4T^rk02P&*7utraSC%IZZ9=*Q}5UGNSp6xu2cZOSHQKdYDtol**Qc zV%1A4m=zmfa1pd&F&$PDa)!H+*^=q0$4>w0Z-Q!@80BmcMqMJQA+mf!ot$#1z;_Hu z*+LwA4|)(jC&oQXsCJF~@9_taJjM8ojjJt!aTV|#aKzSfdpb%VU^XuwiN$~20CcJ) zk?%iHY|~N)#Z`r|yCc`4s4QkRsH|2x*)F;m*jdLM>VImdSmfs(;G3Z}s=*RJ0EHdz z+n~kRAcwuarS+WUrApR3H_W^6E?6GaBA#D@Bv}kCE(RNuF42uh8n_@2+kkc`?81YIGip<9pj(?`w6B(&>z|Ay)B1Arm6O^Y!d3pXk&s_&!_e zSlq0evU%5evC-^$>;KFL*Vem0O_bL3A!M*1SckP~R1- zG-y1h)U!O<$oH0sa0(Es$IjBB+x1QMF^V?PuXn>|ajf$MY8d5dF1z^-QYl-lT0^-G zf)8#T#6gxr^A>$|x&A3cgaoS^AU|d44m~Zzq)Z5c)(Fy3_Z;Lyi8&pPn30 zR|R;@I2EhokPr84f;P6WBeU$460=}*-4qo119FeED85;;OXrhuZa08o&Yct$tVPyo zD4n?r{AR|!Qo1-Nt-@-De$Vev_Weg*!-UfScG8iQCg_9WzvM!?v%@j1e%bC4@Bcq?9`$GNl|4BLz zFfFSq@f%yBCPt0D_o%2*neQG8iWLi1Y+wUaY}l|Nf(kRhz|f|5nBM!$_uezT_uhx< zrkUMjH`!#9-G8#lCjZ|d4-YWkH}8APY4_Yyi-DYiDYL#z!ReVHi)4ICPe#6-g1n&T zvhHLbNIYQis>#I)t=Ek+9ZLq zk7y!dv&s2^`nJQb)&oSf7D~42l!}J@ww7XPS;FUkc+aXnmv}U)bD*p~sPlMy3ynPA zgQh_yuuSs%9OTZ)V%d-tTCN??+nZph4?LS??-pRt2MztinJ*ft4PL0htcAKbqE>bZ z^@Qb*!5DhT0mjK~FRh=fP9O^sF+oqRPpl2Sm->}9$zQ>#8Ek61i_G0lPbGlCV6E~IvEE`>pet=Ye z&=;}SYCYF))=0FNSV`6r?x~8c;8lGC8D%oBmy_gxFU8AwDSN6!$`jL~Z|a2-C^Q?G zO|}aN6IXhacJtV;byKoB-38oq&+sn-oj28s_A&sJ%K3SWSm3ZEK-UBAi7@dJcmO(GbAY!EKY3Uz0<$68GQ-#`B%ajCAnq^Va5gs}{tXE_GD%O8x zEn||x2*iN-!+bLBiL(!E%)&F!imsY(b^>k*dImnDSt!K%Zs0LiI;Qeuqn-%dH+$=n zcqcgTK}#h2T4v!4tIJ%=Psh8x;Qs(RfZ3q8LVq>T#{@ECr)(FCOO6xn$ht>*kxuCS zSXnFhJS}VITThB%p8O@Jy>(Z?mgsJC`QXt!Za0&Sd9>#P++r0w=#(JIu1Nae4AVSk z;0&`DW=Rs&Qa)EAtbn9t1W z^kkO|O9XmF9h&z&(B~DJ>6LX-+gdNx*Py$zm3fuiM`k3;Lk>B(Veq|$CrK2i6g&)6hV^xD zoF1`TJ^`lRgn#3k;H5F8!#U61;I`>*A18tCg3G=_7x_crff`X3UGL z?o#H{-vu_!zS4x=(&~W%k;P3yq@yN>HpnKhSj(R%B?rcpV7G4y^~k7Pg%n34V8j{h z&D4ovfB2M%<{d)AHJLUcKZ~%simmd(EwyrPM2#jfOFl9*Hk1ZDJJ{V)ECfT+hMZe~ zjgsscV6Xxmq!YMe5!8O6{{fWJ#rH(U0av2c^mERnaxjq26Su+ZMK0j>-6kVwhdoHz zQt&u|q_e7Sm&!DpZk1bwna$d>6ibfT)|**Pv~EKlJm}{~OghNapFUTrarF5o!}r2iq!g5=Si^%wEi? zMc++@Bap%4%!0j=Q?3Vne^4{f-B=rRj3$WH#=KtnaNq(?g`53%G{9>719;#kvPJgl ztD#lUP?O(@Z21nh?HxQ*i|yiC{Xo;yV)I%RO&LqdvoZu{(Pt0c2_K+73SZpkXAB~G z(fqBPK8@hjs=ut$bR@_&G4E=lnip`jif_;oKABaQ3I$k`_ev&mxQQKA%Y$KjilIao zW|V%ks~9EML8z?E3bx`|HT#=+3VO6yt&7+Ux-J%HW*CLH?t@!%q+8z8G5!>)W%^sM zmN8ka|LLz4ZuhK#sAu%UeRvbvb-p)E^E3VSqoh> zgL!&~0KGdUUE-uTJOGWW7V#USBa2_C)i7_iyH?2npVZ=U(X5m>Ypv$b^Q`*}SA;C; zMoKn=vt#uEFq=mA?K&Vdj7{SKYzuZS4#<1VTMq}AUKRLjxD#`}@Y_9{|CK!p&sieF^SwO*K@3i)ZyC%g4>s@^PN zeLTPhOJ=e8r%&~2zKSQ_3<^Lw3KI2h1+y?KT%IQ+fovK1^#R6=7zw%t7w#ttfJ57gV z8(g{(`s$Tx_ZNpHHKeSy2OO_cFZJ9rr?C8WqdCwyQsF&(eb&hiTE{}feHyG7bpPS1 z@CaKR;jcRG9~L91PywAjXUR*7Ibs#%UJ)y$Ba zH%KyXSVsS(qD|=KoKB;x-Ev~cY*5R5MFY|Q_V%+?J{i`N0;{>U+~Y4rW=1iG^@O3? zC-mRIAEI@{YNpYBkX4i_eoD!Eej`X-|?4VTg>Ma%If^s3z;K7wL)fETMWi_~WY zBVu{SR#&YT30vs*Lscm{ZFD7#+)`JuWAmW40~5>OoR$Ubx*sSs3Kb;8Zt7MCbxQj6 z2Uw^qzU3g#p2#^e4V_Nv1h=XCWnDx&`O=%A%>lVpC+Fum@_}VkW5b_f6>ZmT#McWyN4hra z<2tF-nieAa`MwEReAvCrn%6L@MVJN5IVzkId|KhL$0`*pkT#rW+=kPHsO|u-8c}U~ z)UqMd(Gdor@-E<6z)vOQR))1v{}l9VCRA02bi%U3{EIYBhlt8HIiCj>wn>#Qkh{Qk zzpiw4Q?JnwbJwyWYJ$Q=t@=h?jc(C-0g+H2T#G3B7pT?Zq_M0hb`xw`R+k)) z1*`61O;gA+%P%8SpHnYE|JS`8RH9?ngYm^!YLc)oec)c_zS9piHbUv|g)?N0pM(N4 z^foA>2Dq*Q3Zq5`XiERg*u;Ywq_XNmTC45g28$*$TJ87_G}bxhJscwX0LWb|mpP-% zL3u_Wk!U>wsWL<^X$A7~Q%-6sj&a6X9!?JsFpH^!Hd19|&>M=o0Ba%Ed@j z4Qqn00Y9V2>>2JWy~w38wTwH<9qQ%`*bGgO!xK({*NMZ2?yICiOOOGUwSAJ?7?_@B zUaUH>@RrOMi}%_D?~cGJ<6M@G)9HE76%;58EI{|q!`w`=Qe(Ex5Lv?`y zFY_gQXI{)Onj7(x;P*i-mj4NEfu61Y+Z6A$${rv)i)@? z&j-3zMPUX?pO6pvYv+$~VhzulMa5*w7R3%Ko3R~|eFk3?fp2O@YPa;WmLCJFQZ+4V zL~2+wzFvuCt<%!Qo2F?_LKS}gS@OfsxR95`-lqi%r`m1082$MUsxdt*c6YO?q?a&G zm)yY|wfGFiu*ZGvV3MeKP}mVmWhu-=Aj*bDAg7=Co~ zf4sB;gFm^Xz_RzvmxHfT_W)U{>VmVcz2y{IEvY6TG62L*!&Z|-o?J9`lX27 z`9f7*M(c9XI>?MCu*X=j%tmx3d0Pa?_S#OxqAuSAMh?Dc)o()^{5qcPfo@iKqRQxY zwh_5}8T3{lLb`cQ`!1)r^ z_aggkk;nXxq5DOQWvd<0v*97DVodamY(`eRFQ&`2f{|XV5Z3j|;^Dj1YGl-LO5DPm zg_9VJy&`r$`qtYyS1MWH_|3wY+Vn|qE}ffE_TxT-R`OeFNMMBkLx>-7KdQwK|Es}(_nw#)wTFmfjiilxUiV;;lFl)+6a%>933sF|HSNvyKhje%vS4D0-0 zms)ogDxJD?9T2N^PxlX`->K}`YN4em5h>yA=sGn*b{D*RE-O5ppR(1go4cjl<*_2{ zlVG(4D0Lv|hWS3MO{`=W+R|pQUBkIpr0;M8{!?%>^K`0JRjY=wEOx;%MZaSAJ!pU{ zHA^irZVcL7N*%~IfLJNxUZUe@B;1yPURbW?TyN4HXwrXhW-AKxA#~(BfSpCMU!e0s zmFNPO5iQq85fb+AfhhKHt&_B)`0Edbg-%j%mt}HAvO}X4tC;ny)*qF^ zkHSUDehcJMcKqj{1UL|D&UhWjlp$vs>(k7TA3O)_3uGyj&e*7jUPJ;?^j>)6HJ8ST zQUTt+3r+pbpN!`-U+lC@vjBRi!nXaazuqOQMN?W9(mqBlg13I|sUqOb?%e^8 zP0N1x2VbaKe8VtM8w;?gs|RnJk!WULb7(!5BYCd2g_3x0fa+2I=BtH!1Jc+kH_UOX zwi-kh!90WRwTpAvAUMVD=d@Ll#ImOA82xSyfb|e_{>5!Z2HffFcJUJXqH=|l@-8)( z$(_v5x7~f5tXNsWORrjXLw%j~p-$H@TE!jc!xPBrCTL<@XV5D1`S+iF8jjXzk&pd_ zXjH_(@LZlwqoG=W?`6Uq_~62jUc-ME-R1 ziJrPHM;f5CBdq5e8DP&?t>OB^au6-(Apg@z2dS?^9vpXqKDX_-PFy3NKFks6#-l7S2Gs51bllC2+i^|P?l{$q(fg*tG;ZW z!cNw51P#k#%bMihU5a|erU2Hg>ZnC|P{$F>RB{&pTR@}}7~a9D-{B6z14N9#!|hPe zG;i*gao+kV_uVNzTg~24sP-Tjvsl=5x)A78>gn#FFOuUKZ3hi*7s&{JUk1_5_hKFLV*7M-h@l z^&G}qgq*?x2j&L!7+{s7&-u%VOs?nM8g`$`zgcSX|6(OS4?SyAQn>SXfi27L!$Jpq zw(@PCULC5{G-zx;GnOlrxZ%kO;MxG}4E8_AZac$g>enHoKNqv~nr2oEF31+uaiv>` ze~;Q!w<9B#x=Y0J4J$&K5~DxArNUE7LRPTOA=f3_6|E5eVU+CRnNNkfEsWHs_qjb_ zCGuLd^b0M=hTzP1=uy z7^Qy95NF}nlBx?iEp7liEl@}&=Tg3|lnNl&tW@>U-v#;du38q)0Dkr^ZY9Ee8Cs3h z=!6f;p%;Z-X1v)J$s5)@cS^4w9@%o8o`93{P)1+4SLSqfkeG!r$TdA(sCM^2bUI1brfyy`RXA4%!N1^ayusDcj#Hm5Y#9~e5Ig5%kO+7&m z!h^N=3|6u8b}+dm)B&fI^EA~WfLSw?$el`Kf@6Tj@X`B^$=qJKlbO1a?Uzd;_Kg&G zivGe|7g6&%w`+zEvto;x&O_$E$p6E-UWVE0EVkiJnO6GRi|P4Yx?DcdQv>3`*w4#u z9~?I*R|2&JIgx+s`P?eU>CN6eEmvuyVO#`wljRLWL#~c<^&@UJ*vi4kchstO7lOlb z*@V4kSl5Czvdf|Ba%d+SD@!-<C5=T z8hD~#e+rfE=BE*4*i*>;1bn;qOS0UotCYGPtn-v0R(m8mOr2`xOeZ$oDiT?SPXe?@ zO=#N<;}!yADm=iIqjodUe00GXv|pZtZi-mtIJUhy?7jGt8R?3^D)0Qp&-YX)pAoNPC1n~(?UyRn`bWP9>smK4Td(QmTEVI|Qfn$vqh-jq z2=ycw&Gc`xVNY?|&tl6P#T)o}=nYPXWl-d}S}eq`@tl8#M6=8|{Qt!MEW#6#Mz(Ge zR?KHzB=%^-bsij3BGNT63wHh4qT_fLh$}N;$KeZ?%{GJ?^K4>|54yGmws!G;Vh@Py-%dR%;PO zl_9bps43Eo46wMzT3|APp8dAVmfftXM$Y8zv+x}bqr<%Ah^_`w&0ua3c>Ne^v-)Ne zP~kYcv`TbVYm?{(<{Ojeq*)&cj+1J>S2<76{+OW!eXYe|=jOX&WLE}K;7(^*Vfaze z0yr)8O5~1p${5j;kTg`hLXK>Q>S}-{QM}To@yL$|SE{?=rc%Av{%8N+`b6!3`=X&` z^C_mYt05@A#1(QoALdrj?SJL7m?Ite_=PsXf0kkQzjcwWNgK(Yd6;#4sCgla|Aiwt zWB(52nO;6C8CaPcIpYG(VcXH?a9WQ-9bcz#qD1W z6rDHtoyR=!toALaCc{;z*WaKWj?yOD?`}7$RJp+Nfd}?tt4RQL;}r>b3rS zRoTK>RRETz{MSf;J^p*(Vx{K77q{ya-;@L4WcUH?RmeI<8pic3Hf!w-Cr}YX?4L!9R3iJ+5nat^z5>6_^d9|FZA1b{L^aWoF-NHY> z@d;^xS301z4)Br2cNPf{uLH>NSYHi?b3RHr*v8hU7Qr~{t*6Tjl)MD_f3933f32I; z^P$^*IK%Sij|Y!~+&d01SByL)bVy`Bd$AjR#uzu~qsW!d^iS0w-E!VC!_1 z&-z}M7T?3zPXhNtz7ad6{0BAMBah4e~qJnZ*h_Ww~CWsqkj13__24l`erkF8F7D<|I^-gM57&ejAVx z<`;CGX{gmIy<5iMplo9Y?>mb|H!nzq3X*9QzWV}vmg+irN72AKdQE>gm%Uo#-5glW(|RSlK-NhzPvE0urDf8@ z`4^|tZlU01LpJ^b9droI>|AJn5md51=x66$^y#%~BqL~PCA{^de^FYHzXkZb$z9Ys zFl+wym-ucccqw-LtMoQTilj=Xn$OElvU6D#cX#PVlbzwT7?LD3;{B|002q=x=%(P? z8pd1)Osar>z26>`aR2OKA4JH=yUsi`KS#e)eTE1Oy9IaQp>sg26OF(u4qIHFTC+z8JGhdWvKcBzmy?iummu9MY$BHwlU z;PeK)6&X?hH(Y`ArPdSp-|k0PH<3!(PH%*B(7RHDFEm7iOou$m8nA9+k*p6!@DZ|E*0F+Gos>-E$!oGl z>Owo@W%jgO?t#`SurKF?cd`4OSaOcqj(r?(Ymw#B=VNuiZ45|yyN8@-QvFuEH*WcJw(t`)uK@q zdTr3Fuo(Wyoh0=>tWk0$t8UR+o*I-qR<}*dumfd74OXu_1^R77=2na4ll=_8A@x{< z3bXPIt5PtR%c)H@V<=-Y@O;8A;*@CSrt+;z=Oj8E`k`|r8o^F?CabahtQ7WV&@N$y zO84*I2{kRIcy1E(IJSuwPqsXo9M&mn@Cq3SKnY(uT4)uN zG>_Q>=p+t#j^!SnA4l_`0xj6!#t+`^aA!kBC+okscj4!D=APh2Hwd+~<4HM-H->>& zANVVm5A=WGsyg^#F_7rf<>2xfiDLDyc`PaZCH6_2kS@UnHOgG!TX$l0uxv8! z9nS3A6L7}2#D$;k2jnaungmC2H)dsTLU+r+U^AzFhWsK-vaTLUkY>8N%|4qTGs7qi|rZD*}*QU^q`rI4Ey`6ir2 zcz1m&FvqUpUIE`fBjYq3JLbO!k02A$`Ttc@B+K=ggMgBS>+=U>x}66gD! z*T=97Sp~{R zG{FI1>wdJDROl5?GkCiJtp1Nr1gB$Kj3#70nh7ZY?`E~`a@b1Yo=HtdMrH9Elg)Fw zUDKetN&NsEcS#$xII0%`Ijc+qCxh`!_=5^A$UEXyq?tD##-BFCIA-%Pua!kZOdt;e z{w;;B&33jHEKDerB3tp?rMog7b6p6fPV%%$Ce;f?hK%9xyT9> zc6sb0-lROyRMZGA(**63-#aV;!9(bDsd^#QaSB*k4xYkDpfoTY#V(%E`=GWAt*ZN2 z{^pk=8Cffl(EsLA*uySe0wnip45KB2f2)P*@vf}bJmyLV5>)Aswc!<yt^4#%u^KFNl?naceVoyMGr~`*Rpmx!c@gc63avz5wr{}ogn=7wZ`_br;Jd)21DUJ%+`tanE}kL)^8(L^=zP9 zs^))eX4jiIXPQMq@xbkBi4lvf|G#jP+C3>xzlWrlKKgWlR>}f)^`dTu9_hOWJ!}&+ zXeh*}WeSbi>I_#%EZl85Wmea%2|Pr(hj`|QkgEwyE73iyHwZpqo#W4$dIV^gH+jEq zkUH!U>F8Y3JdY3R=vx!~ZX8*022?db=hrA^ITMMw397XiUc7ifWELB@)hOv8zQQ8m zhzy5kO65-Nlrl+%#&YCSPSJR|Ra>xhPfC_ph4De+jmTdFzm2Y1hQaj%@)4Y6Uf^1> zP8*|=$M;=G6e?3od$5^j>9n91kqeyc~;t5?jYxZP1lRT85#+eOj-v z{w5&b3cPP5BP2tuH|KYOeH7!q4Fr?G@m3_v4*wbQ$ozZu-z zt{Z}G-OAmu-8CRN<6N6w>7p2WT+@*_S7S@8Ap)&iPJrt>IE!{kJ2o*+UKvnoTEN4t z+6Tl&<)4~9dLDbgio;yQ6=D^^^QOb>!u-DTp~qfq6xdLug#C9b(pTrPZ?hBb2X^^T z;y8OR0;_NHy8^NJ;!*uB?@R)b2gEAOUZay>5U8_KIzIRsq1!1qqQWyYX|a5F^V=C@ z>LQ(DH4}O)Fdkzxs+$2PPB?UwF{!4n;$AWP?U-0(m_^vHcNX_f{StYQIdZTfS(FFa zpX_s#ULt)^-PLg2ZAy$55{B*{Viu!JUyGJ-obJH0Frus$S(*iPapTed!2)BpFD#>E zd|#`B$Xn~#b+ZaqXeG!03#h9GeB2Mc{9}HLG&eGI?u1k#9gyks)X|SCvghsfc zNgflkg-6Ky5pa5Mj2gwb*xbBrM^L(De??^#^(u&zd5I2#(?$eplP zFP2_tX9O-MZ-D3K81v8G>dYO0*52XP(a1Mget}RfR6inhNa;toUy-Gu*MxTRKapTa z!so+Oc~`P~z>oQ_W1U4<(>VpH@*Mn2H$C>>48*c^SQn%5X9_it*qc=Y3$VPzh*4E8 z?^M9A?ZBMs>~Qu{-7x=J zHRa&Rdn7AVtLM9Dmnp;+Nk4Gj>`TC28QR%tXy15@f_UJ+m>kwRG=P303F&f$ycmG< zr&(*DCLb)1Z&IvA;wG^3OIIqx+Qb^jPbTU*hh6^B55u#4V5%Ovr)H;|2u&udMgJ#o z_e@6bL&IcWEp7?eiE*7;2TegUYFSa8ND+%lCr1gIBI81b;QlqsVcEdj8tQHzThgj&A_s~u2Dn^quw2hk|wp~sJuhewL%?!G z4x`8!lW^mclFe`Xu}18tch8LM605@Tu2%D%*PEdG z4tat3tV&cTT3i^OjnNl?HqoUr1E+SOj6SPSM$?#Im;Fi*cHL4FQ(^upz5ErX=1mDpfwY*2G8;aGM`MQ9_3_HC} zz`G>rnGws?w44Tfo76*O^zVYjT%O_1CD^6>?;<8-im}nzn9H(vD;Z@<53Bh$C!Kj% z`haJzT4d|D$PuEBWK^fmuUV0%2kic_$vuy~>-#n)p4b z*WJu}lTuek--3QD?qf*ef#3u?i9*9B+fE17bk9lX>_yoqS4fNeJbWS0GP~|sVAk{k z5BC$j1%?hlN4=bb1B?*^1P_YkA={~EerUQMFb+L|fH&2O!0vwbJ}${x27Mi1*G-IQ zb^4Kg;ItW@wz#6lu?0uNTU5iqW{DPob)6@J70gOi3(X2O@eUaY?6F_yr+SF__F&6L!YSIhxk)W{anuue8&X zZUl-)Zxp)=O=}D1hgrjCG|An_9F_~z#7(ftTgG0xQrQyA$ut!GG&50Wo7<9U-Iv0p zJ2aL`ix)74d4`w3FWZ@Wm~78%sjb<{JPShCg;Ss=i@-@>$NjojTZ9aH`HB_fLL*UN zi_COy_7g?Re9FJgYQkQT|LdrAZ{8p4lbGn^WejTBk2ModDD+3QS0X+zcW@35VyPzj4r|o3-vxbjQa`Lp($Swb!GlrUo4gbwr>Oyt#${b5xIbbS zCL34Gew%@Hyb14kJ@AfJYP9g(oZbY~hlwXpZVa8kc89M7IvrZg_!CfoRdUE8!{TG@ zCBpX}}Vomxp#g3MXSZgmSF{0R9z?1jfa>P7YvUI0HN;hw5~@ z(Dz$6@kAw5dps-tn0qF5mVtabw&hXaUqVLZptqbr%bc8pvNm%D69>d6Vz;4FJI(D@ zhIO3v^vRQadK(U}gGNniR)CiWv}td7`%PX>RQFJY2wIa z8%IN(2i=W2i&+oM#Cy2@q3RhBbkK86aJV3$K=q+1{54t^TYK}|#^ZC8)S)N%*{xDs0g zv0q(!m)|0PhVLr^lW<*uN$U6Y3@P&WLKE%C(^9qign`wr^I<5eSILc$d?-DWof5OG zugGETf$xdKhik|53if3Axz>H5PviYroI3epF%*r+-*GgfDLAcA;?X?~CwD@>Vg8tv zV_H{mE9yfU^nt5a$)+A)q=WLjH@_>l6DTfES~;b&p{YV>W#6Ns8lG!w}+aq$ZmBk~=ws4}q6j+Q|IyadI zpq#a=V3bp+OHP$XxsNG14!yxyy1*@Q!`OjeV}!H0OIVy{F0dYuS}3UvD0ZsVdToL$ zh=k!cVYN7f6=FPIs8O0E4H~1CBi0~vSVJSiBdTUGO9lTi&{;K9yhTcZR#?XL$3dq) z0e#X%L?a!(E?v$z7PsDm-nQK*0+BJb2pH;~fawbvc^`N$0)o#332IVRM_#rp6> zs7>y7R2~3x+vNvxBld=4n%u-v%%SWjJr?D<-1T)0;fZ)bbyU( ze5PCN{_am;&RQ9S-us#5I$z5Zcgoku!YlN{qw?T2M=rh2NeXcWS&}OkYNjN*D72z+ zxM5K3hC$UH9Z}N_`Z!;@h+^o}otnb_=;#1FTl`xZoR$Fx9QJ?p`2T@cCOahi2#j4D zdIpR%^QW47^t5(DF}G@L#K~~Yh+>Z??vfm5qF0GIb%q&kkyxaEz;756X~8Xu5z%|$ z!`GqhDsHyt@cbFbhzFQG9UnwByfH1Kj9#I;nQxZ7RqETYe)ES}T_&qGTEvWfuG(3> z2L=Aq#D$E2offrIl6Yq%_j>J;d*nzcRXcPo5)R9gCZIW|vAz_yjeS&szm43Y&Awu{ zq8NVHhrMB1ZeexR*eI&?i}0Kbp(~Mt$}KUP_f@|mGx7>MHor_QG>SbB=;c5m9ndv( zy`k43_R#L0bNBfPp54u=Miozf1o01G>T7yGT0cZOsc1gy`D;Wo5F+Q$Rm*zTKxI&R z2>e)7pMq|w7>qnLzpvRK2DK9Y+b7oBHG`S^fR$BHxI?X{S3Fc7>F(qtyN=WHA-%5ipfTFZ-NN!mERNW` z*>)POm0F&y0-rOSBl~3^_J{#^Gh5#?%Y@h+C6d)n3SG$LFAt?pO1@eKzWa6Doe!=H!3la!s1|*q6M8O|eeP8`670aT_ZgCN z8#cWb*@@ozOE<#K``JMctDWLptkol&8gpn!W~FLkEgk4|Nj$M1-fI=B=l?1?=BONI zm1gZPWTYyndV+cH)k>icUPK&Nnu9J6)V(jJPmO3;IG0?SywCYkD_9VqEaHg8Ea5*A zi#jntu|csmyYo>=pB0Z0 zYcPr&C}i$*=r2i2cxIHjZVBy@KZbwdy0~9u@_D^aRB~yMLzBFB1FNF{3vYVP3%?`y0UY4?t%GE{YDVg8Lt) zIsldL;L;yBr=O7fid6|rU&(nhNC&Jt;F4iJ8+TbGNs(F~;LEAHHA;hTVHJz_LJ39;Jr=SYs1!rBw_0!ju8x4{roANCF;T|bWDoMLgtzWj+O*tWN%Xe zkEnuDNp;!aVLSxCGwZ0l9X?yi)q|fYeJ2?3`1rx)PTd7949N~SCDUKQlM}jy9So8~ zZ7`;fNT^q4(B6o3kqTk;k^%PjN*w&~COdx_8`};j_akD|yYyOJ((D0I?$dLWsVTe!WJPhhBkyr+E8V z&M#1_z zmB$&Wj=$w%zQEh1nuwNC*7k-o57Z{OsF52~pWf+q>Zzfx_-;Ud<%(F@g^r97O=0{! zuE-VDt(F2AMy6P-+(}^L0P8-6kuB5PYPj_=Yb)>L^8$`Oek@juuz8MImUXfI=p49hUj*z;f`B#=420sTH6@$eb={xlzqN$%)FS z??AtU;Lf}Z$~vs>b}skrG9Q*&_OV|QfIM@AkEh&K<&R`5q0 zypXlWDMgnE-sr*-VYXd6y&8Z#mQ!$40bh)uudNEM5=Bp4fmQoHJZ(ezJ^#JX0k@m6 zQ_xS+^j&9K`yRaj$m7F9@|nlrBD}Hn+RfVHqzjp_3H$XnY>5@J8$Ih*VACQ^$b%C& zi!LP2%CcaJWGUXHEqHO(@Yyu)VpdD$k=x;^eo2OxRW*n_`nx0O!`An)4jE4s09lA< zwO_8{w2FfRcQJmfYzJ#~x{Ncf8!hM_!5^x`Rl698qI*~~ccxxuN`l6E@Y28LXQcvf zWIk9SqV=c*>w=rwSi=Zw*`p`>jf^)b#OOiIH_Ka47WQp+o5AmUpup$Be1qCu#_F#= zp?N^O8qfO)@-i`QIbywo@nXnBp;ypT{^6~fNEK3MuYMMa$8(RR5BXGyWvd>ZPSAV# zw8ahTglypcOy)gUBF2DG!v-0-jvIF`7^iOm5|AFF#Q!xzw`K>Zg|efyR`>Bd9ba{j zHB=~3JB;1J8mOZJZu)f%XTMeP8)oNinx*~vS0Cl+a_g>@J!+Xy53#~-*{A!ElKa)m z$yyKAhQPRGSN!DYj1T)FS&JQJRzB4n!Q!nRu1V&pj*!KuC;L(xL*^t;wh?cF=SwUf z?rPu2xCzYj20KjG<2V6&fBDxjs+k`N4wWrdeB zR-I9=ne2>;P2hB=JVJa{h2$cUJkbF}8z3Wp?GnKJsQwuY^|9V4pnW*BTRYG! z1xtwadoVndYY7l*Q>tP~F&LebS!|&3@bNO>AE70Tv{G(krHPtGg&xb1B(q$T!8&m$ z$j};J=-%W!n-K02U^k3^k4`!oLNffnP%*sJDRo37BxyX~RI%eJV9(Bx1@-)HlUQ%C zGJRStn{*A>E=AAT0pvS1RUVX+fN_>T1FMVa*=g)KMyLv;S@2Z_T+}Ix*!PgeayAUO zJrXPb?eZAqDWs5{`gua$8_>btIln}#vOtv%z%hUKh0J@d3`?p|=UMxGYQX`x=I=n37efv(b7tQT3%GDdQOPsocUV+y%6AzQ%gwYor? zb)V!Q&xrleD6u*YB}&aoXlPRMbSK=B3O`x|!AwNF42o$)7V%QWJKZuMTXmUNcw-!@ z&VnYWMu6?68X1pN2QtL^g0*vGMB>=ra<>TSgMSo0>H(9byh#^#--=eZ65ZiEx66;S zrr-NY$w#^nErkwL%xO<$Tq@&y308{X`1g2gkKMlDkOY|rG?L{a|Gi)wO{B?fhQ9X* zl{Z)e`8BNhDc{bT|L$VpP2%@`xBOH8u?pQ!`%L)Xw8jNssg-@tK~EOBHK)V!j+%bO z?Fou%5FpEK#d^08Ss^p}2=~#;@KRXrw(4eRW#Z`fJ?wQBO!dhM^|~JTP0DXwI#vcw zQjLUaA18BpNX>6%ne%kJVMk=6a*|u5cncIX2zTYdk7dZ{Ls0hd)Eu0n?&uLth++7o ziIrH!cfIV=8NGq?xly^n6*K1BXY&?%NNYnkgr zS~|0qJugeBPHHDT&jdAUew9bD8`G0JcofU>9mGjwao27NTg{~f%<+M5Bm;;@cJMw5 zWY0!FH2>2Ia1@JexJ*ho71qE5(a7*f`JLOXmB26^IMK^ThV*^CMJmXBve-L|bsW=j za)7Ny=PA;zSAkoqokQo>NT+6MH2ZGm8>Uh4@}Z56Y9Ee(iRK=!=ITGl}`i?NB0 zX+F227$n~vXaJUvo2Gx|1bGpRX2@9)JC(kzP}tevW)2FVZVp%{7Xck}oY8ZHczB)1 zNsp`p&U5Imll(adtY+m2@Qck^tOgp@0{QJU|8JLRJq#vmu~1^C$JX*YN4zz(S0HVA zKG4|%e9Ve(mb?LZ#Qk^BudniX2pd#|cqBldTD`u#@j|VOn#f6Xj@}a7jRshZtyl4nH!C^_PkzlD+0a)THq+x>BHxn-0~MB^$Ohuob9A)16$Td>(;=kp%>vX*#o}Xz*w_H0o@|mLncH& zn7B@sp*x~0_z37VfWPV7A{N=fX$qBoj`ooNWLC2OH=wf=y#yQNB45M!onZDS;I_x) zA8?xh>UCNL&)>>@q(+ppzmBn&`jy;aR>D77aX2VWO8HM z8L(GBg+En}^$kkKHI#_1AU$gF5Rc0Om&v07}HyA=8|E)c?fOxu3VJx+gW9kswbf zHA9c`H<1@eamDAt`Qxlgisj{5ZL{5K9?cTgY4zsEz{mvgC1pxg6l6t`nq`Y_H){90 zDXm~m>|fkX2K7;-i`5tHK-d0MyBK?po$p1PGAX^E5ldMAtfW9c+;v!yRiQWy8k~_j zC03i&-N_E<3tTq{1u7n3F z@ad3U?BCM!@El+j)h$#$?d7EI*A4Et`MFTsTAmzK{5`#i`yjS?M;94n<1qBT8>{7Dh zas77}#_pdbr@}957=M1~K8<4+Piv;Ldu9b5fz`4TZYb1bVE5NhF_@TwS1fAnNg&q^ z4_S=&RN&><&Z%3&osI1}@te4f%{LzK8Xds`*mpQLBDJQv4>Z(;R1ZrFwrcXHy&WIV2VC zNvQL8{s-*#dQFf3Tb7M+VZQRhos)!i<~D7zI8> zkU4syyoFS|&liMhbRNFm=jKC4R13<|UiR1;jNxO<9(=#-6S;$}^*7!lJEquu2A|IXR$01~Ow<5~UBww(uf@8LRWI?s4&Q|Qx(u3Y zW8Y~)Uq;y`rs;N2xv5v<;hH)y5N4H$Xta0vA)aw+G6Fk>KH)a&CV!(hT5Qo7*DfQn zS+_H{_48T^_hNbQ7CU8H*{JS=y2e?l)g3;>80}EWlpfSXGWSOC`bN8W;C?{MkrBkj-!e%p5|(Pxk_c@QOCbH49oiSN4h~k8$@-d{ z?pnYOJ|Xd}#7>`S=yQ%4MzJq{C!i*gSfsU8u=835Mca+_6HYYC3F*aVItfRdg#B=w z`3Ln!Qq5}cqcK~x<}oi3qu`{KJs*%UT^yR`y9pu#N08lC)nOb>JWDKo)@mh``B$*a zTg73boY8E*3s$LX>|>yl1hI-F#^Ij3Toe1x10oaH7*fcAoy2ZtXUgvI4Eg40xO$?Z z$s=vse4*4LTN)YTUBwmiGAMV0@p-x{%=ySFwnFRKxR}5lZ zoMc~v>~j;iwR+;Ovz}3X6?xW+Ws|!EpPvn^=P>aDKw&@^x)gBuFy}Q^1Fey#Ld%Jq zH?GJNt6DgrSy)!5ob~TjPGBmR_%Bcc7Aa8uy@GHTAX=dNdT1UWx z)zTt5gSSVSvzVXv%Hu#_t1MuC{Kmx8c=q~T{Zz3yaqqD>jCeSxT`r*W>Ik_1oBPO7 zmzxz0A`7hp-f>tQ>!2Fw3Qp=$`Z+LOBf6$}fv44K*Y_dYuw${-2yF#RJ2eMvT9t(% zvF_4q;l(wf#jMJ*vd9`lgRnZL*fVsNyH&4j10QXg1g?txGM>QNDrlNA$C+vN0rLhv z3y!Vc0bV_|dbbJkiA%wwnyaI70yavEhO-<;i`C7g$3>c}kWK!5ZDfay>@f)FX(Lu? zJVQsg)e%2{?olO6+<=_NF2<0t7agVK7jzT5=mP^Stj%=Id91IW*++CcSTA5@CCH~y zG#jeHNi%mplf_HaPF~BTZ(u#i=r#w{?lx6Or+W4?tr1AB8l6&BAmm2?fdM*ULTymc zLHMF41Vq3reTJaVI=*=fIbG?f`s`{My~I%g!?gyTKx(sGjW(C6)bf&C_LQr8#AHz( zvhR7|l}YWUX})=%J>BQ5|M9oz4we~lBRbIo>~05VcWxqfzn)-zU9toOGgVcZq4? zSe2UTYNM95!9->jIo-2-32u4c4QXVOQjVK*{gHxqg5(-Vo2hA334d| z+}ipF44TaO&sY_#Gt6Ys*A|;)9gC-t*_}Z*yiUhz{;fhXs1RAD`r|i?i zaI*CZ>EW3v`9>qbLDSK1=_jR6NeOT|R;s~I6d2hoH~K?9L+j;!Db)v{oCcs_JzI03 z-+bsX8(U2|82ALM$78aaJ*Oc<#@z&#sUl7FdCq#N6#6Yvqu58_hJ5YBIvJzSfsfhH z%k*|wEuTS!cakAT?V{iST52&XvKd#h$Lq9P^NHv%8z6a??mQikUDUL@Rw9vk#@A-em=CY5gW`{P?%C)* z<6xpl?~*YfTBg0e9w;a1Zv0IL5Q9>t54>yP^EGx<2(K-T^Fgtlq@);+=1|0?>ElQ-eMnVCzSg|jJvn#p( z49l5<9YWjKb)a^N&4O>MY_KzA(U)_KaI}uO;9JoV&4eoQpp$73D zwfIz-W5#i4ubzKrk)QPB)wlSzMNI>FNXy{y=fLQg{xM9iE#OYC5lVm%ho(9Y?zj&o))m^1r-t4u*BM3L$-z<5+{N9tL2zE#h* zQ^_oFquM2AIiUiN`~(fvYA7CnTTTBe;onkc9)}&!zy(l;ojlmh(I~Bc$CTa#O{BRw zPW)W4NR|rChH^THh&~`2J=xZ*^#x`<(UHZ+zOTi%>v*18rCtrX%wQF#cgaE55iazl zj8x}Z#IgaX$jUzK-kqvN!2D^(s)RNgsEL;%3tS^-%B!q*2cDVseMA+Ci@+QCue}Fc~r>_aTYIGVk=}v*KnRC0ih_(m+kUo&@F4B!Cq~bAsqxe zTcN#q;Iur{$oB=ZSjvHXvEri^i>R?KN!N4U=jg9Ju{(IPLi!QCO~_D_&w1bKz&)+) z%x<;HbD*Lm&c0f?gMF8?&n~%JOZ5;mb2}^ep*HLJA$;G+f>FlCS}x`nv@Rf)<*-e2 z)ONQ+EAf>y^J%fCt1{3r9dn?VyXI+Q}!rPLhFnJnQ{FxAZ%pvLU1`ozm27=6FR|>2C0spj4*tcWW!>rN#6R zRRw0Bks@O8Q#su--Foqo!s%w6n`V(`U;124l$YExB@$DrsOWnJ5jg!4@(BS|sl&tE z&MkK`4H|g|4P?8nV%{un4KKn^Wzwzb(joPp4x;XEMdJ~b)lg$1Z(YLNpRtod;kL;C zcJIrU>!b$zZ!SCdT;G!pIEmip`Wn~|pu1P$;}&*lxgVBO%AFLhYp|U_HDPLXHt7L( zL2v=|j@_RX@SLs&{#8)KFj4}WE8lN{g6-batC32rF;$ zY=?tKpou zwNtXNg$GbnKUCTYuBgwA2X8)__-()kI~Y(2ut5ySmr8$3{7N308q<32c`s-s*sKR1 zL^pzg4!Mgl+To!LIAF8d33vnhHe34PfNJzIi)`2t!5!M&;C4{GY#DU05Ucb@&ZKLG z$8sZgaPw@0W?P8j?43V}|Exlq@Vk|&dFIb%+!XM=(ql;kkGUEx--o6%G@pO1ms2g0 z1@D<=Ldgx97|{*HiTMPwcsbZdlP;DC@Mn7$MT+0)NRE0}zqOmVb8#iq%eKk&`6T|b6Ws4BxfAB!JU*C4rA zB(4Kz(S9!#L##7bH}~>CBg4#VR!5e=2xF#kx4+3PW}XUYgZenZn_7tmSch(4wv2k_ zTdro&DVJ_2s{wmaom$0{mzd)#+2}?&gFl5kEZ?wMW5Mx$&hHF37H=TCX8-JizGU)_ zP$5c)E|N|pi2382)wHTU@M*PEd*z1EpjJt_%)*WBtZ|e*o0S+_sMu|#8;Q|EOzJE@ z_vs{Sij*9%o~zfwAtkay&w^OWsespx8D^oTD83d+k)OIPt`drD@UKICrLvWopLd@- z;@0)P&}!B;)ej-(=j~H3qfac6ZxuyUTXlZMXmazlZ(s z37Nd}y!W~H*gby4T>wT(ps^-yjUwo~6A9y4u)Sn;erPA~#zwwfVy^A>a0fRjvIoyo z8nmtWDH_?|llaoxv{2`x17w+ZL~`Ga=h6!p9YlPhSyw>o$R{Y`L1g>QAy*gLRbY9M zZR6QKDg%7p7`1K5H1k}p5&E5EYD9Pw(D$o#50LgsEhddZxzAZ~(hkQcycKRl-(+RJ z;d9eVIXylqOUQere*}8VOgMkLEnvTNBQ>&1pwYSDw;4z@psg37LkT$op971Ps<&6; z`gpb)Y&&Oa;4`p~#vJ@Wc3XIvjL%Dn*8 z9#K2j9o;woqL{~refF-Jbr^E9jddE;u4{O2JG_z({ELCO`!{akji2!SePNkep8M)xvd0R}vP#2VI``vH*OoN<3gDT>+8sixVh77@g{bF&&$6MoVYq^rDfr8Qs&n~yYFMsZG!!fZyUlhIHCT)s zE8-S-lD$r}4d8_+EpqpCV46X6rvtmy@6oektr}DJ=yG_g(cC9AUK!R2pS9ajyuTW0 zd>3##&1&Sm$hQJd?{e3x2Cx{Rv=>yw$?K6~ZgI7Np?21(Lwol7;xnYTcSc)c=L7$* zY{@|EUsRDHP{e8v>$WQpTcv8+uuVi`|=y~dOHEB`84M{9313Ri@R2T4pmlKdZxY&;?(od zT$yhH1q&UF)r*(u1r6QQ3E;G8$X`U)~S9tQs}^UjlGDxmgnD;(Aw?uRahERQ|}#bi2o-#ogE zvhq(ve@JEOdd{;)Z$K5uc=k<44e(4&5OmoEq_VWuTsGx0|I70}`t+ggP$|%;2M!16 zRrs!$_f%q=dBi&{wy5wQ*137+y+j$2T8yjEBw z<4*F4ADeX$mfxv(wepxyiJuD$F5~Q7$n}@wG3?B1h~{Vm-@OQKYgZc7oK1iF9B8PO zcYho2fp<3RH?ayFZ`C%mzg5wzOV-*>=%w7|=`H&fIKK`Zy=UG@_KR>ZvEp09RBD9g zTDlGZX5DBh#o*-M!u*(RBlzN()XYv;2?}e17rol0pPRFpXVa1JRP}H|=AY~APzXnt zf|>H91wW%#ZuavXI#Pov&)fYb=HSVvhZB;}^R#>Pn^Nx)eG`;I_YL;!)xGstebf`E z5NU!|?eqzSy6+S(9x$1tRb$h55}&B0F`?&4BulTRu#*b1l&S8E-v8Hi`b=O{2*3V` z_%#{I=6zt&?J;ma7tF3QGK}T&Y$qpN&Z#{2K2NdWYe+gm+pad_u+%9KT*n=+x2B!V=YS-UDqKu=x?Y^gHlAFi@^- zb}w|*Y|lVHKg0X*HMS}}mGvTUx+o0u-u=jN{KaT1-^hJc-|*Xhp4^9g$yB4p0JZU0 zGah?!&s8BX_PN`8IMjkZe>N!gD)EyYh?l=R!1 za6m3QZ?wzo-nfqEvf0gfa(nMFC}09ux*j|?*a~bGkBYw@z1K1GvY^ES>@Sh-=kSSF z=%k}_Ar;~l=;hzSYc>uT{yAO_=K#U_B8z*9=HJlMSjgBZm zVqulpUg*mEur=TtpB1DQ_Uw`5P2BjKILk&u>T&XVp2M3+B&G+9=h(U6xe^OH2acc@ z8Q;Rg^xm;5kwx}7vd=@+31%=jNhkKvX9|Oaq zfYUN0%V;d1x-eHCQ0I>3gDhX1T8sUDw53CvE3u&e7!N|)m(VlP^In;CfweI)kay18 z@NuI%-oSnap?@&R-g<$t*E-Q_6?!W`-tYBS8EG6(pWL)@|PL%p;Te=9w`M=t(0})2(H*%Ov@@`FbdCGZK0U^%;@*WU-(H4A1UMsW@swg$DI7IFvBb&*a zT7zV(<7C%EN14c`ek2yvkU+CdORZiNaKM}Lc~9RYCG2__>YdEnT_z5(BVzergn1>r zQ>=~WfEwQok=#bUTLH!*@??m&>`?|gc3;m-ph!OxXqo7cnWo0=Kwt=n_9NwX+qTpL zVD+UifDEJV75~$k&?)DfetH&f)bB&3rm_E@rF?F0zI7pe8^L9wj8K4H*MrxvhSlrP zwD8ekH!QVj(DxObX?&a~pZQm7OUx@8=?stleG=4HWUWv>96&YsMYa__Tc*W&iQV?% znLaJ1nd{@twizDrd&K$kLeAlJ8(#CgTjQAxI;bq3U&q^f`Tnp9#Uv^_QgaRM5bEr; zV>l_bt;j06-a}<{5VRfSM|e$I8&LJSs0FIg`_N=5I>#DYmbAmYz7CaDmWxf@5-P(3 z#Df~~V05Bie++eW*d5SjZ&?L$XC*i6a(KGJYPg+$LT}?1?!7aujYwH!jdH3N9CLWB z$W&XcdT4D_K3AYe?#*ign+;YMA5|rJ@;O#3pB#rK2%Lv`g8Vgm(OU2uk@cyg>?}T| z76I#%&KN*<4lC5DUH_bNemO01Pvs7N4>o}V*;nY^t(t8oqc#0Lj5* zym=}$pR?G&N%G^H$dWGjtxJW#VjH{LjkIWCul3w^b9AEIjw(|()Lg}R2Y~u6{CP|d z=a0+rPGVlKvY$P?u@l<40*qH^FVN|;zm@DUY%luu?tw$TMxJ@~;t(9|b*1U7;dLEt z?8fu66170v-rHr1QR9ou*bP0l@XVb&k#83!pIIH>$3Nm2fXfWn*Fb$w#(Ze!CQY&m z+o2;v2L7!Vkb{j_jEgwI+hHbfOh+zk3(I3J-#>{@C#fFY=QB0KYtiy-EpL*l+36q@ zn#cb8h&8kWiwe63-#&eDm83j39(@LmTVdWE=0DMn^6*sOtL`|S@28vFZrh`4Ler6LCnxI%kU!x|fBaJube6`o91+HCa(DyM9o zt+5YTv(Nq*eyT^fzo-kdCM>Fp^@6hLm-UQg*>%ACCVPaAv**M6Z|PAxMf26^5(-Mj z=Nn!_j%EO*tAPFIp+g6=vQH;C2`Z+d6$+kjW7wN_Q{Ii|Gs8Tdvss=KwN$QWwbAXc zmHhm(%Wf_E3SDG&oJB790qpC)QyW7evCbFm+@v1s{a)z0Bi7nkWyBwlq~1SaF|%4G zs}UZ`1zW$0$O9<2j$XX?vR|*EN#b(O?=w2uVu8|2J^%iGc+)$@cvVHZ7Gs%lFNH%{ zv0R(%6CpG3!X9FuBNyI4R&CHiVC&hFz1*QLZK`ddyuxf9a^@l1ZT04Jv4`aue+%G_ zGS20;?;hUZvm)pxsJr=gFP~mwqnzF6N>JkhpRGietb=zuZ5ACoCfQCX{SRd7riU}Q zK`tA+Mdy>*GY?wwyn!Sz6CH_KqN6!;Bi04l4!rVGEYecT=Y8LD$E^ylK!XD|%KvM^ z4dFT~LMP-N4g2E@vBLf(UZYEGh?CEzOMyp!`ZXcNL>%7d)=Qvi3=V9(x?&&iNg?bziJ^e(s0f2JHazxZhSF;&qo@ z{A-wZtOGy4PFC}OtWUXK-mLjnMkj+-p7E@P9&#Z(7U9@Dk~2|{2KSWPz2BXrjvUJ@ z;B$pCZCLs0y*Gxp&inEaAEcpkeR&%54Y5?~cx?@NqcL~!cg!x(NX zxBRIhLCT$Ci+SIL$dOjO?`hoVtjWJd;pjO~%b)08^+Pyp)c&{3xYfz?fo_m}5!=Bc zI85iPm2l6KVH^MNMJ_o^C&AI_K-s%$ujVdV&AL;7TN%{wL$r*qxgn22`*QhxlYKib zpG>Wj)k^*!oi4ZoW?5&vS|7$Fe9F1J#YC{A2zSf)BWY~aKm zMd@X=IdEVHbV~M$b?PsrUR^m+nfgoo^|&srM(Q!03U6X9lIMFN>e|!~iAGF7ZoiSr zg@fOV`Ot*VRNl$^-3pz--=AB(m2o2XTrQ)J=x)=QdhDluK?Ie~+pSh?9+ml-rH|;$63w+S935)M3<%+Cbck*B9QG$KkwU za5s9t_n`LbfO$a0y(jn?_)P;6pb*Yn1twob8kQnKT*jtDQIqj^lyieG(_!&imCAjM zv(R@}1E0vxr}By@7P7 z;@&QTR&%W^_NY*gYG#UvwRMCi_}-80j7Uc!Z2&^&7`ZXP_Hne1zF24Ps12Fmevmar z-5~qhz%5_L)1@|o$L(sR#~hKX8wS}~Vi#LF{#80ESPkp1)0^O7m58BOK5~xPmC#K# zx^yiTsMmKq$7jr$F}DNIvqGUUBg>}R66Eb_RogZs?k1$p0w`h~-(8Hf^=^;{^C!_D z&}|?y;~$|H4@Wf;Sx{*u?>id5D6|`I6VeZ8Jg%9@mNJd91Ml9jN1L(Kkh1WS&tk3- zy5xgPb-u(iAeUNQGJ9n2Xvpl20^ zx!`&g`qH!LdKEe?RIt}=R?K<5R<2c3COU z6U;?#Xtf^Y0R0glw#q)#$M#9`w1$D+YIfm%uzH@ql3SBS6t8A)_@s2=hlecQG-AA4BoWMCV~0coUz?@k^e?!zn%)Ea^FLr z-9;QN(>~Ths2UC4stmhUw*rILv0U<4bq!~zW@p#22hVf%?%+jWFd&WAVKW_%&%c5l z(9_%IAytp0+PG9%VHtepT@%vK*^c0TV3r%OI#TzkD6E63+z%LqMixCjF^-e8o7dC2 zolh0BdchU`Kryczdt@P!$vfJUMbD`Rp`0w6h&9%Xw0s49$FpTS&=%@cOn0~X_~+yj zb~_QyoENH6ThTA*BA24WCEtD=tl?vk+4tGe?PS%1)yvifGH8$9OWdBkm|Q7rrDgzBiIePjhnSRc@RmoD!v)#bIO2JDGFYrN`wGN^Pu&G(yE(lyyh#0CoEZg@BIJE*UZU7WFileXirT?CK2H1^KeV^o42 zdN;V(4i@|PWEOiMT8UpB4Tq@&7b%N*9jnx>6t-qISm-dkR7TBzLIt>y+rT-u@@}t( zZN$qMH8Cu-I&kdWla}-5UTDj^YTal*CKGOmr%O1&e|7D0XDynD|NWxH{i@^^}9!Iyp;J&rj8**D9!=!uwb<>nS4^&mj`FbgS< zsiUJ<8xy$6s$>0Q}cC8@^fSv`Ky}C zy+}7xc(n+s-e=U?p}V^E*D5lRSly=-^~2B9(X%t8G|%R0nCIN8@<{mgP+J!I;{|Ap zUin9#5IBflsdyWoNzs8;jo@Oy&^!4vPY;jFw`+OFI(b(UmxSDTRzPm|_iYT&;XS;O zdy@<`G!|`<=SrNf6H^;i!@pnWOm*yLrHZ3ZqFf2?=vNg^WtUT+c6#-J=}qR9)}Qf> zwB$}G`%mEwKAXXrke}EnH_JQoT@lMU`9AA|W~t?4hs?UURk|ImH44=jlH>RfF|#n9V;W$1g_ zDxWFj7R+5t|MlFixA9;3+z~p`v4(e4qEi7JvYF?mV(0C`$L_IWf5Ud0Zb)Uc$U)%u zJkK&`E8?AEj?e<-V1KL!T4$3{NvB25+^(mfDzZxK9KN68n=i2n-K(G&`gMmAJd>UJ zJT+De)oNGSSbFr8BR}X7fDGTv*>ZU^6Y!Td-aQ)EPRY`y8DpsGQ8F)&)+)*2-k9g7qGg{7xN?H1yE!+*`x13!&{B2 z;*b`}Y6fl(0f+Z__g49dH(8by7+L0U`Ytf~F!yx5J)jlP-}-P0d)uKh-H4Bh(9z(`&t1rU6ZPyEAJV!U)gpr zx?Df2dF=_49H7-dhs|i5d(5Yo_-s=&a#rM?fSStVZ0LSGx-9c*$x%229-uOX-~Ewn zMY7h=nX@z%xMtcWV9wM)BU%xv(QS$&6)nN@Jkd3*Uw*Icg?fok@qY5M%p;L|M1QjA z(#fmWd`e3XSnTHA!}=W*aH=xdU9)_C%n>omo-ih*SzcKkn0ioN-Cbj&qVq5Hs}1-^ zGKo9AET2ri6YVP(uhuY}pGT~+3EVmD{*;qa(Jgv}fw?vMB{A1bu<%cjT4JaeFBEwY z>%r`U$xraN^ypnp<1N&)L&4u-Jw1-*l1X>Q8h8i&D%@uqf%I2sSj0^=E}pASZq*+_ z`#zbh70Tae-o4NBMd;OH8F&b`X$tIZ3xAUIBUj2;K0xb>c-@pyFl({u_Ti6Z8zEA=mH6X2A zYnX0cow?1dRdFW_9!af!jE{X9*GSN<1FUY?)*PePNxE zcPp$UjSZ!7F5mqdc?sLd?)(JLM>D!cAAe@BPrnJXLNnj(L&rZ6J2LutwH=x~fxnx; z;~(NhHbiHncR1lTq})5fHDxBSBD-G+mFN*|;DtMuHCj2}5EvX{hnEmpYP2kHPu~TX zym1|{u7|#e>)UVfHg_vqt(I<8n!{&4t4*Skf~oc1si)aKR9g5Ctj;zp-b#EoU)Z0+ zBq;VpV1S?dPTL6tR(As3p!9CI>mbZBHRvNwP_BR%4OA=R*8>? zUPww)KTz^WyXztA<<;Be*wQ;hHZ8a#JCt*HOsj+}v~$D9p~5`U{8)UQbsS@!{dzMV z(|l{xJz)b}pNEXd)dTeMSZxJhuvay<9!mFa|E27B3-syTp%$pakP}3bI?;4w&tr{K zmBZckoV>TT_fwk(6+Wk7o_^JqvkDYsL@c0d@1@&fr$^6t%GK4-w!{7gTTNagwJ`XK zk3zQBYZke+9?5lErae~1&-DYl(n_otop&nn&w6F($M!>AW-V4^hjH>m??63Ul?n7W zL5)>BeNLRiT6swDub@EBrf_R&8oL|j%}7|Jw@+Dlj^FOGHPLg7hoC5G1|yX<>I25P z?AbUo-bipqm6uVmYIK|Rn~FJu4VnSe$D@;FpmXnvUaNH~aiG=abbP+N2Nr!B=uSPx z%Iz(z8tUbVrw;;CzXt|v3({(?y_Y-?m)IGaZyt?X2~FZRrEa$meRr|E?uoeqV1sN5 zw2fD>H=kmkEp|V@W!?tT`wqB`sDcTq^4PR@ftVTc>4cKabKL>EG7!bEGb6L(%=5pS ztsMSdV%G#JFM)tZH!Jj9$YP&eHU(bs-0ONssV{M#r0zjwkR4;`q?RWteA z!=ke!aBA1rQ16gFvr;>*WQd(saYOZzhv)vPJCkVQJJ3O$JqW!tn@{lV7X1rsKoe~t9`{Bd?%A-;VXv4^BrOKsXwKGz zPSMJni}-z8vXdK#Nz8EPhn(e7Jk+z$7!FbZ^LQO*CoZZwa5eT1C)1)8>Nkl>Q{_h~gSD|YFsl{(bC>ibqaPHTvuthX&_JRjP- z{QF|t%1!hYl;Hh-=&KVp$BFP{g4bXHbX;nGNzF^L?4dXx>`t_1dn?x4bXLvgT~z59 z-ci<@z^Odfjo!3q14jVytMH{KVD%WjR}A)7r5?rRZ@1s2Hj9dWw7p-1!H|c~X$w?8 zo#@9Fw1*X7BH~kQ*Jkso_@m&l{|RgLDXT>Lr&JF#jmKUt@0TKZ$TkN~w^#{x{Uz+5 zEFAdLyI<`KmnlzlBDKXx&g0EvB*k>=8$;~q5antsbTf!v<~I2h?wVGlgZEHBq^!g~ z)h>O%YzDOFowS*lAo6NOXFyvE#1;VASAgg=^biK6R;E*vhlrC`V>eApDu|jc$M?x(XDrF* zxId=Ydn(2PTyA^9HfxIRvt5Q9S!v_p#}jNDka{4lP+eSX-_sfV6v0L=TwAE~%kYkY z<+X-AN|s>KlP2%}OP&O)QuPf){xhx!rA)fp7ItBkeGXqXE0^276ZtR|NF4>nTXke=xmE|y zfqsTa{LQv19EOY~TL+nt4os?%NEZXf+N0jl*ZfJ-%`#OC5%aqI8+ zP!k%g$u85wDX)8c5|}&%RLkMJIdE}-p0r8Cbjw87t*M9*ym`mt?aIdM+DyKVM}2;inoEzURy5sJsqywvxRt2T6TzcUwf5UM%{*>&13Ftu z`9zX8(h;GV_j#nQmp&8^t0&B3?W?UBZ`qk>TKlf~T`1!Ic?4$j;BkO1B{w36z}TK00Y+*7~7&c>_sbEu{;bwAwQY(=UBM#u-wx-8~#T8~Zq zK9<`cy~$ooI>`RmfrtAGqqlr)Pj=FE#C!d((=plzeBalZnvAsEOFmtOy@&mguL@{> z2sl0rUDD&8)jadleX`z7z08)_c5a+Wz;!uuY4XtyvayuCBP|uG+N4!F)$ltbsWXu? zc}S6Q_70G&(l+kvwK@rjl4gHT?S%FU_-1wZL-;c3v!!S}bQX$Bkcqp^y$GJMx&Z(C zdU?LcH}Ov1RHX*&_$K6dw~;kM#H30-L5!JKHcvj$Z@}8^v2aWDOzQtCn+FFqk@Y+S z+JBW*yI5lnyYlW#8QAYsm4Gpy<~t6r>Na^!)0gtuWW7YP?pJQ&K8ihXK#|DBG_oA+ zao(Ll54@E~fS-i|PI@jJ?{y!3r!IwJ(LGoL`$sM*^DW^mO$!-ldKt+vqbXdXLRs*4SW|bdkp3}NX>p9cCP{?_5pUwM51lKB9aW)?8 z8RoXufR*SF=xta^w%S~CUr&YQ1BoU(I(d@w4w7R@Z*Q=cA$qs-5U64>a0vYTfKtm(`0o6)pNk2$I+Na!QN(ryGA&V?wjC}JU6392zcDV?U?}a zw~+vz4KDdyLa7Dau!@MWcj7LEqC3JCu-X7Gv;dVZV?r9z-6tPpsy4L=pK&?1#4~{o zoZ%;N7u>m(?1`7HCZ3>bc63Lqwn-vRYFq3hB-Lj0;96Z^9}+<+LC5ghBg6WE$dodl zmU0ME|FY<@C(n)FswQrq1>m+4NYkYZ?A;{Ku53=yE5&RI89#5dWC9{;~mgaukM15^N>z?YUPB;shDOyt*-*web6fDBGHLH;xT{$ zB;f#lNRK~XrLo!Ye(7_f>*G{X^|J7$AFQ5STXywT(_i~YSbx^EPIgJRa-#>qEoIbQI$Y#Y>;32%|J6V_9&*JLy8qtq5|4X#YY!?zAt%HMStN$!My=@bRbh)My&ablrnE_w|0AIR`4S?O*oC$6*%>P=%OWYHi! zx0BE4*2w(03CZy=`zp6A>=CRlLqsAqE&p+~rv`O4ri&`asB z<+n_Ub-^3;P>;J$ z`|XS`q{v*x8pyW2ah&}Ieq$1z&VdS^=R0`Jp`l+#BFoG^_8j<(a8POsmc=}K&Ia(; z^Z?Ik$^~<6$ljUyxtQrL_g2*EYwOTAR>@7{-I%|&Q5)5MpuLADmLOS>oce}myU^Lb z22)>aZhVZ4i~;E7J$({)0t7p3I*5g!ctZ{Qy4g)C))@>dnDEJuv9&=^A)7 zr{YGoSw?mk@Aa9w!*r29+sKJgi`<)sjM8#2xH)OJ4M6{8bpKg8TBXn%esrF>Ob@^( z-V?H&=*Q*Y@KW&F%fH9sq4Lg2ec{^R)s~CE8=ax3n0uOgs{*;u!u-3d*nI~T$Uj7r zD1eiu*o$aUZ`qkh`vrC%95tY^NDYryM75S=7Rh7hsus z#)kXjcA3wVry5*4&@yK7#05qVOzlCoxDTzLpYe9Us~&&K1^>IPMgy#w!B2F8m5}+a^7QGS#uN!NmMsl4>#{aV#=d^Y(rWIp3+#k76>r0!dl@N z=pI@xeEoZ%8Y|-5z?t$)~K;Wxo!m^IWMR@yz?H z(vd}7P(p*HpveqXXf>aaL&vjH+5%lM zH4ol(2}~6dCoj_YgxU<&&IN;hn-tj(;V-Z0TPx;xbM|vs>wYw*K5YS8%lRg=;?RBi z3@<#o%>SC4--w*JkK1{kJWst*_nGS*HgPa=Og^ntYw)+4cCox>${j5@_L6C};$kQ>t@P$3UIl z1vU>pn`j58*$EtLkQrtC%mh5D7Ivkw%xlt^QWU=ehO-Qh8N9F#daK~>Ne^Br zbR?K6Rb}!#v_;2<@Mk{l;msw6F31^IY7w&RCUm7HErtqm@Y{Qjx?7NC$YO4zYUpzp z9B?D2_72OxPVQGaP+tRw4j@~rJ zdKNk$D~I2^UE0Bawd`w@{S4?vQTgd`RK8I?k&pohMBj?c%TgV(r4o5?IVWKLPwa<> zzO+23^_`dld_LlAUC4|LswH!(T8D%&d>^Ru^n8`rq2$AGL-Gl>Iaoaj{hexZaDrCO zE;d*>yU);L?7j*ug-lm+HqSF`OXL!|2Qcs)uJN3t5A3bA{YhRGKf}ERTt^UV2f;1WQHp?ePSNd&1Q|Ab{uPsHDW59{!?_5_c`At(IvpVs}4OX zlt*7GV(Z|W`4(sX80 z9=#blfDU~Qf$8ZfB9+KQ&z16dzFz=E??C3YpH)1&jGYhi(-l^v7vhatD{_|bh~^;s zPBo8P%>!>DG8HXY(FB1+htw<5^hC zyE~0Odqk({vH;~KRt{}=ubnwyjyyH;Kbz53{$G5RdoD|-s9BpWT~w}t=Z}E|Gpd4H zARc%4D^|=7tb@OY*;b?F<61b+Wg+r3Sp?__fz?{V>8Un*Dk)Sx^iBVCZ3K=kzh}~8 z=zZ3osU}^-F2@KOwI(JaeT<}D4>Y^%i$Hx_Jed>G|H>BQ-N*&Lh0rA)P~!>B2-R>f z^-}f`b&pM)a3^vnSDp4Ar=tHM)Lg5HP-izetbX$prRGD6M2I+N57=I*$wr+Wuyub) z8Q$uc?=AA#8 z*kXe=%I*$~Gs*7iN08b9bP!TP)WuO4^L+vb_00 zZ3Qy)vgR4D#;e2MFMKj|Ki0PIU_e$MD_}XWI(H~J+drXdISbg}`Gmjf=$qLfbX$>? z%^fxmtHtBjgJ9Hi3A}%7A96DX54P8$xNjn#cQuRtz1ZrXvhLuxTI84aL3)6jp$DFQ z9vyBa_?=6K+~bVS&S)uuM+9&t>e!(gok+rp3#{$bbtcVe?Oa1}7>0}*dp8|b) zI@$5~Dz-55gDMB@;8YVrJ{a7f4ya?7(c3(D)@dt{_zHY_zcugW)DEviBnO$b=vUi# z(*4}t;iHtjF1AUW-gBj!4K2;6j|P_af~!N3_UUTl-E@~l+AIf8t8ELC&!@ahc0OQ* z4ORem|06OFCVa>qw#0SFQzog2KGbOr3#?Z|vpbXr^sr~ym-AmCl$j9O zT5NQqK~7ZLK_OjT;|h>(Ctz7nrO!9KL(K%ei9SpIJ@$T`(HC1Uvvx0BP{j@SG_aZh z4^FZ)p!+bpYnFlY7O*j>?RcmKY)atz!$n`3Fm4=}Les(uCiX3)r_U8yX6-fssj>^% zk!WAyRmKZ>|6TT#JsjLN_o$KgEUZJiF*6?ccjM`8CJ(^#1n7)pbcs8KIfRk6g zO~d81p0>M4%LAD^jpnLpfc@Y#jyvHh^cU^nquQW=8i%wIczi^Nk;{O10qwhN~_BB0gQLoCwFP$h2{F z*|U~_vrhQZBko96)uR!3AC`9Z&}`(whb+62eURJx6pJaaub z2YclzD1+H+zaI13Dk?&(+sfaGf4jwJXdEZ*7E;sd_Z(4g{rQGiVOMX9^mgC z?tv6G*MRR?Xf0{!R2d{%ycy*&@ldsX{Y)~p3aJ`wxs1lHa;w9$yXa%yH@ z#9cgfV@Sx7s06zc^{8zY^JZAF15A>=pe=YJ#(={*e&hX-r(qR)Zji&)d#*jEtAa;` zd!VOIFys~1yRiv|xSjj7pB>FrBb+$QC;K!XJP&F}zX@}~ZO}uxu{%VsDfn=kRck3X zu4_~-W4r?YE_EQ4>2t=X&6Z)C%xmInwG?Sn@!gr2*#PyAAQ!6wQLAeQfc|H63;7UE zV$LU&I|v`llhmHs>ri#f5x;sWrTMkA`lw!%DvK0h4??u%3V%H|usg%ucdH zEuB?5^%!*hV{p?;hEqO}mAH_hT?iSm`W1l zV>2+{hi~*ukv+;vCq(>DYKM+)U{7V7x&?c=96y)e2nPXO&zAA-HKpIdasn{ShE|cdl7!VBg2(8J}$wloTn<{ zt@&k@;By00SC@dX+rv&vV<%bc?R5=>|BW?qrsrj7g!7ilZ@GDfH3TfF>Vf*Jp=&(3 z+|0{UK1U~yo1k3THp4DdVm^&?sx7cvxvkfN%}Qu;I`$!%Kiqd^qF;*T1v216cG!*| z>q8)V4U;3QprB>w$pDdQp!07L6$81}Yi~k5El^@ZsI$S;R$GH~`-yI{DXCjx2A-1D@c5^( z!sdX@web$spoNa*O-ndWPLgY{YYXpx964STRv-atutA6$hQEd5p_(P&u!8k{w!=Km zR;LPlglPMAbi6JQwZzi*iL2FN2l!kB?&|F=PBwwK;?2OfO!x4e@lcIt%5Jk2$eAYK zw~3tUmB{Y%?H{EJG+C{B#%A+|7G!3j4h>YPGha2sR-^OGgBm8HUls#HBHT!|oxrs~ zkHll-`r}V6m;c{Lr2G5G*HYw_=lS}5u`$?sSHYd04_dEE zR@s0BPn7|19JZH%K{21T8yz>`kT!T^FW-9DJhqpHto%iCCC^-rJomhi8SKUFtWhNB zNFZm>HehR)L6cbO;D3i!s?7!^FqaEVfQ~+ZDhiP;`0Z2(H!j7dDl$43(Ss?6XAg!O zy=rL0&ag^-4?c1)71rtls*GL1r<|+JA;AjOh*EJIF_}_{j`Uzyl(K^oVLR0t@ ziTF@x=dS!IP<=DZNPT1tNZg6w@J`{chx?C>EmjzpdmIn>QNa7D=CePHEr#lc`EQ?| zte4_H)Tut ztxsfvy4VL@SmnMDI(MrCtod4Ab>cNIUX=h}Y6)vhKpVpc4F9&lp{wBDwcu%qp2q5B z4>p_oVIs8NDm=$fQ4R3)49ad`@Rc@MkzHZez?q(D#w&@{paC>8*7oXCRsCab2<=gC=IuG4%Z>UtI&_pAW%JT;b zq0#~PzKvU_hODzcrkJ^5$EcPGKr4-%A$y@vJC1uUBb)+OCup87WXJS10y8zhAmO}= z<1G7KvLkfF<@mA7?Gs?-k(+ZkVG7!yS2Xw3h|T0rjrN17U&IC=w%e!!XSIuCIhwTh z(V$Zr&yOH`igdDh24^!^SfF}K*!vT}|26X|wp9boR4Lh$I#Bf4VP;Ng%cvx*@wA%K3^t_K?-S8H{zqLAh26^r>2B zj~_y9Tj0$WqOq%yFE86hZRM`n%I@gaWVzg1%|s!Xd4Xr^1$;sa?Eo;k^<%=(?P_TSLlZf9Gk0?=K43JXg+Jm)df^(+{qHmb_u!ft2~U;Lq)t z8^Fkb{*f$J70{j+qOkVueehZgE;bS8-Hw&tgT$tTERtfC4ux)gN>wALo6P&$^)n)@ zRB~Y94)Ij8HEXuMvDN&`b@G>ySw-d(#Rj;mN0A+#pYVUy)$RHkY^+CKQ1QzS+ftpX zQ$K6%r(gYEurvf`Y&GxN(aBS8BMf18@As88U|+M=e+BQ^LY5tG=;N7TB2D|*qkE+A zhy(3mAX{TUQesbxb4klA;4lFEy3iyYZd6{&>!lWfg(CY@4}ptTAVjw}c&SbEZ57^S zqybPk!#tYp{ph|ol6=?}@+{Tu=sK4hkkoS;w0YpztF|6ab;4t# zobYG7-J?`TgXP_vvlaeKm{{6mYq$fb;<5#iE_JM*g=8x;rZKR4uRxf{_o%$$ztFY$ktslwcdC+G-^PWL?B@iELA0W%o&jP+vt+xLN2S(;N zLkYWeA9l(Y{N#0oo+;Lfra8*@>6mJZ&2vV68=eM^#1B-&4O6e3)~g=YP6uDl@Web$ zM$d5QtxW@Z4(W%V0A3(lAx2HpGM*lwlU)s;qsep2k06Q8hI@Y%{tm64V0R%|OT%SW zXC5ipjE>d-rFx9_eNMkglkC=Hymk|TFVP#|=_p%kUX8KHma@<9gWoyE)NZR%6Y{`& z0C!SfYf?{unTVHdlq}L&oZ9`z<=n6HdH)E~@ki!&;Kgzu zG+8#PcTE6jY_p$-0-_4jq3BjtZDEfEXkZty^80+&OO-~Qjxp{^ZBPb$(L=>Vw{74_ zWVn0|;Jv)N-X5`Rodk~E`!WJgbU=-C@w6?9)L$3j4Sf$?%qMveS3oD39UG~mK}&+t z_Yt4ZK~MCq7u3P=Q$I4xbL;nTDssnp3Qj@Rd7q-!;|KQjcjtK!ZfrL1aMQ!x=fADf zC&ok{D6m3(z{leYozT8}+PyZT3%Ns9M_6bd@-y<0UC%^TwIJiJ#DZ?(78)h@tptp} zY|mQ-cJh~;q6b{pv8Lxb(6tiX+j9kWVgtD+eUJK~K<^8;o6m=l-9z$t!EVEX;`b9Z z%I%Uw&n?-G+J&Y?6=S6>wi;+fIa5B0o`iexkc$nET6H?eQQkC zOEF`Q9?vUuG*bEzZk~Fi6tjPAGVq%d@6>Xt!`<`Qha~D0{j}gYs*<1!`Vtsff%rq{ z)?|}_Lp|Iwm)WxSVxuDC>~nKp*9fPa&O3K2n^pGN4pyEg5;M6O>?|=%5;O5EUzGQP)Nk^6% zZGy&&kkyrVyk7xRlXSn9Y5|yApd;{Mm)Om5VO*?Y_|`Lr@Yra8clFsCAUzeBr?LLU zz}a;dA|a_Gkf=FO@54F~d&4_&O^?R|pKqhbc-{W;PW_7m@4#;}3$FHwTcviA`W-La z(L?Ah#d?z5H9tu&R>EniLc3mxI<3%l02@6mmOM(ihdW~>-*;PlKi^ou?#V~v4eoXhUz?k`y_9p;l#3Upty- z7g*pnqvzjZYqNL5wP3_~tX^kxl6*ZBscJ*gQ=5hFZc5Suwx}59Z0)v84ZOu8v7TT0 zsbNLgZMK4)Erbq#AJepwea~Uv^K~9SmEaZhY=Tyz7HB8&0DZS|r>E^=ZqpZHOnw`B!v}V^aI1jnEja_~dGqgpSDL5VaDdm&}8Vnwf^|>=nJp;&$yrhlWc9PA}TxOb) zT@Ninxr*$9etXd5Zg#edcX>^{SKIdhJI}C>(4x5Q*nyA?+`Uip%~$x-ip+2i_Ya_g z7O?N#mb^P?5ubVYt|Dl~BW2{pfR{X7Z@u>Wv9T!s5$AWs+~3oHN3GKJPFbEtlx$(^)LHyZhP<_lsSkL9 z|BNj?ElgB_(T5+2Q4Rfw+xSLk!mD?^*Af-)_9M$jhVJ0(i=mAkTQApjO89pJco*;3 z95>kEWnPiisPFP}ICBH^;CU`Lh>S)4CT1ck9Du_`M6yn{hf>RcC%##JUu|cz=K|h0 ztPMc&D`r%BCf5YpkN2U?>KR8d9 z`90{?3N;{{pKc@8i){jyteaKK__s%3JvOi=o`4Ll05d*sbOZ9V3}5qNdy{X^2p>Z$ zjd1g7Da-?0jv-FS)^xAVBv~yOmx1DBc{WXa~}dKEXOh*QOdc+f<8i zX*K>nue2b$fb%@V*~)FT0w?RXW5O?E8mm(Eh!>ljFW}a$LgeLlQEl$Z1yCF_n9Osr zKfwpu2-SI3$Mta?6hcM=p8k)i+#}Q8c1PlmdKv51p&leF_^Yi+q@3 z6+`(e_0Q<=T~L|p_1ob){FVIXuh=71VWZKP1BfQ*iv2c`H&;U?{orjG)V)pKOUS_B zg4B$Fugg||AD`bAHB$Ptj6CU$%?qfa9F9Gc9|aM%yefr_X;fJ#OP((sU6`Hz8`_e@8eBKob`HLX|W-8+U4Bz zr%|ho_s2S*WqR`S+oPcFKKN~>{I2-1oz6LiI870^?78FWRl-lXa({`}ct!ktM~4n} zTU1(#U+!bkYt!ahk~(s1ncmZFta`H?UJH1>C+1l?TsQ%(_&wdoo%(`KhiANMGuO_- z_n2Rg%hHtE9@fMA2QitI&ZH+uZlqY7|u!#>38~eshx(MxsYtD zCaAN8Z@0kN?QqCu>|3;0TdU{dDm)Tz;6@Pj$3U^`qmIQ_!fV6 zH8)(VPO(v->iH39rO=XRx)-9Sd<~W05wLygq7y4!386J?UG9U8+zZofl-1Wjl|GH= zj#MwV*e0Ofz)du)Q(`e(h$bvLMsi*ttakd|=fJNG?70^Uzl*u;PE}+vg?|8lpJJaC zTfxgi%GMy1g#S)V5(RgoVIJ0TKUCfa*1xoNv~8mF)Fpb|g%kYu zJ(|zWgs+@8U1%HQwdh-K>Mm`zBlK~s3$?ZsN%av}^1T*B_ek6pXPdwuc~hYP2=3zf ztDuG~Rq%5foYfPjM&=L&&-y$^1;F(c_Ouh4EC3pd!01oGhH>&vHNww+lg&vQ^#tCE zA3OG$^LT9hujIFJJRF~88?1-SpaRa~bBX+R^Enk&Mo%bvi+}Z^D`PJMv%=td5S6wX z0cuOYS1Ws`lRMa3IWET@mir?P2Buq77!z9r=OTZSGdWML6=7X>frnlwrIhu5PhLVc zpW@r+vkn_XlVqA1>$}#s3~niA552&D9cTA?0qg`x?-U&^df!A#*`?}ZCplYv^@(S5neW1e7#;HFuY*RZ5N&AAnFme&Y zHhMcPL7$yrm&PKU8*f0)J{1y;Gw&mD96UUi-@gFHGVOO^rZ}6rRHh@1Yy@D^?r(wb z>VUJ)7;aFX9cw-Xg!>quz^JOhiEL1nHVjr;tedm+Xr_E>j%PG95f#GX1(QGU_26;W zOM2jm0iZirY=EL5Vy_XEBA|2_ z+RQM0fMI$crkwNbHNE%VhXE`%OPQCiPsT&*6oFUM_BO)`{_~3 z#Opx{$Lm3v0M|AAen1vUJ*W2-Vm(HV_QIM*c&_z?Eahi1CDHcQ%P+iF_`#(>_3dD> zjI*Q`TD9tR>$KQ|ZoiJ5eat;J3#DJ~2=b7JNFT}FOL3T?`2df!_ zZ=0d}H~6Vf`!t#>tkXEQv9nk-d0zfw?ja5cf3>(Xr4pOWda5O}3Nj3wWqic?G%T}Y z{e#e80vUL7^$8*^{|>DNl3zRP3EN8LQ5MfT6>ek}hPG-09bL_H7>#yh6E$OC88b-)Y_`g3mk!?#(@$h!$mry~(0}?q z`gGIZThCCFc1=+A`_inv{J8+U<|4J2q|Gj@<|>gZh^MpDawHv@x1oHuDp-N8@}d4F zWObCiVj0)(N)c!EAo8sk%Qcy^-1??6K|PeNWbMctM~oS6JU-~d17dw~?n1jP7poI_ z0*WX`L;1v42YZ=sL3|ntY$Vf^Y*=tMB7H85Z}M1l0J=G9IWmcQI?nvvZmU+yb4+Od zR0p7?rEpt3RxKR?xvEerd8bj-x1k++l<#uPUgV86%7i%G3>}|{j$ji8V*Fkyz_a-( zlsH89mU9)mL$g`M9qgwYdTWswXsi`Fox>!`pXe6oI~ts15Gl?C?!>*g&Zb%=$&L^}ial-ixn-HbMPjyHSm*Qnubto_DJ5^!bAAs&E6f0KHSE{om+g1}OWZnV zP@T{2@GEMSualcygVGz@PvL8`n9I|_KJKvktIOa&n+s&s!*qUyGpG`Sf{e08ps+FV zYB43Uo`mdSR%AUD&?;C>0lL;ci6(;!+m$OP@WQ^Jr$(ry;w@=1N?t*QbkWVlx|Cao z%Q04Qty8r;B49{VKZc)7KfM&X`XI3S#v@#l-l9XY2Pj$u4E<8(NkrFZAy;n$+UFsC ztS<9d{mw_UlXraUu0=}a%5_l*S|)3#9I3&-u&}&Yx8on$tIgoXdY&CXFUo;qsQSgy zWGbfg!$+ljZnnGmCz&tMLatV z-$;pEPkuoU+R`&p49u7&BSEBv5t(U zYy8z6&iyNix_zeBSztiFa-aET`Bcl0y$xLdFaMn1j_f2hqGlu0RZdg%GC0EQ_wAgF z@9S#Lp0VJ1wU}yEC>c(yC))lI_v~T4E%?to&|2fS@l>0mJcuS_yS0ASpFoMHleJaEnJzpkmU}uRm%uf2go1*T`TROKv=d5cl8=#* z!+b-fkDAuh2qcL*@mU+IvnrrL-J+u*{JGFfmrlxZA zi^Yzm52IFstpm^{F$!ok25Pi9S9G6~N*UIG4EAH75hC&MK(SEK>B^P5UapSk#Cuj7 zkQL*a!d~y=BnSgN`}-V{B_i}R7j$7~(Y-OKaIN-8r~HU)ZWpWpJt|Ttd(bR<^zC3P zHE^?#dS0&)%VD!MbZZ_@h-ZD)@9Q8^a38C+I3%6fkOevH`L0m69y~;86X26dBEB0_ z^HL9m$ZC<@TEuBLi%u@3=#3Wd-6jKH+w!nlZDS44s?j6lvLF*0IUJmoNvhD( zMb}CZe&ThU!K0n!;QPRqZ+vVkGoZp-I(-iG13IzBDl0%f6I}5j~$PpKU0GsmIZV^v3>Ks(zi%P z^&8}bbph`Xy0<_*cp#MSu`-v__;%>N6zl(M-%yGC3E5LEbI4)@ma#D)h0ryVXrYJc zS_L+4mr1n@?|pc~d!V0jO$Qo8!lgoUggj~}dm}5^#v5b6pJln!!BNC7{5DVitFO{T za;`nkE#tbwWv7@A(efUa0!yoQycl2NI$gwRVU?|3nxm3JJwWUR4Aul+i#GeUKNh(5 zBU`?51d1e;RVNY`HZ?EcN9-I-SsM>M#eAeWeBg($9fFUD+5oJQmMVb-)hh-&+$;V6X5im z)ItFXdMV%TmMT_5XFW8+1iU~=G8HwC+|F1u^JY0sa(Od7#)Pa)C^Iz(kV!UaktiKJ zY?fgMlSChcrk_I7XyrT|=TpnFAD|1t3QkBO2<+2x490{mV!-@08H9WP?Z_1uxK)`` z2&L|m*&&-k^akfl4SH%LKM`|aWrc31ZimwL%G3CnxAMul;39ojB9cL7)NNpC9#4;h zvMX4{I5Me3$?cZ{)^Q2kzL_2DR_h~ulRyd5&T1pzvK9zgSN%~*hIYD*dmMzc)D)bfosvbrAdWxdZW zUT5B=Dzt1WS9Jtqq0l%CB_i$%qz zh_oS9wrZj5rnB%P#7*;&VK3_{sghQuV+kv2z{?c@D>gT{7d!eVdNcId!c+2~RVpVX zTQ=)uz^7XyXkvX>5LTzU+K&RwQ8}IGOd^TFIFPdFhec@?tL00SNiyGA{j+_VD);$z zUygtFW64J^Ohi{cT)q#Ls?A8e5568kHeV|z=^FH$M!65E)rfw>WVe=+wFKY(7Dy> zN0%}oyHL)SX9B87wE_CGZbhFHl|++dwUh9ORWJ`LJ~Uw6h)?G&xM&KR92PPYq*?Y$ zy<7vHESgxRPs%R3YnG|Sx~vxFZFpnMRcV^6kU0KiKz;P%kP@kb)26vF^kI{FPLu!g zN69Vh!Fco*{4@Jl$@RRk3|TRuY4~B=ps=^V5&SC^A&WFu`8%an*NSzQub>9m`Vm^B z>^^5bjmP9m8DV`E&-+S#2oC6%`URi%bHzWqvSfVk-t%oGDccViQr zAtqlEkRM}I_05$L9R>bjy~g)&)|iJZ0k}6SRLhlr_vSGjkpIUyeCoiQzOWhSp1%t_Jc-A`M90N&^*`OX<`y&~3xxl$V-qK-GB2|yP5}vvDMu1=u4KHVZE>l^-5@K2zs-#m;4#|9?#&ha(|Er z2QqcbaSrK>s6E6ed*In$bCIyqmJ2hq^cQH4Iy zV2)YUa+y}cw=KMPHTU1=!%`I#a*u~kZ2G2$FPijV$SQ^`r^`CO_VSrcWHHar0FuD! zu}X+b*z{EMdkyJrvR%iK0?oP*3cSMa5Rv~#jlb)h#9JH0yT?tOCo-K?| zt^>KyA^Xjyh2y%|L9J#W6>NqHIgCJ|Q*MGn2GEzN41*ghWC*-e${A1=*~hYt>uge5 zG1TLEey(&dmF5F6s}UIBnfT0KBe(0VbPBTGTx3!}G}@Syx7MBSGDSEL_rW_suxG)(+3!21XZt7Y$u0kabA z(2aa%8MOs)+B0sQcF6{I)JA8|9(jb7+$-hsv(SxEEkMIVp%HA~5^(ZUAJLnkivp>J zk0Wv_kT{9|YkVu;b^(9OQMcL1AMuT9Hgeyhm5oYd20EUU8BpLbn$$iZ+zp-rV19r* zwrCUlW2a1`#DW*|f8VbE@@e$Ir6(Y7-^+S4ptlR~840+_WRG{F6NK^YRqH?f%uu_g zuutXLbz)WNfa--gzdgk0<9o#RMi`Umlj0k|5wL~Q0; zAJ8i#1Mz@v0Zw=sfuL!QX@@AH39Y{n99zC>HfAFs>N2rQ#!4RY7ZDNvM6 zS+pKQvp6BFQfDKXqwTtyPg8l{1zM)e6JQT(qyQZtCKM00=0;2^TIBaR*4`xLx*KZV zPVLO&I#;c3pa+V2$yruxf({_N!koz1jgfp^&s}-EaW9=NEYE2=GAWB^+r048!1*eE z`V0zu2dRj!ODp6mia-;^Fq(X0&Ll|LQ4@n6$e=gD!eTc|26d}W!2TY&WOwP^KI)cv7 zDfD&LF|4r7I$O*Nj_3orN$v(WtM#a03+o|*49u!{-ml~%zcd(zmbYoht(PR8eJS>0 ztKQ5C+tqTUxE?E>I!5+*gs1PPRzL~mU^iEv4vk8p{>}ADBhXe>f&p|0+#_kuR{ivbNvm{rT62nZPXchzI+M)9Iw`2!txVZ&`Oy21OFW4 zoUdPC7KH}0p&lS^cnDsb$BWSTPIc0}Vj-7u=G>vwh(U9kc++eAKM#p+J@$ss z3oTaF#BRGJMl4gT1OHPW6?jeH)U3V%xksA8?oOdTiL=V?t^*h?Bz05DW96I>M zZ45S`Po0cc!@3aLw8I3}Z(W%Okx&89Un7<9c^h`?Dj&nzsczIedDr_;9aB2kdpGCR zJMt@UKB-jjJ%PSVUOLaPyWZ2ps80X$U@50lm)xkGLX9j{X;wuw1l`c@A6?KU=k8~> z7CRXOuGxNEZ$eu;)!nFLXwTHM=%~9{H$bUqT3|F0Iz+2Shs)ua73_N{ls`?apAbD; z{UA@Tf-l;YX}G|HhyakVi4%>`C^10kbO+@N{ep^>N$!T0^eg^Ov4bxU{l?0`Z8xW= zb;&%)r(X!&9`U<)*v`HtGY+A6Ep{|-^4116>lXxhpxo`HpRM}HBUfqrBF#T zFfyrYKC1+`2z}`^^5cjQ_#){d8@m!5x5dfYZHHMW@SW9L{ddd5tSZKYnKj%?v{RNrwbYXKi={(Uc~cBT#o#qUd@v>;4rES zWP~ev#5Bt)V6-0{z5wo_e=%$9m9sgAXR`VS>7AO#w+}mVI;o-)sO&l3x=Tz;?E~6) z1a(9#6Pzk_?&$?@hl%x=sN!ALPsei38i12k@l5I@QsGip%v;m-3$^M?e46e{uKt`H z8#G~ZGa8U^RENqzuo$m4(_~6NmwSP!dDMHwd^w#u${)+|I{-e;0Z+-$a2a^Qw;=6m z=Yd!AN9+%Q-or!#^myokQ<|j%WlJr5jmhWE^ zI1K8xO#Sf%&g>8DGpH$#;!*2 z6Lf{FyD*+$-RxtboL=%v(%3;i9tP`MU@;Hlrv$~KV#+dKt?S(i_Sng@ zw)3nO4aDq;r;y#HdZ~L)_ew;cg%SeG@uLS9bBUb~iqT1^BK1-56cIb2o|+t`W@1k5 z+J`^=Km8xPO(mKE^;-8k@;}r(U|}F?wNPXFLs05U;IF;tEw$(#)-9OxK^L><0koo3 zXenl!*Q#atRKkr^F0zk@(24O}!|^ZpX}l{zRabB}*Taotz?rFYVe76!{t$6G>(QDn z-$rdBPa)zq`PS}aBLxDu;p2L4~H zVfPCcr8C$)jEa*Vn;2 zM>#y?kwq?kPMC^5P^4twHrp8}n|8kFIA zJVN4tidid#P{~P}0F;?Apr7H9{@(qF24#Bg;qqI*!=1oc@(FT#ju2DvabVyisnI3q zUN^f6Ehh)>NuRH)IIHJkeRE72xkx*RLmGkrZnhXHq67LMEO0UJf_2 z>K0^M8dPKTH>>z<0$<9vkpiTRRjmxV8aS7z18eJqdrGBScd+kD{mPd}1P*J```}LU ze!!wd;@8Qzqt_J__^ep`#_Z-vMMrejeH3n0n_-GZujH#EX}W>oAacaILRoFA)$5_5 z>k~@WCRy-H!T6+p@9XfU9t1{I1gTN^5y1RBXtoYG(Bl-3dx4{h8Av6_uYi!Pb%_0) zp!q^K1gM4CLE2B9T@U{nC3fp3u#w;oxW|>;OX)y^8YItv-eF+0M415v6zbp}(LzZ< zr|6V!_IoX!{Y+>u7LMGC_spX&{@~vSUd17+qcr_u4U)K2HvzReJ=fEj8|v=DhqRmO zAhR&Hzz_5*MkXh6qHbZeUd|$}NreQTSuS0=1ajhZI>kUE)(_HrWK3d}Mqp;%CF|8f z&tn}XJD^kZxd$5`naG@9-eMjsgZfIz7S1BK5-*e0{p<&(^q0}?WJ3;t*QSQg7QZ=0jZ(FC*7j+*xv~6uxeFgH1fC=p9!6QmA2_MdYB)Jfz>1W6i$6N?Mg_e0b3MfbN7wsD5PL;Uk)WhVpA~`97_1#B8KpBK2p6 zgIMkNO=$Klp&G1=4+JR>7R*b$4m#`SPOAc<+q1qFp))Kn?bbx-VTLSI`UENGh+2m! z%iXfvanpKj8t8gwag2Su>3QI_58D0^3Dzo^(D`hk)DupOjeMcO9!`A|_BdSM~~48gDA%#JEOxbLMxm z+Z&+S7HG6uhj_vn5|3mU;e3i>kLD}3On_|urE*NCX`?>O37bZ&W53i-Yd>UzWX>N&!^c&KwVl+fb?_HOggyX6xfL+6nd~Z z4(SF?>ZB773Yq5Y;4^5p6)XD&IhVCHtJQPRM*=7w>y1Z7bjPal*=6w0kUTNI|2t=Y348CCEIQ;I zBlL@7w;8TRri1yd?gsY+k@3Tr8MlQqAy1ca?M@euZ@5@Ll{54z*$#XL-Cz8xbV+=h zT~+9@WNE}`1uIU4_pg;PGRZ8DX-H4Us-bd(wUhOROt-wqHvFi=$fpuN!S86xM6=K? z1U(oZzjag!h@AY1_;H%V1N(9RKOw89w<^O{$(1huI1n+qD1eK~!C*4JlwH6s&Hd9q z;?tqw0{FQSn%~76z@@|hAM=@6wBPL81;FYOy#cvtnTM^KLQFAFX3J}#Pw6jmJTQ60 zHSiQ-IlS{s9pdR-&=GMdR@Wp|$h{0`o*Xa5-zBL$yB@8<@@gYG&y8q08sI9vImKJg zie1=Jip($9Rb2lzbg+>;IeMUJE7$A}J`rlx*sFDkv&qla@%BbvD{I|x`UN^(4zMjn zf-i%+DtwtwAqSDGsUm+%o`(8=AG(5F9OvtR2pKD(c^U=3SRYBtYS;jDv7>lbI~XOx zfxf@erE0T`OgDX;*#pFT@iwdZ*B%wSr&iwOF6&O6O)Vf@S+MW$zJkX9ClAzf@>6Zm;o|nJ5O+xQ5y*5-K*9IvlJ^Duj9`r{f_5zLh^kY4A1u+g1=(iV*Aq%DfF8w;4j@gur}}8Lj0*h^Z&h#EtYR5-w4Q5{ymudlTBO(i)h)yR4(qR2-y(lP)HL6M zRve%;Z-GW5=(86fpNR597xi>Rv)WXfefNRO2Tta%8Ds6+Tr+U6_}O~oNeV0f(2vPX zzrYXk4C9G>&6Q@k3Z8gNI`u05t~AI(*$yl$%bNaQG6|lOp)a!i^+Y1(y?7mV_y@oo zo*_=*$0Zc<_+u=?av!*9mL05M9v-<;l>j{M8x?l@@ecydFx zicRihS`74H8U6K|1CLL~A80xO3 zek|f8?gn%O4}>4pCLmu3K1Vo{K6m-P5xRbc-0%jUr(R*5Ng4i=8R87if@80wg@o= zsO%neae4|sXOq|t4ch3|@Z4miDH(#uqe}ThVBWQe_Oa&){#ak3F&XEL+r|3#bnA-1 zdgS5D)FhsBP(I~N<*fPuT=Pfl4Xdyj)HLcU>COWmp9g*8&xbExW{rK)A-mbfcHm%r zHfCrU&mxD8w{)@}%jKWV9gGJy1`u{3xmZ{r}rTRtiyUQy-At9#G9vhH+5LN)iQHN**`H1D12O61nUA^rRuAd zmLszjeR`cH_%bPm8pomTCHkeaxr}M>&`$DEiTu*jIYa(}C+GrsomFK8mve#?`dEL4 zHw()8EA;6s?%53I5KqF3n-5>i*Rwen$v|Tban8d% zWJ!X7emrgWy8EGn3f6NuJor3yMz`oI$_dFzBHAidwFI6o|CkIth0l z;8|Y+!w>lFbFocPlZ6hs$c}h@&quiDHoR}$@{BT(5SaW-9^rK8*BKFu2$+Q#t7W`*F=x(J zVz4c0GdFA&#V+mSO<|qkTHvsB9YcB=&v`yAlS*XgX|frOvyAM;KX4W`v6~j`&-;09 z6ZfUd^2l{iW0FQBPuIX(0XsKs%c2Q;;OuAJ2-tclAkq%Rm_?$4?Dcgi6SG5ZhKonl zG63=IiseZ+$~Nfwac|zapJ^skfGz>wb^yOHzf)7rQwH>S!AcDs%{uT@a)tFq=+|s0 z35X+GcFAfv=;?_C)xCq9S*OD~!d-S&U=_o?%$&GsnS2IZ%C1-dQRwwu`x-A#meCY%_SFnideAIamYa-V*vbT`xUxO*6E zCHeuLH!eGw@kBgY=zYujs40_ta8{~M^X-fBpONg~Na!IEFZ~qub%wk_^l>fHj!X(y zp~~4BrxoCPnu}2B&<0kqX??f+IW!MSUL6#$&h7pc`8l*~{R*4dn`KOn%6U+bRdsjh zg-AT~P`Mu+3~WgSJ44F_KFjf9?*hsVbOhN3)CC_|9;=MV5WA&6Bbs4?+~K0MNM3=? zdUY9orVenZz^UFZ(8|!y;9MrwYm-{9sXKW}rKg{zzfF$^%RBT+Xu@+w5hH|)J-&{0 zx>Bt6Bp;0T$j0zP+NBqRHM|mn-Y-xlcq;~CLSE=Qeu)}ToOP3V+0NE0jT-H%Vl(HO1o+#(@+eX!+~|^4(;-^Z(s*E zKoLXo->#gM68~^>yiIXujd}*Q*({yox~X2?8#>y@D4Egnnm)?@EdJhzb#6YaaqjP7 zZ!LN)cv%HpEbD`7*Ns~CSOdV#2vyOiCc#3l6`+y1==-qPl>Dvf~oAq7Z6Ja;C za7hhX`Uy%$R3TemDtJ>YvfwPD$`#1QaO5n0TkC9Ez$B3|DuIC4ZP3Weyt5g(HYQcP zr5(6c@r--HFH_pJ9X_t+nEir=urq?Nr{zi^8vO<}PPv#XQny-F7W zY`6tJljmAbj?0{9?M#Z&{m9?%{8>KE<)A;Iy}M{6#zHKpCaIR+N2=6lwE#|d-I3)8 zY%0NFxt74W6Fi|uwy}md#p{PA9zgdtS<2RDc8x`>7I3efB!jMtv)Seg+f=U#@G}Jk zodFyyx4%lQqM;jF*e7H?LL-;>f4B>EyL{-5a_gm>$SOIQ$ck*C}uxzg{Cd<9eqL?dE)P&&$DH-8w8VYLJi3O2V@A5j0ljE}&~XrhQ1jPk34{I@Ksw48oT-X^R?LAlC{F z455Q=g*U!+UEED2kc5HPKCamSRHx(_lmKds_S{mM*Z56&&j^&8!dJCZ6-XF3#K-Kn<3rJsyBnd+w?lHHH$swf#m}3Y?Dno z3|{e-ur}+F7KXkXB!Hflv8y;%J|y{IXPi|e^PHdROPVVAp*@@m^e$j`%ua(%}c@r7#JcY?1iUub{l#mRbGtatSS=L|{{4%7IR1lU!jYp6bXxSM1 zS=$J@hUs1x3K-$9Re=e%z)P%Sx2%uajR)-wB*7r>ii5wKr3RgI1rXc|w|xj#G{TLA zO2;1&p*vEDyfytmtzU#iF_@95-Et3d#cHN@!Rbv{7Bk$V+N|aYHK|x5OMyIDMBJYb zOw8v-bf5l-!DeE22iesDB;6FN9nncX=@*0C1iQ2e7S^9;F8>}ts%?cbJLMu^ z`<(04XjeFq}lR^>82+&0wFs6K*?r}wvyXw-PHd{Bq#%| z(LBbDVDwQP0W#LLB7{7#80{RoQXp;M;4bOWDj5bEF(^w-pkthTD6j0=Su{X(vVH9-ezKPo$2LHCmNu1MeimT~iNmb+lQVVQN!hp1)}zUlIo4>^dIVA_~ z_8=>^I;q#7Me7w?A$xe2>XDHE>{u38zgEdlkoYO?&qzb+4@TnlFGtZqhP;bR12)C@us z*4r(RZ-P=N_<7xf^+J>w-(4OMZIUmED{No~tx#~L&O^VpUgAZPp=*#LG@;W?J z$md6*T{RgeRwG*`^o!tKNzh1Vu!fb?$gT1sPq|fc1JO9NwoysY z*VSUT7CR$WASFt)4P1@FEwP%QgQ0WLf6rIzTV}IX2jmpDmFG57S=`Clz8-E&P%;_x z3Anil81B=}`esmtXCW_&$_jQ-AQ9GTT?M*iJJiVeqQ$aK;^CHEP=7t~XkrIJ4!dgC z46$0RVf4-#IEQ}PZpuFc+?R3EO%s&~Pezfn1FbQ&YBaK(;k*g_a#sI5!cP|a{SLX@ z$6I@34Qrl2W2~1_Fw`$csYM>?&j)4o$vdp8ignC#C-cAQ@XZ<{mRo$HGwHX3r(6py zWXM8vILlGo#+t9^^dw^gyW(Q@vy#5_gZe#QmnXqGeWrXWP=7%NJU(2xPV;3gc|D&> zIxr@)%PrQkdA`?5c2h2y(ghTU$!lYRAN#^a(~APr{mWT-vz!YbeCNhla}Ux4kB;`z zeKJxfgB;Dn3+*W?SMA~#MklZ23dS5W2HLTL|AgFUy>}|Cys5ROznb3L% zcczQiN#AHsWk0lk&BobH*!}O09Jke`?6h5f=Q1k@atMSiw z{(kVXTSlY|8tIiQf#3;2w^}X6oay=QVb(Xsxmv1IK&%D%SE+QP(^P*JuvIsUJ((o_ z7R*)imUKL6N3h>2;6v2`Ct9z@%SJ8nfz0-eV5CK^1pa zETt0~n5>j9Iq@u8YL7HZ8oeHW7kP+1O#mCS8AsLhVDrt{j8pq*oac=L?MbyM0Lj3^ zJkHbQbG0hLJ{?su5d0jjO3@PVzK~rp-yJE`hAz@C#|ab2k;#{W=XU+kk9ug4;SyMROv_$cF=QGFHK zx<-B(U@L*gtv=#Uh9}DOWU1lbLbnZyv`iJW0ngcslgwO&WBAl(Y| zAE6U;oxcOAxkoN`gPtxCyvh90uOf{fMAsQy*FJp|X`t{} zvyMPhWU+!h%UZVSk>;JWDu!_-kCfe5JiUpNbyx@F->$_p77b%_oCf98pi`3hDisol zNz+^5h9SAv70|aJSu77P7Fy#R_tv-bQhk8+lO@ly%j6&q4v{{w zNSF>e;E5C=Z|GSp79-jSOrBva=A|D7774OAWYBtt>pMb~V8-T4WupmHLZMrb^PH`Y zcsjZM_-E8QU1m!z`RL@6v9F!LYnKvFgena)1H@yqkZUGA=-I&D$V}j!RPO4XeA|jW zZFxa2=^NO>A2_R09p`L$%vH$(zgfrS3fA-h*lf^ur5{bhvIX{YT>w=Vkh}LH>o!VT z&69sF_~f((t%g_(y2L2jO^qfY<#VCGLHRB?0}Zu7Ho)7P;k|a`Ulo|L`MzzeeW7Fk zrBb<5_Fz-?!>=>IR9Q^?Zh~?%q0_pkFmjI>MDBA= zfNnnBhi$-gd2B<=i^zoI5?QJBf}tB35bpsuRx3bElDCcN258W1`c5Fq{Yt&8dy+d^ zq4_lK=>uWHEQyI32MF@Ea; zo}Fs);SS((1}myZXRqOtbCFjiye(akSULj5g~>sm3zfZve|R*=T>PAWDJr?5~ zBxchNEqkQ&5`7$4M6)Kc&X7$hGNfnvh`!6ydNdxalBccMYuwomcDmrMxAj9ZlQN`Q z_xe+&ck|@+tZO#c4IF|~0{LI9l8^OZ)0>i5RT5miLAzxiQpG9`T6NMt?LPL?WGicJ z*Im$69=cyMn)x5y8VN&1$z(}<3OvJ_1(qk>4z-@h+p!r&WD@)z%RVz@gs0)}0s8I8 znElY5#Z&QV>4-GL13l0*{T=jiuiWL;>J02m%aT#(c`kUE6tfJ*_?M|0d_$jcAVgk| zjIkR!q60(b)3A5qk5J7*xW5dq2p)7vhQ@AJGL5w^IFC0OP5+lq<21s0;*MPD$CGG% zmIrh?ylwdjspz*uZmC+{O)OXCiA}+amg@tnS6YPz`c-HL*KgM+rf1=O2)iP!nJ=Solx)}{A-e<2Z1L&jg-oE{}S}_&j9<|J3k@QL;p8x*yc8k>HC2b zx=sZ6si7A;Id35PFsT3V4{D~68w;fksp28EeDH&4cUa@x(Fk-GYauIHp+i!FwpfS# zPrjv?XQNF``}xilxT`%}sh0CU-)G4twOZL|Ju=Gry=Ks%fonM>)^Pq?iXd1`vY zM99gNoRu~g@K)$#4*2!O0_9&K5k{e+LhPHxdJ;bq3)V%tjdwB+N89C7 zt!0-~>H%{y;K`Y5(r&Q#GHbW~v75qKR1sSCTRWbc3nX8bNP=6(xgH-Zl3HjXL+45| z*3k)CD?ew=Wxm_riYMTq;5|4i(WSUwx&NDc_pzP^u1oaq?hT349d48CkIF?S!*3up zYO#OM&s)IM0qnw4;MYHf9z;^ER*MWI$TV#2ll;TTtHok78d|hdc4#Wv{T#m=Dt*Uo z20PoOoLzT8)7DF%fgGhh&?XTVF)2fSGHbU?i_O4-EKKwgsr7}~usl`mX0_r)O^})9a!KERajXiwaIw&IW-@lh(Npcq;xYcMiCn|prcZ;@i>R8+s%anWQQKUA(FkQCDrR=)dq_8kk1{yFa|1Rsr zBZ!LU1 z>rVH2HCfoEr^vHW)B(AV;nGcd6;G%I^(}w@oz6OZ$Kzu!^%j$fWv!N7H>AXop^rKs z!)dFB1J4euj4pYZcM%2CR^UI2|H;(=0^`(iPa=DpN}Kd;`P$!(Y-r?NociFf51z^6 zkL6R;A36hLjfC!g`#H$Rv{ReI3$IwdW8g zFpXBSg}P2L8Gn@8*_*&Bi^1=i?rL($E3_6)Oh(3UL0*|=bb$t;J{=bN#>>S})1MrA zH1M+Qp@(%q5~ar{vAau@_^AZ=WpvnSp*M_x_ zL56#(E7G07UGagMoj=S^JK>JE)VjS52@@UUhv^G=RxdkT1qB@EN`T@k=$2Ga`ct73 z%MrG!h88h@v}LMv(rc1v7oVQZ%J#cmGN~UQqP44m>Zu_zvZYt(Ppq@)T=kS%4yWf` zSLj8~yr^V42TbhbO#GdzVnuQIGpzsgE-eOQ4fyrQ+SLrPX!#U0U_Kt3*-A$p`5d@U zqQx>L7Tpyc3!0)XANwO7I5K?<_?f2~KZNw77w7Pd4!ub%CPtqX?#&X@8f{kBZ+s=! zwXug0>6XR7po>43fX_X6;pnK~sWJdBRn(Nwv)3*0REJW3WS)h7^y_}^Z39{pWP)Yu zI;B@(@D69n>)s2Kl3pVldp;yo$7ms41CPQcD4Ya(H1SyAB2RKQZWg8mj zE%3^7ykS6#mC9V!{S>gOhGI^}=SmlFF@5z&ISq+#)dp5KxL2>#Qatpx!T+=URs4L6 z6yZm>Nv!W)4LL0{SnJPO`9rL&97?nfs%212p^HHmZWZgvaE8vs7xxSld_C0n8qzT$ z0cTB*oXWXlxsjHw8R8tjl$k}pb>|UZqk>yLQS*o=aRQEll@@+3Mf=?)3y4(r`s=_J z74V!OL!K-Vp(6xO-3qp?Gq3shN`Yn*u)>a`_OuqebccQ>w}#AzvW|&px67-%J)3KW zB#hiW7@{i;xLA)YOX87ZGt>-EHkT0jN|EJ@XY|zzO1L5`K#mQHn;+>ho)$@BqQsK z)v}5g2%SIC)H~%ld~(fbvWDsr*E0-1o0Rw5tAp<0_M^FDkwCXeZd+-WPed=}!9cvgm$ zx?|4fy>ig}ELsPj?$Bd+u0<3q7iwI-1XitTT}sW@eWuRjJuPUiTZHZoL=Q5hkdv4` z1>*5dbfB%;OnHlX?q>B*@HQqZ@-01>`FB*QaOF>~dc_m#e(x@pn`E`7gCC@fZkA!z zdNJo3)lX1P3BI>~66dl`%Z)tM`cI9qmJYf|0dt{WkN+vKV2%>>X8M!yV3}IRELJg8 zzhCCSqsM9)l5QWnqhlfX?9~LGzJS^e>kcs@$9WIGR6%{sJm+4d;N^IosS43MptoM= zI}It?#jeWbZNCo9;0eB?a|*nOHv>E%f0Q{u=wAJ5ey>a2gU&pid!eK=eHy2qA{(q8 zcB9fSSZ2eaCPhtqFrSxsaIKEz6t1AIm3`KV<*#QzWzBrHjMZB%*jv5_ZhwL7=r-M< z??FWoc0bNJn7}idIfo+fZ(ESZdLD;ky7eQqm>U)PJZUSLzm3@Nr-Mc6_Wf#?ubaqq zDwjIB(8Yr>CNN7}(2U&e)|L9-QBNSLCV`cTO|cx4h3xeL&gSKa>n4}5k-E1ud_kq0dNzH$8D8JPzX{;r9xz<0&*&eKMvByZz(wgf zK_#>mksWyS3y76HfOa#W)Lak`u9iZcVp$pu8jdQ_bg7XGc}9ai3xAa3pRJclxG7uA zt7AC5O&4o0vbs{_BR9^xq-s()>6Q`lqe~{UnXm zSwF!p4P*qkz39hzgLw>hL5HzA0QO_y2xi#nAUlt^JJTs_lgcjw zSJxrSM|dJrrt}(J<|?4MSLHqM+iu<;O#;7P`DU%>w|u0@pj(el`?AJJ3VG)1bQU_s zHf+>m$Zwru??dtycU#Y@Q9THRKVxrGau7%}xl2ZYUp5*TxqEQ6#Sc2s&8*W1dJq4O zORsK^D&}l5f6fGyQpEoQ@-|lw=pgcvNMPh8WNx2W?4-t7uU~rV_&wUA&AJ+j9F!~F ztE!TY^iJg7C%`nl+=RSS_-C_{nV?_zt8=d6mm#zvW2xo55VF zKFz*9W6h;%e)wqKJq~;V^o3-J@e6ediuyXh_ssn^8@5$Dck#&K7R;j_4PIgwu8JDMr zbQ&>_X1B9WwN}&G2Je~2bP^e8eICq8F3<`2zy8hB;Ql4(Ms*U+yFFCpff^c2l+AWW zPOHu6Ia*>^aWfEl4_UIA_w{NDb!aakJ-f6PPx~-hw0Y0XlerE|&{39rm>RJxf3n5g z>VSz4aKn8v_nyYPTNI0c|KV4$sM5{e=3Mw-DOe8mZB8Xj(Y`+r_NUh5^y|22A+zXo_G>p|Dg8j6)3UA*aKcOwV_s8}LW1)cG4+3iO`HU(33&nE!x`iS;^e2V=$u^pzrW)gG0-q9pR2FH0%;LmN zL-HDQJFybm#JU8xqV)_O;%_o4m_(ww6V7owJw+dvL_d@Jt=?iEFt^;<_wkyasEvGf zwL1YH(RT1LLrUa*8B#n9?5qQQzr-O?+!Cpk6-YqK*l&Tl$%C1Gk9?}lNNr}2Nh;KM z5zw`re~qsBGYNR^8azMInj^>fE75Ov>1Zet%mLPy>4Q28?2>oH`*M(qcVVs9qANT` zANf1wN_g-pza=n84zVX_oGiFrtQztapfNlDqbnm~wb5IV8=liItk1X!twFn94!rFg zPbX@3hji&~J|oHtm0B*sCW!&=eRRZF%PC-9e|&YEHs8TlgIWTAX7F@7LtA(PIY>a| z1l`)_&Ngd0_tnaE@{k-O)-U4> z@R0*ok=?*Mv$RsPgxn1Bgkqqkf4OJ0Ose#oDC=j;oCKuPsqC&ou9EYRfqW(vQZ0>O zafo#y>3xgd0sc0*ok1hN$3nsFa<2T&&EdDR+>5N_H{NnyZNA0`>o#j)1g@_k%c>VE zjl4`Wg3Gl_e~G-Vlm$_ZT*bs?BtRzXUd0}r98vh+JVhaHAlzvR#o9VV3pKRIrB$MNLVa~;RfzQn+7&9(&wuR^XULK@B026k|XrXl;b>R~+3WHM_5 z&fOaJsYsp2keX4Fj+OmWpMky5jV!b(h5MlkvptCPv+71XkxSW;ov6%N5au{SuS|*M zY}gOIS#R_yq^|jQrsQp)Hz||s(rV`S!BIVGw)>=#{|N0)h}A{5NwwY;H0fpTHSV_z zp%iAJ{>xWOyw+opQys6nWwZ1XM}An}BHx+bJ)u$!fNiP-#9FDzWiNejn|0q-8Rd=C z0omJu-l&YYlcZ8-i=v?}k*iz}d4ttJ&Z;l1F1{Q4y+gA?OWE@a?gHSKs`)_dT;ljI zD3w0JP-PBxCzC<@|J)$Z_Wnt?3Ob7Bj(pCjbojD_RebEe@b}0Vu?;fNk=S%c^1Inj zIaL1~b%2#j^<#d4u0uX#`eM1uNAxC#J%#Vy>W>=XrWQR*o(?ey5J}i8^M&4pK=ju> zmnUYiE9?G8$459K?CE8qVW>GpT7^tVuA`eilJF9^@E{l%#Co1oB1uBeZLZiOm9jDv zEoTEGtayC~EKWh;$Lm8rtiw?zpK2wS{7CvXYLBMDyL9Svb)f{Uhf*)~&ZWyLO^lSu z7NVYM_(XT|wpjVWeeA4bWR*s1l`i3G(Ho=?JtAJ{e+S>#yr^7GG-mIylT55}Vj>zN zk0XIs%WbYie~Z1LY8f-k`Oso%lW|SObJr|qL3;yg9=2FyuX&OdL7iT$Iv{Igg%G%)2Dq&`;+z031fNnM9DrCa_DJnp&@8Ub_hx!svu^L;e957oTk7=0b z(E7jd`iJEvx+ zak`d5g?56p%8zaY&mA-44C*|`GE?Y;f=>@!8a&ViNiB~*tY+;~yUy;+S{p#mvpC44 zY?CHr>c=t&C4KKRbmwI^{yHXQVhLuUr{wAwu{ta={VSCwOns7#P4#shhO)YHI z%`O>=+RkoP;|0GJi0%Mpk7A{6ce#2>)E2RFF?58-ywwu^=&YXnJm6TNZSI)}J_)RSn=UiNd%ng$@MlLyWucM< z$Y-Nmzl1acoI@hYLEjPv;(gBx3}5F zHqWk428EvUYW*^*We}>g%H?6xjCBGk7!<31B-#T%Wb5r*H>sBSk57%2nAJa_)UmO0 z=8I~fT7>TicYt+&eCX3(@kF`|0?}Lbwh%MSwO>1YBG?^*D$1afjl8v&ot6r^wI++j zLhI<0*eP#@n3p79ht^|nRPme*tab-+m3LSJ9bwpUA^2YGt#exu6wm_=((##VtU4 z=quD(YZsiA0>wPQGbZ#U?FWwKaw=BI3DDFmR@Dl8@Bwfw^>?i5FOhPjO{Q;0>)VSY z+=+*^+Dtq)gRVa54RAVytT<-ER8@aMhPvU3GIxXVq z+ciO`$(1w6PVMD;>!H>rKk=_hAzBM|vmDLuhdjiJZ74Q<(`sM8^HKcV%PiEpOyrXn$+RJBsz@jL0sQVw-odA|qXq{Q$`zcXqkm;VB6%CT8$FY+U z9p|@tc0$#K_5sP^paM?m1&iBwmR0pyd~%2#SjW7b>}^!06H`kEGb6~;C&=pB%N0pj zQ(;lL1T0sg{l#j$R=EnOwI2C^6(Mxqk#~?U`8p`;*uzeZ)_*(e^GeOJtrn&}<~Gh^T>V1-#io{i>ncC2SNJwSO# zwzzoiSdt*t=8x`sR}`MmKppDN$^Fi@N)M`}BMjU7CBiJlKO@XtsS&$eD*b7V{} zMedB@kB@^U$U4&9U?W?);SRb@qCFG<%c+2_WKbYJdg!%K4g(HFJf&1t!w+UR%>!r6 zd{zUeQTYWHPr)LMK{qnXvqs340KOYJKP;=do+tH-Rc@9EUJ`INE)A|$E*FcQkY&pH z#`R6;_Yqdo4u#TX1Kub@?uhP{O+a)$zUp#)2x#RR25 znPA~+{XlkU1{p5j6L0E(-ed1{PJvT~(fv+_bCJ}% z{UM%ZQ&PxJ(9K#PmRUeHm`t#uex5Ob#?+3U5$02ihknOdKxG=&|LD(TMV4LrB+#{8 zSWf#O-2i&XAcUJ)#~kh9``wZT7g#^mM`BzFYE%O z;66EuXN|Fg6S18Zxi+Nl7Wm*I-=J4|FR99O6L|zbk-<64`Bn|&K0#}aAb9*G5} zv$YlLI9+W<#$K`b)>}M_cpN{c!^c+h+CpyYFg~MBeEoYhjW;9#_bz!2KC#|3G58NY zfbw?AeVjPV_SQUfkVID`XHO2 zJBoR!+9FkOY%=z&#lh%sNmWKvD1i2{)zFCR-@y*ApXazCe-x6mBajOQ#ZTRx{#UGS`ME3T$-Nl3-pc9n|=vW zp$zYG5AeUjuHm~5s4hjSWforU6fKT??{~1{6rPVQ16NoDSFf6F*odDiqP5Z^55px5 za-8`uStngzRgspNI@5Q^DoM~uV90C$up6g#Zw;J~!kO5|4(VhDy(P+S*9A-)xguUR z<0)w5M5)pw&g29n3%zyZHJ?n6hqL^3Jb$m`Z%>yzWUOV$ zp5iSp-lN}LhmI~na;GTO_Hc5MJk~eGQbb% zfHo!7{NTV7D4Hj8h}}|`qesj2LX;NB$EfYP4;!*a4}&K=f!U{OdTh1aisah~7yaI^ z!8ZK=EWHJM-RE`x-zI6ZK*O9ioHW!xt^1rdj7=JbhN)q6&|$LW7-W=XOSa6GZCMt> z{XQSD#ms0sZrbiQ=(cv<+O6GI*#CXJ|I07Qy7zwX1BagT9Q62&*J7pBTNfu=D_ES5 z7Sb2r1FJPwrKTD6TA>#WJ2+MiKjU1Yc)vd+An$n<=SHiwW5X(XK2)K5wyDgh;9%sH zA@@7b0IC9=2jem;hW=i#AtY&{Da{IJ6ZcBD3GR*_6EubfHx5OUH*W6VdpF`yfoJO3 zQ$6r6gMK^*pH4wUP3((X@qG30$?<%WwhHcK+f%DFGg%8~02f=vXYRQeOkM!T<@;?s zcqX+~z2LD5$MKaT(?Lk{d_U8B4zj@S$2)%Uzx+bBADE9VeWoApm4_2wB7 z;KEJ-r3`B+UX_yQ`LiMPrn*Cp)}(7fHj?aeW_c<8ITWy#Z19-Lo$q*b#wFaeR&&y3 zL-R`+y#{=HJ*8_zmMt86JhcH+)&p(21+x)MzOOE#gH~FvmB1yv_`Hdcgr3)9mvnD{ zTfh9g&a({VXh7M?c-YLUTlAz3$3i`+kRk#MDtI244yp($@woJ6q&D$0_!>r`MsmA= z&Rm3M}{7?a=VF)$d%PCU z-bULVyy8m{^yePUO5r(0iY`xn5hv7X)oQSaq;3K}6F_4STH{1y9fRCD+zKT=tI~6k z2wZkdBnoHunNZ0;WwcrnsC8-IRs(cPT%rCYUd76WZM%-M4#wIW$Ke9*HvB(q8=;aN z*wspbY6}(r-G=71lgG4H33Iy7{p@7ShQZE>VCxCPYY+8M!2(%FMyt|r#Hmm=E6VW7 znx1#|5tfnV_=OWF?hAOOQ`ljFO}~yyI*{+>n$7*~cD`IWn;ZrM3O6+#9 zHDL2J!a5pR%>XBadp}E+4Qyxeyw|IApI9TFgIVSgut;2BaVzlySAm}edLBp)+NTyn zGt$ZR7Vj*}=zPcZ&}vqES-3L1gDozPnWyXl=(E}0v0T>q-EcJ8NHg}p#cIdR|O5z z*Cl!#25u5Is`KJg;N%`gB0^fLIgz$nJ5Pk(9agBzOQwh8+Y-}TB#&8#ll{$H}mHbPTa z4bjwxT;^~-m9ws5@Z$Q?Ae2h4LCdm7G?q-*T;bE;DRP6DYaaWWK*PI8(~Rd;=z8o> zeJ z3lsXyBin%?9M+CR>cac&o^v{RGqq!R+^tDoEyU;4Y>oUygecnAW+Wc5 zEw-iPd88Rl}F^^lT6VYw#?;*Jd&m;8RRy7Kpw%Shc z*J#Aso5y=k>r7TOh5fq+c<<)6&`qEMT52!eC%OQ?KaKIcdG#%_&(r$ z88b=OF#dwLL{r?4w^`&wcfVI8`Wc-#puJm(of*XGae*yTJv8f8pxpX4p~vmLu*E#G zP?_jfj2_{6lAAjU`I6`d;9;-Hw9~hwR@&jDGuTQ@hTH%PRKb)$d^~qwKcA?t)ta7F zMN|&Eyc7682ralj4{Mqh0SnK5*a@WSECF}qAvnd^+fdOqwCVz_Fz>U|hoxs!H9R{E z{dK4cNBDW>RX6;8=#fk-p-r-OnC%L%Nqs77$KtqCo`=)LiLk>to9{2UH+NDENUj!k@@{+i}>&>N6>UOTgs)vRW$ z0xX$3sXTiFJeCFKJ6Uy$y6xG3wK>_Mx%S)AtRNZc}p zi1=g;=JBpntqYaWW6|5dF=P(gh-oL#v0d8s7Q{YBklt}MiYSUJIIcyG%7T6_U^!r z`x^8*j5T5d&?Tleh`o7y%i(a&->?GD_kSFH>KDOl)vS*Jc-Dd=_f|IAw>WE?z|e`R zMPu(Xk3m|5Tp?Bpx?coMu7kcFvE#!-e0m>L%Q*!dY}F#-qFSR{H9n2MO?uIa>+Hr< zp0bfaMZx==F10 z?!)V_gOC~da4(f?bvsc0CJ?+xX{6r2#QCh@8Q^!2^<*po+7*@wUR!x@I(8D(vPNDh z-*b!qko159w@VCY`HUVguuXS`eqiht{QIpIoYI-wRvA%(P;R%H)DE@mCc`m{T}{FJ zs{@fK@Pg+IQR^Psa1Ch>>my>DIo1=ecaOfQW1!f})sGFKQJE=^u{htVbcWugGS@52 z^t#$KD^&`|Y~g&nhjZBTZ?n*?Ymf<9K(G;4>`*mX+!ZJN3FPgCkn80repX*U2URO!*z6GLFVEP1q5EtSBvG-Tl|3r$S;CpcapAUBj-uw?H-4(0`0>=V=3*tHHL*)e?AT$THQV zKZSf`(i&vJW}~tT zJ(|`ewvFXjN;ZS{uIjH@D;Cjl%}eOo4Agd7AJp~lso!C%To3Jgt%6L;(PHdZ3%P6i zssB?nmrs?Ya(2Hbys4VxQapy+Y(BTujbN{rE^Y_du(sN1j#ZllL_F({=n`JN&G|t^ zS|rp;oy}R53p}W?3KSoPmd?{@%0|YGTbZbH0`54Ek%OI3tjB2V1GbZ3-1`o%5|RH% z#x@|A0Ea24^;FvjjgP~9|HHj*E424@5uGW|B=+0c7~gTr^}LkV4xU1zB-fp>zsX3w zjO}^RU1;USK*RH=%b05!JV|{I_{!s?`;kdGY(zcACG5EbeB1zRZq_DF zysPytdp?10E&*>->=s*(Ee&;|x(7QcgJv#*0_(Yh7ud5grr(Kc!>}$AHdjWf(-+() zdwHsubd&>+8T-O-gGa?}fL1su zz{=6cvw!6b`zE}W&C2LB3#{FHiyk5Pnj8i4g+*M8l|!|yz#vQQ;HerI{wc#V-Kaaj zPd|5*M}Y#>d^u0N3py4NE7DSJBj%)@6|@@_3>m>I&gXJVYzIbIVz7bzHdU)SqWk(d zLA=uLlStk=xSBPDg;uIRpegn9?OGL}`+7xym%(RQJRJ>SDL$&^RF`%@A*Uq=vSz2- zxqSB{<-`W~XD+K41808cA}fa-(j5eA$EX#cQ$1j-u~U^{U74X1^#Hau@BOlaT`c08 zboqiJYSB|(0*b{t6N(_8J*)#Bjh3Lzdt5%c26E*n^BD0lo90Z|La(t_B*tvky$ZZk zTPv%WuB?dhdDT5g>$ zVOMVf%NB0QZ9t;kJU4oXS+CN=@~rF_VrqH1dI+Avf1@LS`~{3p&Lh@G2&AK+M|&6!FFpU*k2!-c&v(<=W71 zciL0z7;i${X0E?-%5id*>w5n8ifbNKxE2oCrea%X!L~wsd?L4_B*T}K0gTfW1-WR3H9-e zxLi(Muj__i&mPnw?Skib#1eSF#m1n`d!d`Xk=_{gkhLWxYSCH1>QgiaBCWyA0#yK& zN_K(Q2k5uJi_hwJiQ7v4GryhX5>;M+-pD5BiIHwKn{G7__>$I7_$Vg(eQh2B# z+>R8R%}$0j#)-NYdGi&XfV0da=i1<-`9LKBKEENh0-G;T{up?Zp@y{eY~o=?oKOQJ zdL9WkW}PeIvBc#djFJn2~jd7&N(?9G^Br>Cd>f^O0Y%B zyA*6F%eK!(Kb!onn0mJln0_cB7ZMm51y?zMrc`AcrQz z7+8bA$0#)R0TwU!-%OzuaBA@>6-~GUkzoXrR^o` z!o<&Us{91onOlac7~`dkCt@A^>M=zPaPl9*M)1;t+`*~{=U;5a+@%j?H1a>7qT@N& zo6!XO;i^ByIqCyPHR?cmO|YNkP(T4^Z4S_K+tsbe_hR(@`L+(66M?Q?yFfdT*bV41 z{~c0TMd^Huq@^kc@U28!^-9%b#)Fj(V6lUCQ_0b`66V1}YE8vq?R^e3WNB=g{+B3(V`Z zO<12^LuVpWIc#CQ8#w{IHfK-@|7jho>O_;sC)%}N<*e^f&er>E9@-HYg6kXO4taO> zLq+XFl|iF3l0)E-C6*0VyxvGVw_J|^^Gdq?&( zf_-v`9rOdOK{S9a*2wuLER#^1>jUm{+YNuZ=Ir_jJqMs2>MilzTO_1l8yekq)ecoDXsFO*~CF%!pA49#}O8n|aAXL=X3;|+91y&L#XfRSUaS9a(PRH&v-K&xd$&k!NvAynCC$r*w8NhO>oo!A7Q1@HK$@q%Yt zD=;_@%k<4aCsSk*ax2;dZ>Oz9eh2@%NcA4H=vR&^0P-kaCrMt8hYiu)e zx!Fh8}@JzR2(H)eVweAvIvzUP$xOrey!=#iA1QjYtxjd?j*LPe+=Onh zjdQ*9|Cg*SLhl0)o)yr}{~ooG3zw{fHa-I$S!iXGq0CN^_k!NYJa?qC>Cv~s206*Q zkU#A@SR)#?XY9?;s5F(v4^+EhMIzhB{Zp&>jG7@xpAA5&!%B5Nt4GUW&RQhXkYzBo z#{dwG8Qibl3#6m}h8Na?nOD_>WZFm$?I*Sj+I?S-U*3?^z>`^2bOMg*0P}tZ9}o+G z*q&Icn{9TwQTNA>t%te|m)ob%DbIcQ8<50?=Z%TeA;6htOWc1DKITPhnen&UvGn(wu`&5ZPkZF;cc=%2JA!OP~_Vs$n!rjN_WVzX4ddk$b(*XK)2iA zcRJT5-?A#Kc_oHd3a#T^ZZbc$rR-z`IOo|Y{NXmo+Zo-ZopbPm&C1rGCr%JqblfS z2kTk|g?HF@fN>7|xj`jpTmK#okLC1idx2S7h37yo@QniS{jzx#mQ#7kn zK7ca+4O=XgrSWxnHRqcQtC*Fr-ccZwhqpEYy8$SG`d|THHN0PIf?lvjLNR@4MdaiY zmo|hPm}LK6x%FXfh9+LoQDM1lM&~=jejmJcVlg~92dcUVN!O|8!)eP$G@n@u} z?G7-JvPYrNb=D%!>#2rL>d=oyEdi2L+G1DJYC$%(fivNO?T3c(-&-~N+Y43GVN=~1 zoqX1-ccG#+(0v1OOegs5Ocnh73DmI!Snbkxu=0JR-%~jwt@EIf-r(`xiUGDsCD{8`$y#c)Mt%5vj*V0C0L{YA`~+0MeIHn~lOSrBTm+50#A)*# zhh6j&l)DArYUeJNZ-vOuM>&no)V^dLFk4OJllM-@v?G9Px2SwA;>+c^)$YHZ2Q0mU zfk%OMyZiYn})F?6BKy7c_M~cxlnKjB+ib zZLo^uk6{D$2+oZ7q%x@lF^;GHi|{hu)Z#FK3||OEujWSZHXKdXKJaZ|z4c)wHFs;_ ztP(8+_Gu_(7hFI!%A}mTDD_X|{m-@|qh7bG!llf<9iPQtlN#$wou_N4Zsq+o+kx=? z;d5wZ2P1jCjTeFGoxuCY=oekw&Wr2=q8R>yjqY%`@@61XX$_odGwsJvbCKq!+!_m? zCY9)g#EP;SzoUF76vMw;;jn>JYieJ-6S%bj_XD8Y0;6Vxwg~$UQguKhaO-Y#8n>{! zhq4q3NPybc!drayf=wpX(C!4!EQVJTyG~SS)+T#32p`=q6Z*D=tKgvJb}#zf1|($( zxW1IQq5-I201mBWMvqgbE;VrU+BEfQuouzmyvkgGc?Rz_%Hm$R9g3wI9Fi#ViD!X$ zUBEtShSl=F*PnV9Udu$+K&N0tk1F~hRM#nDx{wn87OAod<+%@}hnpSsI>O&Z>TOyD z81j>_9-Q=`6aNnUCCapFbPVszFbn96F~+E6>Rj-#7A)M4mFvuSHkOSNX6oc-a}mF> zm_aA2vBOc>g?WluS*6|p+Dq9Xu|%x0$Bu$ZS0O`wsvE;^;o5iM=x%b4{+xK%HIEM= z%8mPY)qYOFAIfTEv(Pen^-7qOXK#)gw*vN(rhcN!y;7j)`M&+Y>;=|7gmixw$xm#1 z{MP>U3@K_#Yk^|Bo(VfO%pS7r@j%@da~}2>6TjIsr+yTlj5*vcZxC6sNfBP~J2fAg zco56xt=RF`L*3_xUx)9(AC=)(VJ=vG4L+h58F3uhcufY8DSt?Lg(!~=DdPn244^SR zAdhohs|NPJDBi@oUj1nR*sOpv%AoHjwG@m!stn|Ehn<{i5tY%@kpg$@?Lp4oUFa4| zq3_#ue&TlBdaK3W%Z&2M72lHMsS7wSK%PvYA?yVzcol4o_VIfVeP@KT#&6s$RsqkG zRmeL07VNQW>v)zrV9@(tGn#qjdUcQGGV(B*_-anXLpZPN%#dgc!DT(Od36l$b~nt^ z^X2g;75r-iH*V#;mph49yY*<`a?#D5D2^8Nye@7KLn^aw5wp(EyXF;I$sT5nyII5E zg=RGUG3rM-?csMdj~yhk*v7zPDKrpy*CY9eljb-4t#9*v&uV)cX;EUIaKC-u3N#hT zHx_b+lWhS~0+gc=H5#Hw0Sd(WrZQ%bNokqmT($HatB6r-r-VfBKncd*^ zy{r%KCEu*%{ZaHaqK(0QFSmD(0HAs_K89{M?KVz~4)c1`)2dJ(JNDbeq)loRSu%ok z*Zt&sfz(`PqvHmnyJxKvzX=ka=SPt5+u}AxJS=#%?ONr-jcs=WD~y=Ewc&pz6Z{w;2NlTmL~DvVAiYBwAz5akG8Zol=1#@D}^6Q zY$a=_i!$r3WQNOaO$I(HILWQxOG6_tx=-JM<9{8zpUZHH8eTzA!_Pv#eE|1PqmiZI zF1L1V1AkRETdUB-4v%fba(Fh`Wmu}$i3+^7QR^7FNzYn6b6sgGfa^lm@T*Xx*X=W8 z#xm}8A7Q^qD~t2B7hU55?n{f2Q`78^$a3U$qLpY-Zr45r9fjz8=o$$O);|yHb$pz_ zqBMyN^v+TJz`qF!=mW23tHtKvKRg}FX&2)Z!H-_UDgjOjwC9$bB0jmu{+QYjZ;6B4 zV{_ETy`Y6PC9HD=Qg8+O!7PtlK@X_o^UI+ZDt>?=`qAqV##mz=Xy4rL?6XuMck6wz z0H{m>eJX+rof2%(9gQ_g?gt*@f474+^4ApSR;7p{1D6xZL9R{vnpB5HqB!9IzyEtA^O=U9mRY9Yy_e#@q}(Ekeu>?F96WjS@w9nw=4s}B zh}C9MWwG3h+tXRxK8~?%oNa~nu;tk4VX+$Z1E^yQmd$sN)<-Cc$_&EZ8E)qc`kuL6 zVlKGp*XgN74f3~F&7Wkvx8yZgdZ8mU9wg$;`oH+C`Rt19dApXoKz=wG2)z!qzQ{XM zx<}JGFg^-pOmnlzvQg|nKSO`?TEX}&kO<4nCGI36G>KSieGeXZml+?830!Az)U!yz zJ=SNV{B(#;6u`IFZsfThIL_mlJx|`H9NntyKFiSmh6CdLJbOJd{7x%RYHSm9LryF` zadU%Pam&yW{f=Kn_Q9wvfif;<7CPV3*}MWQYzJqnkZ{ZOes~$n_v7{q@@$SR|Ngzm z?GvmK`_Eyz+Fk*hTVfe?!RwJ3s~8dg5xc#a(X%v)?c^)?pb%^OPdI7GL+10lIXh1V zWAF0wA#jnZSX-Bf;sHhhfTCdf-`w%cn(5S95^0;_apy|ZVbI*-*o+Q2qYPl2qN<7TV9Pz`|JU2Z&-`(1wKiy%NI>TF2xs@KdJOD{HiH+ksKLf#$loox zKfT26;n_~k*Lx#TirN$ct0}felM}I^WUCR|{A{SK%$6essAXxr@brDaZDXjhrQq(@ z?0Kf{;cgUTX;{lya%nU7|2f2(lL?myx>_MKqOT5jkm&}%alXpyMwITI~>R-lI@QR#utdXNpB z%tHNp%ZDS*gF|ZRtG)_+Pz_WS8sp|LEb7Jszim`nL<_V8h;f2rujnVc6p0)kP$77| zo;@~&y7+Z)Ku`G|M<96>?j(MfhQqDoIFtkAU6Y1J> z3?8>??%m|%!vkKgECqcfcoy=*h1dnFsTktXqTby(7i#)nEI%HdePS$Q&JU=SnF10Q z?&s%WQ6rLXd!5)KYlIFTWB+dht@324Zci?;F1(- z1fBIDt*K~kd0{Tx?G+Tubpxw?3rMbJAM2r%ZF(bYjLTTvB;wV2AcKq}aQqpw z{WWHZSm3ygc!PE5CB6G;n3D`D&Lab2l2N8u(G~2*eF#L~5)bbYEf<3sw@rG~z&2#j zGPorpaf`oat+(mk)J*C?H2OFAaT%2#iR2Ht?8@tM-3atNUWG1LR)?Oq9{Y~-?Jjr+ zZBt`f$>;{(tyew2XIe4y{0=Wdt~^q6g{Eye82xp81o|p7_XMmVGNj7962Onbsd%dL zIn%p%wQWB>Yv_Hji}{MR7+G-<-`=1l=}y-FZmNTkyl?UZpLJ@2^)?|pcI%g+Linjz zSr_wpETMONbh+28!c;l18P!sro~MP@0Y8^R4wF965Qo3_A|1XJ<6|x`fy&waL^Lo@3nHh3wN(X_Vz(R1N^m+Z`0)pD* zhTzu_^qJ^&w<&SP(jfy(UC;1Iz4Wo?vY0_mjl^5jgAqbOBa%6;znxZoy6I>hW)M4FB19|BP$+@6`ate-0!^K^MUafp}{VL zL*3Fz?+Kvc^?B$kxdVCa;(wCNn}T@oB=!RZVf_lefBxJN4|*ohKd)d$2=yzhIMrs zIyrvCqe3QlOWQK*ge&J{c2IUPKvYzyzX{b8rZS$!G!j`jf5KOcB(HM+P7J=@OFImDQBu)`v# z&#ffY_7b0T040x0YqbvbG4omM()%~Pfkr`y7g^F(TrHbsp$>G}lsQ z??=}h)T#FV3@S;YS?vM(`CxyL=jzzq1XiucUg^qftKl}!%jw{J>tQv0K&?~t<~=2d z1miC94Xq`rAP`N|k0i@Ec)F14Ne>yLrG>?BsyLDc3>;Psc_(VxFhz6~Zk z&$-tYLpu$63V3-|bQ9QUfR>w5bF~>-nP8Xn7z8$(`J8&f?EeQu1MyuO&`z6SE70c2 zrG#&%u!McWh*)-Y88o%X-icH(gEH?1o7=5S_hP3w1)Ze@Iw7JOY`F*FDtM*aE>8w| zRbtP8$?>?E@vG$V$W7c>$W>x=k1k7VlHbI$7`deUPpx=M^O@@dG0~fun7F_ zfR?xFeH+qAvBB!FbN&$Os!*5mu^c|a`{;vlE~k*!OxO&E9~J8u^D>>mxa*B7ZczJi z$p&WskX0QUx3Xt!dvN%KbwX_&;Vzq{wd~TZP+pPL_46EEnGh+>xkGg)?uhFYqWk2> z*;5{SDY2(@zj&qmQgv8?4nL zJE%9R^EW_y0#OXxlQMeHM$t6gzr{P$?)sy{d>(O_!P3r$d1DATxdlu3L!o%(Edz^=Q2TeP4zM>%rq z@d98~V?=sb1-Wax(91lQqTMVZi&QRCspJ8V6oR{E@N<2<%eLD0ILC*X(`8-_cHeUu z->WB>>lL(N;sa5LFz*z!Md4{-NZ_~B8k_~ z#qTPQAKQU^_GsC;R?NMHoM_cyOzBW);xX1&XPvy;jy=ciho{JO zoP6@myV=9xaCsK*&r=VSH)^>W;}$at#+ulH*UtPBXzpQ5>R@XNuhMpM7=s7AzsepU zPBemXI`DflGV?a}=r^p^@F=+RjN+Z(m1=}KNUjr*i3*O+V9hzKw!`LF0c$Jbv@FKr z;}yf^Ko4YdqFo$=jGV!bXR+SKt!omA;*oZn@NjHZrLfJLYaByhIS;y>Z{H@z#CsHf z71DMm5FgTUP%zyY8L@y>+^Vx8`EW)YtI>IkdVJ%y9g9YLpr*f39XsW5>Qk)1E7c4@ z1=NwZbviOs=rcPMn)Qgkaj5p(Bmp|p(0@5|tN=TcL=U^IZH@ghmNBziOWoQ|3>*>w z8=K_-8P6h_40j2?09=E`RTIg$di5Mql8&ObAi0>)t`7}rVdNd=6}e6Vf~O!)cCn&L zB=An_1AZ;`K3=mwrMB6ogc?V&8@~U4u>yFmg2K*8T@vbbNbp)S>zJiQrPhXoxGWW* zYtMaMZ>!lk8AwN$ae{&PgZupRh?= zVQL)xf_`p1;T{*_ajlP^6IuI+?qEfnL%_8Up4qK`;8qAs`HcF1z@pI7P+!S{pYed!*A+I*+>#Db_ zxrvaop_6UCdeK1Z&Fifm3-^`)lLdzD9qt=d2b}{JAZd5;e-k^yD{RDtSTwIJ=JoBj z*=FXSBR`#v5Z#{ff$s@#=oR}KJO)j}b7R`X|GfsK#RKq2T?gFU?l{U54MgCTM2|;J zbGt}vnTQF3V}8O8&?n(@k4^Xb2+<~u=p=g;9ypeF_CSSXX~Q}G+#aNi-#Lo$K6+-( zO&Rl*W84gR!YfBC0)L)yT~2&AF(G=0C;gV^`>unoM>*4~z|N>WWn1NSvZm!#MZB(v zS1=vn6!-eVd)S4?qwKPFn}kjl0l($&wlu-(PGGPAIkO$kaF1M%R)9yhIFzfHQPRlY z5qlr1X@YJ~wxvLVzW3k;+mXE;{xN`-8}>1~QVpUIm@z*g5h9>T(UNl%AU} z=v#3yE9;C6wh~M>!*yi&u+yK#g`y@dnc4N~O}$|iTEmVvFy3PA3FSJP)it5P6oKts zDwTUFD{KR_C#2F3q4cO$^u!@zU0zcS-_Pg#E>@PU3C{p4zn`?4nb#p|jR)!t}z_-c1tF|AbF{~H!YDm>~5D+YcpNvQ~jY-!|Q85(&ecOoh~1g{S|)Bdlz@-j66 zh0b_7a9RV_=K?b-;6S4nacb4rQ_OObZNXku#jegoR@6gZ9+SO$KkW|Kb|@cB0%#3s zIr6QZyK=UT+hw{Em`(uGv-!N8G4pH{HqmO!NqM!D>&CY{^Q%B*M5t9-VGMqYlJ?p4bVzG7>?lA zC09GE%C*87-FgKIqTaT8;H)S3-H$z%+#%D=X>~)fkKN$u^>Fnc{PqDDlJY*Ye=XNY4lXXhR|zh zar|`x@aRAu6ZgTI|1-5kQCAwdtr=ZF{b6*{E48EM$w65X$$5ZC^_#uhrSktYqLOToe_ z>xYik$SZ%IAX$mfM}h7uNUP!fG5I0h^{Houm;`j~QbJWp`Tmgk?*>l?VB?Xpgy&7rYU0U>am zjt$6-DJ0d1d4v-Bd8&#N$8UBW>}`Sy>C;fr1l+${MXarxJ*Mr7kQ>)9VlgXsFPC>L zB}zloPmxzTA3=+6N2XMBPkA27sO7d%!;^Sop!R-hB@_}dQOO!S9&7`U&gXZZI@D!u z0e{hmA4aR_(AUlL)8EiB(W{Oe7B|L5)-lC?hKTX3fj+3sja~R5s-AeYp=sVpklDoC zns++d&Mm+rPw+S+W$|qB`8p$xL?d=Da=8Y|SkHbx;cl=_p7-Xa|dB)`e+oW7Z z`WldM3p^R|WVbDaR{Cv$Rw2oDgPn0($r#?F=1b&CB@%ic{5+t2#8U3#)Bk(jW78_+ z?|V4gdKkMLI?2^AH>Mmtl(87E3%NYWYjEkuaNtjpb@+UbN3wXNqDRqqEy+avXcxKW z7CfSijUMXqNc90`uHmy*=KT`5cqJWT)Unh4YrGCR=wME=4%s_3vw*Ol*X~D~!KiBk z*^R(hK-7J%E$>1L^?js|-}^jt8mY5n&^FVa%3k24 zqs}bfe^q%}0sPQ{ful!2tc-1(re5Fh3m{ajZ=joZL4iY@tYf@e0Pm&cwiMUVh>d_A z@{mH_sgvptaQ-wq^m^ib(9ZxohX!wCL}G7V!fHK3ze!X4^Lhw-G{}0k^Bnpd6=J;0 zSC1VQo3sI{=wR^MbCF9|*joEK)Vq*x6j(+2Dd>8p z(b>3$Dxi3_{u(w1ujTK0-bT3dU3mu0W~BmdNcGySnoG=*7s;!!q_dGNJDxRNYQHp z=}~0!;hA~XVMUx6*tnRbQ)|PGtnj{&1BSiQXf}8nP$}50;GR5Yy{y;ya~r<+zj7BS z27|6|dwvU%_*iy^IfsbRRe_O-PBj*<5Z0*qoWA40{PswE4&KRX(US)`S7%=N=|CX+ z3bNeml2CD)&uaMA1o!Q|>}?)Wwgax}vr22yT=Wj~E4c4o>(sTWrK(mhxukPBSx1PU zE`a7LpdNCA;WjGx#6N&Ta@)}AM@4lx=)*m$Bdi0R2RM3`8ZkhrX*~(GoUPsDhrbU^ z`}xz&Q*QtpKMg0Wdd85gH|RRU>({Jc{?DkjyH+Q$mjO}Fk^5L$WK;A10YpeW3>3~m zkJw;q^vl!>=9WsYMEFgt0pu>pvjsOm<9$Hp8ZC-H4=cc6k!}M!5ozOIY*O=Tp18UwIa+h7p0C7Ms_W*~iGtWyB03 zSN8GjFe5cV1Lz98BC3Y@iGL46d^*LeG4#ewV782?FpqQgKJE#l_R=?w$bX>xYTRWv z@;_C~&2Np1z#&$9H9}|C@y;%N46Hmpdnz`;Wn;ED{uN&Jz66Q=d&FO-8o15lnG2cY zFEfgPn|t*t^$7Ni)7kw)=`rf}EkTw&6Hl;P%yDo%cO|be*NdOLOjrxG3OV+4I7M3+ z?I+-~SfyCr-n7*^9Z8JE96npkj@|axBX1%X0i)Tv6?nA+Nw2^}2N*Eq_e$AyjJOwXgm=Bn12=vez8|hIzZE_n zvauK4v>!$*fZ#e^V6OqM9HK&VQ{E%%@`$GhJ}=P4F$+q4%CdQ`l{}N1l&P1DN`2gF zrtNxt3bvay&#I6O-$Rzng;w3C-imxA>J~|!r6pEs8&hMpnf$2J?;Rp4ZwI5io? z`}}w%&EF+u{KW}E>>S;s*dMvleBv_Nv&X8#!VIrwj6aHTy!Ih6ykxkIu-9A6`)3~x z9)=~>LEbD>UJp+-LyIH!G@tww=w)*H&j3^X^fGOv&aB&u%OiJl##n6)sTwu1h7&b1Tu23;oq!^iiE6KQ0FgtL!2bup*X!?|7lZ5YMBmIeZI;~zsAovc2hyV4EuDDzz2M?w#=7x$3~ z-mISxqqj&kdL=GLi#Y}9=T(+l0Vh1h2 zKbmK_wgQbhJrT#z*%z_SbNA!A*EZYR@@({O(aXZl6>-OW{|e-`XL_G$7l&RQq&_5U zY3g`jx7_f^#xs%mL^;?>V6_B^=68${o<}a*Hd_aex9UJn!zEa&()^9y$?m$KHP4{( zjOP(M*dA1oCah3Zb_r)zH#|X%sQC)gNZV&>3OHjoX#@0GX}=5$puRpNY>5rCi(1WQ zm+j0!9W!P17 z0d3Qk{c^g3bvCdj@2HY3>I7)Y{-LH6xmpb_n~;R$mq7{m^G@3{Pw)GZMr;=Gj8+YE{`6XW_OcBT@VRs zJK4)@q;50teN}DhAPUTH%A>X%f5UsR7+KMzBCuGD!D+NE zZUIYq;aS~gm#7XsU^#SfSIht+BYFauHGut7V;Q=`!{C9O({LnfKNZY8%iO;Ro8rm3 ziTI*tGupvqKiQjifX|UI45bfXi7K`{;9I~dh=ZvuiV`AMu&XMt%v zxyZW;R@81@@23x|RvQ#@jNS{cMf40&_fJ*S#jBFwte=CyFL{?5y=f(E?!!fY#IT9p)gu3ivj$lg-fB>Edn;XXS#ygODy` zcm_IvTRHcw)<`|;^fKtdqaX1tr9Riw@T1qNnPBWyP{}CV*#J+j)EM)61(hYNm|FSl zq?At!Suget;_aPAJe&N#QdzvF%afemb9ET}vJp7kiX_bidq>39PzcuRyaST*@c&Y>C7c+OaW=Q0jZnj;jOpYZwJ5P-WZN~& znH5WkpKK(SZ(Fl7n5aM|He80dj&W*?n!)rfwTOJ+^ zA{*B$mg0;dvtP#PvK;KWzikQ{x*3@Hy&yZJI7NoJF<)T^1kc{y9l1@Xro-RB7uXA+ zio?{aZA3Thg5T)4WbImyl=q1GKZn=N_20Ll_W|rJ`Do~Ao~yPep|46R;EwZ27vJ|t z;T>AdnvXKHJ0orY_)l}{5H(WqV)|I*p{;7eRh_Q$TM2uE4m)Atx-=PDHKc;Wn@mLVuhj2g| z9|)Av&j_#EUCe;(0RAWP3c6_2-^U7jz$Q3**2UF!2mHtva1}x-XC& zx7a_RQ&ek)d5*+@m2iV8Kq_Q2ULUJ#!f)KqP0K4x)SzoEwIN-C7qFKRsqVy1r;z}) zP(vD6Z?{dF1Jw3t0vdKJJ)KRWS2U%XCbaEQ5WV07k1_u*uvX4}&&>Q%UcK1;Ut{uG z2q&R6o(8_|0!|+NHV(`xkPPH7;gzgY1ViF`pFx@b%l*saK!zA|AyoGQHEup7`tV8H zm9YlsU^7GluZ-1T3yMc;@)%Ul6QyHZT%}C&D!cQ5d!BY8JsR*^EDBrMZGBkBU1*b< z!)(5>1N+;J$Uc0A^2nw8_wV^}XfTJkl!t7MweeRQ{+Jt;s2#n?>xs-`{Fyv+2eWxE zoLz?BOL)N0kkY_(2{PKxfu&&CyCJNAb37;Nf;dE^L9_(OP(23Ap6BB{2iF`4g?GeC z_QeUp9U{?YWurCXGsNOop~pC1+pH9NBPOP%hJ=oueZ1f{-Sy{pTjwLLcBx510joNp0`YPI|7dD z@Dy{oISwiVPiLWvh%%r~K7ymB5Y4HYu_T zkr3mIB!>xqRCpG=u410o z7{lYh`k<$aL$pdL`L<+?SocQq9J`^kD%+`5P#GQQ<7zds_xE^rlo@sq$NN9<(-hcm zkVlTqx0}ISl?LINwYmye_+6q{pJu#;4u2=q+X0Wd7wvFi(dLy)pJ6=?w&{z2RJcjReC^>>Xq9#~lJ=8Oz4OYf!ly6>_|EO>uaPZj3jmT`WHF+BA zjN0sOJPGa_nr~j28y?hhdCbYwHM(gul<}8O02~e+NXAb&Ox`rr-6)ko7~41n`h;46jjDkIpDQK8#uFjEXNjE zogK{C^cWHJ%j~;Q)mEr$E_&x;PQPpPylv-uSeLMOePNp|hZFi1EPl&V>se1emi0}* zYQ23H>xi%MY{&wzaI(r7eF&=A%4${u4d#I(6RgBb(*yeZB+ctQo_igsM7JyW?ozyq zvxtspQ>SXdoYy>WRzF<#I$Ye%cv#+80ez34MYm@3+BWNlHcsP{QEdtJtYie}%(f~p z8O*H?RdCJ_@7~HStt}yXNi9Hdxen!|8KIU`x%~_poX|dQn0xIjZkL}JfAjCxc&onB z&w0;Z|J;7TlV52cul*hOgY4n24z<|nT4n3whguc3$zuWjBQ8d&Y%^kxfxgH0)gk)| z(}nt{FhhCJYHmif^%{y>$#wHM?HVvU9}3A+2_sk8f!YeL+Sxy`a*RVI9G*bihw7Wn zyE`m{{&JL;_WIiqxDJZgZQBtxVvku8DtUda>NruKU_NpF4x5i!k?&Vn z6ZVnC<{r9!`+}W!G44ukPvziiI6jURwZeA-4G;qhTrM)t)25RRCn_EgD~6i;k=fIF z)<#*&4AzEj$;t7ab;7@!fbJcPP24Z`2IEYK8Lc1Cng8*2*d;S%vrNtx=uof%I~Dr5^Vwd2OK$Xd*-A_Q**_ zTZLYL9upSuy+Wg|7LwI5>OI6bt&y^0_ARi`#Je>-+05vNus+uhe~XTvWq+4}FGF56 zY)F1@^ZrZ6MbEpY3$|8=y~vHdzV0@I9K1b zOTlIh(tJ!4;BW%$dQSXUXaK$*ZQ>O|2W)+6JN7lVHn_!tPVQ)IMdtN#yfgH5>g5bN zD=uM%DccAPI@Qa5$86e98)zBLn$z|*R!t2W_-ZBRw$D|mxsn>-l-1^z)fzf59>D8e zK=rFQ&RA(Aom;zK00#?Ld%aHLOqVL4CVaKf7QF<)`OLT&-zYJr5g#BHg(@i2dv^8N z*UTebFNiz!hWerH^T16BvW>_Rczy*tp9I3>L7PWX9Dp|ZcX5QOvTk7+vLo5Wt+o(o zZf4!>@WQ!haanqbQHpFyEM{((9`ryUySUF@2Hx+1qu+%!mJL>y0{wLIx8Y;p@MGNr z$IY@XPNou(4bLiuI1&7oP^kN?6S%%(dHSd5*4BF4WE*UNGr)UuIX_(v-!HRn`Mrz| zSA1g@7>85TggnW#J0liUWJf)#ybVlDiyH{fPjgZa@mwdinJ=K?RYvX@a<$U>Sovn8 zZ6@!IfsIV`m=>^bqu?O(o;+UFshUwTfW6zgJR_Z21l-8>$-6PQT-m3$p^<*`m;e@NZ~Kz0JqxH;N#Wg4DEw6^YsOE z+=SOObeZd;dR@&I8QK>RQ;r#2z)GL1WY; zDzO(}&R!P8-n)nu>}-d+?1P}|6TkK z4={7iSN2|Quf8;$ew0R2O`PaC*d?Q2ntI0|NvV$3Yh+BQkl>C+pk=w1R^vAVwCaHA zFI*`UW&QCiX0@7UZXj|>4wBkb_$|PX{*Tx-Hk~IPDx+sw(1VuJkL<~E@$xZi`2_AV zd0NUY>$$Ush_vN4763V$hG3O$)>(*29sGp1Q+tF?5Olu6Mij%`3bgk+?5R3-G0PRi zZ9`Lfv;HqhxN8qkx>J4#EgY}QIlWEONy6HRlf}AI=yeFZm_GhCKJ6wywET%>v~JXB0Y!R2n1cz$b_ z4s2%SUps9cW;dhG&inyzI3$m<+HTIKT4%s_v{V2aO)c)~JWJ3ZdL2SuFhurQ>0= zX$-H+LA5OW5kb4=M6e9DweZpaTr&eMnbjkf^GTE(n%WOm&V}}J)o^S5TmqLQXNImY z*kF4faP9QyqzV;@oP_tcnv>Pb7Y^$RiCD-fPR=*Gyd9WWm)=s~`hhSfiQQNh$OzAA z0=GzVIY%?l%1km?lo|g4PutAeEsxPMbMboUFx+bzM>-g`skjH>!i#{M_0f*ydV2RE zr}3VG!AWX1EYkC=TK|aSkH}Ii7M@k6eL!Ra*;1sIpJWpnnIj<6f)_}`Jhy|@C1|%M zyFKV8Ua4J`JD`s?2_v8BrvR+XpKB7{CUH`!#&h75TAP1Z!YT)#x+Z}T(*kP_2Cu}Qy(pqdXqO0R4ObL-LH=J_7~g{yK~ zB+wnAz7!lMpsT_&LX7}#sz#@^+J*+`;zH~l{I$>yb_+0H%GFkv0sl*r{5z=CPr>s8 zK;zebKnLwRoe=B9ZIjnH1zEdAKeoY<+vH}rs1NxOXop%iLnfp`ZB}RBNsjlZ{t(`_ zd`WV+bS%rVt;TivWi*LB4rnl zF>Bl=;GPSdy73bX;Y-WZdVQO7=2fM}8b36>L8Ka=Q#%xxAT#)Fex!Yz_ZM?koy4a_ z@*W;$)AESE$P83C%1)Y)gDE_{hJ2x3d=+;|pL+zo;9cFRnQ&Jg*WAQCo1{S&(m|1& zVmT8?Y}9Dk!|n}|BibT^NaI_%%VJwm!22^+XK|25!LraTT(4-%@)tDQAEW1ntdlBsx7w6c%T>(PmI!P+ z-aMv%_L=hIfanIn&n;NlhA3x2|3 z=&KpbSwGEmve@7AX+-n8fz{#AukZ@rB(+lG=y415mrEhLpms<5c}^$3@KNxes#p7X zcMqM&i`AwVkINfG81ukKKKSZ^vo4hcx_+)#*v2}Fy@Js>nVThXRTzMJp?t<=yiSzX!@v9`6{r%C?w*tw#-(7}P^&DFI zzx^t)`Vf;D^WC2Uqtq(+ts1}L!xHkTaNZ(GWIgZWQ?l6b7MX&BEGs^WJTB`h7v;gL z^uRexfnbFj)y}6bMc)MrR$~^!S~7JIIhc$dG7OIlD>GeX6n?WlW24Yt9QgkP+E@5L zfebR6iJQz?h|FOF4NI-QBOj3Okqp=7X`^lmA4Ja5S6cJK71}S>ZQ?0xM{>?}x15iM zAzh!;tJ&K|p7{iQOwI&Cm$KqQV0WsgXE^lQiPe7!`9)~)=ra?tfNy8v5vwq!{)4r| z=?2{oH}rB>G}yRa|Kb|)uxx`nvcSV$bnZ{&H21mOtH>hq*S~Pp)Z;$QlRjj<@yf(U z_dOR4|MZaOSOa^%)VE8LX zya>o!#>SwmmTF|ofB1eVnJL0@B=?gyRRiZ{A_K+HZ9Duvpws#yQ4{N=`x5t;>lw(& zH^p-CN5RV=D;Z_K1J0yy12jtw2J6HYm)(l@%5bQ&I>qTYtkY5zG`UVmlHY|hWS%BM zt!s}+qNm`5N@-Q<4TZVi$Vl_=w8)!&8S8i-8B8u3QXmQ6>R<2$p}inQGjxX@!iJa< ziv>3$(WbOa41YQDQ|&_HVCA#YM0h0&9PZ>>&P8zXGSD?|yw$C|E(7c!4eXD@39~#U zLkoejO_a7=o(izktN(KGtiMG=Xy;>^i>y2(nf@@+KP-pIA3p$0rnCeK=;AqON_;Y= z4M(7KQ|JJzYDB-Fdjn5g$1`JrT!O13)4Bl)B?nNp1E~{On1m(z4|Ef^EsErsx7xE!!K~wyJe5uuC>72;^W84DxZr^U9zYMn(7btJMtvOPRu>} zAbdOx^>Ruujj&F$fe-WA*!_g;g+k0SYegSnC27Ws>hL1xBF!V&9n3X9-tGCF=s+}FCZmMggyP&wy2QsIb>;;z~2)=KtCr62E~VV9PVJ)zBKtt2)e0zp4q?Dz zUql%ZS8{4K2F%bU2A-Iw5n9Nqx^*9u;;^LolZoA6Uf>2v)_UnfvNQuJ`t^xr9GD-7 zzQy5se6<5m-n53{idlJu{ouo6bvAXUQJI9zv;NG!6&R95!Wz1CgP7Ld!@dRtKOPXE zQk49gXykOVHX}#8u6HS1TZs&@e49$>$>jPaNZ+?5DHxPOzI)Es=w30OwDreZCAHA( zabP42-8K4mTs6FKm*mnTZzDRQ`Bdj2X-ECltkANq>-7QFcr52%VelQgB!&6?9&+p6 zVr`q`a>+r;>xKCYuklorxf_-^#v+5EMF)e z`+bzR+uR?^Ous~}3w0rPXM&r0c*4BYmIYL!$#AIkGb`6IQQgidZ@xgAC}sMY<*BC0 z9Z-K}H*{ldQh z9)2RPA*HNh%5tu#RA(21%=R<-&gPwD6tSBaw_HqGT%i^v>eaQbR$AbPQD<}QB6-ip z&@>rl*w+;j%X&Wc%&pVcEu60My%c5r0SY z3?RHm|LVw3fzmB+wGUnU%MjB`=$}ZZBrv;69+6U@Ttv1|6+#bgR+EzsE-fdq0y=ub7t!G&Mfb^z@Qq!S zA|84iC6*Qgtv?ab2b5ybpUoF{pA526%VD%gM;}uis3lbE%2G-^!@65N!QN+ptku4M z%nlwy`ZoakYT$M)@BNOSg3|DMLj^DJy>*LtNts8_9Vb91g7h!r(?;Z`OU;QF5j#= z51oX(7qDb~4a58qGkbHK z?agv-c5`i|KA_9pMRFCpe?S(yV!_78V-@8CT`IZm3gExbX9gw2EG!QOn*t6il09PG zXRCmCR6y?;-b`OCF-?=MJ#deCY!*reF_37@C;!}f4o}0oZAb{B^hi$fDUp)-#5`?k z!5cu3VC zI*09TeR=8gg%_tq5@kxhz@I&yt5koo z^7H%>JP|Q?B&Xm5^MPAj)Mjn|#+~UmX`e*lgKm>G^kt}o9}46b?jLBWo6uT|gJbn5 zZ2ShK;#<(u^{!jCDG_PDZ6cbm2An0x-PADjL(gl_ z$XndK&gu>-vC619g-?hofuCt$G{w*OjLF%~1@BfZX;rvqNwQQqGK-;sajf22boLRg z0_G*)hDvs*WJE2mdk9J?lUAT?`Az+BXB(EjReZ(l9j5TnNRtpcdn6 z!>Su&9nZ-aSsEQkxE?tR2zfoA*ZVvurcyk-F&|DU#(P=MN!KGaz~0_YWeYlDdgvRM zCss2S&zbv>i_{j*!5A5YBYq4g z$!;s_yo~*l9}UlckF|VHN{9wEKm&Hd?g=q%LDtJKa*8N5S>(tZcy1U;w-39jQxfsc z)T3)aFKWFsM$j`lu7linir46O{)ZARe|IW1J_^bBzQ4!iNHY|f>*itIz-rma{{gF<&Uarsu$DKr@@r9mlZQs947pN39)vAc@kg)(Sv1zgmw zS>Pg-Ylz@LXP4=)d<5Os^I0SmaFJF5N2=Gr?1ga4N?r&6-yd;Ta1t2}rHFzmGi7 zX5f;*{;Wg8(RwjaiEa8*a6cRTmnVl6tnYw-iH^Xre{eg{a{o#G*JI-G{5FKf+@+Ec zq=tL-QsC0Wj<%2mJ_wA>t$bf)TKS#EJVWtrQIh$LXO+~~IddDny zE`~d;{)Wsja@9Lo*_c`s@-sZ`MES)c!zr>C{#)$Q;f7vLrcC5+9hkK|#}x1~h5rIR zW(~F2#Ae4*iKw^wF#8!(v&y#UMfmrIbOI0jFC$X4K+};J(7T553tf+Y^M?65rYP4dgrJl>5Kk4p-!DQ zEDcc+q&9iaqW@J=&uKZJJK>!YX!JuYDys`Ql1ajf&(p~eFBsO# zwJlW0R1UNF=OUQXr7ko8%|0lWx9DZ9#)`$6rtr~tu=Pj(Yfe?m*Bet01z2udyB@9Y zhshv>rhXk7Vnyb2n`B?)Y(n$qmp}*eUxvovj8ZurZW-l$e4hLZ z-gBGAbHWv?**}&ewu+sV9g!GuD!{~KPX%-{fIUsON_fdSLt3>Xy|Kac79`#vo|H89 ze;d~_YgRYP<38EN1NFc8FM_3#ub1%mT$%67$favm^495umdKkE;I;z(KMuVnA@m)7 zKLwmc0zaER(Z{KBi7SLk$FU4I;r;5<=h=s8k^`)EDV!O}RUi8^#4LtoG9n53rp~vW z=~b-DrUlO;CE#(WV80y0PMOt?Hvg1v&=RCnSjjDA?-Tlrm=5++`2@OZ(GjvxIEUmg zUej@XgSb$;UQaH@^*qaD&~~Wwzuh;^JU-OdpmV&SrFuV@*bQa=EkwU!zr1!M?=C&k zQJ_nR63GUxiQ{VP#kEL2K?h0YZKbl6b(k;dV_iW+ArqQ3J>n)e0`(qLvXDXtl$k25 znd}_a+O6W}Ld-_jS-Ant$|BKJZRrR!zJ^SMc>Ug@0Wh2@y<#2W$zzaBQ0bd`o-|=W z*0BFZsglh=-sb3aAi3`+0{i#Sjc{Ee`9*JX&R+vO{zX340zFyxDzgD~qf!NR#ItZR zRV+RF4sy3h`}7Wcm|I!@17f)YL?e*M^JFixqHX?wgF2ewsD6`Z`ZoI825+8>9YADH zVAD+V<$=ZBWYtC^Gj0zyOW4t+6FNG_munQ>=lKoC?D`#b?jH8`cEgv&zIR*=*KpL>a&*yy?fQLrM98&1H5tv-9 zd!<{6CutQDc`B4F1Muq6daah|w|Qfe6hYsg>T51i+wiq*l0mRw^$?atmBrOR^Mhy| z6JTlx?)($9{rr++;HL_)E<0Uf|5IC`HR$iPLVq?eH7dU(yVrbzeZZt$UeyM8rBO0^e&w9I9PTyw!e!$uNsTKqQPtgdA zQ)R(V6_O1fECTYE=&oQutnSS6)hgifBCh!^T1+z@fo{$NGWhY^u0l>sYc`U8l#@12 z;&lodE(1%wtS!yg$R`nF5`bCjr}jShnd6k8YaW<4J$O#+l)?`JP14^+7w~wPq>E|2Avj&BVI4`Sk-FakSgiqtTl%sq(hhco$emp%K7v; z=jujycP4xgIUE+#46z%L;B@@aQa(EfR_5doUe#Him5)_MCJxkCPA2}0VTW~Z)@b=M z7=tbnxQBcYsYbfc3k(dF>Cxfus}Gh3VfQ&2bVaL>_<=K7&4sSp6`2Grwsn{;U+Ym9uBa zHE@s3(s%^w{T}tR4b*8+Ii)8;bA4)6cYCBu4ndFAI;cZBE>CeEoecP?T=TWpSsg^{ z5t(F>z%iaipCVRQi%eh7dJ@&*e(xdeekyH&bvj4G2lhP@sar!e$fjLL+&XYgG#m+< z$O$tGK0AoDT}i~waDND>^zjxdc)`>hHuNs=ey5w(YE|WGIl%wcP0KpFG>P??HUGgL zWL1xZ`Fo>g%U3mbhwO%a_7=q(1K5BhE(8;0cx1mJyk$vR~&^F7!X`;N# zH*fQEp084yoYgNM>s+Wyw?V0GK+f3{~sv z*v!g~M(PAWsza_tR!#A?R;(3jD_p#KxYN8|#Ym@2ELX{lDUO|JV6Kbwt|$BL~Q<#RtI9_YmBuZiMX9r6|0tlNC1 z#ONNdh$alqhS@D;)HXT|Y$R z{z*-umLZjOyojfy6K|EG<5&*kFw_@Cr?4^BLa!r9lB*>eI&a598{z+_y=7n}^SkB5 zO|!y1@K&|-v+e^*cOR{R%SM60ubjmXKZX;H`{+Z)zKWp6S?wnBhkuhl$W5X5y;!~A zsMur&e68#(8~zyASh4JFaz8aTR3_WB1PjAB+h&Hg5GnW}uzQ;;sCMPvGGuQ9&%RF+ zppq(S^L28X4}fAcp9hku$#RuKeTwf*&|{&YWzZ*Quzv&CpACg2l1XOXf=J%8Tc3ln zEc7?Gv zJsEUF8KXCJD_R#eBX=WBG)KD8X9E6B6*8sR&k#J;NG=NcwA@N0vI&S5!Ha`X*cm+E zW)+TbD*e;F;^ufll~`{lqTI}tJ|N23OqDwnc@Q0OP)k_TA+S@a_P!i>OTEt}>Kf&y z#cFPuE@C!1lF0I;ruACcr~_I|k1x7zA#wAOCS&-lrZ{aiaZ^oEizv;Y&oPxzKjOOy z;5-37Cj5RNG%V+ZcC)KmWGocv#^hwa&66JXH>*fqq%h8QmoMo-17d3mGhs{|p59>OYt3@&qczkzv zYB##PO#oqX8k%<#@O>3;XE7cgW*U$`N)ECw=NemkDROZ(DCggsz_7)BT8XjmXV+%; zj3N7;)2-+#mkCypn#5>F`V7b@_MzF{HDuq%k>R_GQyQNhR7fup=y^~#$R>X( z{b%-on_%~^u}Yhzu)`e#ob1fESZxjTH!hpe5NfrKCpE}U&S>jFj~3_}guF56a|O0( z71Fdyzs=sQvw(H7IwiCh_*T%bahDKZmI36W2O@za0>xf5jY_dtOIVjlHn1uN`j$Ie zBvcl%mU1~egvI11RN*$8;NGW;{7(KI5KdlX)bT*M13jdK*=j$~z3e0(9_`Xd@?Qylawo7Sq8Ad;g5kojy^-jbg#n;JrT&nxfl7z*IrqCL6FP=nsc{ zu(^@Law<=v29z zY+Wz+!}DuGS>#XPXXZQ`(Akj1mY6-k_lmc4Pztm~U*xTHmJ5#OM0pnY^^i@J4c$)2 zJ~SfhkC_dYt!{-#BzP@n7yV+nzvio)C+#{6?5r21MJj9tB>nJsj^*E4MO-ud@EcD? zHalfXB#@ltX_g&PAeKvr?#J&`&H&p1WXCo-`QYwjG|v%EhGf~UpW+J|g)+AJJkGpR zp{@iu2Z+21cR#MygX}_o1@|?{Zl*Ow$x}6s%LU%%{xW${b~(B`NUM7Zc+&-2kK)|i z3ss*TJSr_{IJW_}9Bd=e0`{=nor*5A-u=wopzUDbWm)1bW*0kjjeZqohN0Zg9+Q+@ zV>ty6Iv!wV!?o#u220`lH-YV&;R`*T8?_F}mW2N~mUtsPqiu4wk0bWc1MHraZg+u< z;7{Aie&U!^y1;Gp*0pUZd-)K2(Jh^9$66`XF7!r==`isZnkWJOVe(Tuib6OvS)-()%H|L; z0~=i7cabtEZ`NB~$UBNnhecA)iDntEEx==?P@RYEZqn@pqcSM9-K#LYQ*V_W(1BSs z&#~t)bleMtQsc@ALk9!-C|Ix4Xo55FuXSt9AsXBPge+5Y6v+S1RnTL3H+0#qJ-j={ zjnnabToT=v`iOf0c;Tzo{c>25-0ZmnxXl6wgAo}Ni-`ace;Q4gJ-<@z;Y>(IP(;J&Uw%XU6B8${d&CYR;)w3;RmHoo`#+e=os`% z1{5%|nmX&Dj&B<3g=5)g3lu$w%$o<@R7y8GHg#7D(@xe4pc zY~ng-pcv|2iBzlb1HOfoKgJsS!B>`!QlmU7<}Vv%&7busrB16Z&@$Ch#j05Q#{ooe!u5A=gR45wpo%1-C4Y0 z5Q=FBGxVeL>H0b}v5IH5B4_{Mmbynd8BO;lzaH5e#f~oIiZmI-D*PP$GAR*QWFX`7 z(0}egorrJ8m)`VgIJRzdaQ4wKgP70HI633 zzAcxcO}fDDt7@G)nfb?OTlH4HkB9ye@oN>c{u#7))kozWU}yc#KI4<+_|mLjcREjC z&K?;9y`c+%{$cmu0r5tC4o)Sy%qq;I8YR7&64W|ov;x;c_|r1^&Ste0T7@lMjU7}V z4~SJK-%D4d8ulLPQlOyYWuca_+Di8{r^NtgmHC3-&+n>I>!s1yFGW`$MAKC6N5WS?Ua8(aM!pC)TE) zSiexqee4#iSv&!Zm<>yO2yPhWc@GD(YFfr9Z(kVtFSk!$kvM254Q_e|iSQZUH0dkB z7j6;${&7P0awMc+$q+&U_JjbbZ$00WiZnw&)3C_P8fA6RrJH_jNFKU_A$hmMAuFJzYuzVO!S2gtF)*?oaAYK7ktdU# z1>Yh^%6xY?U23%pnyk@hLVdcIv!+R7p&B$lG9GK$dk+xr=1;F$?#-1-HV^dHqUQA? zv&mNxrK;e4?*sK;_-yn9HHf$$AQhgKXs-QGZsOfr#bOXGzL?cqjV+#}Nt`uA|NROsIAVVAt3Rlj(K}o{e-ye{TCiu_fEcW~)xZdXejWsvqE(JPtgN80s zkR``LJ@h1mdb`0#3mm!6UBj+EMc1{cODP;1qZUavpFL4q-fp!uMQ9#YA223NaPuKb2vL0HAV9%D{J&e@q0b-U>XOl+j)bfAuwgdGKSx>c+_bJZ+ znKs!jThN{MX{PqO7TGFwSlSP{-C7*l%Q|v^Ix|v`5Sh>rRm(?Y&1&w;(hef@VL3;# zB_D6)9qej?x3E{>YS|Du;O_vEV2@J$&0Z{8cA8!#bk`Cvtj}_tMMlUT;P(a4XD=(F zPbp6r);`W4d>ER7hPGSo$5JnYx(nP*ij5EdG|3vtXTL~ay#QJ_Tka(P19=zAW>y}D zX2*C_q2Nzb>yFihOt&-lJXW$&tsiW)e&hEM9WRAHExUnS7qk{~9>DMx*G)8hk9O;g zdNaG-4K4>ce=YN|Rk|fszYi?v_(;UEgsZmbdD!aKw+^2GJKv9PV^xQimpROxHt~55 ze*Oe|%KQiY(#s0w#Nz6`Jn4`&a9-js0EYDQ)>&xo3-p(6aBv7sGEu(q)^i!%g?;Up z9pQs~Y90vl`wz3idw9oh&?=g_Yfk$jnt}3^y^Ck|F;$Ew5_Q~HqV%Wy12kH=WsWjwS^a*(~raxb>Vm{x0WM3Xw1l;Vfd>vu( z4t$|x0YIOs&FD~5P&4&aau-o0%eqr%m1R~nTMMi^;E}r>HoM0jVom4rtiOdi;pI5D z5l9jP(MqLegy)axiC~@nqKd``RIcK)VL6?r$g+r?!4M)#~=;sb5erFdcQ=VsRO`q4G=?HfU1@THt8S}y0dz>bsDWp zeF^)v%J2dtjm@gtDfQ$FSe+x*6@HBGs_AZ4=Vvpb@B}0COg6{z>@gw$V`v6LK-?;$ zHVB?z?U!e?D7Xmf@6|M&^DT5`s359KB?tJhjt$j3dlWiLLgrH02)|qgrEk(UJXmyW z2}UG8G$l`=rS2B-sgOqngc~ zBdp+md=fj1LMGJ7>u_9=kMW8682e4|k*qHU`S23&HVyVdB;*R99M)K9mOfr!^b;lH z0(n&-swW3yKzZCHyL~|AUJ1)*{sLfgekekBYB3(4A)tE)RG7kwVr8C7(0_!XNtMg*oG|pw*1cdX89Qc5_TclXWi5+{3%u+qWEW5#)+ylr8nSUrC-pEC zHLGXq2y(4apR)bvZm5TOgPxv$$SVh*0ApN(2x_CECI4tb4ltyY<6 zI{%wbrw!OX0vA{w-a+u%$G`Nm*BP|*Z(KUCc$c4|otYS| zGo{pl0)*z9lWDnABXNNSE3yL>f}?- zb~`0C0y`rH`mo5g=jrClsNw9p2+ze<-ACnZt3HA($im0ks!31<**M@YSCjOF+Hd>^ z(hD7CNiF-{!MhXCOj>!%-`$_XCAuWE5$!m^JqV1;;rk?TPE8EoTLm%x7*~kLc&Aub zwM%pxb)icm23@rr0s~~#$QM|}huGDK+BA(Xp`JpSbFG@<_A8a&VAaWCIA*W(gWDNB z2_LZaQ|XZJE#^!UWIy+tUSVhdL7lJDTxod$%rjNuf&$-x%T12@4`9HAIj;RF9;`HA z@hx9XHdvda>8ajo(~6YdL)>>MQe>JZ?o#U{*{kV#v_BduAX*HZ_maCn^{>24K0>Uk z(#lW{ckR)|{#>QfRhMz!1xTh0s;`)Ij}^=uOR6Au%7e_>&XE8bxsg@AzN8E3GQo8{ zoMor;?T|KTG_35}=qvvLSR>CRpb_wVJ)dTn8UX1rSBY zWbJ!pJR%0_iezp}}-B9#^>_b)#h5FUJ z&{pFZiyUI28>eFdYaQpU)B22K54>-mU#opk3_X~LJWc==i-Bz8-YFdf&cE}EeF-?; zp7U)*CzOlAP53CD=8DhtMHdSvPxza;#^wd>!k06k8M09-v1sp5v)h{GPPYvV zqym|3;AmV`KaymjSBF}qwYtq(3~nxvDt5b`o}N9@1%`9M%?LN-s_3!+LZKMA7+j8PiH4xd}7`EF410K)j?c32aedP*^&i+ z)cHim+&!Mh84isit>NsfU=HlYfYA;pkGyNn6ml-0jz2q#SuDoBG5;!A0YW?y`S4wM zW`8h=tg=32HZQFKIyjOGDwh3ArY};)bVkz$kE615L@p&p(ZGAHd(x;HkM_b35jvp- z`Z%k$Os7`at7S+R%d37xt-AtQ=DeW@xiY0wJlnFutDv5>oD)`eqGEQVP0-#LL4G|F zb__q}jGDIJ&6|{6v~VJG67s%Wo@*9H4m3R@y~tbZh5jP6K%E;`6lo)|2lKuED)f|I zEgw3ol4{aHy+PuSNaPM*#kG%sb7o#5xkj*zd+-1y%T8Y>bAB06E@TZ)5c^u>lC^=p z3P7mVrYzqNmcMq*ShJDHW$RvC!?E(}8#-F>Srs@riPL4jB!FY99%UmdZmoWA z8Yox>-Xzpfr17kH5`5joQ;DZxspF+%cO7VP*__;FMP|bxHi>JLNY8F;o^+&Kuf~aK zG8W~x3YRx|Z>*ZryZ%WeO(CaJjC;#}s98vyYAF$-tbUVhQoKpV17I;h z>X0f{&$Y+Z$tOatCz`@0!FJIR{C+nF%nrpDH_J1~YtlY`YXfqHa)@jOd=73L=v}C@ z&~XD#ozWVe`VqhXZ>Wj27qi=F4@K*M-orjDL+k%{u5{&Z)v zu21l$JIsL<)-DQJD#tZht>vncX^5P^7Eqx8qF6ufuB9py?DxLQ{nu#L|o70Sea zhOBQ9%VPXM_Q?66KYGid*+hTp?Z7Ba>RHD}Vtp2E;$t(ivWv{N9w9$bOQCW62Rg>y zr@`O#wn8M!h@6BjGo?*v#?@G2w)B^r_YRkF#_3=B+AC!DxXu)c_$ngD0q$a|iKYVl!7 z6>qYxSN*(qEa;WpLUy&!l+WagAS^574ZqIs(D%ZYcYc3}87|0}d(}EJY?S&?6K6;s zd;VCG!FY@&NfuEYG;AyrD4botD$CWnOUwc5cA!C@OV%-ioSTD^Ta?MAT(vQj!tPte zy6PE)uVGhYAW0(f!Org?bel_|%QpPrCT;1?25)-q_yG`kk2h=tu2vsO&aeE*=OQWk z+-t;J7qY4bSD=;1rxkeivgKm63Z!Ht$SX!CVl}<_P)8Y-Q7p80KtggcdkX8%-6e9g znx1x(jELop45(#2HA8i^V6l!pW=`iQkAlhmd8eBHlPD0V!$ds_TDtmxbGcvzHQ;{X5;AYZa z06dx{JGjAJ&JI(_AxVTTN09#}v8)3^8>f7tza2{HW8XH(VIwlM0q@Q@XTiF@oK=H& z!WX09-{isye4MrHs7j|1IjwCrTTZONTxi*iM_k?=&1U#8b$q;@#Gl)pT*N z50(yS=aDD(>9wpSjd%2OolTIazzaDhCe@z?Y6qZ9eAt{$78_;)22^}2`pW^lmUBGw zkirGPb4EtE)+(u}&6Fm69!vWZP0}%b-|pj~qL($m-X07y-AMcN%JA8&z7>kH2{UhN z7xZScUaczNWgV1cqWaP7u_?4#Uxi{$g$hPB6$*_5YAL!MjL~aF*999nVFI0Djm>zY zE$@M-b3`R)N-EF%(r<$=rqCIUXU&ss^LO8OzjT|h)ahHQ={lmN(!)9VxzZWKF9)te zbSktiEvjUevK#u~vCaeRvj-V7#ZT3;3Hh~E?hiV&hIg)Z$D)6!G(nNpxh2qb$W~%4 zaEwJJvx#~h%_;ph?-V;@@*e_)6+7*CuC8I_1Gw#Fdcr<8FZ#vwLQIKtJ>J9ZvQu@blJl7Ju!s^><*X zN9lA0WtUJP+Q9k`0A=cg(60{Ydpspan(^d*6O146C{H3ajjz%oOwL;zIuEM546VhY zP8afII~CIT`>Doz>pZzwZK_unvHA(O2R@0HS>6+=bUy}56L8}-z~&Z5uR8F4Et-S} zTJu9|fxLO)2Ndm8#`Kur8_wf#?w^#u5M}8Qx&T0N7LBb1W_Poi-R#vY42$S&0j`5c zH}YboM<<~E$Dq3R;fyl8wi$HB`GEvH>qBha9wd!*lyB4x!l^D)@qo1kFgA?V@m(O& ztLFJ4TEsqw;Rvg6se^N@zvoCWtd9{1pt=RVeTFmb6MX^OsSa=8(cD=tXd6nbE%3}L z$)ZAOx11H+r+YaCp97B<_%*=sVoBs*(*ZW3x1Opms&)FPmig{QK5K*zipkjcKlgl) zh5vjh_C|_+*ZmDFGUE~Lw=VQkaQvQB$sv!;LzIBoIH6wF`>ekcJ*DwsYL_Q19((>;YdD(PW!lTnurdk5jZJ=9_c&?KaxSjv(E@2FW_mq zo`5Th-&t0(b?6=8Jr--|)oyfqWGO3q7p(CHo@}Q=a1IjOjdIxzHa!?;;N9}qy)HCHPG*Tjo=bq(q+3h}b(Fx?#V(kECf!K7Y zV&I>zC1^{VG!0x=33Dg(?;++?L(SPxPqCzOUhfvB-|+5wXmm_#(D{CV#&D4g5XpN8 zJ9xfMvbHR>NO2i^zZI?Rf3W!;;{FNO%T*1!6PSJm)e?o~6f5HGcPd#;oCG~chIk-E z{V){K0`-xL$5S@rbALj&AbIQ2>>AK3rsWO(KbAM01zq+7#rIt_Hju@?UUs!;cS*97 zD+=TgD@J0m)-#FnHOL_y0akzThv5HS?T0Gq#s^PWj+{*;KLkyf^*gQTY+^kdO2GU3 z-r^^pp`|lLMEi75MtMr4cq|g@(@?2@Lc62qjk5_y3BV&j2R_F=;LBtKKhM`OqM;pR zCRA~~f*H;*O_W;nu^vu^SaOh}GzBgq_k$BC5g4xlW}QfvSg=&8^BuJu-e}4?gC=pM z^;X#~eR7sNU&36!jZ?5&ej@2qh!qKEKE$98gv;ykWO$DE0a*`5}|*eT#2qo9U#y=k##|vV2D{^(kod< zxHCcWxT}x#^${sCAL2#8auPb574k@-jJ-mZJo`L=oU4?d2a`yfVIYWA48APC$7-yI z7Z9OEasZ9|z>PydPdo) zXQ4!c$4;)Ck#)q`F93fNV2-Q?PSyij#BcY=BF?D|`hZ~7aqp%2U__c)2fka89J{qc z-f_nPQGD!RfZ`1d#4!xRXRJJf3(8#O~^)@TSXS7ECUwZIz+ztGr>J_0$8-1 zN6vJpa0aN81;`U$Q=1^r=SI;}4Rcr=;QIgE8}RZZx|_wtY);jy+?~#PI7g)0BA;3% z_z~gMZnAGueKUJEt@2#_i#NK5g+{_OV2iE@*p(mIHSQqj7hGUN%*x4Nfnpl$v1wrjck%)Kl5E|n*qhV`8V zUs`>|YAEuTu0s1YhCKim{$>#;AX5P9-*Uw3N@r@x3<~5;rCtIU6o3u$uumh=+j&kk z8gjE(yuM4q(hxkX3!#$Os+#ZPJpFheH6XXGSL#$tj*`W#Rl75_Ut*Zho2k$>?O1~VxLOqcvjmF1oK@j&%;+iZGgeAUxv_eFOhA)u~s%n z6Q@fy6wn~=Kz*B}Q#T+lyP=3mBxg1K?S2AvXQ35G;!U$S+sD$2=4IXiX5MHew2>$Q9!j!a zpuSQO?zjFXGn}f&$e5(bkH9Cr8Pxh#oCLK0;cLj1G2FI;-4tPR0;j~ItkLsxBlk?H zRm56{$d;dvqIgKzvHOf%-M9-7@>fwS+fVM9NMmIzB-UJNVi1K|a(eq@vwno~q^${XdYC zd1x(IXujKx$x3Ha^gd*NxB0Vl0se)s>;ZQb>j1&!piQv(5g%pgGupyYmFQo(77KS-bBF6vD6Sg7+%sADd4I<$SzG1FY$)iZ%s zKuqdk@Y;uD?F~i2WzC$%Rt$TRM8;sh$QdR^K!u6J$S7 zyDYV}a3K^$6X>(96THDJlp1V{SM^TZzAGL+Axs4dy$*0satuM9oSc z;^~uOxhmsw2H!LHh#Hd}t_euTftxJ%FZ8K*yqqWlyRj7UTX1M zZ3p|ESb+UVpWGvFs)uqc?{31`*VbI% z+!c0jk+m@?kAf}h{PGEUDO!$L2Pg$L4P>d7XsKHYSD5thY7@FL;EQVB{9U4Zm-&3` zuCLt?Qa=f34C#kH16)+-XL33k%uQ}7XCS>(If;7wPBI1iWd(1bvkI8~Kld`0&k|s2 zz3hr)HTjBEv`9Uj)<4m`av3LlBbx1i+`u^+BfW5_N1~rD53?FzsGFpa{E8m%+@n{z zf4M?1lg<;U`T!zvE|GQK2A=Bm2JmK`t*fOO-lU=%&FFq`O1vJbwOI2c@Hxl{GR^xo zMC^x>k73tkGKpNL}kRodP~0k4{|CU5~~3)Z&AMnV1ha{={UkU6THJBNtv#M6}`b}dj>M0SS=>Syl*h^nVzFv z`mAh6rW_?3piKOa&iW>F%T`SWjz@w29Qf8UOqz(Enjd}@I2i+GzX#?=v5G=@PRrTL zdhLU@B3bQrY%!bGFsf#GMCt~*y;S&m-J;*7vLY34^||c#w>~et5}aJ*9{}PHAfYyE z5?+T!S*6STR%bmPE739vB!(^wOtxXat*p%|&_{r_!cJXfLKLg8@d>yP=${sP>xDWCa4>=vukcjBIy2>u{4^HoN4rf1x|7l)Y}Tw=0L$0axy&WzV@qv zRP{O^zMvLSQsIO~Vwe4#X-zuId1w80%_~qSwf|sEgxPL;n00rxMvx%JcAx$SE z+~#DNwid_R23U)AdnW$^PY;uDwb^uXB3&KO$yY8nn1+L%)ncW>h8n~w9m9KWdRmE+ z0jbfz%p|AHTg!EI)GB+~3O8nBxBSM>LW_9d@QQydbz64*0R&KqctI;P0)I6Aza9lO~)rXccjs{)DZgV7wYns8_2L2LNLqf;vPJXg( z4k6yv4?V@8FYQt4YM{Q4_ym0$sT)8a`+N%A#H6WUhqgnfWR_!5#IpjcE4rFH=m#US z@&V`30i^Mgkj3~W$(6F2(@A#H2afAGL%YGj>%5oRD6v?2DYT!f?Z}o&UEpq`quM0T zd=>#Mrz=l7(VMh>XizWvrxhZzO59+s^uu}ciq86x^ z+yVA!9$L$%S>l%2(}mtp(9~m`h~rR1Cfycigqk1T!!$H@HwzuNsd?2c7rmC1Omd}p z3yuj{7XFD)pmm8Df?KWjl}={zRnX`o@BsY9y$-#V%0BMr4AL?EfN!VdjL-zqVF`3u z6fp?i2IvFPFPo7ek2$Mp|Hj87D=M%$h9z1{jUEXKSdI~&3kBrKB*%!xGLR1)TUB|sp}m_ZkFj^-ENJM z%e51~_y#0PIdnq?wYQE5W}SW@kwTYcsCFsOiNP=GC4n7K@2BUm&ME3?=}Lt*4y=?O zGSKLLR(JreF+YU$M;U|@-gP^`%^_j>8`!XnH1h&NzaCGs)XL48g+*qwHx*6i7IE4` z7C@YD)hLVI^KB=Z$S$5-sk#1BwNt8{(=vwB6X) zvidmdGU+v^7A-7ftyMseIq|YRoF<=hk{*umP@bJ~Wjq-_9C*6`uUv$T=^xy;sc)&! zGPRz1mkJ&^-ZbTQ0cHAEv+8Oc_r3Ty-iBMdgsLC8K~L90pr0d+$hX76UNp=qe2T(O zUU$dgvCQCJ4~5){eu4G@W)86;J0;85#R%U_fm7>SP!HDxieH#DlZnk;mf1dlkK>1M zW-Ye#`{>IXz+JLFh(B(pY!s?nfy}H+MsIpryEWaXf{kP312|zFC-)iNyw6*a5SCkc zj#vllc5r3$U@V_mgMLA4hYrW-y`0HUuSzS_Xu8lRI*hg$3#13o=CSv%&+gy}bX5lL zc7pV9MLuT=)xpq+d3QU(5ARSrH^R~>y__hcimwQYX!LcG5O6w4h|_kEomfn)lqYnA zI_Raljd$gtQ}*NATqhO>9MDy+4m#}6Xt626r`94#f%JMg9{T>2mHd`mnb&~-w?j5x z^*2X6nEgPoU+gq4ku5UqoBc|>w`6g;WO*g@fo5m|(C!cQu-bGWYqjQ^e4#!Prl$Zr zc(0Eq;!!WxvhFmY3Jgq`gtSbXQ8;o=S3_q@kSvyiT*=$$HY9mcDLMSUlb@qpJutJJ zDWk(M^5ij|{B`Iyq*^&^yBoSgQ-Z<i`*;FahT^fV8fZt%{io9U?vS% zTCMnEC}$j+n^w~-qR^_)Ygu6m*G)^eB+FyoqWN`n2dYxbf>CUMfksq>oJRP$!^eCz1`8PX<`Pt2y<$-e2TDO&^@fb@TGX(ZU#yXvP>->p@+4W>q9yw+q6=icl6Vex3C8~fNmspE>G~g z@ytt5>)G^z=<)ySV)VJtJKVR%y{2mr zy3*yz^VBSl$??G`Q*Nv?+p|V*@&^B+3+OF!7r&jPIeIG`!&xqE@U$Yk{^cxBC{t7A zEpkocBo=9fS4QrKF1ql;WEv#pZ$U8>Lbvv^bqnrkYbs}mrUOoiZ-us837p&KF-Z@Tr>mW?5fJov7XgZgb zSRP%oAloG}^abbl0q!2so4`vgPkn(EukbVI72nYtLuZm1Qp~!_kmc4xwu_b0AAx7w zFP+-1)pC!s2uTIw#VmZAFyq^u-5q!w|YvGO(^wU*xkG`)amm|SK1rp|d-7OXT zxk>&J!K5UpkX{f#W)P~`%UbNrw*1dXZ+%#rQ4~EidB(yNVT$AXV$288V-ph8nT7p1~@f zQp@BtD1bUoNy$;n{R_#H&F zz6YP|0s1?97^!%N#&J&k9gAYb5hn}GCw%Xco5W^!G^62jqDdeAK(j*c#n$)_FvRHy z{hEiG%v;`=2LCijN5l}i{XCxnt#rUUagvFoYm*{Q1B1$l{CzQb&a8MImc@+Uf}EiX zos0peOW-k^{$(eh&BC^f?7Q$k?1g(ChmO*)yDYb=O>D1px#nr_YAMnr1hmI^N*!lG zC02YnXXyiAn!YkjO1K0{Ab%K4*NROguvzOTa3wXuaLjV{pX3{X#;d{%H|*%kq#B;Z z{sFsxuB}GeO{1C3Xc3i`)|aje3oFf0af}3Sfjc(K-+T+0u6z`XeHvC4570k1ot`3UztjgdNiTdnH6MBY(knf%M=Lf!Fb zXWMlO7_Q~%mKpsmjpVm@-H4t1sb1weft`8tX2|!h=bb%J{1(m>W*;iHEvMtSi*YM} zNQgaEYKf$XO>vx&1DdN->^z0ayb&nGI-pJipZ&FMoH*)Gp&bwNvfO9u<9^DCj*YNIsO&|gl1KqSjQ$1Rt5zOVH3J8d8 z7w85oA1l^Xkjh4UrFCK+)fa)mG@A5+|3}h!fLB>$4}a~wt=QJJyNV4(bLUvrRnfI! zK@<@i76e5=#fl;V0TLjAgp!a(dhf}7=cM=EdnmHKy1jo}{O|WW?85^j_r7K3^giH< zc?*IyjwUrt|1=&U9T3o2u5Q)WLf1m|*>WPVyGW@~fcv(}7@q!#ynV>^203zWRjbt#_wqdQfIw=atW_pILEjs6 zoL%%NzLAXTdwvprz8_q#fOkf8m0AbTv|24Z6`%Tth%k2Y3B;T%zD6auUoG?GKEB_k zm8@!!%+gG(=siNE3llh)N~L-5LJkny2~0UJn_T(7$g%YMC}s*-f9e1jug5RCRx;3I ziBRYQzXB)}$g@60ae}*TFwr7rI*-EE%GgoJ8$J}h0+Mr9%Gj&P}?89Rg7AU zidsvcb3L$X0po+36nexLf!j80oQ=?HKKI*fvr+bk=0W~6$oYZQHIF_+WF07>FpykUafPxEb@T7NioaWVmanZb3y?yW0@v%=gV#+wo%(vF2+r4Zp%T)K z$gq3@uJUyeUej{1-V*a$iA^Z=wzA6a1kax%GtMC6V%^$?u_ZbiSvo>gcqO#*yn?x3Q0-Ak9 z<`Y9VUw^N!lYF%5*)k$jw4-I14hMe5!If2KRUt7(nPq7e&vtqdd2G%aJ~cGL?xyq| zc*lBNZW8lc_G3F(+}tX*SD|CAhUy0{7d=_a^M>(8#-aLCShbfC9hzOiGv={!=9eLj zYw#(q2Fm7elkCy z-Tlg!$ah>lcr9YRTi6xVM?j@d6GJ32a}=*&O`d5s3K^QE@JL(^n7hj_bH{Ng96#Zd6JRr2T113#%bz|Wp@gS5g&`-E7yQaiyDC$T%G zgTR|fBCI(r`WRYHw8u^a4hB=067Id{RuoKe8K z>v)cJGK{hcJ4L{{ucpD=qwJ5W7im$Ofb$tV)5CiwCC;yr>w(T%CZHB@3dcHly+fas z1g$_HodECOrOW*uy@1^&2a&-lxQhwTK=>Xc0GSCaf|>HDd=<)ex1ePW;F*Yw@3`m&A)lVJgjZl?M2d*TWOMV)C zXx+0!kih6>4FgxIpXFkWatr(ip#?mb^RCfFc2;~V@R|=rkAi_F@&3JFJ3qI{rL1Ib zXz(b1lWmjZe5sV`Jn+^BB#QaALagGTivLHctXV>zokdgn&~jGkX3{T#wa4(uUjEQ0 z04?3EUwJ#x=y7KqbxBwa+mwDnR?!*S$5kuUW}yZ87*8%mW?J{H&9%&6Z2_6GJS7$b z#`f{O*(mK{roAb3DnT3HSAmU9{JsxtE_U>M1+VFSz8<=R5I&ZP~AwjJ&;j(7$3wc)~#cw6B-;v%3GDQRp40FgY^(`Qj@OM4a@`VWp_*) z6)J035w-kKQvy7_ifg--6PBQ$2DJ<{Q3(GT&lvy)waDHdk{MbiM8w&jomd!!#GHob ztS>#%uab*og-o)xyS;Uc7U?p+wH*5-|2M&_oPCI7+6-q~^}!uLw_ZE64$VvlH{8BQ zo&-BqIbT9oP9L3N)CeIt)*)xJxw{2xv6MVnsrG22G;{Bb@CsUzw_M~rbXlsq&{HQ! zJ9vH!%70Vr)PMjF=O%khL zGu=2Q%z@;$R-v|E%|5q2U3)U?5G0W9(j7>!-#{@LQVtE+1R16`)Z}xMqW$>); zvz+y2LHma1eZZ04A1LHLc+&dD2Z#!F%jFuyt}k|HLX8)(qcA!8Uc7YkIhYE|YQOUJ z8tZC-w--C(4~rSOsMz>BLbaL-3|2{&(3R$z98=@LQ5>>kKf4(4X~Y6f*XKMZuZYSo z{CDUdDpWh8q#7;z4l&Jqpu1ATx)pAsMw+^uJXX;RwAS*8d7d_Tn3Mgag`X2ya{+6^ zTSBhBq{BMSH_--ba54qOSj}q=(${Mzb7+br56J8Xe%-*wto|q;C8_-Mh*VOa%q(Xi zPAR2wQfMpjn06@SWchPIZw9nu9iG+=R?hv0`0a>pMt>^!J`7bHLPAWi(@MOGL*Q;w zzT~c7J0@j7OF#4-R8G*Jks@`XF&oER>ga+7U$|9(;Oe=${z)3dxCmH`KTk~ZP zagB-4Q~FzYrVYt>sdNGNIG@QnAJr#VTND~4TC3$;Y9C7EFQIKf@D1#*IB%UagBqvx zz_tb%GS|mL%kPBtNTO_WkLeO>HvZ*JCifuqYvJw;q|5Wjpgc*@YqThs1GQbnbk;*| z0jtU&qj?1@Uce4610SDCCOb)hYTDqAGP0gj#Owe0N+`Dhs9O*4|Ke73fni6Dz&C-j z*NKm*^sB@5E;8Wx))v6G$fZk4Wc%S?^Rbr88 z)KIgQ8Nm3D&Q6C((IRBORjy{57o}z~F*(dzb+y@sb`n63&T;K-LNlR{4mqOaPqE5I z^$STw<`(K1I^pM`qcSw$yWdBY@YcB5c=Qqp+-9qJ@APgGuc!&N%W3e5*(5}Mpc1e1 z^_PzM+Af8s+WAY^5AqXov-MJjyd2ewh@XuD;XUx>JNkD&#Co4Ge;kOXI{MCoNhoWW zKNi(L#JVj8J1%tQ5%spo7YBh)7BpbAH%`2$A4-E`6zf>6p6C%7*9Ac_@h0OxVyAp} zA(Tr!5>K@=)@&-%TCv$iW5@%uGAE#)0F1mytoK;3v)ni1!6l)Yqrl^^I7h#OnEvrn zLaZ~N{3oY&K@-K$)kWMh!0U#oL{33Ssg=VJYxA{JAN)oHQQwEEc@mJ1MQ0ouuONH25N_j(Yl9){{Is}Sc$ z!QZ!_K=@MM{2gf9E{zn^^TkNd`!t!S4S<0= zTp}@Lx>C>#DvyX|TRYH_y~vtMwX<_LX&4BsLZWnV$Ia03Fpw+7ldL70fK{d%PJ~M9 z^>TSyH}NmpO9qrGU3icTr#|4UXRI3>b+GR7j9 zn3@CfEQ)udrzqAA^lVe?2l2p%iNa-KlM?M@1#4JYF4z4YyjWLiE%-LQo=4rH0bux?N}nYJPFAI1*M`{t>U2%9G3E# z%>}W%;3%u8U=LYfWQ%YDG}m_W?LwfK=$THWy~M31^kq2Qs$X^s{ykZ~TIQnn@m`O; z8UQ)ww`fy{Y7io94eaz{?n#mzitR5pNoT*dxdX-tT>A>Rsdb03;m!f>bz#e&56R^~ zpXtsrCcRLT-S;4vhN{#~y>39$6sSdcPmXBRRwUUNx}ivFfbTHcU~7QAi^PE%A~LbT zsSpM4E$qV1ccjOJl^sy#Sh3o18AN*9?4_rm7|Upu36npu=B9)?ZzQ&L(lL{T&#U3V zDeXsF4)UHkTBt|oNXRrabDm^|=hxF$a3YtFaa%xjU)fX`{+T_czsxVWxlJEczHkADc9>L z@t9IAg)`FSDt85T4Ydj0YGOKBX|vufyP?5#Q0@|a6>feA>%r!-eJs_w8|iRXP(sA# zLu5oAI}XVE#S-(20rEs!oO89llO6UDr?cv%LwX-I14@t`T8k!aYJ-&C2I%IwvN9#aDwL{ z1@ecG7dy+g5qZ=qt0f;!trgB9gkRPZ_w&SyV|e3S{(VKh31(iz`l#i!!5+EOXR^Ct zy`7zZ58C7D;IRe1WEW7fO>rKTYzE>k9hQ&zey8G%vvSKl zWw4J{jTWNmTv5&mf`^oTU*NVjv=53qz?00!hz}hCS`}jcb*bL3KO|~vQ>zD%89Q`> z-4uXp>!3?^Z@EGxW(g#dwMn)jV>r74Xwa9Zl|XR6<|7%I?IxIPtaz00f<ZDS2Azy&w8elmF?KG1W$%SjIBau7+mIqxMJiSE5SXmP5{jSujB6t4n!*Io4 zf@6FM=d;ou?`G)P;HFMa4=T~pQE0miq=nJWCqXq;&fn8W^ll>Z#49xdGQz)qf*;H; z=!d_$nQ=GIxDHydel)8X*`uXE$7U>2mmXO6NWQcSvjedEx4H9>rg!-2V9>u$T$P%8 zSuVc~TH$G%LGop=1wCG@)ExN)e#>Pd`Hv?s8uskqfZ#Ipcx~*t@|Z z-44L}&z`dtG$+)fdjw8`KTWTd2Nvi5u^YuY8htU6ja5Ydvz3c^(HzO~j!LV6i zoq8Rxtzi#HJelOPY5h_UqG`tE2soY=W)i_ygL;JTnMREywcf`T=-T|hHXTJnS|sIJ zG7NOAL028*B5vS5y1X<~UgNv-kbW<7rb!8MbGAh3d@xfWE$qD&NxV`Ukvg65HMLm5 zJm|9y>>tY{fnm<*j0U^sLNQ)@#9|B);J<)pqSK%naYZu96VFg$ZE6*gVRsrk>XRv8 zCtPpUJvq|BU7cJxs#|YsV*Qi4nJczQu2NOaIZGpa8~DY%dk%UdiYteyZ!1McWQv^> zX7PCZW9=diemQq0G9}9V&wQ-L!1agEf$yx2d`fK+K6N01hSxD-fp3zr=|R@pc{&AR zy<9dqf7(}w^+3OXWnr~8&*>Iywfl8gqar+-*!paK6wHP<>_n4n?11IE4c%RX^)X-Q zlyT`~zShW~vtEpyK^(M2R}m786D)!2m==I{qQl&sZxWR?v>@TS!L@azJ;7?gHhf#p zotDKR$|{k(jfk3By#{qHj?>10i&gHLMbMAr?ts^c{-8N7kvKG74-`blK9D&KJsi|Y zAk4j36h8pxhxl_?kH}ZZT{^(@2mF@F6*F1E5$Nhmsnj2`=5c(6!XQOL@G*VG?mO_u zUC3eUAf~rP((uHsV_KnwJ@^3s4XMUL#d~G7+enVFpRD)~(I9*LE@W^Y8c-!HX5kRC zgfibD{%4j~sr(hW5Gy;R*7t&uJ9Q%zc`tPJBJdz43$CrkZW!G*roZ=Qom#bH8`vm= zN^@BIG&p?;NDrb}JBVf1u!<^845q>Ei0+q9ugrHex`SXp$ zx@$sqvI}zxu$A)gMAER*cWN>B|Iq)zGi$}ok#qcmE=LoD>5ABdoV=*bXqNMt<`Rux z_>`o`tL72A0BLzYd-&QdB>UN-X9EZ7MX>&A+~}oGY4%N zk8KbQ?k8LUcEe2aY2W=MYrS3wg;ZA7MW?j~WbHPc5 zv~bT3;M}9zkVAIrqGkIg;D4*6p?6HCfwEPK_rZ6E6rCykNOxvHXuM)SaDSHCTv4l} zNR?Eb7J8YG0twjQQ_#=90?QaS0kdlMZdqVtKUnQ$oekKJVSLRl@^X1Xot&%WR0S%B z&_l_tS0=sbG>h@PA*adXtfG(=|2!g$#J6?W?8kqQ+o6Mqw{s#FQqa1AF4Q>J&#ujX zro#u0qN-4BUTd3Dl`88c+2=qx)cWZ|x`4ed*Gr{NDu94xNRj+%Im3Q1u-qgPyUOG4 zKY30ahVeN6T&gs~sq^5-1n4SN z3w1aAR^d}6*{y}wljTe(;VgfI_$yv8v^M19p{rNay3;mGJi4b1TP+h>EEWUV%Rc}|DE-?hm_wsKkyufK(vO~wT z7FuIA7IN99;WJMjAK_%R8d00Re^93Nh}b!}y=xdgzf0H>{U{LCfMlg03TnD_K}Z&vVal{B9GiiD&Y(D(;Du z4*owNFUc(WjPQQd=7C|0>6%cF9^o4(83h!f!{j7vOsuICBQ0X-)7$PnBz$$eYEt@-!VNk}6*J|JJ(ztG)oKMf67 zrJnUgsnT&pGU`bM1@I#VW@2g>>JoTk>rIp5T_xK_Ob zP5y=Ka|LSEQuBcMG8sl=j03rG)<-m-wG6_`W_MQttNYxK{XsFim3R%$G@m$9XGsLB zx=o4laH`27FhdU(xcUxfk||KC3J&v3Th#!T4ADh++9FV;x>vu%BCmpn&(cmXN9=|t z7jvD>R6(L5-Av-rdnm19b>rqqPs6Xr3-LbIF~rQ0XlF2yD(LGO$V0r40HVf^Y1k-X8rb!sf{5U?1*R`~`iN zC~q~ADhb?CI}B%qwN!`T+GKETnRhZ>`atMix0~2TE7>utc`C%i?SUtmiRs#?hDg;u zV9b0WB_HIeT*B|P%1E=_W6@K&_!AjuAZDAP;g5H#kqfKjR5w>1*D9Iz^z*qkJl@l} zhq>(1f`o6=U*Pj5YLY$?SZ3}$^p*90j>{CD%0@icDyU4IWt#G}89DeK)l-#P7qTwu zOISsQ4j`+n!|$meO^Sq z=M}2i*g5o4NS()iW|~K{n_SuLEywqU{M7B!SmbLQ68=fwt4nw?bE1UWQ7CJQKNhdy z42Xc(2H6s;0cO4_)HaCj89@SV=i3E7Qkub06m@xf`PSx7EC&utfbP3e>=q$0$B6K4 zkP)owemzb%`sbN(Q%4-W#Xm;pA=ajB7LOLF*EK>+%;3-4`_aMHTk<~DFU0nsv;kKC zJZmeL{#)CDu4N}ouQ7vFpH`;lv)%wa5QF8u8r>s_Xb`K5Sro$hL5H_PMJ@X4AXA(5 z5}?x$L@z*w?pFFxWifEhLrUec63b@vE13pOM<(D4aQ~=Sh0*}eu}&15!d=qLX&2% z?UD)ZyU90eJN$3?>EFBkdNkS&+-ilW{dX=_C4Dc^T@3ynkOsFA^iAW7-*t6V^f(KvM`@_xETR?PoeY1)idc-z@{DyZkJE-lDUR zHrxtLzdwhGoQ(vpS&`3WT#WJZK@+`1Tlt%v8eD;@Jj>gMdV)@nc;F$Np z)Qys_6U2F*qpOd-1GPbHrXr0+GFjI~1GSU|&_pE|GAOTRr6saNH>=IRZIcb;lmpqM zc7kC(kgw!TP3)tUJ4S%ay~r%9GAz?igvmFW2R<*=GQG{0c(PMsQ!1^_aJhb03-x*S z`IRq`=UB1Tftqf=)Tcvz1(FF(*qJq%;Q9EF@glYO0X5aEHf$8Dr-V3B0$hx9eFpE$ zlmo2H`lo8~n@m>qu0a zrNi=zTSwrAZwT)~N;R^ZVb;?Jg^lP4>m*Zz*YUkj0hF{@TflUS{3tXdbQ^b^Eoo9k zbzqfj^?2dZpgH=U+v6^mXgv{1EYt5m_l3a0W;Bk;xe_Cl@Y;Z!>@%cRUk9EOa)Oeh z0CycgY6r67T2_9RMEVN7$-geoQ&UzAUku1Uf=_g-^n*hpM95wTo+k8a_Gewr;w@{J zuIX?w@da%SZsNUWVHTqWv9^HdH9(5)f485vSqA$)FxrN#LX;hv3uG0)a~c`XH7}}? z-{wFayY<1K44bSP3*(?~(EoGSh33L{_Y<4k4@OyV337&zTbVRRnTNS8SD{eD*iI!47hY0EVQ7l zpGPBg=z8!_PlaQVR!B-PC?&$22eC8Z>7b*-Zy4^|CcA~{(7I2o6Qi7I&pom+xCuPM z<@y{?q{>@1Lsz~0+0N&^z^}!nNIA4%Cyke&-3~wl2jDx-km9`wz@h=4ubBEl`uebK z?v@XM)YTEkVn0q`$GikKYnWqEz~?o3Pr%$T)>Rbo7+gC-?5K{NTCLMTB-F63WIBw^ zH!=&gN!pNNOcv&Ouei_gKSp&Liey(@ZB}$2-cr0iEn9@Cypo{r={ZN&&h#%|a z4e%#Zsc1lYw?RLV`mD3A@(=jiO@%`PTC|Y~)-moH?Z-0sr+3_&>|%5l`0OFVr}_-G z!A!83Y=J{fw9)jrSP7~c|NS{G05B|}w6z9$Slea31OJmiAWa*0$T1@a5G zj?Wje(n{9Pgecx;=iD$`RYvq#=!R}8O@SH!TRSBLjN-VXTg-ln zg0e27kMst3s2Uh0$~`hmUckcS1eSY#HR9<|fR zW3^5Cg-N%vS5E=5bf{1R%XD*odYK*f60w+of~%npI0PvkBWJh(-BQW!z7CEff@ITD z&c*v+-Uyt8>N?reP!FaMY5pF5#K$7<54 zK;pDxJhlY=se4|&C+F!cf{v0Mey9A*KZR`T*Be8{NRd~OZ$I-T@}Mv`QcV-JN}!wZ za*2V{5s?oBZD#YWaMuXWD1al!xu5JXG->BLGqD27or}a8hE_TI+*`(NET9&Ky_6~}T+D852U|}eVOo4P zka{$z2HKA@zt&ER9TKYfk$u(r7VqkiXqoF1SzncQi`6^67rI8y33kao_OO{q!3|Oa z#uD*stNCdIdgP$qqRejqQl|IngfkPlcT6_O9fA3N~EqkO+X8C|M zFJ!kp#P@rV8|^^uC-7qeUi>DWG=!FV1L;$$7Ya4vL}o2IDH;bm1_U0 zKSt~E{h#*lVkP!y3h}!@1Kyn`-BKxK@?z){xMwYEUN3ioi(%+&5}q^2a}t>kNdtD> zG*tUrM`RDo_L7&poHcRgJ=C<@U4V?F?<9QZQ5yWxr^4?QJ{}Bp$On?m4i#v<<;(b; zo-{3IqE;3(+rtVUfCjh1*DnL%{qSNWc*+MG@51RepK3i`XGq_MGj>aq{?U0|i1nU= z&N&7QH%OKyOSik*T?rL#ghuX09}SXKOhjw6sGVzX@eqrT9OKG$jy!;@DyMH~F?2v& z5ealaUiW5w8NFKd*o2gdSsu0e3|Iaa$~lj$`9P*8Y5FYJ4PNMqP^2GNe3BUBm>BGn@f@-HjVOH zWJjaC=_i2M*{qrK3E=>GWR*I4AeznkbEF;G{-?9(gUt`>@qhAmP9|OKV9F{!zw*6e z6U3X8I1kYfDx&x_V7*rHI>J5ID8151XCf3rwJ02Lb&$!stya)uTNG=WACvdt_D(e4 zV?51ju{w1@uvDXo$Soeg6146=7U>PuAV9^yaAyqo?| zq}JWOf!%%?VKIcm$cavWK&p6REik3y1vyx&16bxJSvbcQ*#Dg!)$x->Gn=5V2f{PS zu^>;SOMdJ+q<|;)0)47m`2Q3oN1_<6Twxwu6DJOqBc{inBN zIbN>`K1s4%4o~_vm1(<(?FvKr-1d4k#eAL+MbE8NW49_%?C=|+xR zh$2e>uUQYqW16l>O7Ez?4rH34DSGG>NrTKEhEghYm_PKyL+4YnN&3W2zT+%S{*FRR z)XA`;Ha_dsY$WIaZ>eT_-hJ97)U?2hR)ZS}r4jX&MDb$0;^k?iTqlwtPh+`aJFqsZ zYN4r3wF#*&Kqmc52*x-M853Q6!h;Y!0UDHG#jN} zcC!8ezM0SWWueR9;Pp&BALK0uoW)gYB?{_}1XoO7fzwY#;;sfJ`?&8s$%hB~bWpeH zICH*w;b$rXWK!aVQ(%B}zfU9j6ALXIVojs0Xh>N*kfn=+`Vs5vu>Kh9b@+n3tL z82NZR6fq7pROP_=AM)EWy-c3fM_EUq^PisPAI9emB(Ku5ZHe>!9X0S%0OpB1QYz*(2m*e-@BQMf$f=sFrtJ6FwaofNg)lbTVy0FnvGgzA=-+{l{%pwBfJ6Jn&VZi=X zE?yeo)F9aC_emwTfc2>3n*!-}vFhLkr3z1)_^FQXcW}Nz4|04ANo%J+RI{_iJc%k& zFfi%NzkP$hkMJG!uh3aXc%0~>>8df=kH0~`8@rjQnLw;ZESpmToO)SHF_>*awpQ>j znG7hug%cpH0<=Ru1siQ#IS%!{PUU9}6i;_37@G2Xfa^!_)h1R4~z2NLmp{;yh&im;La5w5#L`V|w9O|Ja%j&iuL5J0R znEhB~zeB!ThtG%LHA=0ro4K5t#%fZ)8?)|1Ryp#29$ycvO=IseM;2Jj0AEZsXYY-? zrvcfJrxs1%j5qv*unuaw6v8E2`IOn3nhFN6# z3%J^Z&~b8Unc%GoFMm|NqyKi%p*Hp4i^@K*X#di0uT)>s7f;157>^grY}r{Mc~T1B z-6VBxfVFwu#`TTzU9i)N1aIe@3Zlo{GwvtYeG9NsdBsKBQf&XUzhIYE#>@_ z6`^t(@a~uGtn+Ty4tGSL!R`PqX1yg+{n4opxxMJfDln8UOghzEA|R#G&u&}Tkw-F9 zdxDQ|7N|uxffic8sv#~WkC2ZPs^HU~xdh&Bx$kAfOp9cYJ9kKlMCxp)pjqC6UM7({ z)XZ}=GqOW9tm`a2OZQ0|nt27j*bF{d>~7WpV6)Xvu*w(kjK)Qw@+9Dy3%0CZ{HCKc zfvv>OiN|rBQDx6jDzysQk>CVm3z1Xkp#%JxmQB%ac}v2gQjBc4Uac0h5}BGPhkZ(f z&5i7p!B93(DWC@?QS6+rLT_`c6)H4ua2zeQRIc``BvCC^NT-ROPE4tWd6c>Gf38Pn zz{6f&0sB;~1G5$_A+}sVEa3!TVADG?c-nJ9-w~1DPJVB5&@wpA^YN^VxnycpfIHD( z{rExOlP{h1POj#zPr*VAG2E@#>X$n^ao*-Iw}AzwiLrKUJlW4)^5Nc;fIJLx;5weR z3~XmnqqRux<~q)Dan-=)J}HuW{b}&_&1lj9JL+jUPb$?i)+$9VGZ>UtSq-%=M1k@o z&`+g@Xcau5#l8S;jTMvG2ZJEk4wH=Io8`^jZ`!AODyn~Fo*B8qMu#_(;OkBCeN3-T0O~PualM-F0 zVW=*LpKIg}4YR%`G{UE1lgexKM}8WP`c=rzA~=BMW)cTf5jaNy5KJMe-z%Hu+maC-H2U@I6aPb>px=L5b&1fN; z9QUGTD|NE|10SjN;fvI|?ucT6$t-;l3=YH9)(ej=fY+I7PvnQnANKTsx9W%#*G%*z z0Vw=D7$)w-sh~v6%YgALc?s@~0!wvdMWW?=_Y5+O>FH7t>=9x&S|8Z?-V@M#i!(2} zKn{V)7O_r$o1kCFDjxQK^V^ACZQztJ>!GnK(-B=P4{H~bWzwX=rMmS@2_YhgH?l## zC3}&m_v!!=`D@=y4z3Pd9)}lZHQ#o6ahd!wuw2Pgu8N&+kX(MQbVclNRMF!4IXcg} z2_xkqez(({+SJbSSOYxq+I_diz#rr>S@Azv_s8ri6}(^}V`?)w_R@sce~-W2IOwNJ|wl!PovtYGkseUvyE^T8IRr1`iBPXgl(=NxFpEC$;&N zZyCgS!9LTB)dv`{g20XxLjvs!{#yv5G1 zj+a0eP)R2Caa(5id=4(RDvWvYdk#9r-9 zH;zbxQ19)pmnMzCYoj(0n0511>Y%klpW}0@f9U|8+Z=UE{14{?)jpnouD%3MGv81a z@uYs>J%M*?Qi;iY?5GR6XZ|>=xE@{D!@czGvA65F#wK!=)4}p6?_B3|q>w-V&;5~X zcb$Aovwebm-@Ob?C34q?&_F+3aW=K*QvNb2A3J=#?Bbg?GAVoI-!6vTw&5c+0{Cqg@-X><-^adNl$`pkiZ`ntKpI(Q1e2Pcd{eb!fF zasQ?Inpo#^x?JW~DE(Vte^jv6fi~RIIGr?&zM-lXmNp57r= z%{jyynR5p%w(~Tb&aoK(zXP1Kg4G4wd03cwV(sp}5NjJ_*3zbDPG|byO zd%Q5^EYl#xQr0uC2c3I7 zyE&-jR)JiOT$M>AvY;CrrN{<$NLP*4 zfOj}d8>B8-}md4p_939CeUbuKgQs!biEziPeXCpXy>z$ z9d>e12|mJ~Lqq654`7kT9{*RO3ENURZVGj6T!Fn!J;^h8U#Dr_r&VC(Gi}BO~0jp`Jnn9k_ z`Qamux+;0oeF7$uWP#iUMU?7~+$SMX`VrAvFY2R6@x8##^3kREb(uV4l!)~JpW^+< zN!Y(plBQNuG$rMD(?djdlf=%KKq7;qd}9Ce(7BIjVFY}MPqJJ3cw4;R$1|;J?8p9Z z&gu{vLQiX-_F+*B0-s_TloVvoS~&2}Xxe@}hEG|=Z1z5K zl$K2PJ`EU(k*%>`|86_=s}Yt zggG;TT;_v5M@xXvN}^LX4`Z!H;7;D(SKX&K+XkoGB>n@=I63zMAoF_G*tD5B0s8g6rE1WKae6vd8MxOUymx4@bloBwMuWP86v8nL4@nrlL-Q=p}J!pNd zTn?nKg-e#o6)ux^_Ubz66Pw4#oJ`>G1syv-BU)8X>~kCpTUNh^=@WVE`7EIH8eG!B zoi7U=z`$rVvabTjEKrM`T35Q|I(lReyYfN}Gpj4bU;Z+vg?hGd)gd^C`6oy^g~rRE zAM>&5wT|7?u%1Tb=oX)@*0Y@jXRHPGfoAy{?rRXH)5yb6k)2JF;caSsC%@aI^Ibw- zK)GrDQpVGLS)AqWfH!4LClsWBgR{!;4AMeD%$}a z6bb!w8ZS0g#(0;L9l&2LD`5609^*^l7VN$`z(1MyC5l2{Wm<*?`j(u={-(e~oX{T& zp2+>-#T#|)K(CJ}9S}g(x}uk3T~PlDpUhS!-fF+p!X-ocP3peL#6y{FeBYpd46>l7 z51EHHBu(hhGN5-m??{54$H+g7;WcIgfmn8k9W2FM%``H+mL$HO|Ridi-N5c-Mu&8!x)ehvwKyQ^VeJAn;n;bEoTrB8THA3`_J zLx1N3!xCq8F_kV0jGW7wJJ`dqJSh$9CP4GnYZSU9K|cU5!y(Iy=dj2BMP9pw`6txP zA7Hn`tg8;#*?FgS#x*mN@XlJrX5UAMMP>5gybRr|f}EU^*O(Zt-+58NfIcqcTP9;m%q_H~8 zZf)v~ow-m6)TufFig)OGos76mV?vMnw}Mur-x^|fWJ}}|?z&8=@zkQDd|HMLV)dMz zk`46Mt9ATc=VoC0#Q`VgQ^Apg#P}?)Wb>&Xmk){bos7=$tRoE>LIeyPZeyjbL?W_S z&l0iI?E>MXRPY|9BbqDZZsA<|Le(apU(cHN%g_8~8A2*{a;KtoVt}UA(@yG28Px~b z$9Z6VDU!cQnXATg2BjAWZb3pa`-o{M=ej2DWd^;B$>&lhqgZHH^6oO(jV>FI^ZA6V zq5M6Z33ZHd6?3duc?h1IhF6GW^9Ein1JLW~a8;U=A#aen+9rEh&qis4qUP}C3GP0O zy_pAYEDL5)lWy6n0rYvk*nB~=Oa^(Y`KdjAjjQMCJK=>(Y^vQYtk!x0idmH+BfpSHAkZrtSc%8#FBT?lpbegJ zW{2G-xyZaI->yYyoQGVt@8sLZi7-Cz!_apXvUM)8tPW@*L%*xJ!1~9bN7UlIv++3( zgNt%_CJQPu5Au7uj3=hD*1ZNFy_L_5E;w~bE71;nC6)coIr^`8)^^%S8n|x=EPgm8 zg)#+n$x&z_`2CwJ29ACD_t1czK@`Ah7;WaK&5O*WBGjY|(Gz|TgR|K#Ib_zY`BcJ= z?c|$6i9lXGDAQUERCem|GQp>0JtUxd{_p4-J0U6$j%n91eaPnt^?%TA6Z}=kzf5am z9p6JDq>^>&g!--}ayZ4@lzq_0PPt9@p+){2Bod7$n*i*)r5-p3(DbKj(~O80$Spnv zI9!N5Q6W`QFL{zp)Y|L)z;6n;TFo(e2q}_Aex~zKCXpS7kXzT&UdDhMN+~QbZ+j2Q{N{O zW0iXSbmk^gNl0A^^Mq=+e+>Gw>SNPh(a^ljjBStsuHuARbjNJsRo39WPLQJT2zkSce1-H#2fSL#nm+&=L^#2aWe=`l2A7@GSPE_a z+3#kpdx7Wyq;s{N#!ReFS^I>Z$7jX7HCsE#s82wLR=;tTrpRD;Kwp5XHnN9J_%fUv z0>rIWhpeurJ5`zNsugmMw$nM0i{09ZhuefbQ>;G-73nz3yZXOQMA+iR_VG z?ENj)bQk%u&v|BRuoV~N%o$Nj}qs0=yrKqHdFcBqSRf( z6_%5G4v)7VDnF#dSV(#BrU`yh{q^m;YoB{*P>ra=kM z@P;+)w_0o_`wZpmVB}$uFIEQ@mIJeQi4^XIE^4HTPoB}w;g0RPOPk5@7Gp0n{ZjK; z!+l7{hj`8wekc1UP51)CS|IIE+WERz`=GX;`ZDb88U722(-Y~^FU3+jQx~Z!^Y`3N zr>drN1+(BMFacDWVUBLc*&+J$Fkn(;VqB%W?S3oZAZpdd&g0OZ6&WOChnHTDL}?K_{KT)R|p) zuT^@1k7f5?D0LM&PUOU9(R5*>hgtI!R9ABpx;A@*7#-S(^G)HLg~-LJTL)yL6m!jT zpjSvHa4T}{0d=xcZ-jOx+!0?V_d;8oSOc^|+-q9+MDUU?mtY}|O4un@Un=@2)-9H3 zsOK&Az$uxskVs7jdOTmYQQzK+H*lZcp~NVpij_VMw=H3B4eXXK2ziBd{{@{EBW?N; z-ryZT<|g{jHpmL3OBGyrk1t^N&xc-g^pGHZZUwKcoHw5$c0%VCIYtVlSl$=TsuikH zc+R*!%y02jBw+7qjVwd2g0Dy^}RAM+aLKDOw1r zZy5o)=D@}QWchla(asLap#B5U!U%kG06qMXcFA}7YhD)}Bw)8!d*M!t1YF~Db%!)? zMsW*odJf5*>6^t)BhJKP*b7x(%g^JiJOHx$BAB&}wmXQgX;~QSgIEVdnDii%tT2YXMEQ)CA|La&x8yrNJs&@}7( zu~3OlLFbjq938D88fP)iYF1)3VNYXsKd<*eA;Xd>Hn(7#PNRWXxt_%x|IWsP__k+{2YsVlzlK1)KPJ2mEGz%v4c1n^v+>`>8XH)d&3r zXcLRCtcT~8D(Ck4d&Oqy=2M%LMLcgmnZSKX!s4TB~8EIG*khrvLjF6QSXzN0RRxWF{K%7E^!77x^!zAC+ZHz|>D;|LPR z?3=B8`W|b$3OYR?e{q8vMfY>7Y}Z$?7VT7qO>k)|YrY*`wW?gJcm1wp2^EP_%bsd= zf&RjgTgH#ff^*uSs6OK27xMN2uC-NKmnYt)%yVX8#Pfcu1383ru!__j{J%+$2)Q|) z!ubm5Yi8DJy{`>%MzmfMvePM=<#abfR3=3~2G-=Mcw5+A&H6{(_rq36d;#=iu^4jM ztZf)a9YUlB5}mvWh0Nw`}Iy zwA{lLoHC3g>V@X#Xgjd^KW|ek-ZXrIos;oet(T;Lcg!WW(x|aP4HBja8f}L+5?CLZ zbG3?)(;SsjJY$GAS*_auP(DG)KRTO-{aa$_jcPMvnf(E*X94R`AU`D!2ctlxkM)q( zL?Ypd>jZGbcUAN3`jwnG{7cP%)JrjciE+RUW!z(Pp28B=VkkS0w^)3&m}q1luw83$ zPx-}d-CC|4Jnug4sN^|BN!Vu=(4$^MiQloekJPHA?bO`8z{e_5t;Ug=h>}F5**;;O zqtExR@V4`@+Fp=o9SN8U!Lu^Czg?*LVvU1zq~^M-rAe-q)1cJ)qrTj3*4ieEf$A`; zdWczkMY5L)52nwBiEwE%yV=CMm`o^bVpA&@tIe9sXBT6vqX1Y=^2P_5*?f|>lft`^ zPl?#DsdC=_Q>%{qe*n0 ze!o^}v@S#^{1ZLg0G6kaA?a%8sVvr&QYrf*a>0}W#cZ)NR%*dVAGX(|ntbe4%Xyfe z>G7#Eu_W@qP!0c=i_ML-ikZXmwa?)mjKKP`Bk-zi&!&o=gQ?^8|pr0a^8tM7a(fiQT23EeBcN(^B z`bniU@RokLJRm0nmz<;t+}DPe*R7`r-AL@fW_R7sHG@b`t00|nmT5i4o&p5=fHo6I zwSg-t@gmxlemNA(v__?`lJ@9qaP25ntk6t3BN!Q2eb(9;-~^)L>Ro3vK5?Q``7# zp4e=qzq=Qlos%*kXZx?fj^kU_DV7}%`B)i}6dfW;eiy5)LKpq7QjYm}G74YcOT31k zxUOBMeKSu<1fTU94lJH;dF??dSF0DB&{W5iNUk10a~D86!>r;$`39IDBM;q!oj)l+ zN4u4Xidv3rOpgzv`@q4p9Kma_@9iX@?;QQCS04^HfWaf!8q7}MeVtH(Ms>$urJES>i$ea8sK#Hg&wddMfKkK4IVLy=nsoSDw@U}jr{FB^g^stk+S{+IWf7St4 z^p@VD^wmoocJt%VTelu7^I740p+(LtIE&0xi`BWsK*3J{=K-)kDEmW}+x?Vi;B)fp z;0>X`4Qa zX1_GpDA`OBU`hl!;wj*g=PTL!AojmUilrmf@_2@IK|I5|nusm$h40$=#-x5Z&$CH} zy#{Nz8{bnc+G=$(`Q$Mzk~WIOv}s+O!^w)K)GER!p@LkvjGP6}{*`AYIeNhQm+Y=Ui;8+H`hsGo2beaA6bGMmDLZ0IKa~mJYTHIl<$h z%UrI{)vLMVByA1THK(~k=L$Z+Q`ll=MYNE;OO->Oj3Q8EdV)r>#zA0X^)-S19$VUC z*$J%X^w3+Z$f{|{07DM5=(sqvcrsxBhNBlVLM-o+#<^1#)*I&{#lKMLc#y ztNh&0M6#Ug6E#L=b9b}e5V{W9juB=XN|KgC@kP)l=Vj{vn!ZgpAj3w5YCf^aSZA?X zJ3}duI&{+GS|YVXt8a6rLvPZ2c~>HV%UVIMqn&2rKRrmqw;EYUZL&tlBf{xex&x1> zUefejsSa#TK&*5EQJXVSCDg{qn^a>B!yiXGNWfaEPlNVLv>y6RQ>MJiLS5#@Lxt$5 zu%5{}se4kh7fM_{_`4FCcv4ppYkNd%sR0}UB3L8LR~_LuOO=M* zh{ot!S}7IM2`4n+UDScke8orhKlZi6fZKRZ7}!T+1DAm3ZplT*)gxJUz_(-^oz1(w zUagjJg}+-42PUsO_%;jfZIY9roXcG_@sDwB1~+lk5dF;WB~Nb?JHB)$fzL{5k{{#=V0c=#k-4mJ+RA~}CL(L*w@Bw$4PJWvw zm7*1>M}WTCWtmUVZw0Ij%C~N6^WTc43oQ5OAka78$foxX0pGj)X5MSPdcCZ*4f?6V zHm!lnhouo%tS2&C!(@OxG9=Sj1E#wzQ}9h^9loO)u-;pE<`lefB0u*+1-sx9;-_%G z)g`yHL(8*UMQEwBNX;m#O4d*L-}?KlhxjHUWavwFP{Ce51{QYmJQg5NT?pUIg$D1| zjoO67nFC8DGGfpVny?rPaLc1i0`xsN&nKzZDtIO_2oQ zd|mKf1RYzCR|j!AiE7*|N?@`!kRUxn$H=xkSH2v+>BE!NRmtIM#!y3cuGs z17F<^q$>Qk{Lg(JRJKT%hQu|8f!hX2yv;PK9nDqhc1;?rDn zob1pZ-u^I?N36Ts<|ypvdaJA3BWDuX%7inv1HU4@3pq`u8kks>yG{39!ZWt;w+f20 z%v1{yZ;^F;UxUnE1SQ@lFCjU8>#ygzsrs?wTwCD5j4A1s6f{L;Fhko|tDZQ!D+)_%<@Zif>$T>Gpu%w!aKaR(B%LD&e(#lT97ZJaN(pL!`1ur zKg32YyGZAdBttjMXZznemD(F7$aN8uD+0z%L>s!OUa7%sp!P!V#Ns#|v_l?DHau!$i1kjX6}(SHU%b!a<(I+(;FfLjV#C6mLv#ZjK{ z>AHsq-&^p_fzS${ECrg)URGkG--txulpo+VSMv3JcDbBWP49Ju!e@FBnm&Wit)?zp zVnh3MwKVE=NRxD6R4tZ&yTO-Re3l4Jq_zVcPMQKA59pYq6G99Zsqo=WB#F)Po)4d^ z*zDXi{S|SBNdJQD^wly)JAnJ?a=+Ngm_72%AdFS<|9lad*Q8LVzeq0*y`>deFY{fQ zZs(b0;I>5?z$P(qq1Hj^rNe&82Qy^BgE;6Q18Z#zi|Q0sHU>tcSj|UNRvZ`31rM#< zf356sAAsG9Bu%$)h0U-1oqwI0)fSCM_n#A3*UWbKzZ2QUL|kxWb+=SqsYT+tAiYP_fEMPiRIblbL?XQdND?R=o*P_ejT!k_*l3t zU2~-|n2>Vx=x#DsoBav=e;KFPr06za{WiAMi$so+T`^R%pOrOYF<4Y&1J=wr+NKzVjC|Gs||IY}@VE?v}fyZJXWwe#d?CIF_&O zJMYY)IeZ53WRr2nTNZs`8`<gI-+0}YIEPOMLGSFB!i9y9@@q~E)h zqGzw=*iNjTb-qg_DzO`FD9mRi@8m~*HMr@+2i%U$;rhg)qzB()1>D*R2clDG3#UGp zy?8IbP1s)DVDLR1fX}PXMv+Dt+(68aR|Qtnq0sPq@ag`ne3PvRFF|Q7c)XYztS)XD z&!WOd17+lcrCeoMV>l1pq!y3*mB_gJ&}_EZOncMcM;%oj&{)a2=oE*~D}#u0m6gWL znxZ-ANj)5MCsdaOW#cK4 zG;UAWg#I2rwKQ}?b`Vs>tZqIfV-g(EcO2+1h8}WbCHH(SR(u`4Wv{Nyw2`T9rI*8r zm#fCE6kP^WtNGoCT?Z9WiLlqRwJ?R39=f0dvSgmc*2~?n3A}kW*d>-yKD+fBrBtV; zJE8SX8*})B<5nu0em#sAZL-!|DATw``^d042qv`}UX>9{5 z9;Kg5dpw0Y70XO@*(1nv_wV)byQ@Y1fsNY=xY&CmH`}NZYhypVf&OzzH}Lk#TY8;n ztySV#TWo1|IiSbFFJeoIjv}cRsNEPS`jeJ@g*T*8P z$?NO^B)JK1Ie!|1b?>sXsPxz{V_9V|e85hFmx3CHwyU`Bl7ef?vtrls-;1tf^ZEG!` zsAfI7)l0~EW{Zb8`}&Aoxksm!j3?)~YCdlU5}oh@`U>>i9=)Tj zM}Nr32GgGfvdD;V4nUu!#8b%%wY#9X1^CR!DB+fITdRUy`nb6!f|Bs(MsxV|;f4&fU_#)XIev{0$N8+QJk3?xS@*m?0ZRQ5rfJ9ttQ#n;8@T#|M z!87URCtroGuHyzck$u+$JQ|h(X4<$x$%X)Sx1mkdD&Gou=S-fiLLLcOac)Ysgm*8| zW$bJwvM!gdc6fa4gw%aJ;n{7~K$OYB(7*(}NM8r?w^PHGwgZCa=zo#&&L?Ek#uV_` z9X!u?vl6uDmFnX8JCicF{6=kN7t?LEby)>+AR-1j6LQtTJQ-$sZ#bc2YjvJxs|};Ea-@wD|72*Ir7A-%$js@ z2zafBL!s*QOAauE49(s9?s{Oo-pX}4G?Wgm*Ll~(Gs!O7jy&ARG;6wT<6phm&etb! zzTX1xqeI~@hkK%pqfzCgDu|&s!66%gS|Ka#H1Y?H`X8w0Mqrr3U2(5`0=aQ&@5$iN zzq2@3hy6C8TL<5tk=_l}ZesmOTdN81Fchnx#cv^<&oSp@um9S>xi80K)q-T`*CZ5C z0R-`N+d|!AMOqbR>G8Og*vc;g)#8?lO>kW`-?^0Y!5>oYXQcx-9Oc<%v*7LwJ3#|lVCRR6 z;!D^vt=JSt+Dco%Oy+I20t%+Wh`a7%pvsMiXJacC%VB&gNB3GJmQxLHdOwHLOmunJ z!fMZ{Uw=$9>({73LZ(G@`GvZhQygSO*}%C}!h+bq?#(w;M@eEHgv2`bu(rV-< zplhNNroLelNPH}~_{dalhvXs+!uezobK3dZ0oRp_-lm*&3OF2O8gm8b+Jn^e-|XSW zIAZS(%F#C6e{~VC@>&6p#yT`M*bh^?ZK|#Z*Sn!0EN6IhGw@=H81zOp4tJny`j=qE zzRjJ}0?c>Wzi~^rr#cO7O@JvTiG>lKy)?9eZ}&emTNmG9UN>j?K61h>P0uB{3h6m& zlcFcW-m~|ju~T)Gbihm0KZL{Zw@%u0u)!P_sOKgAP))%h8qsp-M@ zQw{#Ti^wb1#@;9G9Ck%78dls4CwfOWV(PrlbCqg!0B+{>+orZrsfH&!05Bd zK6e_nD(9iM4G^H$1;5+H8WY@!R3o$dzkv5PxHymRE`qam!r2|>{k)m>Dc8^*iKD#1 z`wk9k2H(u*jc3Cpqj+}7px`aXrt!zZrDfJ(S?1jad##SOPYWYJFbnI@eQR!Q`TUqn z?)Dkz5&LNabh{mSPbVNKZ&!#q4a+sZayyWD#L{tlJcL#9sES!fg~=uA#OH$!Z;_o; z>S?$l6E90PQo93qpT^pQ<{9@LXv*DWSG-_dKy59&*okCqN)F-1F6N8_%H)q*MLYJw z!fP0I0iD^FiIq;aflVM`OU-k3|2cIW+|irfCT*V0O#hqtM2#*ca%=d-#Uu1-|8<@_mm&f1wHfwGNuxfggg{wy5Q{K``bWp*l6Gj|{sXA7#Bp z%&WcH`R`5TK}U_ysCU@#42xx)yp;30Uw#wPV*@|arWbi{80WzSi#RC_S$4yQpY z?eeZ+^)ao&z3;E#{-VDn5b;{KG3c;ao&0VLpWol1hFKbiW`^0v5E@5`J;si=S+9OM z)hjed!7SBXaEMn^crLK_OY?bwKoIFkH*K9BpHXZ4GKxK8O=~L<@QJtX=VX>oERTIe z&adPyaDRLs8IIZPWG){59Z>jjaP>t<@mgr(Ztj&ny8*v%uZ`LB#Axw?;tgM9X=|}+ zya1j0d0ed}yqEe)n;}8X-gO&~7t}G4D%4~g(iv9Dc{Z`uACNhnnnxYwFX7BrL_a|P z1kGm>Jg?RX(7>foDA_8&w1@R)`c9w)bRiv^`dS#of*P@9M!q>GZh}HfEZPz54gCwN zsTc^jPxzO_Jxbw2`f~u?3@q+qC=~4rY}Tp-EtHN`^}NGLJrd znQBA!HM8$e<6LBEvsHn+Om)G*l~c*15?!Q$+XbwM9USv`S2egT_4VM{68$Z_3PqLM z8sND=?@T=$YB(A?dWN%}g$H>x7@9tHKe+J@ECs4skZU5IRjJEZX(d@lX9c>M*f;G{ zU{h&1_Ok6VpMtW=o(6}V@YIuGT)9TAt`&owM(u)PdXZ{rPC({8l)cwtg2R3!>Y1>6 z{x|I8`**{mlk5OrGAq8THc@4dH2F+T+*jUR5AQy9^)S@n_tZnI$e% zrOaLFj>bf^sc&Gk8--+b`p{*@@>%6H~N8k@XhqQt!njP}0WW8FW+>>9M`` z#uSx-!5m=aU2`Hjj^}eW$vfXQbKiKUD)*o7l6N{=rBRzOcv9>5_FU+v(DoSCDW49B zi67=wMqZ&o)q&>oy>FXm;Q18Uhqzb07gis?@56h%0}S-BLyu)ohP$C6@wU=U7!}d12((in22YG z+$R1TU9Mqtv>=-3grf1lyuIbM6^r4;3OqlzrE0W+f1y;(+R^;8F7bjW=+* zqr(thup4Z#jgo8G3RUI+7li>m$(xJ7z=?KlyaniDwYuz)=PA|l<^eE1!9JW)I_&!n zC#2?rh!qcopy9wXqu^G^^aIJ!^pm+&;quI zq$>6^o#*i)fN^5rQ}cCa>;!LBockrz&9qa(1t}TdYo=JqG@NiBp0X zdm9PR5vgZ2@;fcVRsliZrTbF`IeC_jh0m+cAKGRkUQ_I9r%!ggq)=Rf&BKLM=dxPIhSW0%umVn9SH$j3Qa<63Dx(|D`A zBj2%h2T+|BqrSr5rIrg;>Gzz6!e#krUCAj&3GB;9~$P+iYl&27Loldg}0fer#s2MbBFGd<(h=@XQ#$pWr|EEx7*U`NW=G z%rrTEx0{uiV-5T+O}iI95B_ioH^^?Q&~O*WO6ye}Z(51G@hqG>E!W-y|NC(BUZOPA z;uXE?iE~awx*phiZkg>sJ0Tv~XzR=;_5PXc%X7$UJ`?!lC3l89pn;|6<{A32&fuLZ z@Tu=>m(l6y2hi*UeDVo?u1@Zw!#SZ_2j5~(UI{xZaoN3Albmc4dTzr*Ovf7Tl2Ksf zc~lqLxD65$8&_6(BQWtfN7Qoh|6A;H&f-4RwQetPuX>JVFA`!GR64{x!t4Yf;~k$D zVI?mlBEJlH#Nc&0J)E6f5d6#BI;w}~a^ayoZ3W-kgLle!IVzs2AKIkVp%uHe=-H0D z;D<3?MAr+?({dZF-?lv2d>fgW@NZ(p}5~;Q!IR#7Fz2+tQ zOGHjmNGIIoHR1=CfMw1~Y}6YZQpo;>X;{Psly<%H5sFj(*j| z-SFK`d3R^>YONls9%ClXVw=>;(LZbxRW!d|(_KAPG~PDh5_j3#i7t>O;)A)J;Ga;(*+(^q!0ye@LH zy~!S(rP9`?^ue2_$We02QI;gOOyLldz-eBZhX$v{y-ghdO zm59{Y6zFI|OKkw{fh_10aV)5&+Kvwi9Nud?xQm(NrD5YPfKslqAtcBsc;Cnh9(BQo zYR;_*bh-*Jdyhu7hyI3dhiOTj4dZoB1J?>@-=)DI5PKgR;(f5`y$iTC<1YDh9I{L0 zT(HNeceG_TgLT{rmU)lb-K?8H?>@zq`4q9hdU@*-xTM0~w?eS>8ay^YMHW+M!5CG2 zzp5W8RpaS;IH@dY=Rj!Vv%=%Nh7m1&>bif`aTQZYQRf* zD_+1tZVEa_BU2`0InebQMl$7jQ?$q7#!gn-1B}Xz9#c@y2=v^-dTt9pOdpz=*bQSm zho4$cr*?3bZryA3;Sej}9-P7d7lWxUV*_%#5h+Z^bR^d*yA(MpFj+#p^Z=;i+scP7 zwsNPw4BtPfCOk{`C^HVFPq%GT@3rN&MK`88;JmFW;!gL@;=M7$9t@lGh<%%^)w zHGIT`K;rP;WpWm>@D#P6-wg0CpYinv?t5Y<1-4%ftpV_j^KDZ zJ8=KxQvEck!1Fsl9Dq$SN_J@ebY7sWEOS&mx+D!+NmODxpvO1m-oL z)pm7imw$>p!I~7hC0zrdWS(Z~8qakj4;NV$RQ@6vkZ*`v!!9H9sR%0U(GAG$r`V0> z-+c&vUE8~sv%RDqqH9gan>ES=?(VfopubjPPd+!xIio?Tu$1%cQ2o>bE#y>WGC+&O z7WU?uyHqXbzFDh6zWpg0_~CqaomIu3rE1Kx|CXvG^(gl1x1!g|Qe^`Mn`tXEk1pQ@ zmL`VrHJ!t$z6H!Lvujzd#C)h(4f3&NiPM ziGF)OdH85PHp99MFN$p;Qj|k(YNEaQAm&}I*I|>6>udNIwxwEw*Nb~)L|M38nf4YO zxnA?olUyzWzQNS3M$XfKmP#K0Aj`~bb?VRQYoMv!@qIfVn*Uqn z{>b`OaLBV-1rCQoA$gmHIxsmXUOn}I)DJl!T|uZ7>b9LxnxFF7HRarUvt`kJ@JF&U zuS}$_B2EMErFMnVP*^(Nj~vTVv*^j0Do*-CmadTJ4|)9Nx#TYNf)>2CIaFDVM=}}U zef&7OJLNr6HL;sqzeyE75MWE<&FYz_AmtAON z^MNNGMCgc0J9u;exsCT!eS9_n9=9+>I4!3>>>Frv2+Y-+-^Xj{dsP&vEa&q9TQRlA z`k}d>pxIu8ZsIvy^G?(_|J@Td;R-wZN ze)E}oY3N!o0JPvQ&yDw)&)bli=W@CVxGmc%>_Iz3PfT6PzumI?uKq1#aGH9Pkmoj2 zeJSFMXpSCh@=U`;HU>1oBF~Nib#gq-JHRX==f!Uv@~NX&UN*5CvgW{y&(|6=k4v_5 z3)C4^N-3Y}UI9If@$colZ710Bsf3v}J?TU5^UBBV&_k=PvLzTIP0djwfDRB32~7f zZ?g{qeV3>L>%&`rhTUTZ2bQRSitjhrw@*aLr&H!a^z=&A=*;+a=r7Isr(0Hho>P4X z3G>f*sAh1cf5c0yZ|WQ?NSy|Sl@ldjq5SXw@g@8#$^}DR=u9I<2b(|^Fn`}@RanO} zkXU4SVa3njK5Vy&DGg9vIg-e|`di^V?_r2n325|?wbK+&;;hx=)HT~Q-GPMhTGw`V z($3kp0Z+8kSYYe%{0+v<;QuiAXe1npr^P#=uH+6~i$;*CTCkUehcmL5M!2V#(+xv4 ztE^48*>`MqWfPV}DKhTaSO8?;fZ)FB!+3umF!z|9$N5%s`cB~6h|JunTvnT41$rm* z1hfK=?F7%G{6FIP`q%VNkIZ!29_YYrF){&+xRfz}4jtvl@L)`ZObv1(e6&zkniKX} zkLAcS$Cnv5l&I)O<_@xMxjm`j*ny4rIMhIGEqdZ7(75|`s2)dqBd%>-ct<^(af8)r zSwaV4XwE%;W!(8SP)KCnoM(g`11yA0^7$TfpsRJr(0;wi3RCo1c-HdZG-f}t3%pcN zY7LTdFnZVSQ^UEu<*POZzPE$TF7`bJ{;uX<-F$Dl`Lwtb?0in$X?s%S1?o|JK7QAZ zSvU6yeo>nV9LuRgTLPrcX0L7RbJD&Hct)%r4Rb+={H9J-0^`t$&#rgm??paV_jTUha|0$jhCdT52(17bW*NRG%@F%>l57;dstqDd^2{9Q$LqK~>u`)69EncQuO9#t;*ZFmW+1o2?o_inkOsexmF%Ca zYI&s8pDEA=cIn-Le8TEO?2`HGVCpPv#nsd&zY4V!5$&mPN(je-**)PKa3b+7TZ1mu zkor|9AZK)=z6Omhq4Px)^;<~ze0I<%`l*1SO!N((wB7++Gs2zfOl^;kfT=ut+acQ0p1bCMo;PSr5dqnMb_cGd{yHRPUOezE$ITO+(>X*dw_x>)Pm#~PHju!4YwS=HEiSrS~dR~vvLt}7J4bzJC8RE153}lcgmqo z5Qvcd#5&dF5G-MjJ_r2Af{v7M@;+r_A2GQMs$WH3*f^(N8ovzROl5_`)XTm-tHQ0X zA$)^gwf+?PQ$AcWGkk>QPZnZWrUDfaufs!cwdoA36z}m=20m_rM?5y4Mw4bHO31>3 zpJ(&2%nH<2GGjhCci*?M4d}aSHM&}>^eMdYqj)hG zny(YX-E=mu*VFN6<~~uuj;#2KW^hidAZ@VBskO?m(}0&}gPx2n-C_9kfyP|0Db*xi zgFT6UG0%2bw`SRaqLK#O-45EMTXnidn}KZ!G(AtRhZC$9Xm~$A*WfO(0X$A$PW736yHe+KV-*?po`9JB zK)35ZLz>f1g}b*yLHOU`;VK}sTEt>_-s^&MksaG?JL|SWYb(ty_u2NmYT#kyFfq46 zBYq^$u;`T6>@2iVWZtLPzupD3Oztr)W&Q7>)p$nR*TB;o=Jgz_xb5EuitgqAKQ!B7 z>dA_D+cjWh9BGRUp}Xi3bh^t3{H;~d-;icqyaUjk>!}aum8scK-=N%PXPBWRiaNLeL z;Z)0PizeBp3=5ikAmY{V-fd_BE~(rWtI@$W%)95B*L%2)SWExe%&A=>YaIA_1dnW(Sh`}DErZ_jsJ#cwugOcT@z58Fwp4sD5v71~lxL6!^9@MsemqU`m; zCC*Pab@30}X&dN0;!G_jbsf}9%p z%&%7d|2}7K;?$n4yh^WwPyE8{n4Si4A#bfg+BI^+dHvDG5=o8;i5gPf8Sg5ixwd#@I2VlTz~L4t_9U`{g`wixjP4Z|h5sd2Ed zKmH_-9O0LR{%XmIRqH{Jh=vPqnj)e9I3 zJ>#_(PTFB<)^k&UXN?L_l`4a&kNPi$6bhd0_1AbBj*v#-sC zZvH*_dZZ&5t9UkePS~Q8pv@9&8fuW~dA*I*zXE+851tk<#}UifdW?7hupffJMjeFyR7r-~}pYkIpphEk`QZVz^upT<| zeq(q=ZIU-vaeCKoJx{3-svYMptFV`$El>`z9rbZ{dy&SRmI7lQ=Q~Z-=G#wFkrW{^FxslUX?K)IW1t~S^M{fv<6}k zJNGP#chsfnP=@>5q#Ol*l}KJo0<&A{5{J`4k0?{75 zv;KdlRz%Me`z>Bic7p!YZEaw6g5MLLrGuBBxQBajAMT3+4YLpOsWb?tB3T3j_MiCf ziR+mA=ytQmTzsE(@IP9OG53daJpv30Id_@4{X*{;)~JeAoOh0SkLEs&Nr~s2xV6G0 zW%3S(z$-U|P1@c}d7GaqW4(IhjAy9a1>PEs%n9D-exBozj^ogHCLZMv@Z&e?VLMc{ zmLHL5&_oMb>v8c-;ML3hiZqW8mNx?P;j~xaEX7m)w%hDCEHHo@!PXnrX~n$1 zP8CSl$As6EceF}sLcV!t$~iHuHhYdYUY(-40_yQDv~yF`4e3C%vR{Q7JofIXyfpkV z=?4~%he{$t9_P7+%$^!s2F1Gm+{t(HkPpk`c|=T+kn5~JO*TVW>+QJo7IUB51X|>G z*cG`{aRj|zoO=UMrM5yfwiZgm%117Aaz_n-xwSeHO@scg;bBg=SqIAX%Mo;}VW{Mf zu@D?=hEnT;`&|#`yXS_7%stXI;B$mK#Ve$Sp{hqBa}M=5(q#wIxx`ArVPW`4xf}e@aT*Yejz_VU`+Ao#GYR`%J4O&S>daHSS?^SSBZGT94#>OBJ zFM`_2xr@HViO7uLMz7@@9zW;^TiD$&zJZsZ{~>Tiy_&qPgo--z+2H*^WF^n+;)!~V zb5rb+Q~GDb=G>!AKMK{PciH#AI$gh@D6Db*RO89w4N$;h&0)19HsW1#`k%Ba;N724 zhi7&6cF3|{;Nxs(7bm2afo-1+PG%`JXm|7W3eg9N(_hb?ht-0d*lm^iStZjAGy%sp zBYl7d{T#?^v{Ajlc`8_!Tsx%O5cHpzXJZ~@M+N%0fHm(A+RYBIaQV)1HK1X9GrkMP z7Gn+M@%eP#R{`Ii!riilQ~1p3B~aB{@g|Xr!Yc1sk4*!*!-iLu)5^T6v^svpn)D|; zT0YJ4cSHsn^gS!F#|^)*ZDbwyAgouJwe#)|MfA!RnBVGsz^ftUGvPX8gEH-2cF}I+ zna5teiUc?Ysw;#K$QO)EFEH=SLk~E3s0u1Bvk{wsD@rXNIpx`uCy-Bv-OjFFvm7H= z7n$HSi#Z~bL>qNdk|vMKX`aeUG~5wA3znFaz?r?yj(CsN>^-aJ8r;$+hGO z%c&dyF6dTBaAE^|yHB3~>3&3JsbO8W@Dx#2uo|r)Sq#-OuMx}@po=bIr886smey+% zYaFa9C~2Nuj{f-(QOgQEqx8H(qnpR?eeHyQH{*4z4ADCEk#mDquzIQbu!R@#?>wO5 zT?V_@U9PP}a=Y&z3RksxoYf^xlVzb5=OQnd38O1*NYtBCgH~^EL(8k>9n0pXuZYzn{$x*O;;5W@-^Vvtf+Ww86K%wiek+^qp0%LK7jDre{Mr-uiMZcd`;W zQH5>fbd}Mk3VOx^UGDKSi~}jp=$jrZ6Yr7U1|w30UK-M632S?fGoyD<*hyx&&;9MwyYW5V>X~KV;>`3!k^t)%jlD3+L3m}&_@&c_Z-c}E_;SgE3}&XzYqv}-Sf44 ze~Wzs8MH`4_K|fme}vA;;G)=8aq2GPZc-7L86#81qY*9a#P@ZhmT}GjozE&BA)SpB z?9>U`!M8q9k9BaCKGrJ;6QXJnY3QAnJeLZ+7MyS7Bs=klcvaAA;ENauo)q%6e08G> zBLDB_+e`E?Fgp&K@OeGMmWVtU=+GllTe(+e*;+8{{Fuk*!?p%Id*9ePJ{^aj4&zQC z%hy`zuD?8Hrb>~f^y)<3dED(G?28FIOvHJhnm%y&ka_HuX$uBdaIRjwoI^1KI;v%d zZS1{XR9u>8iLJFCYS@_e9>&cxJ^mVBN&SwB_9wB9OXB-z7LoFS3?S>h&};Bn?2P}2 z_v5`?Z(cWB42*n=$zh?)UbZ~+z$2$7oD6MUPWNQs%j<}1u&`^jHtq4l39>A{1{L1{ zoVHh93N7`*!W~;LE9(C9rjLmowq&$^y}r8Qa8Cl&EcEj6K!C}r$W^p6Za}@hupRD z`8;&-fj$S(O}>uR;?qO?w(-fK4c3orC(D*MP4H>dzkE6M_PI3+pB9wrzB8)4^^Wx+ z-O}vYyX$9RDXu1hJdR9CFp5`O7ItVS*-V$qb!>79;wPLTAN_Eh?z0Vcaj4@&Cq=K= z$kuz@T1BChb%(Vry62v5IHFHTEJqJ0v3FGr=5{A*aY?NUs6;ro7R~c~L zMr^Ams6p6@y>e!%0x0eSPfZKdaizByorcv8zTG#p4`<5K5k__(pA}4W1AA~K^h4J;Am+6PrO2ZH zg%^74srZ4_Li_l);wm-5*&`M;6}eZa-dJagZ8^9;$NnS!XYiik2@1X$6Hxtpt z@N5IRNCFkPtv#V!{(YB?sD}4m0W^Fjp4;NR$c%QdQfTgNTw$-%RRC!u{6V~l3Pb*2 z*IS*9^4kdKF_n4YOtD8e)1 z26PF~cDXkJjuKW_YU6r~J!V1m2SR6L!peIcKMRc41bUGnnTMGUTMi!XwU^kNcMmAH z4M40{M791c{vn>rnFsAnWrnCr!O9W%Kc?~9ex7K>SJ{j8-;Jd5x!gJC zeFVHe4mub(bP3qTd9RO4fVWeF$8o*iVmWZ2e*;*@+Y4lwv;=*xXJ=Fb#0*!WpRQ$g^4nVI(Wl^HTI~Y9IU4SQ6HW?kcDzc##{kjc89=Tm9A)jPX7=b&;OX#W8+BK+p`r{` zYC2p{!(RK464d+aaeFy^GxEGc=f*tv`a0fI<=PQ>Ci&q|@H-{W3)duv^IR3)ieAp` z{`MCWBJ04yZSl{;02=6Be4|M-cxMy0z!s~=t9v*5+JUx~!D&{<`AMP9h=qKwEzZ?) z!=^AMvjD@#;Iti3*)Z$T?E;*C6}x>Kw3ey$s@EtIYcVvwi*-7%*ZOr(>SUgWo6Tn| ze#YPHZ4$a0hMF678Z_+Lt`5g?hb+(6Y79)iuj$EDy@7q4L!?i{rnhTIE1`iB{RnyG zG_+|-o1KZ=ue8Vc|NGnp+rz&4b3$!CbUbGF)34LtR2@IlLaKn5VO1096tf@TE$@w+ zsE2hjp%3c9e&dYlzoJ*_9|UHZy3E_XugyWgX$!ldgM}?H z*V&%Zs|nf5=mmcbSBP30cI%y;59Ozg;KF@(-cPhs%!tE+++sbP&f`A9eE9xt;QP;;3xj(gCU1G)-a(jQvMp}Od%QwRwi_gc8;yxPS zURe*^v)DTo2|&<&TkFib`nd)oAa*XeJSO(QDa|?vf4pb3wqZSV>+;lWdVQgtL607z zs#ihG)&O^PKfKWijMMzS0rtu-a`l(sY~ptth(Gnj^SN^a~Cr zF(qC=H4Q|Takt(=tfi2=zLSm09B!PW^dM_>s2m&%s8FMU^|66dcr-qX`}Qj3qit+W zEz<^(;{^oXLK=G)pd!0ES)NRQymO?C%oN- zCO881wjuQog{IEOs@n=De1HTg22+2Nr z#&>|VPYCQ0_0!P4>u5*Bw8IwLrkuCn^9uCWU}v}U-9|B~6kfhWKaJ(`8EqSMxjI;p z=wz(5?-3IyLl@4n57n#%R;F&afG#bG=TP0P>um*fF+1$v!Ur~=9P9!eQh9djZREln zyc4I#)o8!-QtOG#-D%IDt(Mvxo_IrR*vqAS6 zd^eYOb!h^)5(VT0WV@&Yz8Qf7opWF3yWiCzK!_g3+yeQ~+)UjKH+fatPs8QNkagA? zUI$m}6}3;|Jx<@$>B$AGL|>Dz)V_%S2s@yW8lbw|rn8e_-dwBOfh#?s;+{Be=R&J4 z;|AcWo1wf*fln2Zq!gL&8pD7s)Hr+kS68^iZbUjBif5!28z+|?kBEL4$csUphYr)q zzT3ICiqIJZWSF0VMpz8Km!<&mEvgJDA(a`MnaB?sqm}WT*nWNuFLQ z_ir;tGJtp>Rtvm)_aNYCsnAWrA;tXr|Hr}2rf3fzO- zu=jVX@OgNy!lsxGt@bFGycemL0iIWx+eoiLIUAtmIovCAEeoz7KZ|#yVzpftGxcp- z92z;z8uXdj`fXeyx^MD+GCA3Iv}j|-Wm2rDFbCwxA>z&)kS~6zNbLI`;g`QZdkSuDWGmp?9OPOa zUfEurI+VBlHjwMeH|XVM_;jhqYvYV7puRip>97dzJ{k?MI08yr zO7&zodYn8r@xJ&atmxT>ozxn+&n5*V=CfmLb^iM^I?^q+6kkfK%ij1RocZ%u1W#vi zM)zb^L#LBe-q5wg?oDBbh^b|PXf8L1hQxAMWa+tLZ}z7klLYu?lv{W;uiy_}n@ z54g?y5x3|vEPTILuYqfS!0A@<-g2Omwk|8SHYK*4KTKVKCcQ3!tQ)weO4Vpm4LUIR z6fciI(|;E%b+FI>4wrFYu%avdR?5Fta4zC?65(ob*{ajVkFdT`!SfRW@n4!Q^9s3 zRXXuFkD7Rj`U;+V51n=kw`&=nMq^?NcN_RbM{9#0CPiJA-J@~j=VM0CDX{AE+;+12 z2|JyiC-K-dSp~bthil&lGsMfZM^nRMKOIyumt74*Qxnicd)&?cJs&U+sGSEMn4iQB zf1t%yj(2MXXBe?Uq@ruFnTAab4;OHQw)1?3p$)_1Z*ZQ)@t%-D2KB)@k2U7mlvEeF zfqy}A8v5}^;N{=ZN`TD=%C@OUIK1-6v*NHs&)9KDvNd`uuECFW zomPXhCTM1(<)@Z{skuOBUn}BU)%si5YD>d<5+&5}I)3#h@XJE<(W^v_a1mU&m@=yx+CrXEcDfY?FL$+-v3&ce*SGPx2{H zUM(jW4vTF@T!5~b;G-)`twmCmpjXV6_fB7D{{!~_nBX-5#w*pShp?t2s}zIX_t1nq zqlaE4qNf*}<^H|p%8e~hTn5}7(Hs)}SI9R(GwU?P8Z03{pb%bhd%+`j2ipcc3ZBdD zA+**z;u)$DGlhUrp^mi$I)+>05b#B}9sJF^tPh#N1g;K4K* zmSDYGFGF8D@%T)!orbTGclX*sVJZKgz$zTq9#(xyZK~q;Cm_Sg1=KnXqz+HNMI7if z@^k1f$(__ejzf`{NojNKnxsh0T8h88)-sXR&P!cHqjrZ0D9kx2ZAZdo<7h(UG05dE zu}3Il57NAg-*_f^H0J1852x6UN3F-GDT79lYy9WdRkk)5xmVz22&sIvyXED zd`c8bo)tV~KTW)*G6#Mc2a9K*H_hSyn^bE)Ip}*@1}>ZJ2>4^ZwJQUxWrUG>Z5)y)~TjKq!(x8#% zW91Eaof>Tx)V>@J_(bok&Q9gqb3!d}GAx2oT>w_SqV)H1GjIQ0dN&+)2ACrAjOUI4 zYww`BllQA>8e-Ld*>0V{VhhlygJKUNuH|0gZ}7|nXU^m2c6f1z4O<1U8RN!Sk&J?~ z2HxQPvAnv$qk3tew2XDe*>9R#j%qw$NYyu=>_Wb5)&y(#6cPM+P<1({pPp*sJwKM` zw>$t`4$@6_J$B%DT+W+w^>BQ{rjS$pn9HE}e2V)3D)TIyQu`+RzX6JHTkWWDC+ADJ ziywv4HrOx1sfPBk7qVnh*=9JR!*b-=Rh#(#58^bW%M$H?+V7Qj%)5u4Voh{kqSKs> z2hSSIMYl#DvZvV_9wT(~EM&0PV79?;WK-B_$vYyl$a%e|y2qWqh}UvMTwwPnQ*EYg zgae)n>uj5S9eA&Xww~oSzdo)d@AwIJ^g}I%Z<^vtJ6{X9AMiH;vwdTWo(PU{qC1?% zdjqUir;5}B%>Ro0*{i4G3S&kI82UxB9*Xn|k1TGDQQ;+qXVcs?Onir0FR*4Lrg!iV zzoEY%xBsv_XFCr)kZ2I_4`5~ybXJY5bnS5|oh80$?!oQGo14Qoyy~GJED<{gLiOz9 z(G;2M@;-{s>po=Jh#G)xUmUQRVE1BhSz@bH5y@v_m&3||x*iMk0#zY6By$byc^6f$ zc`S^?|1}Rf5pfmxQ~l)m&>>s5B!lcC+IsGmA$L zUTfWKz4(Ybjjpyxnlfx6&oXMWZtVY2aw^?llCoCL$qmap9!3+3tgu>_+i#MWZL2EM zc~D%Iwz8A2sv2H8U+eJ|o(Sw^Z;*xXlwC@N0ogVo8#!IXO0%%}mfB6K#JA=aQ;i;k z8XxDJ*Fu*kMY6b%B@0z=5!kNPTA<>x{!BEd^=Lf*7u{CPu~qCIiI?oK#q6;Td9?-Y zw4b~GMrbUmMjmDUNqAHj>pGtIiQd#Zz@fQBc29!JuEbx(wTyBQ8yu003;oxV& zJnqeokcWxL)LZs=p5wRxP z;=(u%N7O>yWQ{^SXjJCC;+O*jd}iy`FkNG@$Xc|)JmYGbDtRssE&Co#GsQ9F$So>2 z&*JL}A0oS+vy*_rfymev(JvP(^&I~1(rgS~xZ0SYYpdAtYFnQ=pWQi!T%;Wpo78K| zkSsm;8L1Wl_RUb#GrA18T?UQ1HZVh1JD+M6QsL;Z9-1SUJ(68(m6plx$%)~o>8Y12 zvc{)$_bs(6ZFaIkk5b`QjsEPjzkU{*?Z@%|O|92MakYH|46crXGP09Z_7!~xop2L7 ztm8doYg#kVi0rxmn}_O6Bo+}!F!U0<=`-2Ct_E(PW`0uz917HoZ9I>&kJ;}Nrg{V0 zcfw8}FbQXR;kbchd9pL3syiY$5OMplV_mM}X zy7^>KXK~I^xH!j}kT=oV&1>D6uBIDRF(~&Wd7rOwO(KOoBcnx$x%RS5 zN3xq`oVSL41C8dsuT|DZDDndz97znd#>Gt4A;0i{x$K4BWes0_+nS zU(fSY*@#Rn{&3rw>o*&D!vwchlN|y#{YM~!-THvPcjv9(JbU6pa4``~U|NTy@Xjq% zfv`97dEi>F{qXo9v$KG{p95Xq=JJ~ZG*6Rgx2Kv%7s!+2l#_T@mqU9_IjfOfGuRh- zByej#PrH^osU`ORVx{#U5AWsNefWcaL8PG=P$4sGh2aINjBJZ>?oRb3S|W z8g{t@Ni!;;IeT{0DC=*-e>zC#%yOA}^f8zp7frSt%=#qc?{l8TVAJOqB)k*hF~BGLWJSapoEy#=i3JS+EJxOLTsmfk}r!EuMHUf?GEGAz@1s$$>k zp{8NEhbWI6nfhcsZ#!0G>vI#&Ktg%V?iVpJpRTtY8lG;lY4Bq+5c^_kF0dizTG^av zU)9E~S_Cd`ODzMl+wFPuj928hTp<#|J12CoW}>Y+K_h&gi=J?Xa}ga@>iPbu6(!`e zF!N3xr);Tg0YcT*p+cft-l53*=(^|kBD}RPW1H_M>Iit!bG=<7rus&$+Ks$_QRnhR zonC-z&Wql?ak+YR67RT%GrZ33vhY?Gg7Hsd6DPrLMi*G1?ckupjzRMBsjbEn=CuPk zmaEn5MC`?D1e)~KFb4ig!w@u3Z~uiRb{Kl|=g8DYbwW56Uqv0T&JvxySa}|~5O=Y+ z4m-zIYKdmYqpTW!_xagy8QK8T?cliC_Uoil|42H)Exq+v!{ype{G-#L$f49Oo6U@v zYRyFdX@ir##vQyu^v5Y)dZc3g;EeGZ== z8n0qU8?*s`UK4&0;`He(px9^&_|urm4y%J#+KsCjIngJN(yhUZwN!r(4{>k2AE^W) zE9BlV6S#Or!KS3bD#9ZC>`bM$9$N+V!6)_#|MpvOj2#k-L&qd`Z#A~i9tdyq+>mX9 zI*ztl{tSYR70JSo0bf_f}Wq znLQaCeLyzsa;(y7`$+TRca;U#dT!*M=Jr_{3h1yic!K;Q8(@;_VkG*Y_P3k(cdnHn zVQ)%h;`te)r{38_96OEq7|>oK?>14!x7;Jp4j+8P2`#CR`x&1D%GRs>O~bvXQ~5Jfn0-^aO`GYplxFAv1oY66?|`?!-;tcQxm|1^cxz zJ})L5vQizq)`{epX{T}yWU-F2CsRYzD=lL8? zwlTG5s^?BUhuygy`p$=v>ah;UQ3Vn^bd%ibZzfuiM&I2R*pkIUye>mcP-q|UJP|s( zKE45sxrfGp_xFJ3&9(*JS)rZLttz+e7O=BL*m*5T?_phNWIuwBhmloX@VDo;yvx~V zapr67ht>jI*1+Wt0^>Yl_mxOCIu~++BJN+$oBAO)&*l07cHRDAAM}?W0+||atu^*F zyB7Ld1GV?yA)5s+73*|s3BQi?d(lCho4ypbo!e!eorY$#M2$q!7O~UULo?EkEXjn7 z7%M>MFN?p59!p6B!Q=I^wR2{2A*t3Lv?}Ez?KZN05nhq03I5jj-(fC0#v70B-OX+X zY!Q^i1Wa46BK1LA%fnX9NsU9tOvvErB5WJDRYM^s9uX2d5HEQ)?>ieR>;~6lbE}Q) z^6R7D(rG&ior|hlsGka2BtsuqegbKbh1@KaSD*b?`UNZy^cVQ`8qTGKtgRVvFmK19X-8Q(p+Fx0Nos-l*sL2y?mhWotoBZ8?c=LwU9HX1g&6N@NSFsoU$2CywkQT z5cdp7vRlFe^}{FaKxQ&^n%1Vg13i5_tw+mw+XmhTRlxf@9VVRCqxx5w*VcXnq>0Ee z4d4;{0v?_1=N#|zcP$#}5_Xr1b$7hc*97R2n;mb4m(dG>JhmHjyoNVzWG3PDoL~&E z@tC|n+_x<=Y_Jg`la0wT{UbG@9a_MtJFSnZ-_jf?Fq0?t=f<7`gy{Zi3-Jlf3)x_+Fnot-ZvoP1 zEfn2se@nds%*t7-jLfYZt3XmxF9bK2!4;m#h$joHV>^)W-Z8t76C=XFv0+vqmZWxQ z>$hZxc+Gh)r|m(?57`(pVh-Nl^_m~fw+EBUqw7R7BN+O@#Ghg% z9N_xqobcOZj;)CYupWI$RjXR4>MCT~a^5r#2n||;UbVxZ(+AaqWGtpb)GtFeba8oT zvO*{bdbJDT@;dWOY0t-=fL?aVg}nJz;JaO`c;Y7R%OUQ* z-^RzG^#x#gtp-3({|t&4;`bY%Y@f{f3ee~i)0ypfw8G$hmBB+lS?xW(}+C)t|vusk)IDqd?v*?;Z`?Jk}`E>|_W?9-;r5 za)kPM_n>9VGglg|S6B1&Ka)Xd;3a(!pW*2NZh}T`#`^I8NjeWOt*UEb#}cDPjf%a* z78@ExoU@nM3wDhH_TEJSMHCAvFmxE0fvL<4Fih`#&R)}d@AaFZn0k}k+?#t%O#M@C z{&$gw2blT3b9Py6ufER_^fXtCpQv3|`@7u^W{m~v#V#M+y#dMmv=}YDpxyc;I>fhq zKeR$6k#OG#(Avyiupe~>dYI-!vd;hLe=@~tEN(WSe+Y8GY$<2Q7Eac4G)n3@W6yHL z(V6pl{SmNklG#C$#vccAInJLJq*W|hLl>)^xq7wurS+>_2^1`PF)drg;!1_c zFY;43o2CRhXIjCpU zwu#PnlYLn>Nk6j;sC5RY)wjuprQ4OPg>pZUX|!SPZ42$5T+0pR{$((g zAiwi;r~|L_{SU=rydSWdm0&AVw#bMc*4N~HHLIx{9P9=9$r_8UU^ky%2nA-d2a9vB z;IHSYNnkS|NALpfEkg?r7|%`VVx53S4}%x(DB!s`{Z)9199C{cLX(}8<>cdz=`ekW z8s#3e(?ejqg*{rvvzl`UF`aHFJ6Rn4#b$JRG}dy90ZfK~F=IWh2^ z1Fcv_^x&>j&wz`^SzQ*9-hTWmAIMYCLO(H+6nR7jh0aloXD3XnyspRbO}*B!hFauH zGVtBPsT#}M(fC6b5@)F6RICgwTQ`ty;IagaB#7D8^peKoWW7riFUV>&A4-m4L$l-+ zaGNfzvW>hmiwCX*&X$>)$S-J5zbfoJBxKV`rjZ|tR}Xb z{ihM9ZwKP-Vsj6uWus2QBb#{7YK2Dd*$mNlfNoOmIB;bVY_q%n&o^@egJm7l7le89 zgbG7x*Y&bd@0i;R)%1Xs%b4Raq{kx0`4EXYOrF&xK!u)zXiKx99`YR3e2^394ExcB zs`W*E8tH5{R1~%7ak|f4&)&1(`XomlHTZa5ABUnl^h@-%6Qu~+-+<=$5qZc8N8K$| z>~brRxW-wGegK-f&i%a#kBGY+jhon}TE?_hTs^JL@~p2yuG>8jmVNv>++HEvG4DQs zrZ18Xz|wBy z)SyAQiBT5$zq_*;zmRWVaIZr<(a5PHc%qz7rqw#d9bk3jB*-u){RC&g8E~*=Vw8cK zJb4OfmV(qz7RxJ`7K@Z00#+cN*_Uwx6h9mGCpQLA%y8b!C* zY&?y*$fhPC#>&VwvQ`p9%Uziu`qd;0$Oj&TGc!1&$nnO4+Cz>*1GKbBFO+JaG|he* zeY6%a=J75;H~H^N4RX5F6#(NjWb0NffI~XSef8$C`IY6sMGr6z${(+jQJb{c$AD8=lz zfbq%HQoGANSGUP+A=;d$7d<^nt-tpBdae}cq$s=WkXv}hPW%d@3zfP^8d=Mrr14!d zF^vI6vz~oj_*LyD!Uq2c6hhBxHwNB!NGWt;QnZhiwF^D3l#bnu-6byr@pZuUBW>2l z-7plm0f?93W5H|SSNcTOQcGUYcjPu*D)DIP^CeNft<`!Xt9v2DnvoK0j7_><`a^oj z=`GTuKL{-+}=*3>J%xc+jgKXxf0i`EBJZh4%1DHIA^eiMMa5tL&2%l2T zXcQ&ZZ|o%~i5P=kAyni*eLYA|&O5amB4VK2Hna;olTa#sH-Kunqy#zGYj*$Mg|ZCo zX%kYc5xOshg74xKBN8Z2@NX(y8pw8do?ZlcRZSyMjRW`J3di*Key7H=`bIs;Q5VR} zsjT%I_hZ*rJuY;vhYIR>t3cnru_!2jG6v*tt^^6&B9HL5Urz&bQJivJ(#M~TNV-nh z10B@zZX3@%&q-_b@b7CWaO_3Kr?OsZijV<=c(9g)&$8oC?o+gSc4PUHRAhnkGH^DC zp1Ft}4btrw&n1*w51bp1&}*L50pUlHO%E_G)&wV11iH06AhW5gUe>ZHi3vg(5pXu4 zQ$oiqI5ihsT9>P<;kHe(o7F&Rcmi(Xo4r6Dy+a20+&Vn8>V?o_Iaos1#imCWWi=MR zoFe0KjBZAzp(Fr-Y1Ui7w_VJY3Kf`V{%-A2%b=&a3SBTKh$!71g6m};)VLgoFt>O( zph$WdeX|xqt6fM%%cVzSZD4)n4M4}s#XP81N|H#Z?BiRuv^%7 zEGwiD^j>D??i>s3@(x(;d6FuJ#qalvSkk&furZR-TWq|L0;-8Vd z{*Pgj#(=>$@xMOrO=?$=<&~|?#FFS4gZ2{dtB~$=--4HOsKAZ{$yirSaBwwI*PU{l zi(}^_@<)FjC)+3V!LXY(B2a_nd3osh9iU}Z!`+-=IecOk6V(A~U2=E8Au%$coxEdK zI=v3)3bsxPId3mzXXMdyHkZl_@0etdsP)8%z-L29>k)XPMZe~(jzC41A(uvwX>Tyz zlx~0~BVh7w-m2kbE5uuIP=}>ltg4_#PeRI$;e{K~4q$B2uxsEoJIAxY?+5ZeG`8J0 zApfk3%oOyP&a*~QmIr2@v{9aaTdR?Wqrhi}%o3|(y;GBOv~8q4*O}cLCN%G#3j68v62L(GX?v$JASW1zF#9>3f={FTF19UU1Srh zc@NI*VQivpn$3BbPbEb@n&2bQMJ;ebCVCG{PYAD+0e_EsygF7(v>aQi8{N-(t1RTb za-{BdZQ}V%83;O{K+DV?l71N#>sZ>uj$fAq87DuaPzHccqo=xFhRNz#sy&>@pUail zRw2^mfPMuY%?h*oZdUsUD;<*uf*H<$L-5dg*WeEGq)EaRT8}tw}Wz#VbI?FJVo_Pkg5rsUY`SNZgAtunqmNNdu~|kV?2BuhJ(P7}*>v(x@eqQVtOQ2JB=-4gdb zT?LLZk@nWnEQxO(CgysQ)DqR)EIWLs`>{7UWj*%pC$p^^35u!?2tWv5_Qt6bo z8t9g?x83qN67Vj)+aE2XK`8M&`Ezf#bH}1x91oRb(T(j(tpv(*rPbepRM{@)1wZ#U z>M1~>0U7ZNwEH;5+M*{y4X?XypcSL}U@=ite*nGwF-+Gd;e#P=YORfW8*Q+GlYr`J z)@e7PS!XKZhKd&-9`29|D4tGCbFoCEo|Vi(vqn$C(=YhGU^x<_b10Kb^P`oTt6TcKyUyY4iGsu>a~29r9|-#vaWV`*6QL$^&NcS zwNQ=eX6smU65M8;$+xiY4CrSUy8d;*br4Iw(4S@BE!A??ILMAIOK-ngR~v5qW9NsU!D9In-Vn4O znUF@FPvf**h2Q8QR$+JHETn=d4K1sQx2IVn^+Ndmj)o?-!!g`IB>Q!Ql~Kow6eJIf ztcB&k+wLX;iqPd}x|@BBGTL4wqnE$B%?htT14MV(RfX6&a4sf`Cp);%TUUMaU)h}wDb?JIzzVn(P`T00@8y|8V%b-A ztI~uv14(lC^iI|>gT_7~^j3f($<9R2`VhT=zE9ZMrO>L?qoiR|k<%lUzE+aheUX%F zH8`|RSHqIw$c&Upc3;h>MXdgDiA8tY3$*VFyS?2NY`K^3LJRk6y;NiESmw6fI(n`* zFAO~zpt=W`&m*bE)T(_fV!&C?xF!C7d8$K6$sl%r?YOySLJCVt) zIuq20-Hb3HRB$3osMzKyI`4sr28rYCS*-3+{Vq_t6H0iERjhFL!*8upCo!6*4KfuR z@Atb_cP)FyPl2BKu56NF!{D)dtoUqgKN*0_D-e z(5Y7G4)MUrZoDTZ%5#%)rjU!F%4*)`C!#9+v3qAu_kB8v71%DHljZi)FrB?mz(K@n zpd!1O#4`BLq9fvf{yMO{oNOSXx<)PYW(s{F3miS-FGPy1bj@JcJce75dqeQwGI;e1 z`80UR-HgVaC0pE8==lA5Twu2kgi@(>@*o(PEefQMLVL^iCBWq8zDmc@OR1hlH#80z z)4#bNG4mAG*Fj*{DTn#YIv+Cm(JY)_&K z*8|scBuC0?0?E@onkhyr8}uf%dj*=Hfg!k`e$!}A_cHGY-#O(T02zeJ?A{H>5z>*rLA=M%lAfRdIV;Xx&RD!Bi&rHymf^Z^KjNWM(VI$2-H!qwq;G zdCQxe@+o~mm@yMsR|`eG!3r(&do{bk#|!rNLS;X$8Gy1)Vr_|{23ym=jQ zh;^(w2sPVH19npUI|dP9T&haNG6>0qf?v8hx3QCXu8RIqDH09#kWxU{aU-m}_>eT-R8yr~%7?lY{@ zDy7O7KW_p%H_bmp^XWteuauiziC_=1>JDh7N2kE(6w!pMpnLOe3A!?}&hqb`X1}?>!=l&t@tAprlh1M%>$F#M_}j*wR`Hz0yI)mm?IlV}px}_7 zbMcZp$7v~Rw2zDh^JJR!-OZ`B3RtzdCY>iW%yxobrX6~wAMt}=f-EvDWAppI#@{iS zp$9iHV^(bzqh+xzV67sS8#N;b{e(u4Mr36%i}iJB#{2TDPIGF_u#S38%6B>S_JR%k zjqD(SF%;R57XsgX$+FaNCR;M;UbRJy$g+3$iSFQs61p4z=I z`(!ybPcc}nU_A|3V&{ieMcA$93EB{nYAg4vZ)Rn536YnGU6jh(eyyIb&wzX%Uq)k!WuC)jGI&l(w?YPXH_AxR#UZ!sqK(}-2!BhoOPPkxCh#q2bM~FEl?pJ zJ&MzkoYkNES;!ZBzu?(?$JnDfBBq0bEg{Meysc};6!JI1oWBh1jEbm5y%$5J5%6j| z+{4V)Z>wuI>1-addbJ)MzjE!mi%;NT z#*611&Smd&f%$gBE;vQsjGDyXS0qFM;74Ky?D%{5c4|0LN}0{FQqB7^ps#V(j(O|U zFi9l{5?zPk=>qu1veC++6*?7ZGc)gZFR<=Kp82yO_tbRbH0OVi~P{QlYESezyRh1azuss$~<{ z&BHQHj)i5PcER}`n{bUJ%2qT`@}7`iR-?Qh%MWif-67g_n|QR7TC^97R&G#k+H*_r zBUp6Ex{J56%O?1EBd1`AOz9$?*&-Q2gd6I(PM;6j;rmg)&zHMBoL8?Of%9W}478pP zyjKFT_lU^74^_O+tg}7+k0cL!&ip^koXF1swey4ua-OvAZgjrk=V2*@eyo>mGM@bh z9U2l?ZITq$yUE=wrjy3FfG58jP<4aE&p@V#S~h)^jtZF@aMNbpsk`+hIR?p=1|_DT z9k13tB=jo(bN7inqYtvv4qb_C$fFzgA&++yEOh#Mw8vcV^M-WmLVpP{!{4|^8T~XW zfd<^qz}M5#g#NG{xOy!lbD+s5Y8D#Lqw*PMsFp2ZQQv7d$$oN~zg_PSM#zrMRJ$c&ms$qD)z^>64kQYldBD{j&`G)! z9~r4sR!a)MH?XFE`3!vl+25}br2ohA-_9~)s>x6+0)qQxiM$Y1k9QV{;XdPBN)|kR z;oZRd2_Izf5{niF4BPbFW9%cJzB+X$2~`HljD75@R7v&9L`d!Ig62Y zJ)6C+2ln*l@ukwrnbZXZ4x<-lA!j!bLGKrnvppShSa}9?^Y0AOSmv^hQ;l%?MsAVaret%X4K?Yhu3Bq`yi$%_<~}K? zWet1Cgi1Qa;zA{O+akz@Rw!u-*kD)acIKcOgeR&r88};Z>Y=bg_UkCV_%VKe8@X_r zR=|HbNbu9yu~pD_AE7Jj!(2@@%4gUVsmw=4CHo7a>|XIw^u%uFqPLM$xYf=gboAUo z(;Cqed9O`hmKY##0Pg4pGZjG7x(L09M{81k z{SYIt$%i&4Z?*BpWLy z4=wpKJgpV%<4c_*R0}XhBkyPMW{F%<-S5}x9;I)ESLCSm70gG9;Ac_OZujT~fkkP$ z7-^5M0tZW>&sV@ZH0G?|H}H@qEn@}5LESo?)Ez=cXqf-zOfp4!TGO2BpN|sx)8L z0}(ufZY#2kZVb{QUpwnjyaXBEq?OW&-4X)dY9wA7qmmWO$fnCse*iqPl{+h>lV|JU zsed3}?QY*sfqEh9H`~xWTU|mv9rKwVwu~{xz;v%v$TBhkZsA;A$oF5lS7fCYfoFw= z??v+V5IZc^d)Rlef0HbwHzm(!hn~ zBj7c`H$j0dvKFmmEt*+G<3*K*j%aAoEL-W z1lT3)F`LzuOFOu)LgG=&1kDY}hr&&70T!44u^*Q#pjCEbqmWn0sLC5ei&>R(u1A5epelp6&P z(|nc;KHuWqE~s}U^L!IVSaU2;h;hhQ&fN9-kn3fg9qegLj|$3(6UNGIjFTNMhg-_D zO`FLtIXf8CYV4JNhR6Bik#pqnNPJ*7`aT2AE`jQLq#Is+!DXV+bn$Zu6t_ih1v8vu zY8l48cr!kMuBtT4H9>3t;mWWt@Rx>J;`Iae{W|;3(p>iN7tg((Q0;B#pi9wU)^lfX zs&Y3lP=5`HhqmIARe2+h-EB2kLn&m+x>R;eM^#3;+NJx)$P>eES7D7#k}Y=uNPu#(why4J1Dr3 zKbzsM63)4CrOwu85PyOuG(l}Gm$@8r93P__`m54~9exV3XB4mai&Uz<3Ln+NGuHQD z7BD9JK&?KQNH0`BsYYpC`W0BG_cE)E2Qsy2cy)Yd_1y#1CvRg-ZTR&qCfq4?a#&`N zM_+LkE_U0M9%uR$lxZI8A@ESFKLNI*LVYu1x2l~}-v!6hK&*+G5AY3sOla2b^F4%} zOea*R>m2tuXWkgk6L;aK8E|rdIk1u8C95CY#Y)W^+((4Ta$z?B!*Sk-=J{E8C;s4@ z)I64b%u}gbp%^qHHz3x-KaF#uiAarA`_Xj-s94u+Jf!eKh0kVZsopA{4pJH6k&;cw zzjN@A4zu@l$oo&h(H=(H0&kf039RH)XtiCVfo>8x0v{vw?H;~WIv`)@E3A5NxYiX= zn^~&YTP9J>9H{nbBy5T9Vtw4Q?dSV+cZQD(xT({V1q6=XV_oRp%xZPkn`IlnHy}OR zwSu*Kt%0}4wVOR#*EQ?+-ORUE7qn9Dc2TG;Gj!+3yaXx>O6;|Ml#S7_~b&7*nf-YEbKRfi?~e5|t6V z4TaiA;?E~Z9dV`vn*mv28c@D;!6O#qr4A3RItF{8oI9w}^gnzidii`bt0Y&TU;8H6 zh18hkth;X#G+PT@QwtQ;qjq||Nz}-E5-Y&NG?hAk_Q~2Q&(x%fosxE!B{u-!@9qOH zJD~5)N^d3VX)X6YS8kPikcw78Hm)f=S;P6-Cv(;6YbrSfs52J3)oMK>?P4e9wcR0? z=@1<{lER;cReXzeDVw2O<)_btt{UueKz(BLu7a?Ue=#{?dW4XkrUIpgFZ!~zbAI_QOPGn4MJg|F+ry?>Wm+NHIprnh*ga7i}jG*ta zt~_MRuokA z!S~VbTD2Y+<4~R5Ai=vp8ZGF}sLif%oP5iZbap;r|&_aDyU9q&TN z9XydLPjRnW2*$g?=6=rgEOv1PytoBAO+g<@(c2|fHtP_+u1?@{3p=166f|%7I8D?u z(1B2o=u!+9YxjimBn$GbleJ(fEiukV{01c)miibx3pB z6S?5LQzYjhHIUXorN*xUntI* zGPS-T*wSbQTalIrgowNREcgVk3Z6rxTs^;=Rr8qabk~WE(8sexgk6>X7E5oKlQh6K z6QGtXpGcJ~gj0(-?RsS%qfhB}V3DV{I;>J@U?=0y0Q3i~AH!~~63y=Kj%B^&@Yxu0 zHG%Avc|hFc$C=Ox9gT!r=AoPy^dq-~RrKPuAEMh99k4iMPSh>_YBc;rSts-y!Dh;p z5wKu)l~0NBhH?U|V3kYwoqSThr!oOQY^_`x{yj*MYU$yLHmK41UsdV*$ii}yC)D~` zCkMOTs|DU&GJKsn2Iuq;X{D<;*5@<9kQ@tLT2kx;1=x%c}o9IrFzK2>Dr=} zLtybNt3Me)yRIR7Z#|lBw0zg^;0ZcTAP{cZ^;iju@S1c{r8WkJ5}~CDCHBdBvcYWvqh*QdW7qQcD7TBX4uZP|G#R`3qyp}` zgy*c&-@EwJZuYjm1b(M8mmJ66cpzl4k34zX<@ggM3J+BU`>7Pq-)`V+(fuNDW>P!G zk0B|G*;TT1X+FHT6b?=nvU0Uu4`cVOhaWT1`QuoR^~2)+U3Utn<1+Xm7kSn|Wb#k0 zMZbxvW|WiRpn9Dhb%Ab@6Udu*3L0+?tHA0ac*0Kn5j`4iusiKurM6%g>>B000o3Sw zgVr*`cSaAEdrI$QnbaxC01M+X6YOU#qs%=BR4xC)GHW7e-~reQr5$Z-2)J06uX{K- zrjZp^kp{GR>koLA$zj&-v-fyufOeX}+b8h-24aZS@LR9goxMaQ`Dw2-;J+x+=QIUR z(SUpe?R|pXZ_&_Jb}*@bbxz2G607&y500kLfR^j1exm(&5!bpINB2QiXqgnn@{#_E zOx0V^U&nQp)bd$2eJkj}tUauEpMJ<3R`W8?mFbV*@ZW`G;{XTMB=FTkolmU%!}s$< zCHVx+to>VxUJK+J;L00(0{DL&3pz#Xq+TQJ`6oV8pXSXxjjf48a-@rCWfrpuWn3aW z^|08D7;D1)tOvUcntUY4lPAFGly-#BG;rh2Js|Uq!-FCtYQ4<2t1r?siKDv3+qBCTa$}qOU zBtC^UF+HMKCOFX|X!X`7;D9~`j)&j}%P*gRCR*TuX-$D2-`8O%$n+BHOklmIiiEr( z>9;4q{j_`&9ApN(0+OROyivoy^Q0AzU6N!$1*zb{qbD_Lt-K%P!adxZPNc3$3eeGO zkaipWl#hevj`4zb>{Mv&Q_j0qbkOsVzJt zU$(M}F>k#FJ`(G+FvLDzax>^JF0^MpAC_dzdX*yq)}Z#a(tnZ8{V8BJ?E%xBK)qY5B%Srl!eeipY%Y(A;;p}hS0igD*sIy#nNYzP z6;26qwU8MECVQ}q8vQZS%o{(@i)0dMJ|N%oZQ#nP3;qxNn>)hAI#!M$?}*AG&FrR} zUb0roe4bc#AiBuC%6Po_y2O`NX9X!e7{-_cOR5u=OOw#)OH)(Ud_)~9J1G^vp4Gr zP!FZ#&P6t$d2AzYh|Obkhi7dVXc=$Bc`|_ELpw1m{iv8H-nyhd7fiF$L7flu-@;ci zN;Zzw({+LSVcExQW`oibllrzj%yt7>O+4CdCGYftvr1?-U-#IV5l-oyVCnmeOcn=v zspV&E!52_3W|3Lm``136`jA?m2K4Te<0U~02qYxOLAKSu-Ie57Jfb^WkV6FlhB02=z;=sjD!MCbE@Z-T2oq~?5WxLGLO6Khc!tXln{z8X5N}9;l`HQn& zshgO;0zc#>;WkYXiz?c^^!ffBS%bxQ$PwYyDD})S7jJYIevD1{jEXr|9ngyQ_3jk3 zRXV-#Sr;A#x(SnsXqiDNtTmebZ3DvOqliVtF>fa-YA1XZm;}LH`t$ zjejTyE3|?YJO>xoGFGWmsKvDIRC!&T|Ard=NgCUo)Eb|7}E?q|%MX6yMs z(f@*8yTIpNFjPhiV;8y4r#q_-&R~QZoeOSXb<0)t3;h%;u+?qV=cR!&{D3=-m9+sE zi{g#y4!Aqi7(U}BE41(`tQw1Tn+ISwPp{G>R?#C~+u%&>LWzmm!^s*W=K!??X4tO7 zdO=vDmZ8`x@96!`sF<5tff?O(q`);IKhm+%M|7J%7T7(&40UoJy(%;0eli)dG>era z(24nIF!>2@(#-=3v)ZL@*#pP)a*khvZ+NTU3H0KC$C;sZWHTMGSFfol;Zw2>by7YC zwpN#ZBkQ+3w@<~^{!31vQRO^ihyfk;j|@;{i>_P3MdehxXrQ@elxo&nz5^OP7nq|`W>I8*I)(pgOw;T z4_=k5(|Dn$4t&(jEgK7<>lXHHFB4nj%h3w_uROHz>YiiO2 zDAaE0z*eam2$!>y5NvaIf~HA}M$6N1%c=hV`A&V`XKOsT$dp12RAo0%uXhRHzDJ97 znzc9T8E%qjf%VBXc~L6W`W)ZKsNFPCPt3O(95%X7{19+4KY%iCIrZIZkXH1X)b?-_ zRI!_wVVtBwFEPNnlDF*sx((jL_sj4Qwfl{_Iq;kn9tnK{GABox{d(=t719ZQ+E^j? zxr*7`^W=Pe#3A*NT~T0%iXiP5i*H*7`Y&qi1Ze|TMLc_@{LNj7=80Yx0XcP$<9@4?5r$J|CG5o`Q;n6$>WfDL*;(-&EdTwwQ#@}M$wpQ`3ewGQwu0?a^i}SMfTqtw z-W`wvJPcZ-*u39aOV=c7rMF5?d~3`?uKp z{YPl3fOXfYb^fpn(r&emkRv+4`cpa6`mx?8c=k-V@<>fG9K$)MMwe+&3-v5Qwy86% zXe(pXlYJF~q~I=Muzz*f%kE3Si%LE}{X2qO3;kWG$z=QX0mTxaQ_ZXwLBUhRyt{o3 zvN(b!W4A2#OAC~+TT_7euHe3K7dcXs$h%I?!a}?knfeitno>?EB4hARy3#R3hQwkF zgIX^Md^*P3%IT(TmDZL4(8gGJ)roLJ^E}^;h0JY;K=3=@;5g`*`gg(m%svkwt46gE zo7!r;E3q}-ktQ^sy;7`eT{n=4?CYu(Nlt z=4O8L@Co%xx=VXx2i8KrtdLkV2E6R(3o2#s_kP``5xJNrKIL>I+A0biSslG;lCfae z@(1?o@vQeMxNHJmLK^~O7Kgu@H)6E`>(PAWJ;;W7G|pA}j->d{`H8Md$bI^=z_+F| z(tQY9EFW5q=bL&mGtP1Oz#xEwo@FxFIO2F^A z@}F2zi;>+atYLTDWG#385Xa(E?hJA9VBtEyQ|ML=)wP0` z7I{K5q4E{%ff$&!;R6^&%AKqDVa1{~b->T^J9o-SjPr`i~OcIfyFXrT>>{X^cEgpW#?XG*_~h8qtI=_m}%4gSABP=7qY7&mJS&eVe`vbnLd{di@bt?iV`)r^0>O zOa#`tJ2k)={}sh831IqS_%@Rp@=Ejy=)vL;)SKywV7HEl-FKFzA(YSEo^oe6#F|4r zp7oCK4L3oGNm1K(8&EN?_mEVe15YbHRk6-ahoIzccG-;_oj?~N^Bmqpp0X-3(!?S@ zM0k;g2jIvlbc1ocMTI(r_u~Nil$}LZ6}*9U{L>ZDTR5LxR0pZ}xQ-6-f9hg92*pT^ z4p~Rcd$GP1^(7GE#yb3Lg-ECzXPs%x@;}xk;$NuXoXvxS%2`jBdUitG(7hvx`Y~j&gYY#`Q7?W zNOxJ61T_dbnpB9pw{$>Lk=VT=L?zCWUMS~9atjUuod?JS82~b&Yo-E)YD{>K$|lwr z>t7@%iN406)o9)ZO`1K|fy|ty{&f@_&|4iUnnW5dL_?U-8mZ)u)z1}40u=cMaO@|) zp`UE59whsuSax0&8P3*os~(DZoVgl==pkHjIr6>@?Y-2;GX^~()uess3iU>4gPe8w zqo@BF-$nE*)<4Y8)By6-o5+p@K+B~4F#KzIqzCjeB`XkG%#Z=G405lVkastUGIE4G z^)_jjGG=%}o3J72Zwz0I%6sa-t5p|Kf5JEe?5|NKTn#^Mg8q7Zx!~7;YRPw%7A=${ zYy#w~oGq1_4289#Q4i}%_d*Tgmm&&EVo!K7kwsDuwv%aZTl?HoZxol#PsMoixu4EFP7^yz~yP zA!ikwV$U$`Z+Do&}t4 zqiB_FNSr0;I9Q>;a3?y%-@L_QD~K@NAeWL?*oRzt2+6%eQgj*o(Z|ox@;H4np?~~L z-D=+9OZ5X}td~IT2H+f-@8*NyF6w!cq5oHb?%6kYM7D3vN|_VhhK@j`MBEt}_NaQHcvS_fFNYQQ2fjp$RP zOc~a3i@vERBk_M-V`o-kxDqLl&8dam1^vVV`ystS9>ymet@%N!rpTS_Y7|b*h2HAG z$_k$#X8DZ4U!!=x3&R1Rw}TjfQPWlK6_*HwkGcY_fvV_O&Yv3I4q30|Cfwq)Sm|1& zixBxl-AX08Ud2eAnyw)jq1K;yT9MX6`f=E(WCF_xHCD;mA#u_Hm-hwq{**o>l--`f zJ!HUslYZvAd_E_;moewP^?&(5KLe-BfWR&6{$5#2-yo}fODB3?BJT^m_QFS@nqoho zNEGzVe$ChIcx8u!o$N1B+CwUjiGk3iN#<(=EWkD3^kU6ob=T+=T?`61t4g8%%}{v; ztG8}Dmc_PGdh`}XD-$w;@gxtUC0mrwn>SOdp@2>NpQ#f_@&66?!9(9+6>kUs8PZYN zFK{D1!#xkxTnLR(dF|JPHSFjnIYqy(JKZd#=R52z- zq(ObwYxyEzezyJ$-Kzq-D~-tO8t{TQf-Ks0{#s|c9{n?2Qhy#A4_m#>6tU?hjpl8u z4>XOY0UmGWtg6!tsn%NE4yS`vwW<}9y5)(2P7rz8ZcBS&n762`O)ZK~xsokn@)Lm^)P{+rfesq(9N|06IPK`L2pyICd8 ztfUSNWmqh0W`(;5d23k}R^4Q^4s?rVbrr0Eyigfsz8#EitBCLu9ih?5%@z*9*2R2k zecfij#ifi%6qDbNtQiS!JwQ_BFgT3xXGRZ;`IZl{hxJkGWtx-Rx($5A=VW*BZ#%P0 z!n2LC3kcz1bqC#CyuG!&S1A)x4o7+LY2M(a@J=k$L!D+&C%*{G*w=u!yo!jt=|-T; zbAdpIHtP(2vp1nl)}VJntM3CL^Mtj5@g#C5=pCu8ifzuWEi-mZ&9~FSObt3FF-j*g z{%i!I2Uu4Ed=UqnNA0xIFLhMyWa^iI>=4UH-lxgZC%_sm;-)a!D%R7g3&=FdBqsqD zHS0Gi+^d}5a2V$sRJ<+VCUu@A{)=zFRuhpbY4BB^W^zU?#lOj!qJM~LX7t|hhtTaa z&}W<8ECae<>-D?P;v!e@W2WNkl z*liq^%QB2y!SC;Sv9fa+%_GCG;Q4%gO^iPuTulj`DSWCBJw|@vCuW!Fx)I2Bv-5f} z+qPDdk>m}?wKD1w%7YESf^!UNCLafky%Bt`I+b--3OO6z{G8u%+}WTXYOBtdXm`8* z(Y>fA1$ml4M}4ADGAJiQYk5j18ktRY;aoo1NtC1;32W!~_mq2|C6hf{#{{e6?Zt{4 z*R8yJhGxJQ5w-r&Dg5+2d$2xJRv$XRZl?L|p!6|GJ)cluyd} z&HNr0=nBbp%Q+!7$w`u^o6$H1pj9k4H|e>t8q4VkVA8M6SgMKauNYdp6}nVvGA*vU zNx4T4jy>pGWZJKjht>Lw<>Jv?!QA;eiC&YdcW8S+RwmCq0wikKZJ}lfck;;uwA`hf zGwfiMa?hA>YZUUO7`nsHB`-jmBV=+|H?$60cyB6^95)_C zH+iWhZ{b09o}{}u-3GM{8Pu-U3$D`NEZnZ^E1<7RoeP!j;;c81dL^gcD1CrB@Zk~P z6Z(oGO|HPMHcyS=VqD2_1eb^8$E^4;>mKJPa{2TNuylpLoKLFw-fo1R;eB$-$xs;} z+CQxL8*~!xOmS^=*mwb|yx84_e#^eJ5{AHYdn=xJ9$)n^0k>(I!G zp`hJR(w+V*zYl-N})U&Y~FLS5Lxm1%UA}?R}NBgA`#V#)=@B2*Oj#YOJ9AO28=`S+~R+vqrMUDsY2qs9$YGU(uDj?K}KxUYl9s6X*SR|`Mdff zAB_Yo##>PZrcyOVTcO-;nd6u6?-t(*=Bud^enPn4UhVd(OP~jIQD%#P;X&4GmQbBo z-XvA$Qm4DH;aMqkMJG8SE+|&>ahH$CE|ZAVFU}K-h?@Ov`K~4OHt&GiGx)Y$Y8a^! ztYCRS7339YdC)H-^ggiquW7KFC*+BeEq@5<)1mu;;34LohN~tbCQ^#uFe19lF(ULbrBj(m?#7Ry)eLMg|3r~U~E+ag6sgPT~vZvIkR4;-x*5;=aH z*?U;Amxm=pbL^y+yvgl@Qxi1HzolOyA1Wm!TmkR(l12EaHmR2jIVJ98R?GJ0UI3mQ z0T)*DZ@OTMycb%I8XB4`*JBiW3EH7n9$D6lcbTd+=IKIC*o`{FVmFaCvKp$Rp@uA9 zB-8|J8T&XLOj(W(H)^sv%gY_pa~Y*dBSc#8J86=PXe#(!a|8o++uDOZn@=9%4Qv)! zuc!Moy0E3|GBp3k(aaRDON_77N3=l=r}gAP_WF%rAx)?BF-;~Kwp6GH7t7_jtmZF4 z4!gRZY=Pxs*>gLC zSWdBAJ;z@x+mNG+pg5z)bhxQdE$?j)kj2|5)6iTA(hon4Nm6-*`7h$P5#VzJr^Ynr z@wLn{CXa$$<1Z?JnWIiGA=iCa9FeXVyhfQKO0KKRW3_*GMc5<>V1XORuoF7a4}Jhd z>$IJf^n&>#eG8#pyLqKWvvr9pgKDiB@oRLP%N+eadFxZbQ|ET-KJJ{IQZjOV2xQ)Z zEAyyCHO=1RBegy#c=N?*ehQgKM!XmYCER#A^4`3{#%1Q8-O2M+LJp7AfPvk*raB#1 z=YqRVsDsQ^WM`=iOA7R*(k7qa^V~sa@-kp<`Q6-OFA+SB`-m5xPKVu2B*y{hkVuuj z=d7+Y37W}-qp8~mx*q^ts&?U%EOrc)YP{@|e}qe<=UC z@yGZ9EmCE-7SuvZc1v83G(fc-I;1;T%MNzF3u}++Hm!00a(lpLo1}^P>T6}AY~fw= z7@9OB`$-EJVOaYZ+is2Hc2DR6OPch2XUZtdRXEw};gpn}{cG z4@Vep2hWenLRX^nCFakssv~Gyz2t24$xFy*s}pF7I>t51ecCQZ0Zr>&cPA9R0({wR zE93esR0d8sf0MM5cW(ffj}kL&6zeP24V@vGSYMqEGq3rF66rNbXA;R{gbt*^C>cXk zO|dShjeoOPZ59#ZOqpiRqy>s7OV#-QC%^td{m;)!g2Lq`EB z)}L?K?}Yn%s25s_Uv@yBXHJVL+@<+piQX&bO`Gs(bO&H$Jbgf=f;_ zpt}aypQpX>iP;C%&-pU3{A-I#)d3;wUNB?c_d%W^e^OSsNsVX+6t+M=5X(@?)+x1G z5xloNW8IYKZGptPPTPE=L(1_=%@lgcDUlJz4Ep=o^mNbY>S6ra>taCIt_7lOY= zNtExoB1uHH?`3^dUB~$%N>FB zLLOGjH!WBET)g!hwsM@97x1^PUrzx8HS#lOx2Y4?(FXU5+@)5@U^y72NUfLn?E~;) zvF(0jlf`|E8{4pk68$pm*FJ|=C8Xv?qh&keDRD&0-bF&XVrhq;ih&HB&lsscw6%8B zAUBo!;NV&Mn>jK`-QDnJA&@U;kBj{sP;s%WXNB!SJbvm+u=z@%@jh7^X6a_lBUTj& zPL|ghlL|Pfhzcsshc5k`d27|Gg3Y=sCXSy4d`7`v4>MSYZp)`4cLz<5d&PAhG%}49 z&4qOtpBFOrD5TX?D%tbK-j z`nJB}hxl{=nP8cIEod3mM=Kg^=5i|FdDD5&>wf6DP;OG|)A_hO1rO~JD$^xSH(_sP z$*ultw@y2V4vjz`@yHH3DTLeoJxV4Zb~CrzNjz{jKi?1Yix3A)l1hAx z<+2Y*{>yttS?TusTK@%5yPf>mkL9r2&6=zO&op^A!{Eefpbj&_C}-YRQp+x^4)YDQ zP6c*5h((VMgFo{^^)T{RK$6@5EyL@d2h2=2XhFhL!N~6Bigi#t7Y|mi&f)VKI4grU z@Y2doK|^6a=N{2Ku$2>}Yo`_f%T4|#K2d+}-a;2Lzh#~_%Kbi%(c0C#ZTZTbCbAMJ zCQ1$`^#H!eCb<(DAAs9fBT%8@nmw6+te>CgqYEzPq90Lb$mgT%)uOorj5jRn&^q_) z6fi-K3v|OCw?H4wP}H6B0=u2!>B%Ew7Jnu7z-(Q9H|lugGb!lHfb zd=xqu6Yj79HuQziVYEln&w&6xng3%#K8>>c`f+w((XuC*+xjCN7fynaUijyD|FCi^ zIP&{s#_kWsfTj5y?OwTT*Mk%qWR6i}$B0gopS2j<6MK$zjcbQk{Yf(%+$LZ9tHEhA z`{D*Yv76q$2X@-E2prI-iVD{s_l?M4a_`rF%0wKUyx+3TBB>B#+=h$F_;%^Jd*mHeL$5 zt`)m?q7s_T)=_4<$(gr+Zc?0#=V`aRrn|&?Tv;cD5iRBvBocuB{zZVsgHLTlvg8^v zo|>hA^+W%xq);<`5zjA#YTEd8L?(=SWLlR>xoiMS8##~cx$U91!Mq0!2aQ{Ym~4)FFt=XR58JJ_=N z=qy$>pgC$jKsL^Y{(m~)jn8zlY5Kz{=I;08HtB#Vxq`2-d4 zO*?q~QXh*l&3{lA^PXuMzXATe?1|hvekb}21>d9(XuW=h2hTc3l9@^uQmRnVIEHju zX3=fza_NvXWUoS*rwd$(&*co{>2%Pj7o)k)1w$9$^U4vT1oYK@k}>v5GCBCqVo*tmElQ+#9Uc{&C(fk$#R(C>nerWv*=-AGw(TvtQiY}{0SwO(L zEm#(r^%tc+MYbW&I^|Y(oK!&Br!wAza;GmgNE?*B2U?>`2!HI3%6YCIxX`OmlY#JN zT_E2U%K+f^Wqb;~P~E>PxnB}GDQivmrJM|%Tes3sqSHvE(x`YssgKG z;a()(%$CceTEWn4q(dvR%PL3r=pH!rdM(DkUcd@=GjA3n)-Wy=0?6SJ_@Rcm-;%R@ zF0_HA@B4#g?7oFRBUp}!VD%}ndz+v!Fk&~K>}K?F%}1v?fL?3z?r#4QTFy11elFBk z%87YCaOj5KBj9KXu9*U>Y4GAe=AiQUbxx&zdRA4DUid#KKUOYS#j zA5}sxIre4VOvBqWWBtxk=Lv>4B13X{B8ALDdM@i9;ExS@o0Q6u8$0R3_CJ0T-ZAsG zFV=FNu7#rK>qe+J7hQ9nT*2Jil-o6tmN_A(Gow=ti2X4E1SX)1Np?)8G{3##cLCQ% za6t!B=>*$Dx`sDm!YU~L_fhR&VS$+DX}-v{p=CaH=@w0s2~M;3jej@+R8z1?P3tQ} zHVp(PFjk#bnAZfF450b>ZrWu~;?h)%SS|~fwad#m{=v^_E#6e<_^Qh9>tQ(lTAB zh=y)w^hd$9!7$Jr(tGe~U7>|!PCTRk&!@>cENZ+q%Ka=*QynlZIBBZkE;@c{1tV=5g-+EC#PF)&6L^~SavA`?o3xj>NsBZhba){o7CNt!1p6=2Um3$=5 zoYVPyMBb^P6i&fz? zNCb=h7v6eIWcjayX6cYA=#K?!$GR?n&lPrS(%M5Ng%oEnZ z*eu0WY8^1Y^uwG$-u=W+qH__C2>LmX-d6KK48k#%=Qyq{k`25D=tC3_4$9%zekHan zW{b?$o7FU1i#4WEr?*BzIfOLnX6`~{vRV1~teMgJPQ)ph4#YPOv8qgZw`BpP_4?P) zqq)Dx9LYqm_Nv`_XMXQqvI=6|D&(*ACLtRRI?dCIkSANh1f62uX&=i$ z?*o17mGcJpUB^0xf6lxqMCF-0y8Vw-F!kI_(2T8T+S5pm574ErmSom%v5EkH=lO0B+d3DRHEX?Wh@#Uqo++Ynob?@= zf$zPSZmQ*q-{Sw1bRO_oSLfo_T6e8`Yt<_5T9v%#ac@)vC*q5^W`k=4Yat}4~4fz|;`T?;#SF8S@_rHuuA6Y|| z$8iwJ*+c|B8y;wrD%}&*Vi{e+3YKwVLI3L<--6v)uRGy5l_xY4$=5Dt*mn!F>Oz0_RLkhP&`nVKjnvp>s8vC# zY-i<6mSlbNfXEEdDL4gM?a*Z2_Cv5V092{bh#1sCsK)H?WUH0ma)<;UxiMP^LXl5Rwazhm>gn6cB((%PKA8T-V6X?77x+fR12F zwTg^`b#kxJ(bdw#Z##GzD?yq}^C@}C;P4>$BR^RW^RCb2b2ZP!=g2YqEz*F5n1N31 zZ~R((OMVjXV~_q$JayfU3OpS|L$$D?4WXa90>uZ7PO&q=bF3>7>LJ?_2|{F9o*<@` z;j1N(e^=?v#ot6zY_G#0q^OAnOYq$TT<>!{5wtWeqcAwvX_9%OV>^lH{|ZJiO}k0h4a@ zYXow*Te%k9M9(hil~KVb%k`bm8C^NyL3+Ayf_xil$Z~6tnEhbl-)=#W=&kSIV%LQ) zDn&j5U$00F_Cuzh<3yhgUe;#UB7Z}hmm%jfp`w00kG0MP(p3KYI^b?~zUdl?ujD%4 z4a8qX!>y1kDG2Q(V`qf*zAq24jvm%v5iGNdk~LTApk;b*YqYfMS|~9_h(bXfw8h(toSBT+|W zKQ>1!D~R+&WPsgVuzj-@IO4cyx*WAw!_j>6F*2u8lK7Vhj}`$tq7T640?ilei*J!m zaw@bDif6hwl9$?R7r{5yIsHuVWqwcdf%fXtp4vnH+9E%Na_qG95LB}TxleCMN7aP( z>nL@(*2AWe$TnG+TA|hffXZnt)k?5M6x20fA*TxchS~o$>}5*!$$ozY>-1b3Mxxfp zR%Ap9&-sDc*)C0%gtK)(cOm=jM0P>we5L9a-$k56LLp_+i3E5lv`7|#%U5~s1aGE$ zIqRM!hI+v52ltlePA7ci>Ihir$3lzZEm+yW!@4}s(Fv#_)rbg?f5-Q|@@>{x&I;+4 zrbuXZ(xeOdY=c-IRny*M&`O4+L)9a;OE{=HGzOIgO7<*&F_lkaq>ELqWPi*OLJD*^ zo94L-u4%&(F`H<_=feZm*C$fySg~amC&3NSBX?Pb6)(P;-(syj&gz=-ffQ#sqQQrxN)EGEo5ww*GcpF}ShwE^9m0kncORafaaJ9Cx?I7j zY7|O37YIJBOWj1U5gr?rXgtapXrGc+)LOvOBAmPnq(BH zY?R{AVzAyrr?~&;5@bt=+Sl-l{vOASKtJqakXK3JMj+4yHw`LYSLDsrGRRx<*~>?A zrdFX9=Zp1J&*jWjCfj5_xW!k?j(hoh6OcZG)vSjSdxW?FGVDG6qxU3#)k7WRu+u}e z1DVnbN7`ayq zRQkby`6Qd*#wmJ7ZI)tBrl6w1$$?uC9D_Ii0q#?|A0By~+%UWsz~vg(>&jep(C5!( zAH6_(Gi$h-v-LC(Jr16-?h6Oul9O2D7?$lI@H9A&z|mE*U&gQjrgc!3vf>0F8|M91 zabe%Q9%Ce z3G#M=ccMl3&(0{b@M;xzAY)ivE0lzf91Qdz0S>aWv+$ZufD=3Y?129ISzjxfZ2*r` zl%EO4;c=A@dH?UB({YcL&b?dpN9?W(jMi%@cTUO%Er+`68gMxdo?c3xNH0E4z zL-rdM`d)WRP|CNJ5!R=vSFJMeS|szC5h1C>gZvzEzd^y*&l-P&j{gwOz7jbw2;QnS z5e{r-S7~y&3;_F1`G;dxmBvG5(fDGw>-V9n0=HJu;OG{O!mdBg{{$KNXCI4JJOQn| z4oE&H31EYXkh0dj6z9 zc*dTUI-uK#t{dYj>r&RIjY!VRuo_D6zP^Eka{fc~BDM1HywPVM&v2e4uI!p+om?iT zyJhrKF4uqJo!;)Bz;BaG#IRj1WjAy51#Fj%zENM2eyPR(yPbTIz1j;@qM@HVd2*Lz z$u6|z1Qt$~Gy}n2U_CA0aAq7P`?g0mxC$_~)b}Wvv+$Kwa&*~gUYS$}h29UG4`Sa> zX(#b!%Tw$pV~w18=y41Ux(=OkNGJK8tSo-Z;yLujg_??`0b1G(+~{nq>qCF^*1f+G z+9T!&q>jU<74W>HvG1;y^Pn!gpTHqW=V4U_oQW!dT9Up5K0o)z$_o9HZ{gW{j%Wn) zG1Le#V~K_Td$iC!h=d}u$FI;TSeenge5Jgl zod0x#TICe|o3+yANi8t79H%Y`WEShsaszvSF4YP84D|JttHuvBj?C_rsIc)Qxzs?l zS9;P6LHO7* zZ|>8Rb(c%^Rn(TAL|*)*U^|y}5j!NVa4Qq)d8+B$$bs79q!iub_bwJgP~>LSpLQspPNRQ20~E1+N=kQGTj9P!+k%gR?ybx7}$9@&r9oRFt=Lqr$*BAW=> z=-2DvogwgMGrWF+Rwnir>XcK_Q90=HDsX)v{OiT+zDg)pbxoK&4mfxr@b41q=T{1} z_R4DbrU$LtiPT)C7E$~JN!BID1?h4N(ybdFJ>=r$$7Ett0|kwmPxwLYCS$AzS-ng) z^Mq*N{WMl8(Ey;;1NRJ*YxIcPUHBQ0M6>fs!NB%QP8c~(pFtc zCK~#qHDul62EbXm@*W*#9oS4fH&(}qucm4PZ;1r=)p{cxd#wL9U8{J4P1Yb&(QV{e zx5zC}BAK}^jdlDYVpysiGo^q99k1Xwi-r~ICaT>#kP^c_&~@E2RT2 zvzk=;L%>h}<7@C?#K0`cW8Xy#RL*{T=n0Pi>+ z&)pn;#z`eHl{1hY`;daCKtJVpKO$MjJFGjX**EMi(#*tM}x8=;Y80ojOz9?vTwQ_>jYM$B1S>+J#oeMs#e~RIMfSnYv zGRq%r&^^$(O_Ck(ol3q1m^SOrVqLR9>qxI1p_3Cc+a}q2uB%|>7E_);wsX=`>-Iti z9jIVCt2@a0(m9J*wKXR~Eo0rYP*@MM+3XC=WDtFrwOM~0>w0T-lE(w1dQE`7YOqH3 zBQY)0Jeju+Gq0^(?ZmMJy6c2$dRSXG7#%dI>NGaa%e=J%ONUBg){+Uns?p;2Lx%KM(-}R;{9@fLv)IW6P--b~x>NeG{^As0 zX6Pt##(H%bkULkRrI=OxK&-NQi650D`2TkBwK3E!mI2lU6!4-5S;xqi5g>I?3b9*< zSaBPBEn;Q;*a7$Q*7tRcT^x{9bj$&y6Ir9+)VfF7+0$rlg8$4HqtG}$M`Y_HYc;98 z7sz(u8O;uMfagnft5|<@n;CGLn6Gy(`sSDJ3^_|L<*oJd&k$44flG;dlhthiI;r4l zi9U=*ujJg5s2}*NSYZw@nwA(I7I(c=NDvl|{x z^KG(S=)T|+v1!a6T&=_MI2kc9T-}HqivxC6dzd5m>d+#)HJv&RG7}^tY_%P~K|*#w z2N}HIaw9K7Zq@KJM>p#Ro>YqMj_lHSv?`T5I^?Q(vdwOo2DcZHLBw=U@bG^gND1+8 zsG8~uy*6}!cvvy8I+7Ws_3Xs5oy^CXrS!$uHqOT>avy7GmubzCWLCKw`g@%#``jel zn2a3ra-qK$O?IzuQ!=sS+d&K-mkfTl3WNl$fnH978?u0FiLUeO^#!=$pDut>;-plL z(rf)LsU zdnyo00fNQQ$9|<(H7hrZ`8fRoIoHST6-ji8j6)3pPafuUlEbI;+<^0~I>fZdA^9G= zn9z+njc=$#?F@T~AA&9lkOb{;<0U{Po;4mT%d_UYNT2K;~hZ# z0i<^W&mMrftzPR^zFo;DZAg$wc?HXWoHRU$XR-TQHLqI_-kb@k<{1rU(+~@KTBgwh z=5?}}ol_FWmEV?3?#pn4^c`yeHYcICt-7HGI_hSpUC{PkcQsh=ULGd%_P zI~P5d#<>Y9lJB_xa(&tvQ3>tzNromVUGw0hSh!+L@vqCGuth~amI9tbd>Psv01r*9 zb}zeHp(lXpMDE(^y_RXU%<C#BAjQ{f)$i_#!*>&G1 z`>qCtMMlVU=~Zyb<$t(FCmL}#pCyXSXzXb zDY-hm@&lfWl{oiks)8?(_3)P{9t-Lm+Q9aaDHqT-T?L`2S6TT-NU3ANR6Ogu+gTr= z3fV1>^7Aw?e`EpB^+5f1Dza(C#N}4zJxTN7K=b8 z0g+ZtNzF>mJ)RV%=K4iC1Ux6{^i`)ttlnxY$#sK!n5Y1r+W9wIu0A4rshL4ab#iJ? zgGw7US<0Y|JAFE_ra)4m?2W*pQ!mzC*vwrJNS@d}i+D59Jof)ESY`GXkZAxGe{}uYg!Mnp zb<2o8Hv&gHuVuln$LT$4nGqkb%3ghl)s{g~0dl-n_kpQ*Is4uVwe-r2T0YkluqcIZ zEH*yDm7VCve(E0Ef&Q3o_m*K$fHa+y%H7yp2;baPxj7pK7j;H-3 zEnw9|Lg_{`Dt`}I-vOpfDYe2nYZ495rS%i|0?TX|$^HfMzh5oGZA!k-FL+}V6!}vi z|G9pqqe6Z&76#Q`*sTRckdaICs?~;81dT||iItEz0DvKovUOkN!Btb7$ z0b7Ds#%$RNq2mS}wvvf85A$Tpx;-e@7ucuT}ec5 z0B*V%%vuNMBH0Izn$NySc0mtqa>$(vlq1o;^te&0|0M1p-Mn{z(_|L1+o*pt=Ww%| z&C;+_>?OJYsIeQh9zk<_F)+3H6in3MZ}Ufy!-7S5yOy)BT>Y=mKJNQhkjJ-sIa@>` zN6eC~VzuN$2$kGC>s>f)KUVcWT&?TC%A^OcjQHi?oIW1D18kitwOrkWJ}BY$F0O4+ zI^YFXySIchZ<;Uhe-74xy%Fh-I9ZR@YADF0ZW22SB&62;u~*JQ`jtY(anS!JrGqE< z+ewt=ZNCVa2d@CFc!@^tlW$8FhUv?4?%YTB1IzzKdqHd8)od3c?_e0*nC-AnEdOqp zxWEE;JDRkLXET$@H%ljQH@G&T*U2ta>pc7>-0z{+ZlvEHiO_SjMiS-e(1e76!VAa` zi&y1p7m{(7j-cjB|lIi8PLq5 z{sB65{l%AHkDC2gh>VPY(k5lQ^vkH`%2x2olv{m_cQrsq`B>p$-f^Fc)%~n|91p}f zo=j91E#D1ZS2B#*r4N#1nk2^+YK}wVjcO*4$tB)btI=@kDi6Maip|i<Edr@(MpRX zvtp*3X}XwC+v?x!|9EKhK~`qHuiNApM?WvMS}^Mh^|EUcn=)7dlv07yUtEj+4ct93G&%%0y*zEZXArl=BXFlJ5 z?rf%Lx2*D34QD=%Wt=i5w`MczkP$oH>H#zJZy-%~{<(u1ybu5A^ zMl^?Kh4pk~K&K3_yLc$#5w02p(!ceb3whGv;2`IZWpF_^PpyZusXgW2DSkJP%Lr!z zd`U73hVBId^gnk8d^57@O=P2GP;^T)SYPK`d>lMw*&D=zPiE4q=a>+Zwu{#k4n|@g3sc!t3jS*9aSMG zKMRhM1z16W5NG9FJdAZ^lVV1p`zbImDI-{2;8p9%vE^-=uHEq0pcZhdw#-8&vg-5D zD*cj`Sed>kC2|wjH1pSH&C&JFYUnGm>A!`wH>Pwyhvxt4rp03F%$W0uV)YG6IQv_E zLb@-JP3+Vvd>zt3+!kO+zOq<;2sL7|$TdqTG9+1_z`IeXS@=!YhiyX6UXNGJuOUK5 zjhrr(3U*1g793!?;nBJo*p-Xr7w5=EIU88r1?Sg<(q#u&DwG@LS*3;nnz3B$NF9*Z zl<7W_&Aw|DEd^y^{Xwnm;P9_u>v1|F#Zs$P*ti3dt#R4`-8~DnH-L>qY)Px+Q9Z|H zLbW+!HpF`1x00Ta8TzE}@;PvR6}(2Y4k#Ri`rGKiX<2W?5H$(ERlT=dc$4mQHAeU2 zyF#Z~=XiN3q@#Fu#M}GI(bgtL#qR-sHY5@3P`TbZn8l z9jb)BlHL9CwtV5PhO08q9w!PKOTOB9p$;IF z7{r3DN0nJ@P>p%Lh>}BLmSZ(0>h}>Fus-CExB+;Wx+O42L;$RgiS;F~&~5D5x^ouk zs95!LAGFp7C1-FBzZP5Z5$YK^0dq3V0CPQ91|v`j?^e3e1(DdjL;82Jpz*{BlO8w& z-wJdln5$C{EKei4^#PVkiX@4`^R@mT?m4V*PM_3`zA2mKc$vfRcwFh|j4wc#cmcm* z%hDwv6WG;B3vr2IDAc;vQ~^t~anRMsjcLX2$%+q&#qNta6C^?>Q_w;oRAasACcNcV zR*?yKfG5lVse`Oz4N~*VP=k*~qh~-@FKH7nFezL(#xSIpIFTUdxeq7KtoI zYL96tzm)@X^URqyd;ov)FUSHTu8ehPl}w%NnVBbwpIy$Doj_s?x(O4x>H%U&)GrW& z(N3OWGPnWVTBBLWRFD564oNlvWMX)~)do||1+>y#H1+Qmfw2iH1K3I($PcSB*dgb0 zRWkhFiRE&XGtZFq8P8=VM}?SooIDd5=BKT~_HVt(+O=Q5P^(a^(HZEYUCrxa?!q1kMW1R`p>p)`J zUn=XhEC(gmqM){>Ztrk zcO!?2v4BVPEUvtQv!+S9M0m-3v;X$yEwNa?<=RgpftpyWW%uktn{EUOTO~)@IK>v~ z9v!1L`wAI>ax%PfmGxGmx2|L=lV(`|7yf0i0CkWzwx8>&fZmhrvtOE!ah ziSX80X&kCntV6W9+^HAx^BKH{=9r%$a(E3q<@ls5Uzd$+cX#aM#yfceoXI{%Y7&Ctpyjkn#sD(*X~fI`?-sf zE07(#q2nyhB}^>?mR3#uu=b!E%-3({hz6)A8lLEbG6u0iSF+Q1urP|8Sg4nPA*$_NJBgu|hk=AE2v;ondW8jm?mZfx3;dtt0e^(AS73r73+Epgt-KS;tl|L?1$| zmCRsLp29+yf{QH@Nd9H8pWi!J8}q^Ff!Rl_DH{u(=mmUgSppk8ej0Y$FWC9K`E0oj z>dEG>o!Y_EmSanHgWvUjBRpxbdaOIy#7^kWtHzxg8bX>-P482nFY6vMD$8XI8YP;h z8FMYtodo?^G^>-9m`A-qe&u7)c2CGJ{pIdHcUKSx)P{n^cqT7q<-{x{o^ER^e2eTx zj#MJY=#s|IX7UXF=|a+v$D$m(T6PvREf$qK6-h+L3?1R_2EnS6NvY=T6F?WO0<>O} z9wkp32r<=Ot;V29^JUg$u(I9y3TOTuWJe6?)6mx@f0Lx*IqZ`%o)sxWdWye7z6eUO zKJmcGexAQbt$*rPBs-n9&@{cAKB`$kn(mX;M1}V0r<^r6;H$aVV$AB&e~MxjwwWb?W#>RZ7YehmFjvhIK4_jnYG;|88^h2-gS*Cr3UYtf;v zxqOY54ow0^yY*AJBUUT?t!Un3q42hv3duqKP}Qmle4nPw+~WKH{K(kxeMJGus=W8;lYEyo9x6L z+G2v;M6T< zVgIK?lg0Z&dRr((-V0`BuUdZnlt$naXJVb0m#o`=8+sg+1T_8_6!c2)6Rht#-ZAP{ zgAHaZ;t6`lzZQIo*X~zfK}k8@Oe#%`3%TJ*Hc|NxcUg%=-@r^P{VJ1 zJ}b+VOgw}IViOu-v>&bX5BUEqX@J`giPe_p(*5lZZl5qeQ<=j6^uz81r0oH+Bvw=5 zb`aP`Bl*p{@vwOKe-A(Db}cD#y*p9gVc&`Td#Mf}sW!p`)2;`)?2uM;#RT#pjVqe4 zd9%orpXbS(lxg1~=~5rM32Q>dqHb^ME}3O-)_(?nDK-%&DQIi1D*-xrdU7b2?@}DS zwcw^sDELRx%$>gpsIEkkSmxt4R}EcSJ;QDA1M*voxC%Q(nV=$(`k_>@59%0*Z>0c} zB-tS4tTIju^?vwkfWO+bg4t_bNBkmBfrUBTvrP^KPy6MZs?S05jR3(i&B4yF=vv58 z@yv=WB4nM>nzap1=t7_G2LjW+mfyRP-Mf&D))A2oc*wgNJnJdQ%Mfon}8F`j*kUqSFo#5 zJe~BphxR`vet=&`=R+uWJ06YP}U8bA|2 znUX=OJz{Yw;}!F#%z(#P?So1!x1Vero#e_cvNNqVw^GeNV_7fOq4xlkRnO|Kq>te} zz=k;!E-aH0MRYa~4j2cfIr@$FtRm3Y!F>kj@nPM9Wa$PbmUF@BM6%I5Ps{V_nO$7x zMzsKIZ3w%%8`v>ZkW*c){+N?rJbT_vUWN6`@N%=;>Bwxu$2^w`p(b|WwacBt+N?Lu zAQ8hUv0Clrj<^@vY$y0KS<(o1waGuwlOC!Wahd3KCfb6L5~v$Xn6>8X#(-D>QlGBh zO2#kUdYbiIBq;PlZqh9Ot&>VwE<<>;otA=aJ9o73x%mq_cuy%itw)Bp>7%ul^%13O zp@(jGwasUf9g~h;xJA!Fo5!){!^o_|f%TH%q>2uWe!4pUr%78dG9c+O7VYv;urEQB&L z^Q>cmO*RPR19X~k^Y!{&=#?)25({;6TIP;Xdh+a}?ju#-RI1Fu+JJB>L0fn6+zuVW zui7h*aV1@Cpqvumvw`16c-K}R#`?24yXok}RG_dxp3^9zN=4{{Z?RUD-N@5@!2U7! zp!4F;z<1&YtVKIobnKU&?(h-2iJ+bfmb=KqU5r$>y8JWngm-BLP$k|j*59u@Y(APA zq(dC1*#fDT1gj}<6bzZPG5jO8(8srr5~PPv-=5o z6<_=L?DQXQyI8hLqdo%uD?{~)7n2>vq946}5BOAwvR3Pk1#hx$D!n+pGk?I(0v~Fe zm08yA-@&&unf!}NR{IKT$ptUJbL3?Jw>)hj+P_Z%d}*J{(HNcBvzs)Dzs#3L zZZed;$+zoXzFnaMMARGjo&0|8DS>aP#gQzq{T|S(er_fH?@Ph@9w$ z%CMg~1CZ;${x1Np*Mgt5V6L2>%#;_i{CnYqK|BNIQ+iFZb)4O_!nhmWgdP>xgp3I}hi?KPut++QkCrdq#(uw$OOT_NE7LpV zbMWk;xGyBu{Wq4O`8$S`u2MkiM?7n@9?$cv3vnL!o}~)ABlM)!Mp&(Ni^Nd@`?X)C z{cf9I=$K$CiEw<4o}z>DCszblozL#tk>5$O0}JIFU!a@ieQm>*nT`0wF)PNGD|1YA zDAcdxo(@*JGq9*n00-}a7O7<7?W^Gsx+251TV(>t zJVqCT&w!ZK@mdY?0dQ}6YLNdwBj0?ED-5UWYdpUXtNUy)LDp47zI@wdzy)=jXTF2& zm&Wrlq~7n5@4C%8s`Matv8+3gBEEYDsGgx))iPx(9T7mZ5*d*|B#CP$^m4aTm>MSK zLbo4n;a@u=Ct-iJ3sD3hNv||;b^w}~WH;7XdkozEkk71}>MZapaa8|;i7~O+IM!cb z3|>rzMr))%)>D~C4_&|2y^ho^mM)$$U#hhM8__yL23!>n@5H!2xWS+vD%%!3grDV+ z(4F%8(Dm*#V$92dKNEBG8K8Zoy8`;7&xL!2UDZoHax+F6v6^gpx8)M;)yBXq*D{Hg z7qN-n=UjfP{L-aqg=T9h7&&$BaD-LqSe@tB;7p-p*X%jf(549!Dq>wf~`+Olb#4K73zRk zWT_1*9j1yFZz_~g<7d=73|Z`QB~-i3MY75P&WdD6lKV$LNACGhtHGYdh&%KTE+35N z0+S)|$P7u|Y`G(`8pWM~%SL|CH&Vi~87zm9@e4xjSPGfs30OCk-IBvkYDvLGEV6Ws zw9&OP7HZ!mMOr5}0D)7akMl)0oY^PM&N^1Sj8<;q{XZs~V!xitl{>I5#+~`aEyrX( zJETt#@wBjR2P*GD&D-@`8mSYk?olXpQg(~I)1ra@@~eR^`JsMDih=NJaLz~aU3lmf zovZH)&k$xe;$62W?FOY=hA-m`SgjApR+oka^R@@)?9|@B9oZbOg+SK4SF3b|KF4q7 z`MA+p)x!?xs$5DWSn~9gw7n<-^Pp|=6Okll{U~L+YLxV1J z^k5TZ&&z=L6rYyEp$EV)8Urdh1l@3|$9{$s9lF2Zt#&%8c_|PnWmgkkpENyND}ayB-Zi{7%K@{G!MF51RZ5dBlYE@8na%vKC#&lF;$v4}jQ=B&+lP92=GNh1Itd%m2s|Wh{#AN}MX59x= z=mWv(E%sgt)^fzU1f1Zk18=Qw;I1+KCg_zIxMdotz`0DWk}0_g8VKZvA+mp<81utg z??A=RX%hZxcP!VBLl+ftIP@v&Y?lJV{yD18_|`uCuR z7-%(1J^cQKOLTWhlMBm}d|u4+HtFkBX{<-jU&Qk_Kz*%Xd8>PZH+OMjsspN}at<`t z2ku_SqPU(ZQT0l1Cg^jQ?tsq5;K4Yq$i~j0k`l_SgxYRH>z^vGA)&X6or@b;RzGFO*is>>j+i@HMQw?ymij(MdM`4F;K%QsC>Eh z3qAF~^eWDJ`B>i>5+}vH+oF$&ysrWr4RDUH0BY&pGTeKxaxD7M;{NK(v;i6Xz9v9r z9STqDUBMTwn)P|@lMB&{Wm3%wmb(e8s4cD>KWsAA=ymApU-}BpT%Ot3*7Ds(8N$hg-r3rL7=;jy^X^a=7Ap4c|o`SmJ^=I zBb$nL47j{FAT)T%pM@%TWPSvozL60LA&dj$FR?F zTIQ6njrW}wROn7txmVwFHPHIIvIpw0&Xw1Oo|i}HjE8Tl37@%VZn2)4?1RmBQH)O02*Gji0RAV*=p_6!CH^(mE6#^?hX8jeZccVZ~p+186m%cORe!T%P-ceJ8*lbVGnb#-NnzkKn$E-H@G$jm7jk_SOa;{MPoN*4Sho@L zYp!7hsbDIbzvk;OSo&)~PYP&;i5l#!9c(Vp65YZTCUY(G{{r}YS|8B=al>vtw)bLv zSVk4EkC=|PZXx^m_c-3s4F%-Dr&h(b7rP)!t2qgMTjps3S2g3iM9u|Mx{{Sp%cv93 zfcX@u9TqY@*>Sqm0Q)HL9j|G`ji|o^rt75y>DaF+$f?D0R8WIPJ0B}1;u$g-UV0t0@tdr&1$lu$M8^chC%~F}+P5oHF zKv^zA8%%NC1~8rp4i0i{n~ZZ^xs*bEGf16hWL9jhAJ!KBd}`<9NsUHV7*s5G?kKU| z4jb8>NwQ70e`wP($->GRo~TkHxc;51)4Q|@Xy3%Drr_{8T~3 zjX-Hw%s+!aAWVh?II(=(B)J_7T9%n*yxrpK;D2WLAoY)T(VS%SeLN6kk|NKDVrA>J z238hB3WkI05wq*0rD#3=hdZpN5opOUx2U0-Bc1Hz4EW z5cO!4Z03v|hT<$b1O@6KcF!)2SDO}9FImv%4tY=Q>}(Y%m2fYe>VYHE;JC^%_^p1q z1ei=g^%s*}HAlbl)$A#O>ud$oy~$ih99aJ1_Yys8bqTz+LXIO|ny8ThXIANuc;3}6 zyMTHvUEb+#!%oVWs!$-+vdFIl`nAyeX-Z!W;;u=4v-~xD)y>p9YC9Q^L<_MY1Kkpw zAO+gO?$WSTIPmNa?P&JwofdhdsEOHx(>Y5L5o|QO}De^_)+~-c4-0nI?qLGf@3l^F?93aRd5Ec zK*Cgd{JmIDd*H6Hp6x$(JLO7vBHdXZU^4ckC$v}Z7P3lZtrmdex1hdie7?>42-fm@ zx=FwECy8~Xn;%X=eyO7eE~~7Nf^Y@z8dl~I`6j67cdpd$c8eufcayar)?~37l6$Zq zb|dR!kiEOnP4Vnpp@Q6?2ict~SF`{5_%R3MZK;z!O@yL81|!zpp;(Ii9y0OFx>~CT z+@G*`CZXd2c4H^k8lYM&*7emWCI`5W>SMA`t)oPWS`6VJbUV#|HpQS%+XY?@4eNgV zkh$_ad|C|*=?SH+{M!p=jwBen2SfgPkH`4}%y#bvo0cn|e5vb>RNP;(?6`Oz&1$+ zZF~-EAeURlpqOqQK@!z*-+E~MWp>^t4~fkQBCkcyV3tvq=IB=aQ()c=(}puhW2;2j z57tJZ{sAn6G;%vP1=h>NvUIRBkc0F=gu+HxYmqL2AFay|eq+{Jj$hFxV!TI(gkj!J zpFm)o1chyp6Qp0`V) z*@e~YlGB7N>Sv$*V1{aC?xM4yT9v@b>|_h4j%U#!6JXM2o(v=FteZrhz9$Xrs2&cp z6KWas-L4lSX>F2x6|&CaQq|DFuY(oPtex~yJXIw=7Efz0pY%f!=iz622+e&OYqRKj znM9$BtvZG&qCjOHHe(bkOi-&l+Nd`XP3Y7%sHavQl%0IuD~se2cDSFIpmn|_hKFZ| ziZk~!XML}>a!wdWmY2}0_jQ@exh7YSfi`w(J!deggvI6?Q+pp8)LCT`xhnu-*rx2^ zRjGg~YuWinN1}YGtbdiB=>z$g_b}PjZ-y%9m4iKXw^$9B<*zUE*Rjq5e>vFtk;Y3K zSqXK>whp3@gRI43&c}m2>oQ3mIq!+qz5K?cQ~6uy23CY@m0{`*p5Urpv0N0p;(J(Y z??OTEE8Q{N9wmy54m1DfS=tI+7$we!<3Dt3v;#4UG zod7@mJHS7$XnXczp7LUTTcXuE_K>;L9ghW3W0dDe~iAQwC0K|sf%Yd?2q!C4vLU69%!?AH7nc}fh|p8%y#qEi~c z?f+s&?&q3uS%U1Gqm4kn$D0-Vv<@ltFv5&fui(r27?KyLYgqL@zNu0;JMH}@{Z?%^1&cHV~s8)a6Dvipl9{oP@vyv0zA44{~xIh~5);@*w?*}K{ zP@zp%qxK(qFNQD7AKNGCtd{tozfpTsb(8CZ?!&so_38QkGna)7v3ZN;V`>Fs_oHcS z#w~U`w%i!pPpynh(EX}Ge&z^l9ET3KcbB$=F=I zgL)57J}8#qZ~?e27G@7^tRV%U+7yFs-u#pI03+`&LDY z$$s*5YtXCPrIIH^g)Mf1n}Xepb8i+5Sy0=gTBk=JWAm5a6dV$B*n5F4SaT4`15V(@^k(A)J~Rv&1lCV=G) zymgE`EAj^UepE~gnq6N-Y)9a=_xWao>`*(`SYN>g;Ls;K!rf@{I_TpZE%i#q%57-h zhonkU;l5I7<`mHB+vH5KTI$8Jg?>7ffLS9}b{~5(TQscJ<;rtUH?iSj@E-?E+c<#{G3T$Nv|s5! z;;nnstE_Lg?Bw1pdWabPb$Drrok4v~(B?Q)>D(3go6WA9lxv~lVeHZf-jah9ouYa@ z**D8~BbdwtAAKGAYdBdyA*-`NP62dfc&+- z#kV0RQo&nPXeo3W2Too%oni<*Nfq?H{76Mk^k*%WxXLsp~3f9a}5~iC$F{#-e}ihmj|bu2d_V`L&BE%eFQQGuFph~PvTE5dTUh4^bp8$Ahe2RAC9%+x z^=&vH_)(BZm%F=wX_OGbfL^R3Ioeqd!ddB%FQAEO)^}JBvl4s{`lcM_Dyws|`II(4 z-RdC^^R&;Q2&-o|U2HQ;nANTOkQ>D^t_PLaDC?LKn0N1uS7edZ-iVE zVimR6AB9rH-)p1=`frguutHA`xUEPUnfLX-z5ytfU>EOZ=Tu(6k7a%%91-P_DM->< z-|muh9TdDtS0I7!WrfV+l|&?KzLZd%S|2JQBVk6r0G4e!j6^)d{>^$FXP*bzf#n=q zEf*Xg*!%$NOVbBTK9gRcij0tuj<`ZSMRwpnTBTOav)DCb$MtZ+szd5UNefVomM>kR zbim!m`zC(2a`ukH;%noC%fxK<`WOFeXWo`YoRnTh=Kk6}u1R`(n0#>A?Dt~BsNRF- z-h-{W727YHypIk}V|7TJNdCP8T-9h5IQ|gNokeGAkwDa=YBIYS(-5$GU$^sS%l_HN zdF?$tK7@_PC+|s}dl;OQqDPys&<15JNaMF6_@R;AujHyC{?0+eH^?3=+)Q|Km2}Fe z{>@n|*(x@$^3?hqe5`9YwWmX`*1L$RCGABPeJuC5*TemKd&r^_gRC)?6W=s^VHA>y z)Ea_9XN5{EFfhq)Ei$YVdR>G)vkDt`(nGO4uNW$t!df2(`g9{z`r@day{6@6n?cIC2Sl&r+BWQfc-HkBS1oDmcms6S0%o70Gh7Gvo(Zf< zp}x!XMy(Tzr*1=Xt&~L0jte*qcxY@nYlz^@PsrYg8aZ0p^hd0{8ZAvfKF!wxEc8k& z@@`Egzy5FD`ffc6ZeJC=3=*xD2eDPGf-*y?q{qix$vNp?F4|?lMT2nNpOJH$$ox%# z28M{(MS~aX9^EhS5x+eHv?`%>`nz!dVx-0sJh`1KjNfOF3gbu?^b6GUW1w#_YU?=W zmC6x$8|mAi<7!;GT&saXx0LC3MxrO$p)|GKAVHY1jb8J*Q_`Ug%XCVR4}cGzX)s^SO6%aTX|30}LsJ*^i}t1=HtF|8k|Z-C7rsE8~#bihS2 z#NH0*RRK?9KlBAvv(ZPXg;sJT|9s>ufn|)`nMUrnvBMOk^k#ig%(nlf|1o=t*Ca9u ziyS%ati?FF1POKuYs&%-Pbhs+SkFsP=>+nu3m&;!Iq$&j_#MgNxdAC*Qbbe!EDSQ3 zv=VLEf@L!gZ(gm>bR+I5;JX#=_k}M){*L0ssnZyx53YU;m&^L4n#KtpU$ivWhD|;ybKhr?e za$~Ln=39{z)<0r^*~0W57xR&gXdK^+xH`2+;tzD{2z6LzNVI`&57SK;j4aa>FpkwNGS=ggJE)si6(K0u~3x9B%#4vfpDwIzph4p4i64nuU z-0boD;Dhcd+}9<)aP>erS)=7#-6u1SoEG`Wr?Qs;D3%^A?5_aHSS03sJWs6xKSnG0 zxzn#jR~4g;KF~nN;gTI-VG`&L$YCH~&)NBN{;KEMyIJuRk|kH@g#+Yz)GSA#ad%`LvaM!ywUV9ND|Q2t2sGeFBeIKGbz7A!_d$F1yykK z=YG2;yGvPD3is9WCd(c_QLhIw4@6YL6CFgoGod|dDu4|1jZVk!DmD5kxVL)O0ezBP zZ04T*Lf;wP%i4D%#nWAv&Y;Qe0w&f!yMy>1epSx3{qh~(Os`OjIJe`cvd*x~0g|^A z?XBN(l|)}2mvFJiE=T*=>uYj4Z?5P1N!I#4QTcX1rW=V9 zv}-oGTQ>1JQwucH6E)yU)pR(y2ui*iKg=>T;$&zIFquN$jzWGt} zs^z}UAl)s(7>T^`V0!>cz6gy^CbwD@ZnxVYg-})kZ|R0ZlE5@46RG7%ll(sn6iix} z?*fa3tBzITEt<)1x!iBD<&~Uo!uXD@GBiQ5hPKDGXYf{jlo&2{?H?ecDKDSUlhX+QnFPZV-7wAEc2ad?cQt-j4g56lZ?YXQr zhfmrxnf07Se!=t5yLr_u%Vn9~93;pLaA5VwZS&m@!_zHdkfAWl>J31Ll)TS0=7)(?sM79iX_T z0BW2U(FzsKQFN2c$}Rj({WehBASOd=0{fkmpM};S4Qpjkd5he{nscG346d}P7e`Lg-f{-1 ze&X35xK^OM1$i||T$;*jyyoSKPm=Q*bAOOk;wPZcEu1M!H7{)QlZd6U24a9{U20QV zMLqOFE&)${Kv&?Gj)Bj%XrFA?SB|`AKf0~O{X|>APa`zHN2=f_v%-HUt0mpr91yFQ z-UIJWqT`kU^M`>h*{VRLfnA^JA9du~3br-AIM14AHJ%9I2sT9-bab>QR-&urGU-6G zZv$VY@*FaA23jA~Bp;`DV#)Uinp4T_QKx(8q|q)BnheH0^xp?fOzUU5NM?|whvYE3 zH$Q{@MTZ9CLa@1mIxp+zSBG5sOf0*H{=8D@%CU^d6P2yL0zNCFs+cpDdz3e}a?N~~ z!G0Hs5`$@B{q&u~q8ws<9ZcEm09xnJ@#AJ0U{(Dt%)aMw&1q`$1F?V6!{*DKB68e^ zHQx?BSpA-PM^x)sopGJ@_sBzf?M5DUle2IBxJsm8Ir~fF%2M>t-QMCrN zus#qIynm-!mFg~~;t}{U*MXhj;RCx9NT(gH3Lo$*!h9-LJf)^(@z2Pc?1g+FtiS=C zC+DeEf!w0>*y8^c&{2-Oh?LnY#ZXfi9Dht!!#Z@p?V1AqnAsnz{ORw6L;TPCrqQ0#^aK(CNsE>?<2e`k6uF(qv^J}&0 zThN0|Kb=CG9F$unh3gN=Wjal)vQ=t18Q%=HionR(oMe04dcB2RJjhA93EDi(NBIqU zsd`Oj)kFRQTb9P=5iz(ba+ zWQ?Q9eu$>egPU??8XesW7U-COjWNZE!K#z*=PkeCoOc@?xtf(uQrtJ~m?5c^dbd93 z`u%0nrMI}p^tB@z&bo}+l$=herwMMzl~g2Jhdk$Y0qIsgu^wXWNTBHid=EFR^Y#_~%%I%`YLt+h-z!OL+GL5VHPzXnyG8I^MA|WD|0Wu>j7&2Akpc zfL$bVmHE07g=vG}xnA~TmCb=4=A#$dJo&D`C(?a&Q7pm>iBsh0S`#F2ILd`ZMt_zxt!RNA# zn(T3G>@H;9ZJI94M>2Y`_T3h-8q{p|(}JEXk{RAU4UUF+QZMV<=0{~dPpShJV|qh~ z${A?+-_Epwb#$raRA}8j4oa3_oyfG#>S5kJrB-2akiX2*Bo9xz(We$0XQ~CV)ux+` zNt~L0^(~ZzFzK-YM}Hwvhf@DQ_ma{&L}y+y;<^s z1bK+uZT0jv1UL79R=FSvXQn3lkk8t*#N9*J=ACR-b&!ohaBJOXOtRr z8B*(NI)hA4gTJPLf%QwD;lFPacd|3x6!dB50PFlY3V+NZTMh%|S^n?kByd2-@jmS5 z?;d!gAE-{qXY4pdtp8^VPiplw=-y=3k?w8InFW2_~AQ?-I5BEZE436GI*^ zLNU61fhTJZNCk8xuw0d!wNvAvHOt(&nLDvEc|Q3U?2npc^n!I;HQz>Skmb!nu93%~ zhFtB%{}-c{U13(-6VB*onrE4`d0q~vojZOT46zQZYGv97Sh4PA)nYT7hWYCZs3<^t zSHWimO4lr%kxHbj?Qc-dl{|J#j1pK{HP#4}Q-Tbe;{8ps5Bl8-of=(|l>odxlS%np z5~K$xlGh@6nvY#%lc*DALdUpb;K=7j!`KYSlTIW?6SEKU^%8|Hfbpk9xKH#;m0m}j zPx{zbs+cWjGv+(R6E(bww-v%=^v9+$^)b%#miJ}70wSOao9z0D5I5CP{RsPy8YUS4 z$DL%Qd3d2i@8q)rF<(Fc4yk+5@9;^KZ;?!2ibfeDbB(+@>^Abjon`(-@vdUs&b2@H zovg#EQ7;1K=esXml}2ft4C}4_LZsSv#J8%#%YATBUg~5tKgN@^f}qj>c{0I zSIy4MlktJ{3RStz;u!{K^K|@wlFkD>%kpac?mCOoYSpT>Zf#Z2y!Wy0jSCkl;=qA> zAPR~Vl@SOb1PECqgoNz9^FH^<-h1za+3~fXU+wyL`L(wF`u`ogyd=r{KF>YQy3c)P z8_@;r_@gp_U@!OAK5(&w{?wL_T;+~+yU@IIId``qg%@TRK6&^2_06HrtQvtgV7MKn&w*ZU%A^&ZcLUh2p>@W-t%`=e%XQC3{8 z)Vr4wW*-KQuL7%1W=c{{G{d7V;0cQk+73pCcq^+O8@)bGGt)*4yP6j4_?te*C`%X^9>#*q%#>0M=$lrn}H z6QaM71D$+hx#3CN6*_s^qNJ@e_$T0$+Js`Y6z1}&)iN2BOSCj-l$%&d5jgA>^Acej z39N6-s9IH=Ddb$Qn+A6MP^xOQtmlbYa6)VfRAarP>8Au=k=@KYm7Fto`}f%|R0qtT zl4j0~d(~pz(qx8bEl;OGe-q}m2rl}7_KRXT7}E^+cw9=Mx;~lEyEWD0QQ^L$$lhGe zqiiK6M;8UA)eiH7WnSm#N^kMI9a@LP>vNC0XBfx4AbrFMBm|>GJEd`#{8W_I-z|yYI2PS=Ow)jxRr>@E{Feu2 zlhM=6@0l5ToVa3gI6}peEfqRX>ajH3%DmQvpjXf$WQS&oWm{REtjb%^#t2fWgIU{w zKqQoGeNBdeUU`6h%{AjK>(y6;uG3IgIwK_5>8EA#YtQKcl@NL5-a?{mp?}^(nyhE2 zWdpwojPLcYO1_+;XR^ZrR=LS<2UfX69KQx$D%quFca>=y^L6lMB>28fEXvWk=n}66 zJi|&oMb=oSkNX@fOOE|pJjIYyBRhKCb?oeN_lQrCg}`8&tMKQBzQC?9>ni1PDtcDx za_lyj25&K6E}Zj(V2OaYIi58~G?NXg%3&Vd)0atD%N6D02)t+V6hhtdd}}Sl^LusFr*XV2Z@`Rx_w1H0tw^ z^2D~W&L5#Mj~<~HJa5th92@j4Xv(Vhf1hXUemR6i?n#~KDGPN>O)cK#dY zKa2A_htE%jzjmu(9nGIlt!CVF=&tYbuI0yXcTa0rh$`mYB;A0mg!)Ko5u{6*(eviR zE=mW8|FJ4|{mA@lz(=`kkPG}SA0sbG3E5)izp=OX0RJo5-(%2bHscbD=(F?~ezv;x zdbL;wwS2PRPn8rkpWd+E@Atd6fOsA|Y}GnGf3=1TYp_`wx&+RBoqfK9_bf{Xp^!mW z?(fpQ%B{=?8bwXG&z$w6K9MzFp`6*!gV`&)^i`J*L}roBJ0#5Au~$<z-cr+@{wyJetd(Rz%4z!aMW~)_!Z<4D+anSDjz;`m-3!Tsj z6W(q_I$F0!s)p+vR&LUc8ix7_|6*NW^g3v00rOo4M=2HgtH43my?HeRdun_<=uTAj5KGrn_ zFIjy4X-F8nJiyNbf6I_9)g;9h3zz%`Sur9Nd}C3NR{5w|qj`czHDp!=V-fubRhdOE z9&UZZS>&VT9$6l(Mbs~Jd0+vEv5#HQLb+PD^SkhRp0={%T=;7T&u(I#3~03j{+W@- zwO6du?b5>@s8g9AcycoT5{D5EvZRUkCWQ+DZli6{QCi8c7bX19ip(fmn*u4Sa#?DdY$Q5MnNlxz|UZ%<2nP5FXR*J zL}vZ_$c7?If4Nc>m3hf9wz1&P0U5;3H>gBWA@RHxg+>^y@5ocb+)uK}aIyG@J-SB! z=sr{WtjGaISPu+LmYNrpI^{rkRO`sXvp!d55B?b1r>YO%lC`f?utBHJ*W}nMrw~9r@cS{muHcF+tGO(L585r=%AQm}#FT?RST>qxeqPs~6r*W6mA|DE% z$}&#$7<{%X;G0ifAKbV}sq`pTFaGRcx73TpQIRQw4z^ym0G(uH`V7)y7%HX0jcg?< zgltyq6&YAX$qi>!=gWE|%!5$k_3-iITE+RcO54%48d%qmcIf{`bb;dv3HubF_?5R) zx0IReEdBxuuvK8SuDX>#zCg=~y1&N7`=d00Rd;aSJdAfB#x*fdgOWKZ>s*yP%U6Y> znbSHhNstd6tb>Z5M1Gln>1O277@ojZje{e}q0z-*GT9h0So#F-J3 z0u82twdeFTHD920X0|A4i-GIX4_K915H>J&8FXcya=W=36o-V!1Gx5Mose(Iq?YSp z;;c6D!~jz3F|;dA9{A!QYnWoMqhjaxK4jSxqakVOFx4!Z+-hj(Ugo+RN({6`iQJGb z;J2E!S(O0t`t@nnU~#?atiJ=Py-uuJ@w=QXrl0I$jj__jd=2_x$j+l95?=bG1o{Z% zb9utAXMT$)C0d;SmO+<$cw?^G>zQ#1 z5(oFiae9AC^NF`=@edo50t}aX;9ItlIzU>Hzig@a}vxC zLaS9`b+5D0!kVzA=0OWzJ7O)^{cc90CZBr+?u^kUXyp{fv<#cXhz`O2j*ikGtMm`# zEn%^O|CVX7CbDaMM=}h*nALLwSo&BzD~$JZd_MQ=HW~8e?4ys>Zc^(t(IV%f8>|I3 zs)vFtaz}8DwCgk4#LiB_YBS;bu>&oWp8~%qc&U`xKR{xh51ouL4*IW}1b;=W>lXbo zscnZ9Sg7jH=awhZCCy~h6>5=oV*P)K`v@^I>^X{0ERTaIGiF=Kr+fKrHDmX4Tg3h; zbKwB`H_8)^S~dDks8goF4gH{X7A_p;-C50$L4KYFWDn>hTvy>5IZ1EwR|xX_n`e;R zcflPo8o@kU(Jm5*W{B}OfJ?hEp8##3*<;Z#d;SOHHb1A-YJq31>-c7u;n6j@hsW|f z@s&Xe)DxCLXvOjql4J~<{1900kw{6ER%|O419Tj-&tT=q!~SGd0`7uNivqLkQ<;#H zyB+z&nFcj-L*XPcPj(7fiY3h20$#QPfwf|^U5-pE1CQ6~F1V^0E?*~B>7*Q5U&}7* zf-F{+taurCM>$o-!Tj;iy+st93vRHc=^DnWhev8S)q7dbdL2;9mOc%hG@DqSH%`gm zsR8|6KsKxH)}O)q?cDR?S>d0sZN48d0{@z?-~(<57GpmEREf~U#@8(|th@?}OVS%< z4>y@^wa!gNsoTIrGITO>7PmW>>}ojdU*eix_5 zlSr9oe}_w4TtJPO@fz_T^t*-;Mkun|5^WUbTT5*a4a zw}&-Ty97ycKAKRClEp*x%p*dilMu^;RL_ROHbboyx(YZX$^lmmMSKQ*J;UdgZAnfn z<1`XqQ~|Y8dy0`q;GbzE;YG3!>F%K_a;s!D^xDr^VpVp_!RnRZWQ6x7;FnxESrs~) zmH!Ej#_vQOFKOUO%X;D_2De#OuIYRGnClaKk>k)my|nOYeduo1RS~NsI&?LAu(~g; zt^#O30VWFJ8M_(9aL#O#0(lOYtPpHDKF^(mwCUrNuxd8d_}!5T?9(d1<+8d9&}yjq zz^-=ad_J!R!#BuVzj{N#gKIYdvs^He>MQka_m7}fD!7rCL$_^`FBy)$PjF0( zKLH)1M>jzCk;sD=xs7h~#flY0t2r6-pv51mjEUT^6!=4WCJhxt| z{8R*I4bUhDI(y)m9<4{mDhDSUkzp^0)rnoBefj|V#}@@=2HAH75U-Ey@2(Z9&5Q4 zjl4sjBjiJ&ycUe>>(a)aE)$E);{MEhP0US%2(~z)7ufqKp2M|Jv{~^A&_Jzk5ApD< zb_gHau#e-tVKPb|^j$Km>Apf{s0R)m@wUak&w{f`wQ5^~nkns2e>XHbU+sQ_tp?0E zEMkq&TMDwRkx1~{fP&dme+{oUvc84V%MHb(hTWMaBpF)j(iv_`7N=}pAi523SHXVA zr+AzMzcF)JHK7CSeU>#?1g_Pxn1BkKm_JbLwt7F`kl)UE(xY9_134JnsI1;bj0?*O z>_*r^rCJ6Ui++;eW+&RpU9wm<8NIW|H2vD22)&L%yOGqYx%A zG9l$2@J{DlxqvgQLlaq9iBgx+Xh(&PUJId>F#`&m#=txot2l8BY)Q@>Zd z{4z9$_<)aPsRqnz_7qNSBt|<_OAU#jTnFLCM6vTY0caGGVx-8FdWwM@oKf z$mp$2I*=NBHICh1;-@7OswmN;!OFehXqT+zw_Y@M>iIAm{RXrZ*_((Lo{oF|85z*kiGyV6v_9HSw%oT4VX=9H@o`A!&xjuegWMH{bOL! zOO_Sc#W`gf7dJO(HAkkPzdsYlkPa0LB0agAGlS`&vl1^>42Fk(XCW_=IITQ#CleeD8PqsUA7{ofZhYJV&u!;L#E#GZlq?W{Ck|DP|CzqmAH8gdSq-z8PD z32amW!FK;KwvTcp%8OOg9T~1Dh2Ae?AYSI8tyRL2seF1l-jYS395HYBX1Fm&AJ#<Z0%9IuopVBQmfmnnXCHxw@4>8%mY_I;plRKtcfREYvS|vrsi2sX*R|LI+1>?o?Fq|zXk4QULTb_OTHxpEBg$5w})zIbZB1IyXy zW!5Bi(W4esI7D2f^{45UXor84RnCEiUWVrIQoeVr^o?c4_o`h+6IfgTveyhtP?aveO)G)X%|+L12PC z6T!$QU2-`TT@6k26Tw23xDwewgkQDp3*&`zWAw*J=hI{~D56Ecx}7e2wLr{U*9oOY zvI-Axw_vqy!7fjo8lro$sA@5VR7*lO>|s93>@pvV)g^}lWI5-=5_uDuxC8zDN3uo# zCxYx4UB?dkrB$t)R)y5SajNvWg|3%EbyQRYvN7Ny3}+=tzl)SUp#u&7a`y%{-FmBJ z2J2ZVtAiraz!9gB_A?eSw(PlA)=MC-%D&JZsnxTPSGjsAT!pNY2)`9g><-S(3Fzn} z&F2jw-q>*&wtUOWMYjV!=5r;k5M0@vI1@fbW7kyog1i7GyEs$L|7X$e770REU!wae zptvGtp%R;36e`pqvETdIaSxqK?*hLTjcc0!4xN%t{g)etN^13XDELO;VKP0QU147Z z<|eu0^8`d$b3JIBtlYJX{5f!?jy`zw(R$tembY8>aV_etlmx*PO zQBTgbLs@&du@(TsU3v#2n*H-Ae%l6hmjT6nz6&n4O5~^TZYwge3QFph&2We1)|qXq zUoO*2c;+Q3_jwv9wV6W!QXX00Ehnf)ukju_N|TD95J;Bm7POmaRy06OzHA|P5B#>G z3qD8veXG-DIY^n(>#1wO%=KzjF7iy7JWOGc`0mK&MT`s7WbE9{k6c};f94zQCVT|lveZfr%Ic{!58 za~7>;@$RjftQYtuu-n8=sE-eJhrq4Zi@{VObk`zTaLTP}w(_uk5lYi?pA%6hQF^RI zYL6p!TB#Dl31~Hl&v#alw^7#X3%qeMkeR?o{8#M$Z*UeQGkOaYRErGQ&dM@bU$riW z-aCNFGG9ke@;0?PHD#P?X9AP=rP}xM?0(+-0rWPFWZ5SpoLtk21lp|l0OMNBn2 z!G0^7WdJI(UVI)}rCx_FaF4@7)V1*D`#1b$!ynzO`Xt{V+jJ36-mMo%HB{69T(W(F z-|UK5T@?GFE0(r`1?ycA1q6D4XFn%nSpJ(EVhuhGdn#Kyu;yoJbTA^(ay2q2>8yHy0{Yz?o}e0q20^<->}k9 zF%1~@u~O5UEYf|DJHg#rt#qW9O6X(@t2+*=*ai)@yFN(-e&!WT)cbrY5Gc?gqAmK7 z6|KlQ$#nMWHieLG4GAp7gMo6l>_5%>y*40ek+)-7I6_4 z>S((j#X>1WP>J2s6QSZfD4`jBbT>9R%W^E$N#=|OD=GRMxD4?l!fFFP zxjd94c;=DMVRkkjdYj}7f6HwSnVqo=j;@m>O3mgaBsJ5B1Okk1aE4azs&W={>Kkt3Vj^E#{Dtko*TdI`(_`6oD^dz9E0y;j<> zb_`T+j&9>zb0cZp8cda2PRaQg;Mxh}@Zmo(0@%FqLhberVp zyXZ0IBWQx((xJU#Zk^mi;DjE@B)YHz?rhXJJG%HB~pJriBh9*D52Xhzlkl1QG4gK8ItD(hlt>L}PSkp4O0!iG5pJQ&Q z23~zisJDlXzu9fnJCR-M!4GFVbX28>LsqA%5~?-p3h^Sr8JguM++Ig?08lAp=1To` z*eEg=j<$H7`EYj)G%?3j2zP(1ykp?wCOCk8^_l>Tm+1~DH-HWU?h3{7b9f+H4slkc z`=}!}=sUmIxo~^vQn+%Rt1*I zuFx7z&-;PYTCI~_Jqn&%;4`QKQLR+40QyFgqwZGdrAgk_dVTBqo49Fr`@L9d=(Hl1 zcVY1(Q{43rqCcdg@$;XOH^6*&v!J|gjnPgq%M6iez=mv3@c$N&!d?O07J$nJs3>2~ z^xtO36+(?WrH_Hf%j0ug6en;O9ul;5EU>q8vMzOr$So>_a#tFXhx9_Q)S-5RXp$^o zv{AP~C!=bWN=#2~$3ltb&8Y+b@_7~CSX_G~_QX}x6WH$ZIrs0BfBAYDmNP|}cSvjT zU#4^4zs}p~7{@*FvQQpUAI(Oa(SrM-qA9t8wYEwwr|>jmXUP{4Gwg5$-_pApxJ|-a zJ=(%slQN`u(8Tm9i=*jr7N3^S8WY7LHK#Sltzp(?D7;xSsa4d1qoR%~+k z2C&_9bM9_}n=U`HpMEuMDW1F2Fc2qp7EPj9N3a(C&Eb8}c>Ru&Zx4LT#_&y7R%8*< z{$!-^Pq6JS;;nf)rgyvd^c|^II;m)z)bU$%fVBbq?A22FZA1^%dX^Hq)u0c-HRmhY zgskXJ`G`|6Pa^bF-HH?_AlKr=P(2pZc=_0GA^NC9h#O{{F81&$tM8;5Koe0hd-y#O zeD=V*h0d~z?qr|j>H(u+=CDZ8T!BIb})Z$gU? z2_ApxW&hd8x*v!|L&ov_JjSv95f(ji8gP10Reuu@*`?)RrciEHkC)Q=0KD%<(D}}0 z&BZc;Z8gL=UvtN9LZj=I55VE5tW#x`oqC0T97*zY28m(C-w8c{y`@`U@%hjP z{wm$AS_&xOFUBkY@}#4GO!PvTE+Gkp$DogI?f}%4X9WSEz!;)ygjV_ zbaWA7T7*gf%<>(jQVJ_>MAq(uD!YJo25%Ot-Ld0&J4IH&W7Gj-&KYLh58u_xG{0|V zAA^zttt^pz*9DIE0LL@H*u}CIE}nzVI1FZs)hZbe0-+Mu$oZAW85{%8?2{|Qf9E!E zAL~g)VoY(Dn~ObwtS$Y$n`FibFq*AaU)1t!EhF0e-dm+RXao;wK!B=9dZ9CKpUIsK zp#s*0y%Zl4(IHw7y|F$d_cV|s`UsvJRN_Ne~l*{&11U){>W$Mpz9+0eL#DehnPkocaqVmZ>@XbmJ?){YP%yy?evHt?)qJD zQ*#ioPOK^}mznp#&&U}x>Qd-&A!meTK>wG|(1lO{wVT0aJ`(PIvAD#8$e13DWd@5w zd_5T9Q@gQ5qVv&>KujOGkvYwN`foHY>XYdy?70N#e4F2gl(G1daV#)@a@K!_D0AqM zxC_py61_scauq^nHg-D8XZY;liwUs`Q2d03M_5s{n%$vKuhFN0M;MF@f}wb?jC7|G zl?v{vSjRF?B!T7&R%2GP3_9ylvr&=%snKvmwd81~a*vSo&}&Y04HS-zMTg6?>OKFjL+%|vi649Xgu-@YZkwQ4)MC>kT z?C8Ktv`9{*c}8M&fb;P&c*!D=iLVla z?WCHogc^fHS(qQt{NVQ25WU6CE;ON&$ezX6eWv6YC9V*fT?(`(bQO5C_}N)F(j?=9 zz$`)9;OkA?knPq-JSA&s0Ea`!;pNafnl#iGW3mT3^T&Fh(j}F#nq>zbl}Mh?2zf1* zW=2ZZ3|Yg;Y4L?EoJ5|u=u$Mqg^Klz5vL`^U8O%KbLK)$AoITW1Bqktcjw7~*v)rJ z5?P7Wo57wAm8YWjUt857RM&E3k-K-`5z7f6-d>}n6CY)-5AY7xBmKVcd-O5r<@bTb z;t+Wx8CoZ+rAw^N*L38-$gHcsyU3S~V`lW_}aIM;HwMF|hMH|^)zQ#LZmN-4{ zK)&;u(H8Bo6{(^WNG0I*-3^fuHv=*KT=&(Hr&T2;#q?&Rika z0$5kGUSfU0X1la8r}cib`*5>#>Fa9Md#rcHhsd`CSER4%cIV_w?_fbdZ6LACjSR5U!CY3u$4=i-*0Y&dez;1y z%yeA@-CEV>dL^se-NW2O zbc)6D_34tpY$_H9U@@&W;*eT?@jmP*R{Nfc1j21nJT#fe?S|S_I*$G{fL$$;r>r;3 zOCjJPSNj)TjSR86e(ougz+;$iz5EL3M!RQZckn$OzzgtK`chjCxmD61)}2xwG$AE! z7y2+lrFGCx3DGgpY8||aWj8DMkn`hxU&kGR=z1_k{0KBcd?k{B+NBaF(X2W~K4q2N z;H3yyEXIzV>rU};^b5=L#1`sGVABSil95~YY7ta!cg$X(v5_5rs7Hr~gx;p|duI_8 zVPsY=|4zwHY1B9TF!J{}^o0eX^^9)$tHom36Le01)68!5C9AX5c>S|8y!Nx6ELJ(g z&cfl7wS;x{K_flTO#wSK??5YWVv7Jv*+=G#70ZCFM50|AvMQ-&Ei54Z%%b$k+Siu^ zdd6>E#OktzdV9PzA_twqorUbCOLO$JP=zdK{w8_VKLV8;gC53BhxdlLOa9eu;|_4C zMmZwP-3mEh;@DX%R=?l+??LT3nk*SQ?k3z1NZVb2#%2HrT9!-dz%r*Yz!{lpk_xT{kdafmO=q>4{Y*l? z#F>YzGExcHvfS)Ey_M6j1G-p`tVo0=R&!X8UiT&;9_WQSM1!&6~1{I=d9@z5-} zFz{qIIdW^*=PKlSKb=HNRk@2>@0t!yn)SrW?B&L_P;ZwPxZ&*cJIVd7^W|{dEAWNE z&SI)3bTOT^)}xP5yF@Fwu~Vs!+e#r=vV9JTW%H2tAzQB}Z<&3Fw`gk1c)Lz^b1OW{ zCCFJounInE=JsPzbktT2eImCxVqt)2i(3e7l!jV$i`KX#*`rZtcS&e7A5iD?M5*!3 zK3A6ebm;9VvSY~|K&M*jZj?{BLr1xdp=#9LI}3hbtMUvxeC z=%?PQ@+M0Hmd&s`MSJ-C1?|#AXTC1;y}~J29K8^=21Ok!ozAp~O<*o8wc1Yf1XWpq z%xbuBl)a6K@E$Cd+slm3&T&KAuUCLEYjG0@po!GNlEAFuK-6@amM@@R43rSI-t~neq{dHLA?MD8JF$6)hOe?bhYjk7SsC*kfC0$2CfD=nD*QCPQ(ft+8rZB%-6tMIH|8>A(|}0T)dOUD2rrv4y?H@@6_Pjn-6+ zb8(!<#c27@D}GZ}QRR04!IR`+pTLUB^a9sN9z+XT;rn8_4L{+eL=Vy$8TWhjM$K?x zHD64%RQLw4To1)`XoBvb3VRj1%?bTJcu=fEQJU<6=e(ps^U?6<3cg(k90s7xD|M@! zpmV_F$JC!P%j-91K^JngR=ZhY3y^%y*Gq|`)+}$Eg>($7Mif#h4>*j27oyM{RHTzo zQWc*!BeSh%xOtg6r2yT63~Fg$ykY*0*9^G6Nk4&3E2*@R%6U~M7097H*0Ba($)sy# z%)6wE-vi0@cnILZpSf&C96^7}gQhMd9-|k@(hApECP9sSpF7x2C@~-JdWl@>e#|Yp zlDZRV(vKZ3j}gdE;fnGZFiaA>m~LFm*Jx3{m--TLH;K<@BiPv^GeG7_weCe*^{4E_ zDk|1+GGxeJ)~v zJXh;`YTDPBhJ!JEP#WZy{u)+Xt@q0&uzNf!H49Fdvz3}x>@3rCG8I%-8Ys$Kb@^XLqi9kp~ttMTRM#wm|z`taG0v z`96J8EtbEEaa*|ESQnozIgB-JTF(WanL@M)T=8kd6cYV#Frp9Qxv9n0OEjheiHN;w zzOL`M&)srufE%sriCH!hkzuoF0)tpZEN8tN%v&sIt~RP^k$t)@Vp0~e=N_V&xsS=P zuMyQ0`v_DDVSU!Cx)x37aVLdVw2(PCpD?$_bMTb>76vFXkL- z)t?6UhO6})w21^xf<;K0W697wSqJn>_gnP6t9%EsyMdO{2VkXE`!e8579q4}I?K4m zBBdK#x{6>{VOc6%m#6a%)bHw!u`dSd}UpNDIvc$S+wc)g!B z8PSc_R%la2%Xzj3P@%@ zwMD9RuWpe&%wjiayTz*P;p~h-mR_gN@{HAE%;jG7tfWEFmfgQy=gXmRre^D6YzZer zb-(r&`_&5!d*xL2N5n3880258VcaJc(@Ew%YgO>^IU~(#G@lah$%=MpIv84_mc3jo z%Ve`)so}Ge9DR2A>>bWLDh$G773^UiJ8R~I&k>7mANM=3LXZQB&FUUbGP5qdrgk$# z4kLAcht#zAg<%b#Aj^(k0S#ETU_A0GJyeZ9Y?!&M3dbthi|vtoaQ=h#InAoTisiiK z;CJ09DO!SV6RTV0RmHj|&$}viT?phG+;&aUNARx2<6CMLI-1H?p=@Z!dQ!jTvxVw= zNS!2oLTa!!RC9KlSA%+pa6c6zv`Q*?J6h_%(?;r;6|41pe%o0$Gdf(z%kXEd{5eF| zR^&_pzZ0E>BwdYXunVZ{=56z!U%@V@R|1v%!S9B{hsd|O7r!B0qk~3$7=K~EsxF06 zUKMf$LIJ+?laPb&vH$&ySS4CxmCs_p%wK#|C=QsG5K(9G+#O)pPV1CC}3 z_i_i&sE5{e_+G8m7lMa?d!cw;!LF`En&ohAnD1e(wgnX~l^xotYSmaQ9~{q~&(!1j ztRHw-z8f{Igc}L-%*cz{CL8?m$SiJ;V0o9XhQoT{+oR814>e_{VU%ltAC(vRd$HA~E-hRi|6S33IWl z*9mZ1sZXObhUFq(r4ML^Y>^&fgnDErtEKK55W8Ehg(57*o?9i*9YNYOv%(xahvw;} z>lANe%xjMpuj5w2xzU1Pl^q8Q(7<*o7-*jlS^Gp9-4dg!VOj(S6`)|b?3E!f%6D|3T4 z^i7TDcPc{Y2K~g30TZ&NIFD`*c|BU^5|i?*Tf{!L!qx3^h2N-cXd?|m3oe}lQ&J)y>PpsIrWONq18^9Cb7u54@KP%$hs$_xJKaOl zdB&9@Z<6LdoEmT7uB&Tfi+v{~8Rl zw+`MZbuI9UMdsH+FQ#8vzBKxZq)9oD+>iCe@^WXji~EDszT6KqW~E;?!}|wWxp^_n zVs1VbtlN0*E%%C(6sql&0;~t4>@*H2w{d1iqqjWb+m6udgkKz@t~6s=-r+mQigI?6 z!2ZhRUyis2ZoE(;E9gb8RfBCiy{E9o4n)6f)CU#`bY-GaTu_fD@poj(AT=6R03oY=zErHAx!v-u z=Ws9YSWOA+B&>i+EIJ9chxG^UA-x_56!0Aps>rw7z{;q5o4wr4-thYZng2xteG?s_ z6FTUV_hb-w6l;>sKoNaPrm^f%teIdh587uZj(9^)ldK9Pha? zQFVqmu`Eq?WDjEf4an5~b?9pL_dCBHIhCWM(Ci{kYKth^1uqm!1N&ISO2#7cpsy~V zOT7c>*Cc}H2OO07#~H+vu%qte7K+laLx_l)oM`64)PhC7I99>wf0hZWSxzkR!& z>V69C7>5!K3~lEz)5r3zY1;ZC_$mqPPC&hS*7y=KBn6C=K|k+n6Q3sFS*>8i1nEL@ z2$6F1$n`0@OLqp;ECGW%wL@Cq+E(__3ja3AHh80jpU?>Sp*kPyzu0YcY3!~-VqGD! z{S)Shmn`ieDmxA;-w0fY3}nt!xI9TsquE9*kLB%gX;SMAU_E~d)nWt(+4~-3KpT`` zb+X7$WaqKS4XOi69h`Q( zgZw$L6{lx`orP`*XMY@&)T@84DG}q`W!|dZmFlRhV!h+w9cvdh{%&Y(5Q zT`bpW7ra472#u5;tVZbUZh5WHQ>JvwE-c5E zYu>?aVHDW4v8QtUuuZy!tnA(BEcYVmtWrrW(z_i#X+|2)#miH~$SZkwOvkl?ReV#s z0c(x!;7(bFO^uilvFxt79`8J#?L;2jMVu@*9yIjJpcm^$m*m6D%zZ&?v=U#~tDM^( z12u&T2jLIPBCnPfwcfO^z;#uug(}S8WuLSMCji4LwMq*ovc5RYlu~iX0`h#ImJjh{ zTkJJCx1kPyAKcSPG{`@Ki^!XJ6n=O{LUOeP7^8s7CZ(ZXPVW|Ngp0^K<(t>Uv>fxe^+1pJvi{9(fV+CGtd;M$Pvvp*E~_oosQ(~xAs;(Jjw5c0Z!AmUjL^=2>;!oR&FfCBX6-fHW$WQp^Wwar zHOR7J$pP0Lz`aoZ71;gZSALV`K(p98p}PTO1CVy)ZB zH=p}|dODuUxYlWj)aV-hd+-txX{B0K!)w6%dc7*RTL!fp9zI3T6Es#+eWMm|hiTw^ z*g-TFHUKbZ6(=}xfpc!+zMMLt~5eA?_&G!yCr|kJX$Z z7C({%Oox1pbOZkv$q^}(!=9`OMtsA!`mbCA{AK>ke7LJje!|IXnsT;e!8-zgQ1cLSJ630^W{~`99G3Wo^`(r<<*eiG0iHT1lH)6 zQVjLv!WB!o{|vC-2k7-;_LW8{)GgW%JnN7)#ESy+DNTldGlWQN&dxFEgKk>XA~x^# z^R<5LGJ88V+g5N>gOuMXZOEQHO+YWUy4)8s^G5Q-?$=hR z^KAAV?Xu{{zKr+Fxq}g}ALL?>x5%!(P^bLD&%*D-&4C5#yMdYG12h2O(1cVQ1720a zZIm&sGTj@%%%U`U;l4)Zv>dkcpzvHEJE}#_DkxmcditRK8m!cICtc}o3wHsTeCcPO zmoQp8ls|&@Vz^(%r{nrCxUp0ANWKyDViCq|jF_uEP{LO7nd{VYHw(q=9#&NcTcf0? zMZZo!7nUbc>4)?psI(WGQV$s4$^JL ztNaX@vhejYRO_!emmBdme&s)uSSTi!S#QM3 z9S^6H0VrkA>ID0{L`HQ6sx@u##jwSi>;;FhJd?xcrZ)}o^cXO0M}qHXM?|8t`t^J_ zg{S{+R?AB7V>OZZFUYHvtBA#)1m9+zn1oKv{!Sz+)igM3fD={}u?`b6=o1(4*KVZ9 zh`tM+>6-!_P9Z(X&Xrbp0eJ8LCmM9=> za<(359WBafJjA!1j?4r9ygbP|9FW`qzyBvR%bAx5Hn?+X4(}a+$0niAgGV@F8K*15sM}2mv3o!ZT%M3)6<{Xo`gPM@y5aD1{Jmia*9Q?+W!lM*L?a&|MY7^_kHk9Y_>q0Q$# z1Jud-h8EYt^~d>- z@X&~49lsI7Ocdlkb~!Gmv)?Jji)QN*(;H2uS7Y0>>=Ev;U<`|oUX0Wp@t@(183(smocVT(F8>u>BvKKC{5wkxoWc7t&w>skN(9x2$ zkXr|RzNJC`;-AxZfUTE@fNDN(bP@5gnz#(BJrkn4OQoyP9h`(_MJY$SG~1eo|4n1+ z2R3JVyN~{%rqf%O(8sh64lqglu0H@ghUL+qOuwmTP8C!r&F&3{q@U4dfCyGn8RKt@ zz@rBitF?Da(N>>^cXo3!r4vz^j$FB!r#AxmG$gF~9V4k*^M?0u{xj$aTl9T)w^8!o z${76&njO+t!OA$N<@wP2e*QZEyk}LB1jXJm{(9slcd8s_=JSARD{DK;3=Y!Q&L5n|Eqld>9$9Rv4q>};5M^MMOhfkRu+ z2}U$iK46VAto#OeWJIWpfQRVEU@un&p_N2Ip9B5_`rqC%#v73*yU`qKWKd@K&vBcx z7=Cg`(3sv;Jk{lq}tWX2Soqm zsJh8~p0guC&WA#;0>dM^Kb#L&(4mljiN%Fhdsy3KD4ul>NEwrG;Mh4qLg zFI=e$;C>z$$Ide5XFdf;Wy%D2&yZcZn9qms?PDiVtKQqoj!${FO6=HkC8W2ThH}V?&ui#5OM$Il=g?DbTS;-wc-ji$Vr_h)3r4D+-TFlQu zy&S5k;J03$H3|O`lx@*KmutPABDgDt$4Xd!!q{p0Lsub%dbU)erI&;MSU9?0V}av<_TVM3IHXa{ z=B-+dfF`eyQs{RL`_Gjnu0UcnUV$$BYPPCtkj`10Fc-oFj}R?*FH*Jw8U-)V(-p)R zRQqU&@?rSB(w*o(cG&%e^9a4gBK#}a{fvK&-{%maQ3EtP^heT*eF42&$MJnp1(DC_ zMFeF&DwelL-U^tGc4i42Q|y$;pdswYQJO4kd=%8vK|CK3#D*a;Kgfhcaq3dbn_Ge1 zfyeoDzvT1pq;vvl)5?G3hJn|sj6bE{2Ab1Qltpn_JPNf^n59G`Wj)^I0jRdh*P}t^ z=`v?ET1!}CAvE!-Udy_vLCcCOkq75M@z}EjNhDT9s!=`@t6=+OVD`y)*8k5Vvad)h zB!g!vsrHF>2S0BQ&B)C>vjPlu_)_3=E>_D{IR16*griiya4p0#2UPsqDhXg{8{xMp~SE zEUwkdkAo=5U_WSqdM;EzO(kdY$#PByeXi`{b1z=kNf+nFmyW7_LM*g?#4X+AdXd&J zlhtIJX2111pmE4^cvIG6549SVeHu*`G7-d*CFFpjZFDg{H(O2%Zl`>ICr}&n$LUjG zqX&At4+_OXNrXug_N6G+6ph3!SIYqC;TUB!B+=Z7w8w)aVmlgGf}C|nRNd;9~M z=&M~GmY6*EiKt>(fgXvRfqpDXs{=1_Hf#B@7Glxe4VC9>i^RDEVD)FO$m$!REYoII zBPp!bJDnOCCxhRXV`&@Y^rOPH+#)A(Y7u+xO5|l90T+xQj|-qIZrZ$)3Cv3P>j0h$ zi?X#k(j(l{dV!Q#UhBZjW;EL=X#i%USa&VE{CT(ApWyG;^iaH>FJ%$YQp@h0f5-pQ zRlG^o`|h~{Ls`{@XL(!f27US(QOZP^BkLW5%SU5&d~^P$lZ{hFPUu^gg56VgV3 zQMr_(5nG<4)nva<61mgU-$h#G|9qC59l~*_rjV2WU`*hFNNX0=`Qb(gZlC9$M za5f|H6WG&g{T~0G=a<0o*8QbXE(e-xz!AO(Fl=!!7Pohswkzi#a4@gUJYB}$=7oZ9 zwLpfD*)O_UsniK*>6PF(EoWWZ^&I&QJNOXpnnIpbvX&ickpQQ2gE!0N2ES6$w3$0s zH+H3c+$T)FT1EhJ51jVFiKD<2`O0cW^0gHS&My>uv zj7(w8q*o|#*~pW{Nc}ebmcP~20jEk%sT`pej!-=biI#xn*t*TVr0@7BO$-&Fw=LvT z>RI^$%>*BKXF^v{hr0l4?bmKSa7cvaAJH>_uW{}|h6?+6Fb-T_5W@IB!Qs z!P@}Th82&1vwWbCo8#WQu2F~4%D7p`25@3MFKU>(OD+Mf)y#D@c&^5_+kuY15?oLN zLa`FE*9W+Fa_8ci9M|lJ1IN;Z;JH*2&0oc=r4m7sjdlU z5ql%6kRYSL{%r71MNoLYKq^B?!4z=^^>P8+Y*`n{`nL2V6`%)C^%M7@6mo;x3%4%U zDyV6-+zriU0^vw7bDi2rSSu^><2FOhAL#smGnKt9aqn@O53_f49zJQ(butQ<-2`n^ zfTb=tR^@-(_wkoJB~au4c}>jFAA$S=`93v``c0{`~OBFP6TSAoe>TmzD% z6?%;EUq)C|%e{;Fb>60*kXZM+V5?g_U-JElKD#lR<5&0bcak!m%fXE;HL z-hn!*;Yz#NZP$&mSsz8G+X(zsA3*DE(G`w9X~f@m=xW}*5{fthzuo&h+l9sx<-e~Z zWM)MIGwVPx9U1~VW1KToC3kioQaK1dF49w>?<(*zgH@qPpX0X{ZN>v-7W*b__Vf6b z`bF69DzzC%Q8yni?KoclG1=(`__UB2Gxb02RQS$no(#w%5!;cLRZy%|1+&OoqCdfk zogC;V{AZs{>sp;gi&nM1!hM{pgZ!M84sB$`cp#4O1a~q?RQt5V@8xato?vH!K8IK< zb`$kLp8ld>*6IyUX_mZ$?boU!44}1)qxEM)(_gu1N#rzcf|CZ(syo9TtGQhn1z)+uTgO2)~Lw;DM zb`58a-IJ$ufnqJi#~2&2uOXFw;RU7-u1oNJ`ieVO$VX(&PnecKUfvF|m?DcnTO?R=WWJ2Rc@yBp;_&g| zhdQ9`dZ7OX)VwUfE-H_zRj;tj28El9cKg9$J8+)VP1jk(P@N_N0kfRU*QwwFXL0t_ zL35eFnrb%TY*u@;CqmQZBZCY0e}lZKd(cB(N8`p0l(X~NU5eU-Z?^K zGuVGjCKW59o(|5*euXZM^|yqwfz^EYzZ+STFGKKX4D>S!UlfFgG)k;O>HoP>wJgPM z-3jFLkyz7UuOEn56xqY<-}1^o12?w9TwMiCtn%g10Gf#8F*5xSkwN49SFRc4gqEmT z**dfpjbK_TBnJxl4D6i^SDBUHe8pr6@UszGvZ(G-;L<2#5(cJr-<%Z%bDnRZ3;2Et zo~zT_^2Q7wg_hU)h%g=q>2!|zI zM}zH}20oYTI2gkoh^A_GU}O&%C+k87rB%vV|2Xwcj$-z4)^tD6fFd-*kINoTt{VNo zZR3 zv3@j)Sv|;3oAeO>welHNDtO`$_shk6!*Bd`7`gj=C@cqA=V8Vx;Y`4vtxNPHfse&% ziPu5bP#ip-4W`W_G>UyQUfZBWt9e<#9Aq#8YpV=seZ&@Vo+q)}cBr?8Owr%EQn6~~ zFS?V0Jk1C?kdgm%R=xf}5IBogI~L8ZLf%6%nWt?j)KbO_7jT1^E74Lygx7!hHQLIa zOSFMVo?_k1iI}GCP|{y~ieT;ZS5O_KT+Eug-)|AimzWAVp^8k_@i|hjQ_IE6i@@nS zLX~CL>b7uGE`YbN&|;BDz-|!74zR%bm9m;u;SoJzcfgASePdGqkN@&%Vp*(ZdKnax z1Ef`2xXDt(R(6BmA9K$b%60nhpkH0_j$WWpOp`R_d8J3nP5MxHS%rQEPGgMkK|G}ypwZ%yEjj1hPEIVw(4nE z>`g=3rj4=_3MQM4Z^{+xAOFs39&-lRMCjthCx!ewE1v-qJD>JxDx5>4rtTA})9NP4 z3ms%H2V`940h4;3Eru(xcd=J8m1H!8Zvo0aAbFhp=GiRw`!x>j2@kmvu>j4RwspM> z$&JR9ylwwJ4hNUB>a*2id5Emx`_-;XTG=nQ1#YKV&{r1`nHj*I`VX8%S@1e_G+0Xs ze;?2yEY9`pq#0h_ufGeA;ZK}~&diTtR`LeUsz~JU|G9E(f?-*!DZy#pakFV*PcP_+ z*jPVfZPfyv&=6|z1$fN@Bi7GpGrJpb_emYN9*_|5uA{5h4yY(qD?(2}f4%xLo|sZ; zgJyrI-C{oa2I%8?GD@s#ZMD4a>>d^d5}8s7POL)7Up(|E`9ZGU4>fgzr(>YI5Tg@? zs}^C{#v055)rU8!1{+Hm7T0d#^Xb(LPCGSPOW>kuXxQkaTFon&`_)k7!o;{AgHw16T6mAQuX2&>Wma}!(R`V6 z$|CFsbpa>t5Zq1NvW~Fpu#%069l-3ibJV;I8^Cd!jNs{B0f*U*b_isSm9$P^mVetsT6`kd|#vDaVv#jN;r zH-iPBL&uOGN09-5&xo9&RPNE^+5#4au-K>R1TgE-9jvnvOjYZ4{iDm3H^eM1NEd7d zeb68tc6NC-xB37`@7A51B$n?{35PX<>3e`vie#Y^^gw&#o_>Fgv>b%B@4LS(TU7M7*u|Nm=t*Kq3~3ttZ>1ka>pQe=OqYolXT?A90r! zrCqSmS%>f)L;K5ZJVg0qQBq`uLa~3~`<-h1yp@*;MGkHA%puTcBF_W(tXE z9!jg`_*^E5iO+?vJK3jo47C0-=kck{L?j|D9a@QHS0rYeP`PE9FN~scN&k+o{oJw{j!#;6v=*IxCLy({lJ_+H|MJRgdz7Hr9O*c=ht{ ze&YOm3k5V%C9}Yj1Ipb+@-{oXL5XrfO*O!L0?7RY3^i#r&w2z4*m{@LAqlN&(t1Ia zX@OfJM+SCUNKxcARzZd0t)_ognK1WGT6MeH6s=mMvBhw1l|D37fy)sl5p!jxRDsuG z{fukzse^NBA)z?VXzZj4Y;TcI*x^Y!5BjYWo8MH+*^qpEXrmjSi|K0XSFjB}oc2v( z@k*P$cb?ntvvm)cn$$JyrG>x!I;Jz!`dT3CSchdcj7TKdiiJ@MyonS7C+n**fW2!r z)i@gekHO!tV6CG}6;Q)l3moh86(pN^T(=7qJ!-R{8uU)8L-2t~xqJ$K=LIFoiQIq7 zseX#54Z{I3NJ@{Sm_{~Q@3N=G@mx{jckFx5zdw6b&v)N41K&Kdr{-+-b>(r_b%Yvm*w_-o9)*nf}KSGIX z`HO%{88R>fdZKz1zAzAl~^O!M4cTH57u0?9n;196|M~~TQ7}BiYEMt)|)05 z9_-O2K(OXk3trRY5S(FJ(7G4kfoGSo(!?_g_%{KJx2kEP zooYEP9Z02SG3ibJ0l024d5PB5(elTd^+rw~)vT0$20AV_1C0&{{AL=;B2pG3#2X=> z$^p&f4Hi!v;hOzG)ao0^bOt(>IguY!NfGzN`e)JPX{^ioI~`ymTr-1gV&JRTLiTwVrN?xm*qAwyT#W@J@MfSfBn7}R5qxdK2mhq6 z$0oR2N+c2du9AUUMp;jab9lSz#H7P0n%;T~*o+eEJkmt0$TG zc~Fz(P!G!oRB>D+gRE{2odSyVK$r%PjXLXtu|jgR5IR0y%dw#=Wdo;{<-l+jlSQm1 zBvvx$>lCtQDi&k`a6#8YF9Xoc3?wYw{`rHK08aXhxA*FLJs4&EKtF>by43s;%smy$ zdNw(Fr6x;)TvEJ-f z!#Sue$emPOqVzdXd;vQ2*{jFkqDyMBsUf<;3ReSNMP)hJNqGT7L~5 z9RnI>rB3s0ubO`wD;aL7gPZX4(R<@r*@e8EBki2Jf8~;e`9rc9SrUQ~J0tD|=d*!N zuF{nRNx2=FkS2TaWc`S}FuO)i=k3GVCEbqh(Q2_tEOq{z2HY=l7N;3tmCuvW!U+rR zsFg9{3=8&#^a>$U z2PMHxVkhH?;WRd(hj@Oyyd-;Mqe4AuzL?GI?GTh?eaBkGL(K#FE4Nk5SNf8gk9Jzk zAMMp*eWn#UBDs8924<`ihIs?63c5?k@Q|(0Ld1iGF&n>7_pbi#uPeCI%zxR^iSUejc9R@$&$9tA$h8VNhKfH+z<7`R!_fTNxX zzGM$37rp>T^>QV=Tm{ed^IMhP6tv?L?-QmefQKfal!0{ZfnN%_;vy*OXjvrJN~mkX zBt1fZ?YAkBYq?1$1m6*Fq2nd`WG$?JB+4x@ELP!pis-njn%E%E>FNW3-n9q&*D7R+aOIU1eqEzKTon)YZ?&R4d%%$ zBA)gYaG_@#csn4&+6-ok!B~XVrSKD73|SjdCw`v>6V~bAF@7%A{nej9rR~zFOWj6j zjLJ4V0-LDsx=omp6h0vr(-%Wsuk1#mCCiy1)n_{8n2*94v7I&MDzms);dbClrWUI? zlFzKifk9@uG+;HCv0A>BJV}NItY$b)s9<277NtIH{o~a)>GV@h>CbBg`Umaj>7mY( z%XpUcLN$+iK9u!qXI_eGA!7u2kReuuHq86?6V>g3L-F8i5?X5#Z1zE`UDRuhKp724 z=4xmpgzG+{JMr;KJ(Zpkm8VHs$I6~^Yqf-k;u>m_bA^h2na?-mJtNs31J((w=5@L~ z$8)a0PpdOrCAi#!reC1QGv0QL+UcqbPNFJ}^$iR2Z~YqSW*2?>r=S&x&_j#3&`$l& zphN%Yk5}sel8?KKP zC})U2oNR$Fb{Dwp13Dd?4abGL1|&rqlsL-sCuEq5|#-4^&~7rJCr>=aY1=7qF4e<3u? zv@_SkZkKbuXL7dEBMHpoD?om9z(ra5uPDnTy-dk)mKb?TA$hx^u=OUNvxVX(W7#oR0B(kA}#Ti zXn;lJ-d*8UNAz6RjJ&J>V#Qodw;AAbhGVLQ^s}2?_{vL=Ms$VXe#;_k;)zygz?p|@ z=(B`eq2q^4J1-Z3=M+{+wLe<$1aNu^cP-#ac-NrSI-Y(4Sj*$BQ_{w|JQ97M`&~ed zKku(mt0W}u422U=VTZ&gXg0(}> zW-=VHH^JA9!7lCBFBL_YMwQw|M?8Jm_0PoZ~I*E zm*6twAFdGC?+T;o+{`ZOuDdfgxFpvJ=d2@gv~KW=;3YJI-o|t+!qb zd;E)A2Yr7oML@k48{9ltd176*u{?U%L6m1ge{`_mj?K|PBmg$}33oJSuLOQGzXUBySl&?hQfoT9L?*b$=CpqT-C8BH$p*U1$ZVulH&~_q6N(_N z%g)BrJ-3`u;fhoi^HpA<)YNT>$_4$Zvp55Q=vyoZdAKnq<;pE~%$vO|iH$ZMd2 znRqbF56}yg-vi=@>;D8l1n;um;SGw$0yg*yh!9(}nn`%tD9Lgxt13|@n-WRR3hn}S zNEtss9$gnWoYZA}Pd_;*qK`EUh-KzH#s9rN4?fWjLf zpUE|NzgY#;T_N#8KY!*sJjdr3>H@c3vyo+U!(G5(n9g2UWxA4QkR1i5Sa)^n@v~8^ z3ehH)7P8OU$Q|y7yel6o!y-*jLJQ;}vbL@8?E?9^o8YQco+TgYxSVBEY4Kjq^0s8czu=ZmRZzu+N7c()?5%Y>RZ9fN>(z@$k8V?QMPD0 zYy2NSoAXYWT9#fSIOvx)?B5XTJ_sjbXMiW`)@c!@Y9L0P(;N}H2;Jv?y;iC+t)P$5GGcMa4;HHB_rT}SH>@T3wBPHNZPz+_TeS)pY=Jis$Y zG+&#*=-pyd$4lf+`9(0p)2R{|d)_yq=7Xa8TtgWkHt=iCEMrIpGO57;_AhqVfYd=l zmcv5y51!sBXm{sF$*nKa9bt~(7j^eyFa6nV0J8C}63ah1OoyX7prbBn z6iCNcFmoCIkFj2}oUHpMc>= zX)9|lLmn3DqtI!b1W->Ub;`wXu~o3HL)-i#hz7#u+yB-NbK;{@h77XHd@&ojjd%fg z2inX_)pAXTYtC?-dUoWaCQYyJ=bqxwRT47=iV9mC{nMO?ETAvBw36++9&3zHDA~JVimIos8&8v-a>Lr zYDaJv?;V%7$tCF3GU-7pEMiqXoc(tD{}(l^%xZE7;NiZ@ypOsI*sS+<3BYq1U{&^kW!N&Q8L z4Z>+v^=;t0T9MJz`Q)p`pDfB|wJ#RYFz;TcEC;?S6Fhw*pE8kH=+vNAT{8gPl(2_U z=C!q`Ri_xYTW3~sqk>Mc8pIp<)5MdyBbmd`_bS90aE{Ju@A^~FfCehM`eNV%S( zpZbJqm81Rr@P{HDmWJ&vTF8jQW~MsT#p>^aJ9%qJbmMq5#wA)I=8f4zd^ktx)r<}= zBJ(W58Lmh=^xDAsIz1{&c(YX|-lJGW!M|N5uvo?Jrh#ai_Q0(Xq;RVaVgFSFsW>8N zm6D_vV3`ofWChG!AqKV-O`j%>#E(|H9%T4e{&)UD-vK_1hC-x`S?L~maX*|^F751g zEqb&UzUaoPGd}3h8f>aDo)k)_UMlzCONBf2a8Cs0?)D-nT7~{qQU)iPKg>>H6JXf9 z5|$Ys-Ay=Iy0#%_HdiYV95MCBYe1|}b7SAPkJ@ZrbJK(}=9gf=Mb25NrxNeIXT~O*PJw|6J|95)9G-0~ zb?kW>8>L^E1H;-azucpLRxts$LZ`G9xZscCop!>Ug(ju)0QvteJGOeOSp9dnn)T7k z&|M@C2z3FDtP8=ScdEzYbt;!#dP`t4f$j$f*6W2%=0G36GV7*a2D;^!A+twZF0tS% zsk)$^Q!WE)C2AVrbcYQ>2F7-H?E`2ITLn*VJo~b9YP?_@fyGf|#~|-2(1X4S%*={d zt&UX(tpNHRywmdGtzXzSFxjf~UU3cZARbTj?^QZ2ov5@kHbw3j;s)9VuEY? zqy(%Dh;<`>gllT)`_va{1Q2#m5l6i1ykwNCq8WGD3v(x`- zAip0hl*%T2h|g*!yQ_kl%=T@QaV=x#7ALWq`oFuUWUq9|ar%;sV9jq~m6P(Hp?O)8 zwH}RaKKv782hZ%3SHRYKAnt`28}wACo1nV6c){k%y-2lZ+39Azn;q{#_gVzAMGpqn zy`z$M9fy7L1o|o7pBY)B&-2y~4fdRFt6AAPxX1Ehb7TNJwm~^tLGfg32({Hx1r=}L zne@g%{)}-R=#)L^kf@Ma75Eju63;3lcan?{-RiAIvjv!M_I0vAKgL%`k2c_ZInuKd zUrC~Vs8N1~^nk?<`^?q&Oz^)1Per}-A*GhHd&}Lqo|Q0-3|NoJcD(!Rxi?c&^bKWF z7?hJMv0}Ly&v3#y&$mkhRBQcPn5GeF#Ii2pxer1MJL$}IEB{)5o&(r(7Pos%kORJ7 zv$(%R>h*RP4eaQE$Fqm{Tgh(*<0TRg{>y>m@y^cA=CP!Uf@{)Sh~F&0vM)lnSX6(1 zc(Ho|oQ;M(?AEe6n4Ah6mIKWOa%-+(#UnuO4Xsh6vo^!0OimD+ZrF~5n*j#es7BZ= zwQe_DSpjA~b=F~l$pBa@P5Le;?i_YxbHO*WXL`G93E$fc9h*^Mk@X3lZv9k~Y?TWx}%ol-aRd#PG)vm{*tr)&vQ^&?#ey?(Cf8L=7AHlLPR6=3ZuAReO+f%zeH zUzacQIq18O)w(Q3&URUZ7fgN!SwA&=K)hmtqIx&){HOGso8-v{ z(Ld(3{RVr9>CRyD020FHhJB@v0HK3ijgEpBE$YauLX8ufNJ>PR6w;$mO^FmwZYbsaa>x8}&WQkrY5v>6}IZ+$9x(@nimU(iQ&qt)G=PBoDdY(9~VZ+$Gjk2+rG)jycdohsNm+o`eriR_NQ@-vdsnz;r2^ypZSQDL!dn znhqQ)$&a`Gr>F2Y(_4Obtm3nS-!Io_AzO^AT7+0CYh=oZ_Cp2DdRJ5xoW6uin+7mA z4HfSJLS(jy&E)&iBeU7TB5;)J&3jAa6lfcT$HF1jTX3POe1!E_tklTymSwV7zL2Zj zJ>aue^Y!}@YP8FPg0AxGnNpmge+b@yTOZU`G{|lG-|n{&@|gLa2%eIs!%B$xVpG*a zTUovi*mfa{t`Tw!9Frh?qh?}(pY2om94c3`H^gFiLrBgsoz`S9yiYG7M$qJ*fsXP1 zFeh?|6UBovtaPhjrGrS;_2B^8r<1)#5pgX9n)nB_7kI9OFI#vP`PuR+)KjH<<#cwB z-ec8+?9wcRdw|V0q$&9W;ANQIk0Os&AWa1TkN0rRhpI1ypD3zc{foQi+j+l4GHmSR_j{I~;}4xu?E4>ljzBLH9Q>dEnjyw;F8gHCnQsldil@($L7f3s z(6eH>)0T68HZsJdJh|WU2=C2D%QHV6+vFI{mqw_dF)Rd5c%B0CL-jqaMJHN6x*S&v zlwdTtT!{BCl}-M)w;XJ=mJfH zBFbb7yP`{*U&vd4o7*VmO17J8Oz}sxXMzDJWPqj1db;zj(IRZ*s8CsvE zMasDlxRsG(zTUrzrBWs_!CoMkiQTh9Eo<>Rd{H}~ruD&mPJa1fod-A%z$J~cJtXQ1 zx6EX=S);AoF>h8+mwQ~2Ci2sdFo$>8Y|yyF^nR0zLVW~>vN4quB@ z>D4P`1d1cx!_WDitZcGu$RP>M-w#Y=JN};(-70-xxKFGyD}crJE|&C*QLSF}mL;EAR=%@)aG1(sL$7*De~735Cw z=>VS(QsEm%=88obbJT{iSFspEv(i^sgs(Qhq$f~&|w^R+;@ON#Ie6PuPM zB;3u|ZB&8FQtgy8kqy~C4G!6j71FIQ;DK{+?VE7ce}%_!?<6u#q0(YGF|;Vn5qg?1 zHHx!Ijeg{gkvdM9$I99MQlw;u{M$uCU1LDJSpHTW!@BChBGVGwOELua?LrPWa0PRt zIDe3-tt|>CLIZ}O=N%p21^dL=iGs-qOa`5Rc7H?UyAo*C!5j4I=ktA3L)+}?nVP{n zexNx}MGp|^^BG#87G0?0&1UKLaPOpRMR(9EL=trapH9KCqu2mMeu7ohtvYaOxh>UD zU%WPk+)hF6;DYBpkt1oAfp48^p(6T0nYf?S8D>4 zQjMg2nYC_#1|Q>!apbzynjMd4(Q5bbPV)3Z=%X4*F;8!EaYA2V?qCh9Vgws&343Yh z%0#r8d0vJjP1fAhi!LALjfGtO8^6;(>g(x?yIACDwNcY0K|7!iQML&?;H|&|obTV_6fQ@h@?FqW=4Yd>!BAPYiQ7KfQwu zhDC3*0qjaxk@c6~$`lBzqmPCT`pI>9H+;vXO14js`SKSiwM%w!nrOu8-U{cx8Q6@Q zd|eF=+t`J5t!vU}(I=&1oe2088niAbHEs=j^gJX|fTSHjhrcGj^M7-l5hlh%!H2>w zvR;arZ$~5&S%-H{e&)A(%K~bK(&B})mRo|Ch3&^)fP7>Z?!S*@V;J*R3}kL2w}Z+&PF zi>lJ&0y;8ZZ3pL!A>eHjV$4eazizlk>wZ4pp!qtDU%~ptg?bO)fd#HPDBGgw%mMvV ziw@`I>ux2k?%$ON>r7|w^N}^zkww+V@58>!{UKQ4H|S1x8{MdS9Z`5VF$o{(2~bBP zl-CJeXS3$(;f-l+K#N#Ul>MyH&Pta^nSSR-{W(a$_c?F$=q&9;YM2GCiZv^AK~*r$2nrrGJUP;)mY?PqnE^-t^5LhPKCpTe#~-f31s3DiZGbmR=V1k%a< z7LzwyxSF?&OCS2TLoDmFSjhs{E-c|zbbv+a7lM&)f4XGBrIt~>O%maa>Zlee()n01 z5j=nNi4H#G?!8L*oaO!?bAAojM?F8_OU_otd(`18>p#y$%7(RCXx1I+~abb58`aS%lRm2`Y^KJ4m zS6lZ-JB@Y#wk4(9+ohKRBP1k{u=$4- z#6^J|H|CU8#{@6X}%_a zc?$hIm|35X-Wen+ZWCMA2bQOOBlP?3*BKYW|MBgY@!bu~{oRtJDi8kqbB zpUEDu_TTOuWa7)pfbJ0GDb%4#L}HYlTAF-I19VfO#ZbA;v9|iEPPLex;Frq8 zLR}>0K3Vv=HCsRU@np&B=l;yf!q!pg%vxxD0-oV+sO0SEuD%%eB%=r_#oC}U)wUuh! z5#T+kQLL(s>#sp78o+7{8%A;+fe~LH5En`O4kll3c0&HE7i{d0#&euH|2= za_r!LgT2YR#XxwQTA$_`x=?PC%cK(SvdoZJ-jb-ds#V)r=89!i^&?fS!&W12&Cvor zA&*(C&qS5X_sZ_-)a<)>-jN3PSQp%#a8(R`J95XN`J>#GYEtzgC}$fOI78y_SG){# za)_(n3hqpkV=JItBAgNS=zmv7yhZ8P!Fvs!3{$ZT+*v@l94u`V>)jCpy;v2E2P#ndfVe=h=5a1<;2c#4bl35z$_`1C2pDeF^{luJJya$?)*9@r z9prpxvb$sTN)6aX(bxiM{zD> z^>|gGoFOq!-5B3eSEv=xIGuW!gWwhTOEKJX4-%6q5xh7Z(BZARNva*wgOLW7rgCvOnRW|80-&b&&+Kx(F}aYv$Y9^rEr zY+)@PDeUNxYDcCMx|J0@^o&dQXpW>;kp&4z| zrSb9uI4y_oS1GYNB-=ln<(=-8EEz{rCh1#Zy&hKs%ZIqWpL22+5}*mY>NNZ`^t;gk zbY`~3;V;kOJohxuI@#9g&3-ri04GBW?@69M3FID;9X#~|T^~%bA37~^MXw$btIL`z z6X>#OsD?8Df0a0VDSO%uJ$!Z;dU#-X%dqa^`alc7Zg*f&JWkf^$0{r{+{auK%@yWz zs^dJqM$`2Qo)eFjSgY>^tGPGiG&WO@aYOh^m>$P-vUQNXG$2*z2weR;7m-n|ftI#O z1bKeFSiM_2_CTsOqQgd@k!i7LJ3Xp&0>1_Qp42L`t^SO8MZjZ%tKOFjHBp9TM5Yz* zj^rqF6l5k?kCC;kQ#8gg(N(MiW}}VGNpp9SO#9<_ zUlX=PH!|Q!c%fI`@X<(tv)JVaYW-TGp|}*eO+)D4I_sq)hjwER{17QTKcHI4%?PHQ zb)!%7BQ8f;brZW>%X1s#F(|*BcNu2-WNvUD{+CDjCImu6cZjqTXO>yIPs_0GceBe+ zz)>Uo*)PXP3tkpFJh1ad{?Btr2XoyD zgfit6@lb^4w1uBqhxGw4(9LNS3(Dmn1=@IPAut+|0$D&bbC6Fuz+#!pCy$Z&0qnI; zmvLvWlIhFt3!r3+T$E`&72OSFO}!>h@OdJXa6RkTsNVyA??ZRzNh+AeM~*(QSlNht z;VzdBIs;`vQ#ESy>^_C&<~a1a>wtQ8$ZGV?r(o5*dSo);hpSTS|FTbB;f(#YtA?UO zVDcKct(%KO~#5tfr6;SQyZT zWdd2uG=j9tLL$x~HjcUFeuRhHy`LxKg(x70jA_z@Yf>5$PFgY<1&x3-q z*;yaw+X-#q|6#dT8j*U|>Glmx1@miMB{W38EB`sTONT}dKo!4s>8#~m@-+taLU?}w zIGB$y7OdpBcDS}fF6U%!RG9~zSvFcFa42Lg=A*9C)yRyiwOt?QeFyvjpgzI<)b62~ zdX$O`)^nxwOCE2b8eJ_yyM${7wMWg9jYX;L&}5tBN*B*C>39%(3%(R$S(@#xkt#_g z^KY+MrjT{D*vFeE)iSQvC=pt{!$owZe^#^fzk(K?HK|oZOJ@V81?aU#?wiZbho};? zshMlMK*3HI>3oSMhV>!Y#l3QBaIrR_5zBz)->`+#q@A_p3H3v2KCB#UE72aL)BUaq zNF9wV-Kq6%71HZ|B;$MjU+yK>>!0qcDfxYcM5zK!9PB;~DQW@a$57@oc3NZhMb(1@Q zWTI~ayWB5vz{XC17KxsQ{+RcuCjid@w4Pa{5g~t&@6D#zz;9i8et_=_>M4{{xuQjH z@%ZeBV2ndgd2+2r%dyHl3hH>)`ZD>GzZfdQ3#<7|Hn$jmB3yrdkN|JuEk>d^R_CFM zMl5A!C&=acQ(uitcmW)~f$cnserc34uC!@yAGuO#WDk$1_2|fi&PISby9f7AMiMlK z_t%2QUwUeKu-r<;^86HfwYW*So(<)7qTv$4rAUln?tdAsw@jl+WK%V8viy=7WY!T- zw#A38!@3v{=n!b;NDo&Gvli9i9hxq;37fr0f*bsi;QnW>L%#AzeOILSviA?QPPZz3 zUv!OHoXY;*N zz?GOSPqAM7dwKUlFhg!C^ni&P_{dBASG0?4n{yqR>TYLe&f%$qGUaKkZ-4-;`w3|y*q=zf2p@|gzf;~S7 z4yvK3e0dp)Uj&_5&ty6#sZBoW!uIWxUR|ma`mH}oGmz)Z;s6J+?6m@UzFn`aJ`pcT zdN_-o7qz+pIaw|@`f`0YLVg-}sP|+WgNqKRlU|HORcGj=>;WUOnhJJ40-hF~>j3Lk z4U>)hoCG=-<2CL_!t5bVv)WfNU$F{q8W4;4HUTxWYdroZtIKGCh6=g2i}!s34M)Rm z+c~W``8HVM`sF2_Wm8;6be|jNy=}5iY&AasXDeMTd}uYbeL|)aTxI>4$$5d}sY39B z*k#Nu)egOYPB{Z|9(2&JZTdFj7+Nrh)^VH}n(zoV8c$JgkZ~sHF!k zBV$Tz4#`gX{Y-QBS-t^GByx(d4yk6TSx*P%HUNKH(K$+_hZRp@?>5m{h`1%|nTOrM z#67LYdvhIdu)fdBr2*|nUWGedFK5ql$%xCR*X}4@xn;xvW0>@Q9+24##Aa$cP`}wX zU>gm}1e{YVrqMl5YDP1o@wJ|{jDhiDbXGe4G_q;ID-}z;E0H~>f+LHPnU^201{$E- z-=;P}mnwR8K@GV~p?5oocdg>-85)aBvW^Hg3x<;5XpqZYwmiw2H^F~hcm$~P73zT{8;@TzIr!8u zOQqbTOjnQ+p3p|D|5NappjANrGia$?6R}8Y^gdSH#_#p)#cErq;3t~=qp%n4Q^;<~ zz6ZZm@&#EM*+Cb)qR_!x)TfWaJ*$wSXcPUFQ=KhaWx0Quoos{dEU(To7tdh7X*wX^ zgfDaINMMyq;Kxcv^2F|OD{Ovhq~CIzyc z&GNwl(l=RClrp&qS~L4-rL@U*IRkl}gMBsMZ<7(C7|B4UU6;8Lc?`UTNLbJFQY8iH zW}WA(Q_K|WgNwoOsGg{wbL~oO;hlm{UB{8*8ytQK8HTT~3AgGaK9@CJE@(lqo~G^Q z`QqJcp=0X?eJt3y#+j#M4>E79)aa*jflKjkfW?b+5O2;YL@X?iApza>s8}_1m0Cxo zRw;q*h#SfAcy>$F^19lv7-sq!u(Xrwm`w&82Xzy4Pe*e7qbp*6)&b3?IKQuVfmNH! zFjo(`L*BB)3Xv%(V)=aaV8eQ~Fbxb$&?y=#hY4joHN?8I*uyR$btU`iCGTz~@EMRd z{Q!Pr>m1OGpLY@(v#AsXtkH5mE!NTl{<^S5b`Yhod60d)X9tw}0i0|zth<3o6IaiH z#;(K0D}e`y#3Mag*zqJ(vj^=mE-~!a^4&Yp9cJTvPUaK2$w0rJYi(vj9#8#CU>#LU z&=i^MxR?`vA=i2S-L81E;fw-F7OQjL?U9Olt9BCw-KCcO`XQ84CR8yU#=CX;u{iOR z7>Dd3ZHl1}?OJO>hIdwjR{i${>`R#TvRK8Yp)n757OFxWRvb9_rOIpyCp= zL2^JO9&9R-*CG{3JRyt8E$LW(mEgCDsg9@XgCX^-@*8SjOW6H;tb*K0x(hCo>s&SK z8;)Fy#{Fr~LH=j0G-?r6E3=w8)9-Rscns^QRI$EZ>oiL;pfk(YZXxTI>}P#HI8(N( z$`9Z}EClfPteE#A1Db5$w6cn5Yq^>(&cjwPk4dd=(NdW}qStFKb4hHmHKP8=>xg`a9C#dR-Yiv;>(ba)PC%s>_YG(}5!XTZDpMA)o+M$G z4H9$=nz;UHZJgjTKp?9ocr^ zhv=8@SDQgm&D5G=`9q{l^`}7jUyw)!{>umPSd8N|spbjI{+BL_$j8}m!Vf)D z?9nb3)uflKYyyTvP4y6~dIioNlS4euBSrUtIVbqwk)u{gZRfv(=$T3R3K>y^$7vdR z+6PpvV;PyJ@_&(vDT(GxWw|9xV$(925~jm0(V?Q6P~2lgwV!maKR!d@m<~O_64he-7}t^Q`q}<*1=m2 z7p2I0$<%9*m#e|ls47n>(L#Ne+Rc-(yf&i2MtvzZNt~>77LT|}t;)#e22Q&F%Txc# z-=cJ+ljTy!YBsQIPN(ehNv+Uy`3gGw3OUuvI}b^+{1mFOt|itNYKwja<&Pb{f|_=o zL02K*Hz~W-dUo49C%Wdc%3`F`vGOJ75Q_+%A~(u|c&KCPWLlt4aGr0_a_D9bS#C+} zs)$$~^>&)W=d1N8{G%PL&Srf!YOj(l!#=WzweEp#$`nro*szKNqMcgJE-e1tDy*6B z-$(ZJVkNI)Kh|e@6g!tHay>RQAKeykTg}rF!N_jzv3#_6xGc_<`w$JX4ykv6-@-{~ zKIezMa6zN)<&@G3^;e?3&ce6Z4i~MES|(|>L8pD-crRxR&wE0CqK@9#6qRZu^01U3 zJtu&VB0XA^TV6=>No0U0^#7OXGAO*~Wa0I^64}r;~ zei20A#GP>DAlR@8=mv%PdYLW`pg{QSb{TY>Dmk_0;?F%z(zx?hBgV-pjVrB>w~aCS2IH@`)-s#?`Z_ zpB_*sqg6+x3e<=dS_$$ET-{0PpB4fb$NHW_j6xpK9uza%S+j*RlGe;@g@(C6qp zD6>oU=ojpHFW)%ebdM&oi*qEOJ*`87m?d~NPf{v_yYX{W4oRWt^@C9|AX%(&HNIYWRFYt+zzKTuv;l zi4%Ph{-^tyg)<3lbpg}YeVp1kDw=xICa_k8jc%27buL{@U>^PucojX9KS~_S-I(vx7GGAUmd-SO(-`{RJ@% zX27sc=ib9JJ4*y=ru9iE8JW4Zy9=?^LTdsL@PG zrH^*A427@hLw+V}{<&X|d^#FgTZcdNdoD>bBq`Xfaa>g;S=y;jyV*o<@%IFD&Cw0+ z3+R5iY~oF+`lhTV8(XH6ndCDE`zSP79<6n}jad?oF+b zX67uc^-1jIW}dwrE0}K1{)vFIwJ(thtz@o)%{Q5^lTd9o=Zi{cG*-+4c^b^svy(RI zl|5CrYb%^osDBQa%t2p`SILJth}_3dF6SQJ#a<}2O;5pd)}kjOBbx>95_XjJw;qC) zs+kzk0*~ALs7lrrA%FWeDdn069eyk6RLcdm?C?J1F*!Np_cmcSbr5|y2LFmxKH52& z*bNcxLVN);9g>arO~OT^XuADK=vrYuF0^U$X~{vhPtkJdI&@%#kSBrUKExGRtmwb5 zbQ-Oa2V_&2T1Tvr{o6DHI^IBA6L|FtB#v%dh1wSuYLfoer$c?c@Cx~0&`6x@!QL50 zE4}JQbRl?9a>5&lz0qYGx#7`2pP@@fkqdpC;tF{~InX-Jza$^n(EwiS;mvVYPUlRq*>!DDUOy6}4oH|yQ=?PB0Ik5MWl%zk0vpzQKwigwxE+X@ z|G_Gp7b6LGu=7L6oc(IqB|G8qNlq%(OK6!ZhNouBo#-#?>-ZWgrZ)(?vs}?3@-bic z7lGyV@JKt@%a8&nA>#hW(Z4Y;I2J;I7l7jgy_Y%$rf~q{-TE0i;0s+Z^p(}4;e&i& z_ME=zN{OT|#>yT-!p!5HmamnJsAsKOWY+dH$>jC@ zTJDp8sP!0_(9?lrgV+hNgPrzp?M&$L0WkVMz8#rCfn=z3XS2g9_iul|9^SYnaHVxD7|{~J zy9;!|6~DzurKAJ-a`tC_r#RO8or{;d@jOif!5%2*CfUiS)9@~xvgC2rKP;SCH5#~F z;O=2no5gx!-HV1W9sWB1UM$&2mc4F!6xN;2_1SKRlw(txJ+vO0ox<{aSlfb-JHNxO z`b$tMoQarNS%JsuW?(%GEk$Hf8`U`WX6%A-o?9YsLZw$oKi^e(s}Jdrvw-eO+0VW# z>!?eYBhd!H(FLwhYSp_F%>Rc>@C#gKW#Xsj$BG{GgY3*f& zS+d{d$Pz7ss;iMpE8*jO-;H;$2MA>87@Bw|tKBV+KwF$drAVrvEV@`qCm6m&trBBG zn`MWL!Gn+MsH{0Gd(DgfjC_TSVE)E@Y_u<-0P7(-uKR#%7oSec47?tFlEj@>&6W+f ztn(HVv4|m6F>;}h5umA(cgr%KQN=05>i=#5t5x_!>^#yc*8sUexmU-b%S<(k5=|hJ zGQ!!W9s1g*)*acRH-oxNQ^3MBIjCvS%Ct<8N75mykzL<|6L;ym+)<#{0S~MaSqh!y zu%=$DM#YXVl`cNLO1@-;do(J zNXN+BU_hSc%#x>-_`%N9NvcenSh>aQDxvp&`B^ZU;;|Bx&S)bC{>!!(O8XOp~}d*E2CX*oj8pT1ms#dOxitlw(q zR;pD6RvCqi{oh+N!Wj)(9YBZ-IY6pl3qhoaeSCZ)uo*oD4LrPagUCT=L z3Nu@=Q{Y}TU&Mg+VgFfniRD0Lb45G5>QQ13;N3b_S#F`-qF)p4S-xB=D|G{MAmm&B^p^8JAPZ?M5JW`6^J5|t{UUn7O}0QF96;g-uWW+HYT5_&Hye| zS`V&lGIk#|Q5I4E1j$(qO9+_}$1)<6_nw-3q||qz>cT zF9eq4_2^HCq0EwAnUZ{6%u~mK@(?>ilW>OK3!k4Ug zFZVN*ft6dP`55%J18UmG4$hSxs8Wb69mCU3P&`Dy`4*_XlWI2NKyX|J^o&h`yksH} zl)X=x?S!A6`7|<(jc*;jtjn6&{j6PPA?punDc{y{hN{qFzTFMInjN*peIVmNag%!= zTkvJQ18U7tk5n3$pZO2a)hS|{woiT=TeV+`zgz(IZ1cB2hc=DqM#*qf~8|)>YSsAIWSa z(p4^9{^tMf%Y&0$oGNgL@(uC`*vaQCOyn|B4vc;ta3+>jSn%JMX0e<9R-@CT zABsiRw?&-_%vYiZv+1+kh4kElr>dRo&{v=on{qzSr^+5OA$v5QlS3W;zJtME{dxL= z{Kc0bi#+?hS*~VhR5>qJ&YmzqLwHwZ;ougEy;w%Dj`$S(3)fmrD3z>;P8 z#0&E>fyjDbSAtz{7Gk=j$t!C4Ew#{amkj7`AlV_}`jLh^fG9N;l1fL@Hn1nq7u`pI z4)w^yG+&S}v_q=Un@t*uouqT|f3*r-Sfqth<@s_K5O0FNKjkDqwH+KmH5MFWn%wf$ zpJnf4xo9)qnGMicp1vuTYta@k8QbsCc*Bjha`ey8# zO4)^NP{p&YZ%7P~$wV#<>Nc*}A)KTn*T0Eu8fNEvfGM+7cu$s0`0c=8MCb6$QPBAo zcNP+83TV<5h&`!9$xiS?)(QNwAKXs^MXLk4&?RaIzCQYv@?3iQf@SL*a-7&~gCbeW zReOaABkXHPD*4uWQBG+awAak)EIZ-@WLOuwr^~ilPpqr;V>KQ76yLVu$;W35XS{@G z-1;-6K@DxP9zOs*nbhXzybbPKwNK2;(Fc7U$LG{$YL8r`lgLauFUkMD<|Kb`_IiS&Ii(xoZD>nZuI zpO0O5$eCqWs2{MOJnVZqTERJ{Ggh-Yi#_g^hoo6EfHWCeV!5X#3!mW0MM#ZNt}27V z(&bhBR`gDV`)Y;rwhjd~tSwD5wTM&Etu6yfy424SDwUzS&s{8XfJ_R>B~s9!U%-#M z^*+3R$6yD%%Jg>Yk-He|P+=-w6MQY1Be!#&=!Ulb;!akprHN>U*19#4fLHAWD8E&f zv4?1+biB?*Gp%x;klR6TaBQJ$SB-_A>AsDM^>^ufvCOGz#UU$H6+@i|!==RjtEEEC z!tX<)H>ph?P32rcMxroblrA2aml0_imE+kYAIoiRZox z$DI~#Cl0op4$#M|%||=Zc_0!&8l-Vf2-Rkg#VMW6lqbRS-JFi7Na0D{*ybIQ!aHn6 z!ZdU|2^Z8ulU8@f$pkvvua=cWH&E$S%Shjf)Uhs8^YQeMw^5zXn^$TK_u$PynkGxT zI}co0eshP+WaU&o^5kyag@!2QnHe$+bYA0mW9S#_o7N5m-2ormgGFm~e`#D*C!2h; zKTCVT(1~!%4*01ENs%jCwOU5lUk`gJR*SLhM}oBi$^HCpedPM##A;U1%k(_zbD;Kp zWFXViQKq=>TX&q6a(6!TI_bzM){9tWFY(q6nHNs#lw0RZ@oLBDW_^|O&wd|hq9Sdy zMFx0gw49-XNJ@I*0l|McvmL>Yv?9AKg;h_pqjs^pia{;l{~XcKsvYW)Ru;Fl9?+ROLB5cAKAQAx-aky{%5VL*eiX_Z zVouO0^mF+!(4>Z%^X1jh>BF)LS@||ISX02~tMKg{G++dJ|Fd5YUiJ|u!*eZNK_Pgz zy2Cwi_y9U^KhRqXjhrhJaD{b7wF!U+k(ak|M>lkDS?-lWOi~_aJ)a;m2E~WTGQxR( z7uPL8`ckp&J2;&cLfZq_JKgX$o>|^#+1Plu$lq+zEO{I3{#dJZ9EtK5Hx)#nE$cuq z$mwggx0#;nd0)T$gsvr=*n$|Sr&}IUCZ7S-kHoT`re!TKwX;AIYrz)d`Vr2-#q0np zMh_s1%+oO~H?x~&y_3!yW8k#}xSIEHh;Gqi62o^}Bm+!sbmS}Y8}(B<8*YnZ2V^XO zQ|qgr1ZN&EUBoW`IH)R`I{roj_NLTexbz`>oHHY}W|I{Mj#&m*hrl1*~hI zuMcQDbV4_EAn}8Mer3)&syE{=D&VP&x(HjV9SuM?W8j4B)a#ttmw$Iw6-4e8e9TD_ z>Ynrqpz;|&(9V_)dQGD?VF2ykrIrtwg(R_h6zk*AD$nwNt=xnCV%cpY?DuOZcS6a^ zz%EG8bKwpz+wmC0iB;O(kH7w2IKXNSilLqf>D7nW`=ETRXNt`y`46x91(-e;iG7x~ z!$%vz4&4cW;|y28I`88d6LOm035EaDy)9{Yh6h<6J;yX1DE=~Xhm;`26s$+{_NV+z z?(KKWB+cZA26{d`lN^-mHte7V-ZzN+Y2l6$*7g*8=*Kfyqt*pz4*p&1J4L0N{wcJ+ zPgaK<=ou2xz49;>LpE70lZy54+$i#THh+h~Y%?0lx*OCGeeLoQeoB>Rq0`T>h8oqn z&(?&Y(leTqL>AOy-Vf*i3!BPiU_S+f%Jh1uaz9#VP%5Mi==TEgQqBove8@CElLG{V zdf>8R-^Mj~0`zrv9nWtLlyjkF8KiONR=rPCc*=xOK@Ywai}xiV_lk2_KE5LB_JijN zs!JF3_0Y>kZDJ)$xtFf#$c(pGHGUZ2R?HjQp!9Wc1#t!4EpyR)1MuHB{%R)j<`RQv zuA7XJ%T|vJToi1TY~2`6fW@07Ro>AmEP(5@j&*;etC73PRpeNMJ!lw+(RB)VFfT>U zmoyK0BsRP@{I@?8-mT{f-Qau-?`wpovY_~9kanK|xeBC>oln;aeMW&HHJ^Msu9gj7 zAWyQJ30H`|CQlc7|L=e*CY|;n6jR`dpi6VqmN19iv`A%8Bg>(vF;;f8rxzQ$so~@? z3M4wAjGM$d6MqWywje1QSOZxP;I=}k%g2H+J2C?aYSp{L5+a+YSjPY~8U6oBItw_z zt}2gHqfT3BX@R-{6)MU9ow`zYDxvO9mAb_l#UsaAZ!zX|JXGy0L|hqTMBn4ZO&r}vO&=x_1K>=-@JF45ZPf8tI$ z548f(EJhe0dbFI?)+&{#paQIXvTL!+JrlRuv&jA0kY`xomC5=(i@e`OCDdHOEuaU9 zHOU^gLIFSHL>otX|1{ZUPOp8ev<$hD&u*~?Kz+pFvwy58_~*v$RA`4t{0PgAWs6SH zNTVq@-7A*mVS67$R*Kbme9G6X9SC&uTZ?W*pSsk(%Da9(eGeM`NW)Ry>>@Jauhrq< zPP$2c-}2#)LX~M5e>(Z?N%TtUt#kYI>Yv-K4k+RA<96$n!uxn?2|VFcJZ`zR0ouMk z{5HOxH?Ovfprs9H2)#O)`RB)Tfmk88=eOD2x3Svfa>op|+SVEsV04~UvF@w&kcjo= zO>9cmU`HUI7xGRv?+)wQ;4#_fL-R?v8A~DeG-!Z3#f#LO&IJOS<(W;zcBM8kipTX_ zYXz$0C%>6aISd&ff*zWIV8;Z zs(C((XHF8&$CG#PruP|W*9h>P1ezXQF-UBPR}cQw9%6LjaI}{ddHnh3(K^QZWrB`P zQ5`3L2ef)1BjzyfC?nG8Rs-;S8X2asp=kn}F>v3;iSJfy?iARrz2p2zaPN${MXt`4 z>Yz|%1H>yN@s%tB7AbIhH<(Y?v8=CzwHM=8cnlg`t`Xq8Gh7YKx9A11m;~GOxj||S zPz4WfoUlo&GL%N6Krz*b3IN*PV`mWS-3$6{)IsN_UV8?*r8}JwJ*~%>u?C8*vn^_c zdwvmbg39J`GbvY4UbWR+UMUNh`YXlt2LLZAIEBG&>ej;L4ErUVw_`}X1WH+-c%K+!7& z=U66^>2yZpt_ANzS_0O+I&X*lWcGsSl7v^rZBZJ3+-ifxLTlxHinoK^Aw36VSHKr& z^z5VuiB0}Lv~iy{YSLgaylC`RfZFoul(jXxYu-Y$9oT|BPK+%kXXDOQ-2Vccm?l# z8?!$pJl8pm72U-*o3(1zsD)4(ZcJjug&K&0L`$m=)kMsk36*Zq-y*M_*oa-X#NJd= zp2J(o|BLl4TN33&Rlrl`p0H5Z8i*J^9j2$znde_W|f z_UOKR@8MmQ*o)<{nkOdIp=tCbI>kZHxzPIptI)T>{bh{Z4$j&*4lV414)=|6 z-^Qw{{R~?c{(~gkxJ$#(Fn2+_lQ&DCz#Cba*VTx@KmqSu0WK?mk#|_RPK%Hg>x@Rp%F^>BP5@d|+*&tpv$QMX+NuBrA9mb<{wLvnfAghj)(1gc}&MOFw6Z8p!D z$-w^Bsdv~_CVOcYT};_uACZ$gsJD80C}89&-aU|${ijh933?hgIjWn&k5forvYm|F zQf67fj4AOpr9&BR@2=Gv_M3{V@V+%C=vMBaet##L050#epUpa0&f5>E9bA>do4q!N zNd8;cx& zFFrA^%IS32sv2?k!{+Px1igy!jZw86+^?5g;E0&y7TBk??5tVyxYfG2c&95%~e+Vo9Hf z(y)_@9?I5{oCiMdO}KSvGyCn}mbA_qp*cLv(envnvF$M_%3`mnQKgPx^@}8+NPQCI z#k8gJmd63m9SmHLLpj)NxaYYpH3{833C&LP@0nJJ7UH!yj+*^AtMDkFG2KG8bG6Q9 zzICjFh$y6Bc1(5=vfx4UN~7LOdb#!UbH7##T@>gy>CyN&&a)?Vxottl9uEb2d_uit z>y)q@NzrUiVEH;ujk?s@V)sE@!hQ5L=&c$nR1w%Xgd5cYUCX+cK+DvB;I2kr2IH*d z|5~&ehQM?3{Dbna8_@W#PArf z&K~CNW>o>mdwtdS*sa&OLZ-(q_XmgxzgUm)?Tj_>rc1SUY~~&8{d7j8>zQge-)|sR z%j@%xvOXe5<$7f;-}ge>Za1A_2VT`@85~Dm4WAcap~*yM_mbgDCLL$iHokXD`l+mS znYo9h-clnnE}>i3mN$Xz6rTS8o%%4myRDYPGpjkNj<*8N=%5TZ=IiWiy?qBi*Z;(e z@D7Um+nez0D-oGyP}Oha12hmkOV)AbIWnHNu~l~ihdz9~%QRx6td}|#HpPu%2mF!; z{>G39&04w-m+er*1afA|=xv18E7jitR#a?;hWvg!r7q}u3zW!s%s|v3vyfYE=vGiy zY>SaLCk3+B8FfFKmS-c6@;MQJIzZL%&L#F*d`d8OwVtP51#VuAdrhGB81rsM;~{&{ zE)CCvFP9V>?HzT3|9VzWrvbRO1u0af3lhi-CCxjhy$u%>N10K&Rah>#Y1P4u->nj? z`x~{v&JJ^wud|Tt=~1)lL@ov^`;VwQ+Mwg25ojA~4;y*sEvV>x#$C((l1Kn{R-ed0 z510ei{+1xd9KKv;E78Qx)4!wo#1K&zn~?QmoOWBm2f3I~#D(Za*-=9Ad=9+yW+F7HdNqNsGkAFaK1TyrZgirgxP`za# z%^XMPp-c2*yQR*wd926?QBal05@K{14SNRJXH?=~j74a?Zlx!y)F#oG^X&d;9ZyZ$ zFtYl2&VSD+%;)}b2w4N3-GY^hc?UT4@XSJ^1;7QrJQfX)8L#8BGS2D_=68MZXW-ummu`u?D)9t5*C4l<)z)dFb|RxUqm?#mzh^k3$O5-uung*|@!D zrIy1u??*eL^{i+m)Kp>O9qNJN4|31P3UmtRG53co1s9Xh1Rge4^aFnK+qdf<)An%~ z!8+OpSJD3gzAA|h3F%?R=E}QVd!=XUO~3`Kpo87a+hS9E=F!-z;SA@dA?W7~AW#7n zIA4t-3q2ZR2EBVySeJO`JU;QtNQZM47XhnXKy(o3{WWSeeAM`Eu?X3yU2P?DIuHE( zzepdqj`c@}f#*(SNDCCY1CAeoRuAHTkMF!bN@IOPXxLX6nj=)^aVZz@Tm_tYqRzE~ z5bIfd4BmsntL$vHg}|zZF{?#QWvIIXnT5uQRnhA_yopr!OH`+tM3E+<^$B_pMo;p_ zxE>A5;KW5*%`P|Cy~wOmFuO`O>wQjHa!%mXyO4>O*&?-KkHSx^6wyZ#*ye-lY3NX3 z!Zkx`PqEL>h#wlUXJ;eR}y=@o3i+jQ(zPNnLwHY{Q z2#v9W$c<_{I{pH%|2~*=>sy;u;j!(9-wLfBnb%}bz!~|${rTsBpOVCV@X)pLPK}#+ z;(g@(Tg-a`F!0+zvz}(85@>Fxoq~iqnse1-u=B`Oc}y$d(>vv76cx;%&<(ap^Y}?d zq9uzUa-|9d_x9(L8gV1#S}bc+7>80j*-eL`>1dPaaL&wc zK&fv>-j(@XZr}r~<~;Sn`|fZ5XOyj7jN_H7YVFg|3iOj;=no09eW2v6k=J}&Z&b^& zOr-HYfsn`6#)c0*c=ZskPga87dk8B#%4$URH(KjvvR_KI!ZMM>WJ^I^6X3HMcn(8_ zRHSBoP24(bY&8-Whym?JMjp3GQ=hJaYF@I3vBs8!H{d!r(=La9N_A_r3p?Av z+~y~_#g~FrDjqOOM^p^GmUGSz=mia7Wo-m%A$l9??652#H;IIt=5K0mB0chvz#Y)Y z8f)kCbCEzRL?jz~+8}{bdN=UfHfB$Y{u!RtBxjG?ZN7?Z`~lGO${cxij6P4+gYhjQ z|3v(R1oqpr_hDL(e0oTqg+hIk@7%kPGP@e9*RbA%<2IAI=F)BnyY&4|s}sCiHxaM8w82i%IOnC;xIV-BRUAK=@VbOQ;OTRsOr+BgwBu2TUIlbQf2nCwu2mX& z$&BlDvPOl4mG7}t@g%W%?0*xxO$VZ6|3&@4x8Z~TVdYS49t8{w;EhE%K z&#~W5q7Tq8Ijj9F*a6Rd5ib2rSO%9D0g(?ifQH$@7F4uwzV|L29nj4C?DL7R#10L)Nbqzbj|+5J!hI@7 zB|eMp<9trRJ3YWoO5zuR#bVV$*EcGY)y~#hyE6PT>JLlth+BD{ zEBxWi-DO@aY9-LALelm^8H=I}&gNtBC4E`N(Ah?0hW9j^QXAH&0Jq)G+0)A@GuxKg z&qF%#EA;2V68W~(!f&Zzlnyn+(ARUE4_Wq_b#dBxBw4%Y4-t)_4Lbdmp}{QRL~XE4 zL;J8IjbSb1e9)Yb&0V!g4+GnA?x$VoU0aZn9lX~Y{VCL#R|nqBdh(r?kR4;lp}AQ4 z8`(FuLm;^uoQ=V+9gH-?`uAWvq6Y>raEZDLYKLEKhQ066AfN6*id8a>%UQe>`q+wX z8&A^T30=3y28rhyWw+cyxHnzT{3+xFYzbRRS`j#DtZw zGpY(Gi*}To-f2xUX@^~t@g{+Bp^6j*415TbNJ7S(k zKh4h1WS<@vyW9L!XpOemur;AMHgan$Axm&8yx(pi!d}K`UD$}dGDo#}zI%>c2_JM= znboqACD>>)t->W8IA}?n5bA-LR~(~T1k@GS6STy?xi(=$cz}ZyvF9(kUEv)sfBU*GkfpqcAv_D23v39hm5eUT|!ds0>5AQNtBOZ#KSRF)btqHO9L|s+@O?$o3PN>8z53jbLayQHMM(^ZYri;;<$QZSo zwKs84A_G{`CkwuE40f>QBNCO-50R^L!LDb%+{!Abxz1g+(WW?0l2JaVV_SC@NHvt7dX%5DbLGm(kwW3 zJ-3dV`S**A?6&n{C~!EcwWZACwGq7P%4Kl+^Qwt1g3_@2^4n^4+0l5Nx^)nmM$yII zW9)U{w2nEFV0Thk`{<>cdA)L4wUNOhDr%!+xK#D{Q@0&-;>X_$WcJ`s_q)op9l-9J z*kM6D1T{xC2?da?$Ib3IyW5UUc=h8u&s;qe`R)f*N#WKkuHdTXazj=4ILGE?Z!K~QE(~};}B^< zcp(Z#x0FPVeIx2&S8eD30Y3aosO4Tkh8w!(0gO6@x0i0i;cR?msbG1H)MlDe9ecTWJ6md6>yvGS%6WdDl&f54*@Nz%4tO9+=Q;mGu0A~;f20W)i7yQ+a z#_{iHhqmZcTLf0z`Z`G0+pmO5VE>r)YQFxJ=+sxuGcD`5SFJ|+HO3Dh8>h_um(6e} zxr1Q^*!J7!Z^8-60GmXnVr?Hw%!99w@MVUWKY zpz5vmA-}x@beoW-M8m4d{3hucB?3ctYl^jRM&fno%V=0$9ro|of~TzoeK4O>elJkl zV=pJ%!s2yG2CNC%*~17m>ccD1XS?(%`=shS->$R0aH`wKC)EdRFR?)*&J)-Od&(4Y z=ob4d8~}Y(amLaOI_xA%glutSC!HmucUX2*6qNzjKJaun`s#eRd4+uw9C?QX?@meY6KK6#^nBoy=@rp1;Gb&L z_F||DuW+uqEn+ikszQV4BR4MH&a!dvJ81i(+mwr3G^^(P=+P*AJg71t)r9R3{l$8L zm|L~_b%3QKozm=m+khOZLVumo6IFj^AAZW8!+EuZKR`!0;NS*3AmJy7ds077T(5L{ zGEr>jg5x}Jk5vR)+60u>fb?o7?)yIDY(W})6DjG{CKiV}(IpWYyAYbCKOXz(v8NSr zqu-7WzKhdg9RGz^1%3)juLf?|g?RHFuo$Zz*zJzq7dAe$jhTddEPftZ@m+a5;+QqX z#7_~8=u)bTKY2jA0xtpCPjFV7je(^}b6c!B`>hq*|;{yy~ZHe%`40gWL(*%YYyf5(=UXEuU4Gc6^Am2xt3)$$}Hk- zny}}!AzgN{&jx6tN_1;r%uj)_N3?kTr5er}>S%E$RdJIK?A0~D-S!7=YSVD4N7+nr zqL2fG{Mb)zj7kkd6~+bjhD8znTwwJs7McmmWMw}K{XE~ORNaywdQZc)hR71Hsf;BD zTCB&4-37F+1rB43)X&OV<+1mRfxvbp?V_-ORaHZI8##6FMFS$16zEYefjELLTMK1= zjD&9hCMC?3%Pek5-^j|AnD-}d&~>~ER)Ee_$kz}Myg^y4vj%<|gUZUajTst{8JvH} zpnfcLflnR{9tAPRYL*I%pW4COj_45i|I$=20-)qql{>0vac(6B_La#n|eqtbVrl zvwhm`)`Vt|d@a!ZIJ+RXTSW5XZyd5Qn*oN@NoT&UXp8-s)!fMad>l<;Bi5Sw`0Uvm z;krBRBVw&Dh+g37E1|Jkn+bH)i_(c&c{TFW)^iee!9_jnWQ#3j9p3MFnDK`=<94YJ z4jx2a#f&zB4DsFy*hn}Tf|~S0-l3l)QX<2=5767mz_;WnqBnRCplQ~P{mptz{Oxwb zjj=~4o(x|6UO6fJ`|Pd&3i3Lt?(y{#gBr?6^`yc!y|0M_ERGJNWsE0@I{V~0;B6vLfa8ik9xI9YpjtorarlxvoKb@q1i zY=8=Q;BXP446I^i*=SF4DT;rJNcuGu~zmP=2@4qCcRAE><=3dSt_}F6Ax} zgHt3GFnN!Coz02u)hu2IzD^@E`yf1H(~LC(_4Sx%4^+YP)M`=%v-R`VIQkEj-{7Vi zBYzAYx=7n1w|edb1MAu4TK3e*w_ES@IHhrJEp!9a2|De7k`pko&t*X>Ct01pHY0QM>(=LE|)2%^ABPW`X z29-$rOungP?cb0`i9ZCz7oe9C=}T;rXD|K+>6Q*A4~MS@RSzT@p+&E)K%HT*{4sLs zak=l-^F`ZqAr!&Am)|||==w0phyx-MfLn2y5t+d|ZrAQMsv5*(m_qA;ktTE{cG}hE zC#Y8v*~z}W2FC>XxI>2D9RBh=mRca}^EMC7;MQsj3=6Cgk*T-iNhs!VRU#|N9u<8x>~?;1Kd5~%KAyRL8|D)qNWBO~^<3ityH3-< zj93eEeV~wvjb45D8c|hQjW)w~)Exu{)FH$|S!8?IZ?AT8o_E0)1MJB$epwu2kE@nf zduU-VuDx11>#+W4EOO*(>4pZm{`dD5BaX z$Z79}D?3E?5PnS0ep0*#80YZ&5R&C#G=6F$*?RpoxRmvpusznv+rN$LS?Qu^lQr2U z{XMiq4{)dd-)I%{v@655CfqjgvQ0u)Yq{-tE$u0zvLLa#Datu&0`Qb%9Us}j$d=RayLH1=A45HU%Y03%cw(3x#BkO&Ao%CR0Fd0kTPf&T z?*Z#_`**-rYoln1+sWuEhGsWG$*pTj-0S(@HTDb z9^_ptI$13+;mHlq*nT3mTdTB~HG0(=aAAICUdTFjnCDFNqaFNbl&c{t3S($VZnK%g zGih+D$1wUG)M?E%!TG?X$JUWq~c?N|3E)LD0skLgmb=j z@i2PO`SQL4TSE_;Zx&UK#kyY8CeB9Tmt36~u(ug+GI|D1UJZ|~*HwB7JH~BBUXEpo zO#Va~R7^yu(^^dRUvXk5$Vy{n9@n0&Ztg&b*Z})MZ?F#bF^FWjaNmxG*vU4sX2|zP zYVH6ovyf|en0edl8F=1^*9oWx^4BvmxrXe1NL!d~6fd7wt=ORk5%(zcZdU$Z&`>Y2 zjw`|G{hCB$Ye3^_18aVcc*JKfaM~a3Z3uhIEuj+Fx>i323wSA9kP^y~e1ph0w;uP| z!^z$8H2a%GYL`I=+($%ZrRdE>3p6wUT*{gG%gRh1Z0=_q;|7#(E8x3Xz@iZ-T?@?X z^anJuR-?`z^ijYlRYt5cqn5~P8|}}tC6>p@L!Cvqy3n$y#JwBn5dXxCyNQpkG0$_| z&CT4YZZT4fm{k7u7=$EzIfCu;8u+V>T&g8Nq7*9ij*N`~dp1;kRCIds$MHFO(=LkU z6F+r<-DnP@rFL1Aqe{+(N8ycG=5>));WDss zCmAE>@!ndEAkByg1>*P(Y?bBksaqCD%sb^$)e^Y5KY0XPO+u+n+|C-T(o&$b0d0>` zIqUCZ<^uXRF2@SgLnQDGfokQ-2O^ILYBX}9^njZ_#zwkh(H+KS+v79}r~7TUhG!?B zvk9c}2o_K@5^hygqD%ZST8(~(cON=w(_0!t!y_u&JPz_#(LwgL(8!+F*^pL}Ayf_r z)Na%Z z3$byva~}$5Nf+3e$@NwZ7ME)oJedP0r^~E~iwtaZ=@fWpBhvi^J+Bt{Z3yY(KBNNkN*j1q;LkxlYyw)GYCwHh-+{V*$0^># ze*Nr3f?}T@#qKgI`f5P>SOyYcb+{1SA~npK-HA1(ktbII=~67Wo>AzzdoSCq(9R0x zI77?9>Q?JFkA=JkSr>uhZJK7z_rcJ$)V>@8+WqJcvh%os8Ru@-Y>(FcXM%g2l~w0( zQf@|W+@ocm$k|^HUd8n8A7HS%w}Xx>+w-9(fDDh#PTdeW+4l z-C&>1HWf)u;|;(ig0>%tJ>!2E{7eGhB>7~2jIyElgOOuJj14xStv;S0e!_C1v_vkF z=TLdSWF(~?0`hLR9f9w%?5S{tygv*vgDMMj(dK+40!}H=aTl2WqN+K|y{kb9RUO{6 z3T)T)`gHbU>(yDIz-mR+sAvteJA#bcfE=9yhwII2cez#NFQE)c(rYuxbk^&%^g4KK zwGLvW*6OFhB|36l{fUi%x1j^K+>HTGth!JpxrIQx*>aNAY6XfNc1Bo)lzpCW3TzyU zd>@b~QH#?paGnRWudv7M(nJlK;@og+Jclf&8s}kK6`qUnCfkL;!mGd50C`SsxX^3n z4Z;BjLlYT{SINk|;Dfr)aHdD(D*PeAU4c8wi%H_E^@7VV;N7Dm!!}NfBf!8%M2OGA zf`o4uIq*fr%0W`!p>0TF^7C!h>`hQ{S6Bl-thQ30%hqt?uSEV0>+kVZoD_J2S!mXhet;Dm` zVjzB6g6IqTEG}ZKT;Rh=jxESzGG;6tJdUFuEYtnbpCZ4X?ge=U2wws@!QV1lw$*l4bX^3Qc>(F?!Ly!_#gHo#E7=N&yzk9K8>&i1UKiywm8IsH+&3 z1F6HK;xM6hEd*j4ln0%T0hfNfu^v@Tj4&s?SNL0N3)H4H>;)SKFiQ)?cAgH4?nYPn z1~SF7KD>S`dMVm^hmrHf$@YSIjCMKC|08UP<`KhLr%%EU*;yeKW!s`#@$;>+qv7_? z<9{cA8@|Hae~hc+%ftRrv2C_9bO?X4^a!)khbaC<{9xkT@IQ%K<|#937Qw^Yh{Wl` zcX5zXsX%_A;f0OL&0`h?lr!{8Xz+(N2n4o}rSdhj7Wdi>gP$|7Xn1tUO1XD?oqV_3 zq0gtlvFm~z`dLk5Gz?XFpN_@s$$e{?ypK;6OK22`oMi_d^G-&(QtW%cYCinvJ)b^hCH z$a~j1)dTxhrO{1s3F|Epc3ay9C*5f4tyAynzOVqD?1F@6zLOycgQO8JSKWReAutoIi1PPk3#zKl6d)(AwmxJ=H@s8IPedkwyB+-9tUUIgL$q))c90|Zwnf>j%PkW3+9p+6Ezwa{qMezRuY$V-wIX~6gnRrdeM>Mx5w5gJew--18$U10r`U4Kv>Ln4UI0r4@achYgWFB3v^l(J#L6NSc13T7 z8@Xu~*_q}YhP<;C5b|3BHyqd zA#n4mTw5728|t9?K6u973>|v3TuHb@_)_eJ2!(2WyGfPwJ-gfoCP(FEYoJ~V(>xr+uA*2EqkHV_AtK}JijhvH%;ESkL=07Luw@bCzGL;?H*hVs0hT!dK z^V*>FTZ2cAw6}m{5|2X#7=K-|PW?cA_R~;Dg@mDMHJ{s& zdq+Les|W0-6T{F8#bdK#KMh#w#%v+f@GR8&0@(y5a2!3fwVn82DwfdOoErLxr@2{- zBL99D$i_28uZD)Ooe_#6V(je^;2o^bvTd9beGfOc68PKog54TaCz{~>ti-btidfeq zPrH4yU*wk|7ylHkwZo7PcPk_DlGZ}EQ=FOj+YP%EYn95;pBD1grMgdk6LYIfCDc$4 zZvPo>z>9h*v^A*>!J{5#?EkZiJ+8^Ctb6_<)s|R4_Riq9l09IPF)T%SK)D}@^@zH3 zA+?0dk$Fj|;w9jj1HPt#&wR8bBCOCow?I89z?~Rdo@zr9=JR`Z)Q|O&I&tvnAxMs8 z%$H--Ab>VJ>WYjd>k#%OBtRp&hgbM0wU^nQ_x&vh+pt{!CEOo30`)>$#jH8i*2tCEvG-R zICz}n7H$Ip8oRYe<`<#4dnety;t{P7F>NB+G!}at4_)R_X>KKI z*EO7KHP#1ZpOgqlr_z11VY^`^*u=2WkAlBkJB3@>5c0tz$~YsD1!UiY(fj#iR0Yu8 zHgdN+u$Q0{gNatOMVAXyh?RGpFX6uIHQ%y`B-mj?tiMhZRsdDxnMVkv8fUUntrX0V zgNpvT0r`@J&6irqJX?VVTqb-+SROrsGuJk8zEr~p-mmUbAlHoqbDO~@$jx2sXbZUM zw|k5mEVR;~o8;XrO1QrfF%xA$B_CrWK9Dy8G_x_rZwuVkN8`}yW_HgV0BU&K-U61c zFJz$~;)P%&y1@Wb{OCsC13G7}<_sG(uk4WtHrv?UG;6pN2xnL?r@&+4Ow)wkWXDJl zt+aF~Xi`Uk0nZn8|AJQlq{0_)nMB`!!m-%9g|L}%n{80|fT2CFIdCR0+rkaMQx`E3 z9tF77ISYQbT9qe=$WcC@dW`&z#1_8uTPOMw=fjGK^Bk(7TAM8hYtYPI0{Z>Yq^hu` zRDr$;iZhEnj5iaZJ4@`xEK55axUFy>@<9g9Sfz9GL$d@VsX8uE0= zz7^)9aksJpv3~apCH0QRBtdALs8W}*-cR`oU@_n=;~W*y5O<{G`l ziGd_Pj1w+t?ZI<1>+H5rfHdgm%qtACFY89tBTL4CmuKca4pnt39b7dT)e^#PphM&< zmV!=-sv$KG+6??u|0+ z2KKrZIw(f(T4P0wM+`LQ$1zxGQn_(H6_K}vG%hGlFp-S%UOX(!q7pG z9dcI1E)!mlo`UMfs7Wbo>O{7Q{`HA*^b6{1Q~RopJB8c8rpXj-*4wsGUbmSpvOxJn z)<;%26kN`&_{)lkbSEm>R`a_@dTO8ee~BrCb08nq(4?= z==vzN0gn=JxE&8-9bUW=?gss=!S7&i$$Q59DqIoMUCi)rBL%*w;wUJc%74SU+O7*8 z71zhw#`rG6U!IFju*rJ#ucXK49s|vt1lPv4CZg|_{t!3Y;W|qu&-PdJR;$IDq=Ms!p~kwdlKw=-^5RW5q2o_gCu(0ty*g1$do1c3A|6s%Se!Xo-NSr=2f)_N#;j-TYr|=LjIgqeoS+KKnGIB z<9B*=6*TyDpzXKDg^qJzSO@GowN5k8o7Z=_g%x{TnCxTbbdUC$sC6H%cIXq$F!o#~!?C4B*uAbT3bt_bHnwo6`wi@q;)}uc~ULVEp10C?u62|cg25A1k zfKCL!z~#bKXp&jXS*rQGw^RR{+=g_($^*71um+9AUb*jSXo_rXpd2B4@T3Fte(+Dn zoH!0wf~7jNg?i|HNqjmxz7$@j4!SiPJ$6|O8I_!R9nSoj!{G5h~%THI~YpYxOm2w5$2k zZJVMUc7U3pL(jJB0M@|ME=;6@Tk;<`36^Px#n9&zlJ2uG&;B;sz4?VvRoEVRzmA*O z{X9;(yTM=5vUrQE7GUzMU5Q1v1e);MUJRYiS4O-Vnym&3^BI*+L+m4kUEIg)Z}2bu zXjH+e<(VARZea~CX^kGkI#hxzsR@4y--kXtZ|SCB++MxD>$p`g+bBP^%j>F-akoMG zuqW?g+6G2y>__@OJWKU;ergSA;YX~B94X>1=7eT)dp_gu614j+^rGu+U;I0BBa2=_Rs>kwVbc$rY~w~ZSIF)W!Owyjo?^y(C+Xn@adM6>>q9jb?rcc;RQ=nq2v z>o19Rc=Q#eCch4)6Ay#tN|YsXY=hOZ+cJ9+3QjWjcHIMirAApq|D3Aws7V_*-*SON zpH=H$(I{u-guTP9;&k+YBC88ju2v$n63JPuZFVTWwJ{@a4KK?yUa?lo2NoXf@I~ZF zo94ioUyGJO$ywm5R->B1&Y1#4{uWQb`(7W$ZyyPYT9>)s>3clkMTFVH9lE49G16^tFsJVnr6J8OAL!;DjaY$)Kg+id?C z9)gooh*fzLs;Ja0T8>O30?0OoGUV_1iL=$$jwzgOe>^v9#dl9-N8iJp&WAb!0Sf(~J2keKUQl!=T`7AAo8f z1~)}?uS4FD&*$|59)wO_;JdA?ED*!m8F?S@jYyViXenLZ>u`Zl=}51|)Ommsu?Jcs z77VW$wInR1!%06kCGSEr936*^x|Ws8Qk9y>fU=(AsXSGZ=kVq51{77q>eK8LxPFQD za$JB4y-$VmlqcWHH)KUCl_yXGg67IL7+KH@fwjng#%Yon|G|VXn zh3!I;4+683`F$YzO~{3+sr3kNcs4NS3j8=_X<*}`P;cKvqdXM2{wd;~9&)V%Oyr^o zc0~sDG}u9$3e;drxK;Q!fmk8nx&R6LB>E+mN1MQR)um>+j=LrLk5I;q4*Qr^>(5v? z>8_??sOUP@M0^CaRs`-n9%~BP@qSRRsvWsqjr5>m6&l_&+Uy2qnFn6en88odn#0=rVJ^IN1^WyBylYMunsr<2FNm zZF??i(K7oX67a?tKX6!}Q?-HF7Ryv>#pbm8U`Xl?kzbC4{bd*g$}d@;xX}~aP%N*z z&}W{DJuc73p%1;Lut=?CoU`n4cAL%YEqXOEqs_L~K2$#^;!>>rbZWP5Jg?p(as&z| zJDh#fncIeKWn#+C=9?Gvh1pG7WUiH6&CJjj-{xzEeD*H@!FP=s)3(x1j88x>J31<4 zwl|T*oAeFVw`De!G;}5s;)w7A;Is^CrDlaHvDL0qE_o0Wz@CT^Ang?u3ZZBs?zAaJ zg8_e)-~=sJ9?h``E}~`yPr0`3nnSRSx&XW5lr<-=hhp~f`5so;gD&j)FaD`$m3iG} zd{l~&6yLRJ-X8=4$RhB!p1WR(sOk>Kx(?}nGLNz$-qHS)ya`z|87<~y_1@kq!CyDB zbp=#P78u#&Cj$j4&>)`*m809EDq!y;)na=~u}q%d!CKO+0NShM&h1{8_0|oC>;Mj~ zyESXGwxXk)0lX%H`^B~+1zPaOmDqAd$%HfUAq9At7^k(It98i8J#jt|UK?&zhF;c1 zVAyQ|*-GOCeVg8VYjmRZBXNho*NEH;f3H=Mhh3orR;ZOw?I82@z)8zQOdJwnEq9c5 zx#vvbMqoW6GEJfFN-#%GKWBBX7V?JQRU4qJJ!odw+@KrJnr|VNeyP32PR>y`E8T1x z*n6%Ga5}U@9XUq#$HaD>4cy$@{J&61BQWS@9lr}y&STW~jCy?0O~zRdZs}|Xj))~U zuj(@hM4R}=BavT0Hsg_nYV*zW7<<{7TPWwrXSH+^{nI1DXeCM ze*?7F%<7+p_DbwzY`7ku{{*q=-al##?Dw*QSx{j+vwMc~ccAf|NRv%aq1UIPq9D+i zZLfyoq1skXn;Oo$KPS3*@&x-LdgKN6Hkc|=m6px^O29fFR%?lEwa=p*WdiR7Iw+C7 zPmb?GJD!3|c3L|l3yRw#uX90mHImQI5s%Xyh9j2AwMOTyDWEZ^^YHw)lGC--I@tTk z@*H39?>lOB$gluTPS!K*<$hpuoIC@v77Me-2jfo+RY;dq`&+a#sE8TuZHL?XbpjCd{zMxLy-cOC$K_0m+BLwckMo}_ zG5bp52;k}V)NJtmVX_Hmd*4Rx^13s4R_AMK4@0x@OQ1_tXi2mVJ_I*d)z9aicYBl> z?}wbbIJB6@dt9e`wNYn735B|Zo%sp+5%C2(M8y|#eR~`V+6_K;8yXV(xg1FSfc4h0 z@_x<-kEGtCY5M^%oU$WpKI0U8C)|jx@QU6ID>>yoD|M;XgqP4X&%oADV$>T9ui5Rk zo7^*@i_%G)J#9xmtU<S8gFApC_uvTKS=2eb7}(LQmvpgeLtA&HZ>hen?;RJ^X(mpOJ5jiFg7x zo~Qb^#D_q6l+|^}BiC-@^8sjts6x1G(6W@zx5M`3*yBL*YzH=_7>cb08b=`81P*HS zcJ!{iR{i==i{yMFa!s$3XqZAIp?Be$pKC^?9F=@6uuj5Z776yi3=zgy-2@6OEy>RKttgxJMrjEgTYNz~4>K znA_>y7O_k9(4*JiqVf-XP5y)--N5c{_T8mMW;|&&IX}9TPm37;N+5!rknued%I^{6 zUqfLFc*pg#bX&@EH`}G)V#+S!mhG9K9a=@~2%d80Aq(2bUq;8M1(H?VU%WzlqdXh8 zjCIZMzu%Kw%bT)wKqQ|@jg_fjJekTTg-wrnb;RCqac$d{fuLa(BjStt87Xe%%eApMUN|W>BHbvU^mLO z*019iB;p7vDy`Fp;j@gXZIFE=lF)<2IAY~-N3zi zyvHmqjTS>wW1QqIc6nl!VY!CWJhx_((QBQb`s^2h8;0VDW~ z_uKYBiF@HNkEENn8F+6HPVxA#HZ8TK>d`6O3EigL1{dW4jsHNuY+)ZBe{e8Qr*XfE zxk+u+hwPm^eQ2%@2wkcgp7Oj5x~{5T3li>sD@8^ww^m!L32`$;lKwuLV)l37t8>s1 zmcl<5vUk@;^P%`NZJl-Ua}``3c&FOh+07cH;yUOisWo7~)9BU5tYlEZMc~2eq5Snk zuB@ibDZiqXpS6*v%28>>7G>F4>kUjQ;$ti^NGkTjO=b2IjQ z&d(gq?~U-1=V-KZ!zMmEA{vX4N{xC=P(cGQ&P0p*4m%hks;<8}pz*c=uVE}8c$is85=@JGA65AC5=jXlM4C@R?XfKkhgy{?F> zu;K|{%*U1vp6K6W#Qlm+H^8IHsE7nMcj9HrMYGJ%jMEhRo0fNqIT4;BCJ1WrY|sfq z`-3AU>>74Iz`om|P0#HsWi2i_I)S>|kGx{Yv^*PZl-(uuDln)*x=h(Pt8He)8TMQZ z&1Az(sbJ^@MkHF79Z@OThB^6�LJW?0e=F5XwOXqln}ekDT#zS`{#E$CejRN> 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
  • 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�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
    MdEeGaRYCiD}uviR@ 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±5n5*Y?{4SD6_b{ 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

    +F@M$Z?qs?N_p<9pGj_&cvb~Wc@@DWt+^?PX4Ng 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

  • EqNofM-MfR;6Uw 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