From 4063ac7708b6d4b83eadf8debd8f63094a46da98 Mon Sep 17 00:00:00 2001 From: anubhutiv Date: Fri, 12 Jun 2026 13:08:32 -0700 Subject: [PATCH 1/4] chore(customizer): refactor shared code + integrations (automodel/unsloth) Signed-off-by: anubhutiv --- docker/Dockerfile.nmp-automodel-training | 1 + docker/Dockerfile.nmp-unsloth-training | 74 +++- .../automodel/Dockerfile.platform-workspace | 4 + docker/automodel/pyproject.workspace.toml | 2 + docker/unsloth/Dockerfile.platform-workspace | 1 + docker/unsloth/README.md | 35 +- docker/unsloth/no_override_requirements.txt | 11 + docker/unsloth/preserve_base_torch.txt | 7 + docker/unsloth/pyproject.workspace.toml | 2 + .../src/nmp/common/integrations/__init__.py | 12 + .../src/nmp/common/integrations/schemas.py | 157 ++++++++ .../tests/integrations/test_schemas.py | 97 +++++ packages/nmp_customization_common/README.md | 21 ++ .../nmp_customization_common/pyproject.toml | 38 ++ .../nmp/customization_common/cli/overrides.py | 107 ++++++ .../customization_common/contributor/base.py | 127 +++++++ .../contributor/config.py | 25 ++ .../customization_common/contributor/jobs.py | 74 ++++ .../contributor/transform.py | 60 ++++ .../integrations/__init__.py | 19 + .../integrations/compiler.py | 49 +++ .../integrations/context.py | 45 +++ .../integrations/runtime.py | 151 ++++++++ .../customization_common/schemas/file_io.py | 186 ++++++++++ .../schemas/model_entity.py | 99 +++++ .../customization_common/schemas/values.py | 26 ++ .../nmp/customization_common/sdk/client.py | 340 ++++++++++++++++++ .../customization_common/service/constants.py | 28 ++ .../customization_common/service/context.py | 105 ++++++ .../customization_common/service/images.py | 33 ++ .../service/platform_client.py | 51 +++ .../tasks/file_io_progress_reporter.py | 97 +++++ .../tasks/file_io_utils.py | 181 ++++++++++ .../training/callbacks.py | 129 +++++++ .../customization_common/training/progress.py | 112 ++++++ .../src/nmp/customization_common/version.py | 4 + .../tests/integrations/test_compiler.py | 68 ++++ .../tests/integrations/test_runtime.py | 210 +++++++++++ .../tests/tasks/test_file_io_utils.py | 36 ++ plugins/nemo-automodel/README.md | 2 + plugins/nemo-automodel/pyproject.toml | 2 + .../src/nemo_automodel_plugin/cli/inputs.py | 90 +---- .../src/nemo_automodel_plugin/config.py | 11 +- .../src/nemo_automodel_plugin/contributor.py | 84 +---- .../src/nemo_automodel_plugin/jobs/jobs.py | 56 +-- .../src/nemo_automodel_plugin/schema.py | 33 +- .../nemo_automodel_plugin/sdk/http_utils.py | 64 ---- .../sdk/job_resources.py | 86 ----- .../nemo_automodel_plugin/sdk/resources.py | 167 +-------- .../src/nemo_automodel_plugin/transform.py | 26 +- .../fixtures/integrations_wandb_mlflow.json | 50 +++ .../tests/test_contract_job_inputs.py | 32 ++ .../skills/nemo-customizer/SKILL.md | 15 +- .../references/hyperparameters.md | 79 ++-- .../references/integrations-setup.md | 130 +++++++ .../references/troubleshooting.md | 54 ++- plugins/nemo-unsloth/README.md | 4 +- plugins/nemo-unsloth/pyproject.toml | 2 + .../src/nemo_unsloth_plugin/cli/inputs.py | 115 +----- .../src/nemo_unsloth_plugin/config.py | 11 +- .../src/nemo_unsloth_plugin/contributor.py | 96 +---- .../src/nemo_unsloth_plugin/jobs/__init__.py | 2 - .../src/nemo_unsloth_plugin/jobs/jobs.py | 106 +----- .../src/nemo_unsloth_plugin/schema.py | 20 +- .../src/nemo_unsloth_plugin/sdk/http_utils.py | 64 ---- .../nemo_unsloth_plugin/sdk/job_resources.py | 86 ----- .../src/nemo_unsloth_plugin/sdk/resources.py | 162 +-------- .../src/nemo_unsloth_plugin/transform.py | 34 +- .../fixtures/integrations_wandb_mlflow.json | 35 ++ .../tests/test_contract_job_inputs.py | 32 ++ plugins/nemo-unsloth/tests/test_jobs.py | 6 +- plugins/nemo-unsloth/tests/test_schema.py | 38 +- pyproject.toml | 2 + services/automodel/pyproject.toml | 2 + .../automodel/src/nmp/automodel/adapter.py | 16 +- .../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 | 91 +---- .../src/nmp/automodel/app/constants.py | 40 ++- .../src/nmp/automodel/app/jobs/__init__.py | 2 - .../src/nmp/automodel/app/jobs/context.py | 96 +---- .../nmp/automodel/app/jobs/file_io/schemas.py | 220 +++--------- .../app/jobs/model_entity/schemas.py | 123 ++----- .../automodel/app/jobs/training/compiler.py | 77 +--- .../automodel/app/jobs/training/schemas.py | 28 +- .../src/nmp/automodel/entities/values.py | 28 +- .../automodel/src/nmp/automodel/images.py | 20 +- .../src/nmp/automodel/platform_client.py | 39 +- .../nmp/automodel/tasks/file_io/callbacks.py | 44 +-- .../tasks/file_io/progress_reporter.py | 96 +---- .../src/nmp/automodel/tasks/file_io/run.py | 4 + .../src/nmp/automodel/tasks/file_io/utils.py | 193 +--------- .../nmp/automodel/tasks/training/__init__.py | 0 .../tasks/training/backends/callbacks.py | 96 +---- .../automodel/tasks/training/integrations.py | 182 ++-------- .../nmp/automodel/tasks/training/progress.py | 174 +-------- .../nmp/automodel/tasks/training/schemas.py | 4 - services/automodel/tests/contract/README.md | 3 + services/automodel/tests/test_adapter.py | 91 +++++ services/automodel/tests/test_images.py | 3 +- .../tests/test_integrations_compiler.py | 83 +++++ services/unsloth/pyproject.toml | 8 + .../unsloth/src/nmp/unsloth/app/__init__.py | 2 - .../unsloth/src/nmp/unsloth/app/constants.py | 50 +-- .../src/nmp/unsloth/app/jobs/__init__.py | 2 - .../src/nmp/unsloth/app/jobs/context.py | 95 +---- .../nmp/unsloth/app/jobs/file_io/__init__.py | 2 - .../nmp/unsloth/app/jobs/file_io/schemas.py | 205 ++--------- .../unsloth/app/jobs/model_entity/__init__.py | 2 - .../unsloth/app/jobs/model_entity/schemas.py | 119 +----- .../nmp/unsloth/app/jobs/training/__init__.py | 2 - .../nmp/unsloth/app/jobs/training/compiler.py | 7 +- .../src/nmp/unsloth/entities/__init__.py | 2 - .../src/nmp/unsloth/entities/values.py | 35 +- services/unsloth/src/nmp/unsloth/images.py | 37 +- .../src/nmp/unsloth/integrations/__init__.py | 8 + .../src/nmp/unsloth/integrations/hf_bridge.py | 98 +++++ .../src/nmp/unsloth/platform_client.py | 76 +--- services/unsloth/src/nmp/unsloth/schemas.py | 26 +- .../nmp/unsloth/tasks/file_io/callbacks.py | 30 +- .../tasks/file_io/progress_reporter.py | 96 +---- .../src/nmp/unsloth/tasks/file_io/run.py | 4 + .../src/nmp/unsloth/tasks/file_io/utils.py | 134 +------ .../tasks/training/backends/callbacks.py | 112 +----- .../tasks/training/backends/unsloth_sft.py | 44 ++- .../nmp/unsloth/tasks/training/progress.py | 105 +----- services/unsloth/tests/test_file_io.py | 4 +- services/unsloth/tests/test_images.py | 14 +- .../tests/test_integrations_compiler.py | 66 ++++ .../tests/test_integrations_hf_bridge.py | 204 +++++++++++ services/unsloth/tests/test_progress.py | 2 +- services/unsloth/tests/test_schemas.py | 6 +- uv.lock | 79 +++- 134 files changed, 4479 insertions(+), 3572 deletions(-) create mode 100644 docker/unsloth/no_override_requirements.txt create mode 100644 docker/unsloth/preserve_base_torch.txt create mode 100644 packages/nmp_common/src/nmp/common/integrations/__init__.py create mode 100644 packages/nmp_common/src/nmp/common/integrations/schemas.py create mode 100644 packages/nmp_common/tests/integrations/test_schemas.py create mode 100644 packages/nmp_customization_common/README.md create mode 100644 packages/nmp_customization_common/pyproject.toml create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/cli/overrides.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/contributor/base.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/contributor/config.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/contributor/jobs.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/contributor/transform.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/integrations/__init__.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/integrations/compiler.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/integrations/context.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/integrations/runtime.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/schemas/file_io.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/schemas/model_entity.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/schemas/values.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/sdk/client.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/service/constants.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/service/context.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/service/images.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/service/platform_client.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/tasks/file_io_progress_reporter.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/tasks/file_io_utils.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/training/callbacks.py create mode 100644 packages/nmp_customization_common/src/nmp/customization_common/training/progress.py rename plugins/nemo-automodel/src/nemo_automodel_plugin/jobs/__init__.py => packages/nmp_customization_common/src/nmp/customization_common/version.py (60%) create mode 100644 packages/nmp_customization_common/tests/integrations/test_compiler.py create mode 100644 packages/nmp_customization_common/tests/integrations/test_runtime.py create mode 100644 packages/nmp_customization_common/tests/tasks/test_file_io_utils.py delete mode 100644 plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/http_utils.py delete mode 100644 plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/job_resources.py create mode 100644 plugins/nemo-automodel/tests/fixtures/integrations_wandb_mlflow.json create mode 100644 plugins/nemo-automodel/tests/test_contract_job_inputs.py create mode 100644 plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/integrations-setup.md delete mode 100644 plugins/nemo-unsloth/src/nemo_unsloth_plugin/jobs/__init__.py delete mode 100644 plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/http_utils.py delete mode 100644 plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/job_resources.py create mode 100644 plugins/nemo-unsloth/tests/fixtures/integrations_wandb_mlflow.json create mode 100644 plugins/nemo-unsloth/tests/test_contract_job_inputs.py delete mode 100644 services/automodel/src/nmp/automodel/api/__init__.py delete mode 100644 services/automodel/src/nmp/automodel/api/v2/__init__.py delete mode 100644 services/automodel/src/nmp/automodel/api/v2/jobs/__init__.py delete mode 100644 services/automodel/src/nmp/automodel/app/jobs/__init__.py delete mode 100644 services/automodel/src/nmp/automodel/tasks/training/__init__.py create mode 100644 services/automodel/tests/test_integrations_compiler.py delete mode 100644 services/unsloth/src/nmp/unsloth/app/__init__.py delete mode 100644 services/unsloth/src/nmp/unsloth/app/jobs/__init__.py delete mode 100644 services/unsloth/src/nmp/unsloth/app/jobs/file_io/__init__.py delete mode 100644 services/unsloth/src/nmp/unsloth/app/jobs/model_entity/__init__.py delete mode 100644 services/unsloth/src/nmp/unsloth/app/jobs/training/__init__.py delete mode 100644 services/unsloth/src/nmp/unsloth/entities/__init__.py create mode 100644 services/unsloth/src/nmp/unsloth/integrations/__init__.py create mode 100644 services/unsloth/src/nmp/unsloth/integrations/hf_bridge.py create mode 100644 services/unsloth/tests/test_integrations_compiler.py create mode 100644 services/unsloth/tests/test_integrations_hf_bridge.py diff --git a/docker/Dockerfile.nmp-automodel-training b/docker/Dockerfile.nmp-automodel-training index 10dc2cf079..81d21df1c1 100644 --- a/docker/Dockerfile.nmp-automodel-training +++ b/docker/Dockerfile.nmp-automodel-training @@ -32,6 +32,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \ -e /app/sdk/python/nemo-platform \ -e /app/packages/nemo_platform_plugin \ -e /app/packages/nmp_common \ + -e /app/packages/nmp_customization_common \ -e /app/services/automodel # Re-pin nemo_automodel from the base clone without re-resolving transformers (already in base venv). diff --git a/docker/Dockerfile.nmp-unsloth-training b/docker/Dockerfile.nmp-unsloth-training index 7e74010e03..e0ecca6511 100644 --- a/docker/Dockerfile.nmp-unsloth-training +++ b/docker/Dockerfile.nmp-unsloth-training @@ -4,14 +4,18 @@ # model_entity). # # 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. -# 1b. flash-attn — optional for unsloth and not installed by step 1. Without +# 1. `uv pip install unsloth --torch-backend=auto` plus explicit +# `transformers==4.57.6` and `huggingface-hub==0.36.2` pins (transformers +# 4.57.x requires hub <1.0; platform glue would otherwise pull hub 1.x). +# Unsloth's resolver still pulls unsloth_zoo +# and the rest of the HF stack (trl, peft, accelerate, datasets, +# bitsandbytes, xformers, etc.). `--overrides preserve_base_torch.txt` +# blocks uv from installing/upgrading torch into the venv so the NGC +# base's PyTorch + CUDA remain the runtime stack. +# 1b. bitsandbytes — compiled from source against NGC CUDA 13.1 (same pattern +# as docker/Dockerfile.nmp-automodel-base). PyPI wheels +# only ship through cuda130; source build replaces the wheel from step 1. +# 1c. 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. @@ -39,6 +43,7 @@ ENV VIRTUAL_ENV=/opt/venv \ HF_HUB_ENABLE_HF_TRANSFER=1 \ OTEL_PYTHON_EXCLUDED_URLS="health" ENV PATH="/opt/venv/bin:/root/.local/bin:${PATH}" +ENV UNSLOTH_SKIP_TORCHVISION_CHECK=1 # --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 @@ -54,21 +59,53 @@ ARG USERNAME=ubuntu ARG USER_UID=1000 ARG USER_GID=1000 ARG UNSLOTH_VERSION=2026.6.1 +ARG TRANSFORMERS_VERSION=4.57.6 +ARG HF_HUB_VERSION=0.36.2 +ARG BITSANDBYTES_VERSION=0.49.1 +ARG BNB_MAX_JOBS=10 WORKDIR /app +COPY docker/unsloth/preserve_base_torch.txt /opt/docker/preserve_base_torch.txt +COPY docker/unsloth/no_override_requirements.txt /opt/docker/no_override_requirements.txt + RUN mkdir -p /home/${USERNAME}/.cache && \ chown -R ${USER_UID}:${USER_GID} /home/${USERNAME} -# Step 1: install unsloth via its own resolver. --torch-backend=auto tells uv +# Step 1: install unsloth + pinned transformers. --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. +# preserve_base_torch.txt prevents uv from replacing the NGC torch stack. RUN --mount=type=cache,target=/root/.cache/uv \ uv pip install --python ${VIRTUAL_ENV}/bin/python --no-cache \ --torch-backend=auto \ - unsloth==${UNSLOTH_VERSION} + --overrides /opt/docker/preserve_base_torch.txt \ + unsloth==${UNSLOTH_VERSION} \ + transformers==${TRANSFORMERS_VERSION} \ + huggingface-hub==${HF_HUB_VERSION} + +# Re-pin transformers + huggingface-hub without touching torch. +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install --python ${VIRTUAL_ENV}/bin/python --no-cache \ + --overrides /opt/docker/preserve_base_torch.txt \ + --reinstall-package transformers \ + --reinstall-package huggingface-hub \ + transformers==${TRANSFORMERS_VERSION} \ + huggingface-hub==${HF_HUB_VERSION} -# TODO: Step 1b: Flash Attention 2 — compiled from source against the NGC 26.02 torch. +# Step 1b: bitsandbytes from source — matches automodel base (CUDA 13.1 nvcc). +RUN --mount=type=cache,target=/root/.cache/uv \ + git clone https://github.com/bitsandbytes-foundation/bitsandbytes.git /tmp/bitsandbytes && \ + cd /tmp/bitsandbytes && \ + git checkout ${BITSANDBYTES_VERSION} && \ + 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${BNB_MAX_JOBS} && \ + uv pip install --python ${VIRTUAL_ENV}/bin/python --no-cache scikit-build-core --no-deps && \ + uv pip install --python ${VIRTUAL_ENV}/bin/python --no-cache --no-build-isolation --no-deps --force-reinstall . && \ + uv pip uninstall --python ${VIRTUAL_ENV}/bin/python scikit-build-core && \ + rm -rf /tmp/bitsandbytes + +# TODO: Step 1c: 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. @@ -93,16 +130,27 @@ RUN chown -R ${USER_UID}:${USER_GID} /app # 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 \ + --overrides /opt/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/unsloth + -e /app/packages/nmp_customization_common \ + -e "/app/services/unsloth[integrations]" -# Re-pin hf-transfer (used by HF_HUB_ENABLE_HF_TRANSFER above). +# hf-transfer can pull huggingface-hub 1.x — install then re-pin hub + transformers. RUN --mount=type=cache,target=/root/.cache/uv \ uv pip install --python ${VIRTUAL_ENV}/bin/python --no-cache \ + --overrides /opt/docker/preserve_base_torch.txt \ "hf-transfer>=0.1.8,<0.2" +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install --python ${VIRTUAL_ENV}/bin/python --no-cache \ + --overrides /opt/docker/preserve_base_torch.txt \ + --reinstall-package huggingface-hub \ + --reinstall-package transformers \ + huggingface-hub==${HF_HUB_VERSION} \ + transformers==${TRANSFORMERS_VERSION} + ENTRYPOINT ["/opt/venv/bin/python"] CMD ["-m", "nmp.unsloth.tasks.training", "--help"] diff --git a/docker/automodel/Dockerfile.platform-workspace b/docker/automodel/Dockerfile.platform-workspace index a6602fae08..9d4854a15c 100644 --- a/docker/automodel/Dockerfile.platform-workspace +++ b/docker/automodel/Dockerfile.platform-workspace @@ -6,6 +6,9 @@ FROM scratch AS platform-workspace # Use a reduced workspace file for this partial source tree. COPY docker/automodel/pyproject.workspace.toml pyproject.toml +# uv --overrides file the training Dockerfile reads at /app/docker/automodel/. +# (The docker/ tree moved out of services/automodel/, so it no longer rides in +# via `COPY services/automodel`; copy it explicitly into the workspace slice.) COPY docker/automodel/no_override_requirements.txt docker/automodel/no_override_requirements.txt # nemo-platform-sdk hatch build force-includes docs/ from repo root. # docs/fern/openapi/openapi.yaml is a symlink to ../../../openapi/openapi.yaml. @@ -14,6 +17,7 @@ COPY openapi openapi COPY packages/nmp_build_tools packages/nmp_build_tools COPY packages/models packages/models COPY packages/nmp_common packages/nmp_common +COPY packages/nmp_customization_common packages/nmp_customization_common COPY packages/nemo_platform_plugin packages/nemo_platform_plugin COPY sdk/python/nemo-platform sdk/python/nemo-platform COPY services/automodel services/automodel diff --git a/docker/automodel/pyproject.workspace.toml b/docker/automodel/pyproject.workspace.toml index 32838681c0..6018968fda 100644 --- a/docker/automodel/pyproject.workspace.toml +++ b/docker/automodel/pyproject.workspace.toml @@ -16,6 +16,7 @@ members = [ "sdk/python/nemo-platform", "packages/nemo_platform_plugin", "packages/nmp_common", + "packages/nmp_customization_common", "services/automodel", "services/core/models", ] @@ -26,5 +27,6 @@ models = { workspace = true } nemo-platform-sdk = { workspace = true } nemo-platform-plugin = { workspace = true } nmp-common = { workspace = true } +nmp-customization-common = { workspace = true } nmp-automodel = { workspace = true } nmp-models = { workspace = true } diff --git a/docker/unsloth/Dockerfile.platform-workspace b/docker/unsloth/Dockerfile.platform-workspace index bb6fb9ec89..2bac553792 100644 --- a/docker/unsloth/Dockerfile.platform-workspace +++ b/docker/unsloth/Dockerfile.platform-workspace @@ -14,6 +14,7 @@ COPY docs docs COPY openapi openapi COPY packages/nmp_build_tools packages/nmp_build_tools COPY packages/nmp_common packages/nmp_common +COPY packages/nmp_customization_common packages/nmp_customization_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/docker/unsloth/README.md b/docker/unsloth/README.md index 2f06cae0cb..ef1b12ba15 100644 --- a/docker/unsloth/README.md +++ b/docker/unsloth/README.md @@ -49,12 +49,12 @@ docker buildx bake \ 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. +1. `uv pip install unsloth --torch-backend=auto transformers==4.57.6 huggingface-hub==0.36.2` with + `preserve_base_torch.txt` overrides so the NGC base's PyTorch + CUDA are not + replaced. Unsloth's resolver still pulls `unsloth_zoo`, trl, peft, + accelerate, datasets, bitsandbytes, and xformers. **transformers is pinned + explicitly** to `4.57.6` (override at build time via + `--build-arg TRANSFORMERS_VERSION=...`). 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 @@ -275,7 +275,8 @@ nemo models adapters retrieve qwen-unsloth-smoke-out \ | `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 errors with `bitsandbytes`/CUDA mismatch (`libbitsandbytes_cuda131.so` not found) | Rebuild `nmp-unsloth-training` — the image compiles bitsandbytes from source against NGC CUDA 13.1 (same pattern as `nmp-automodel-base`). Override `BNB_MAX_JOBS` at build time if nvcc OOMs. | +| `WandbCallback requires wandb to be installed` | Rebuild `nmp-unsloth-training` — the image installs `wandb` and `mlflow-skinny` for integrations. | | `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. | @@ -300,13 +301,13 @@ nemo files filesets delete qwen-unsloth-smoke-out -w default 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). +- **transformers + huggingface-hub pins** — the training image pins `transformers==4.57.6` + and `huggingface-hub==0.36.2` in + `Dockerfile.nmp-unsloth-training` (compatible with unsloth's upstream + blocklists). Other HF deps (trl, peft, bitsandbytes, etc.) still come from + unsloth's resolver. **PyTorch + CUDA** stay on the NGC base stack via + `--system-site-packages` and `preserve_base_torch.txt` / `no_override_requirements.txt` + overrides (same impossible-marker pattern as automodel). +- **bitsandbytes** — compiled from source in the image (v0.49.1, same approach as + `nmp-automodel-base`) because NGC 26.02 is CUDA 13.1 and PyPI only ships + prebuilt libs through cuda130. diff --git a/docker/unsloth/no_override_requirements.txt b/docker/unsloth/no_override_requirements.txt new file mode 100644 index 0000000000..26a31b7a46 --- /dev/null +++ b/docker/unsloth/no_override_requirements.txt @@ -0,0 +1,11 @@ +# Preserve NGC base torch/CUDA and HF stack pins from Dockerfile step 1. +# Impossible markers block uv from re-resolving these when adding platform glue. +transformers; sys_platform == 'never' +huggingface-hub; sys_platform == 'never' +bitsandbytes; sys_platform == 'never' +torch; sys_platform == 'never' +torchvision; sys_platform == 'never' +torchaudio; sys_platform == 'never' +tokenizers; sys_platform == 'never' +accelerate; sys_platform == 'never' +safetensors; sys_platform == 'never' diff --git a/docker/unsloth/preserve_base_torch.txt b/docker/unsloth/preserve_base_torch.txt new file mode 100644 index 0000000000..fba1e7474f --- /dev/null +++ b/docker/unsloth/preserve_base_torch.txt @@ -0,0 +1,7 @@ +# Block uv from installing or upgrading PyTorch wheels into the venv. +# The NGC base image ships torch + CUDA; the venv uses --system-site-packages +# to inherit that stack. Impossible markers (sys_platform == 'never') are the +# same pattern as docker/automodel/no_override_requirements.txt. +torch; sys_platform == 'never' +torchvision; sys_platform == 'never' +torchaudio; sys_platform == 'never' diff --git a/docker/unsloth/pyproject.workspace.toml b/docker/unsloth/pyproject.workspace.toml index c056cbee8a..c26b586345 100644 --- a/docker/unsloth/pyproject.workspace.toml +++ b/docker/unsloth/pyproject.workspace.toml @@ -15,6 +15,7 @@ members = [ "sdk/python/nemo-platform", "packages/nemo_platform_plugin", "packages/nmp_common", + "packages/nmp_customization_common", "services/unsloth", ] @@ -23,4 +24,5 @@ nmp-build-tools = { workspace = true } nemo-platform-sdk = { workspace = true } nemo-platform-plugin = { workspace = true } nmp-common = { workspace = true } +nmp-customization-common = { workspace = true } nmp-unsloth = { workspace = true } diff --git a/packages/nmp_common/src/nmp/common/integrations/__init__.py b/packages/nmp_common/src/nmp/common/integrations/__init__.py new file mode 100644 index 0000000000..a99c01cc2f --- /dev/null +++ b/packages/nmp_common/src/nmp/common/integrations/__init__.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Shared experiment-tracking integration schemas.""" + +from nmp.common.integrations.schemas import IntegrationsSpec, MlflowIntegration, WandbIntegration + +__all__ = [ + "IntegrationsSpec", + "MlflowIntegration", + "WandbIntegration", +] diff --git a/packages/nmp_common/src/nmp/common/integrations/schemas.py b/packages/nmp_common/src/nmp/common/integrations/schemas.py new file mode 100644 index 0000000000..ca678d28a0 --- /dev/null +++ b/packages/nmp_common/src/nmp/common/integrations/schemas.py @@ -0,0 +1,157 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Experiment-tracking integrations schema shared across platform services and plugins.""" + +from __future__ import annotations + +import warnings +from typing import Self + +from nmp.common.api.common import SecretRef +from pydantic import BaseModel, ConfigDict, Field, model_validator + + +class WandbIntegration(BaseModel): + """Weights & Biases integration configuration. + + To enable W&B, provide a non-null ``wandb`` object on :class:`IntegrationsSpec`. + Provide ``api_key_secret`` referencing a secret that contains ``WANDB_API_KEY``. + Optionally set ``base_url`` for self-hosted W&B servers. + """ + + model_config = ConfigDict(extra="forbid") + + project: str | None = Field( + default=None, + description="W&B project name (groups related runs). Defaults to output.name if not set.", + ) + name: str | None = Field( + default=None, + description="W&B run name. Defaults to job_id if not provided.", + ) + entity: str | None = Field( + default=None, + description="W&B entity (team or username).", + ) + tags: list[str] | None = Field( + default=None, + description="W&B tags for filtering runs.", + ) + notes: str | None = Field( + default=None, + description="W&B notes/description for the run.", + ) + base_url: str | None = 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).", + ) + + @model_validator(mode="before") + @classmethod + def _normalize_legacy_fields(cls, data: object) -> object: + if not isinstance(data, dict): + return data + normalized = dict(data) + if "run_name" in normalized: + warnings.warn( + "integrations.wandb.run_name is deprecated; use 'name' instead.", + DeprecationWarning, + stacklevel=2, + ) + # Always drop the legacy key so extra="forbid" can't trip when both + # are present; only adopt it as 'name' when 'name' isn't already set. + run_name = normalized.pop("run_name") + if normalized.get("name") is None: + normalized["name"] = run_name + return normalized + + +class MlflowIntegration(BaseModel): + """MLflow integration configuration. + + To enable MLflow, provide a non-null ``mlflow`` object on :class:`IntegrationsSpec`. + """ + + model_config = ConfigDict(extra="forbid") + + @model_validator(mode="before") + @classmethod + def _normalize_legacy_fields(cls, data: object) -> object: + if not isinstance(data, dict): + return data + normalized = dict(data) + if "run_name" in normalized: + warnings.warn( + "integrations.mlflow.run_name is deprecated; use 'name' instead.", + DeprecationWarning, + stacklevel=2, + ) + # Always drop the legacy key so extra="forbid" can't trip when both + # are present; only adopt it as 'name' when 'name' isn't already set. + run_name = normalized.pop("run_name") + if normalized.get("name") is None: + normalized["name"] = run_name + return normalized + + experiment_name: str | None = Field( + default=None, + description="MLflow experiment name (groups related runs). Defaults to output.name if not set.", + ) + name: str | None = Field( + default=None, + description="MLflow run name. Defaults to job_id if not provided.", + ) + tags: dict[str, str] | None = Field( + default=None, + description="MLflow tags as key-value pairs for filtering runs.", + ) + description: str | None = Field( + default=None, + description="MLflow run description.", + ) + tracking_uri: str | None = 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 IntegrationsSpec(BaseModel): + """Third-party experiment-tracking integrations for a job spec. + + Each integration is requested by presence: omit or set a field to ``null`` to + disable it. Activation at training time still requires credentials/URIs + (see compile-time warnings and runtime builders). + """ + + model_config = ConfigDict(extra="forbid") + + wandb: WandbIntegration | None = Field( + default=None, + description="Weights & Biases integration configuration.", + ) + mlflow: MlflowIntegration | None = Field( + default=None, + description="MLflow integration configuration.", + ) + + @model_validator(mode="after") + def _reject_empty_integration_blocks(self) -> Self: + """Reject empty objects that look like accidental toggles.""" + if self.wandb is not None and not self.wandb.model_dump(exclude_none=True): + raise ValueError( + "integrations.wandb must include at least one configuration field or be omitted/null to disable W&B." + ) + if self.mlflow is not None and not self.mlflow.model_dump(exclude_none=True): + raise ValueError( + "integrations.mlflow must include at least one configuration field " + "or be omitted/null to disable MLflow." + ) + return self diff --git a/packages/nmp_common/tests/integrations/test_schemas.py b/packages/nmp_common/tests/integrations/test_schemas.py new file mode 100644 index 0000000000..be94f11fa2 --- /dev/null +++ b/packages/nmp_common/tests/integrations/test_schemas.py @@ -0,0 +1,97 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import warnings + +import pytest +from nmp.common.integrations import IntegrationsSpec, MlflowIntegration, WandbIntegration +from pydantic import ValidationError + + +class TestWandbIntegration: + def test_accepts_full_config(self) -> None: + wandb = WandbIntegration.model_validate( + { + "project": "proj", + "name": "run-1", + "entity": "team", + "tags": ["sft"], + "notes": "notes", + "base_url": "https://wandb.example.com", + "api_key_secret": "default/wandb-key", + }, + ) + assert wandb.project == "proj" + assert wandb.name == "run-1" + assert wandb.api_key_secret is not None + assert wandb.api_key_secret.root == "default/wandb-key" + + def test_run_name_deprecated_shim_maps_to_name(self) -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + wandb = WandbIntegration.model_validate({"run_name": "legacy-run"}) + assert wandb.name == "legacy-run" + assert len(caught) == 1 + assert issubclass(caught[0].category, DeprecationWarning) + assert "run_name" in str(caught[0].message) + + def test_rejects_enabled_flag(self) -> None: + with pytest.raises(ValidationError, match="enabled"): + WandbIntegration.model_validate({"enabled": True, "project": "p"}) + + def test_rejects_unknown_fields(self) -> None: + with pytest.raises(ValidationError): + WandbIntegration.model_validate({"project": "p", "unknown": True}) + + +class TestMlflowIntegration: + def test_accepts_full_config(self) -> None: + mlflow = MlflowIntegration.model_validate( + { + "experiment_name": "exp", + "name": "run-1", + "tracking_uri": "http://mlflow:5000", + "tags": {"team": "nlp"}, + "description": "desc", + }, + ) + assert mlflow.experiment_name == "exp" + assert mlflow.name == "run-1" + assert mlflow.tags == {"team": "nlp"} + + def test_run_name_deprecated_shim_maps_to_name(self) -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + mlflow = MlflowIntegration.model_validate({"run_name": "legacy-run"}) + assert mlflow.name == "legacy-run" + assert len(caught) == 1 + assert issubclass(caught[0].category, DeprecationWarning) + assert "run_name" in str(caught[0].message) + + +class TestIntegrationsSpec: + def test_presence_enables_integrations(self) -> None: + spec = IntegrationsSpec.model_validate( + { + "wandb": {"project": "p"}, + "mlflow": {"tracking_uri": "http://mlflow:5000"}, + }, + ) + assert spec.wandb is not None + assert spec.mlflow is not None + + def test_rejects_empty_wandb_block(self) -> None: + with pytest.raises(ValidationError, match="integrations.wandb"): + IntegrationsSpec.model_validate({"wandb": {}}) + + def test_rejects_empty_mlflow_block(self) -> None: + with pytest.raises(ValidationError, match="integrations.mlflow"): + IntegrationsSpec.model_validate({"mlflow": {}}) + + def test_rejects_report_to_on_input(self) -> None: + with pytest.raises(ValidationError, match="report_to"): + IntegrationsSpec.model_validate({"report_to": ["wandb"]}) + + def test_rejects_enabled_on_input(self) -> None: + with pytest.raises(ValidationError, match="enabled"): + IntegrationsSpec.model_validate({"wandb": {"enabled": False}}) diff --git a/packages/nmp_customization_common/README.md b/packages/nmp_customization_common/README.md new file mode 100644 index 0000000000..a8e7cc070a --- /dev/null +++ b/packages/nmp_customization_common/README.md @@ -0,0 +1,21 @@ +# nmp-customization-common + +Shared library for the NeMo Platform customization training backends (`unsloth`, `automodel`). + +It hosts code that both backends previously duplicated, under the `nmp.customization_common` +namespace: + +- **Plugin side** (imported by the flat `nemo__plugin` modules): a parametrized jobs-client SDK + factory, a `BaseContributor`, the CLI `submit`/`run` override machinery, a base plugin config, and a + `BaseSubmitJob`. +- **Service side** (imported by `nmp.`): the job context, platform client helpers, progress + reporters, file_io / model_entity task runners, training callbacks, shared step schemas, value enums, + container-path constants, image resolution, and the compiler scaffold. + +Each backend keeps thin shim modules at the paths its entry points and the customizer SDK hub import by +string (`nemo__plugin.contributor:Contributor`, `nemo__plugin.jobs.jobs:Job`, +`nemo__plugin.sdk.resources:{Customization, AsyncCustomization}`) — those symbols must +not move. + +This package ships into the shared `nmp.*` namespace (no `src/nmp/__init__.py`) and carries no entry +points; discovery stays on the concrete plugins. diff --git a/packages/nmp_customization_common/pyproject.toml b/packages/nmp_customization_common/pyproject.toml new file mode 100644 index 0000000000..8e9f460f6b --- /dev/null +++ b/packages/nmp_customization_common/pyproject.toml @@ -0,0 +1,38 @@ +[project] +name = "nmp-customization-common" +version = "0.1.0" +description = "Shared library for NeMo customization training backends (unsloth, automodel) — plugin SDK/contributor bases and service-side task/compile helpers." +readme = "README.md" +requires-python = ">=3.11,<3.14" +dependencies = [ + "nmp-common", + "nemo-platform-sdk", + "nemo-platform-plugin", + "pydantic>=2.10.6", + "pydantic-settings>=2.6.1", + "httpx>=0.27.0", + "tenacity>=8.5.0", +] + +[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 } +nemo-platform-plugin = { 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/packages/nmp_customization_common/src/nmp/customization_common/cli/overrides.py b/packages/nmp_customization_common/src/nmp/customization_common/cli/overrides.py new file mode 100644 index 0000000000..f2e0791ce0 --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/cli/overrides.py @@ -0,0 +1,107 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Shared CLI override machinery for customization contributor plugins. + +After the platform's ``_add_run_command`` / ``_add_submit_command`` register the +default verbs, both backends swap in the same shapes: + +- ``submit`` → positional ``JOB_JSON`` argument plus standard submit flags; + loads + validates the JSON (via the backend's ``load_job_json``), then + delegates to the original ``submit`` callback with ``--spec`` set. +- ``run`` → hard-fails with a "submit-only" message (these backends run + remotely in a container, not locally). +- ``explain`` → unchanged. + +Only the backend's ``load_job_json``, the ``JOB_JSON`` help text, and the +run-disabled message differ; everything else is shared here. +""" + +from collections.abc import Callable +from pathlib import Path + +import typer + +LoadJobJson = Callable[[Path], str] + + +def apply_job_cli_overrides( + group: typer.Typer, + *, + load_job_json: LoadJobJson, + job_json_help: str, + run_disabled_message: str, +) -> None: + """Drop the default ``run``/``submit`` verbs, then re-register the overrides. + + Order matters: drop first, then re-register. Typer iterates + ``registered_commands`` in insertion order, so stale entries would route + users back to the auto-generated shapes. + """ + _replace_job_run_disabled(group, job_json_help, run_disabled_message) + _replace_job_submit(group, load_job_json, job_json_help) + + +def _pluck_callback(group: typer.Typer, verb: str) -> Callable[..., 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 command.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, job_json_help: str, run_disabled_message: str) -> None: + """Replace ``run`` with a hard-fail explainer (these backends are submit-only).""" + _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(run_disabled_message, err=True, fg=typer.colors.RED) + raise typer.Exit(code=1) + + +def _replace_job_submit(group: typer.Typer, load_job_json: LoadJobJson, job_json_help: str) -> None: + """Replace ``submit`` with a ``JOB_JSON`` positional + standard submit flags.""" + original = _pluck_callback(group, "submit") + # Drop the original before re-registering so we don't leave a duplicate + # ``submit`` entry (Typer would otherwise keep both and dispatch the last). + _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/packages/nmp_customization_common/src/nmp/customization_common/contributor/base.py b/packages/nmp_customization_common/src/nmp/customization_common/contributor/base.py new file mode 100644 index 0000000000..8ea7ed2aab --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/contributor/base.py @@ -0,0 +1,127 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Base customization contributor. + +Both the unsloth and automodel contributors implement the same +``get_routers`` / ``get_cli`` / ``get_authz_contribution`` shape, differing only +in a handful of class-level values. This base collapses that; each backend's +``contributor.py`` keeps a small subclass at the entry-point path +(``nemo__plugin.contributor:Contributor``). +""" + +from __future__ import annotations + +from typing import Any, Callable, ClassVar + +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 + + +class BaseContributor: + """Registers a backend's routes/CLI/authz under the customization router.""" + + #: Backend route segment / contributor key (e.g. ``"unsloth"``). + name: ClassVar[str] + #: The backend's ``NemoJob`` subclass. + job_cls: ClassVar[type[Any]] + #: ``nemo customization `` Typer help text. + cli_help: ClassVar[str] + #: Description for the jobs ``RouterSpec``. + jobs_router_description: ClassVar[str] + #: Platform services the backend's container submit flow depends on. + dependencies: ClassVar[list[str]] = ["entities", "auth", "jobs", "secrets", "files", "models"] + + @staticmethod + def generate_job_name() -> str: + """Generate a unique job name. Overridden per backend.""" + raise NotImplementedError + + def apply_cli_overrides(self, app: typer.Typer) -> None: + """Apply backend-specific submit/run CLI overrides. Overridden per backend.""" + raise NotImplementedError + + @property + def _title(self) -> str: + return self.name.capitalize() + + def _get_config(self) -> Any: + """Return the backend plugin config. Overridden per backend.""" + raise NotImplementedError + + def get_routers(self) -> list[RouterSpec]: + """Health endpoint + ``add_job_routes`` for the backend job collection.""" + config = self._get_config() + router = APIRouter() + + @router.get("/healthz") + async def healthz() -> dict[str, str]: + return {"backend": self.name, "status": "ok"} + + jobs_router = add_job_routes( + self.job_cls, + service_name="customization", + generate_job_name=self.generate_job_name, + route_options=[JobRouteOption.CORE], + default_profile=config.default_training_execution_profile, + ) + + return [ + RouterSpec( + router=router, + prefix=f"/v2/workspaces/{{workspace}}/{self.name}", + tag=self._title, + description=f"{self._title} contributor health.", + ), + RouterSpec( + router=jobs_router, + prefix="/v2/workspaces/{workspace}", + tag=f"{self._title} Jobs", + description=self.jobs_router_description, + ), + ] + + def get_cli(self) -> typer.Typer: + """Compose run/submit/explain verbs, then apply backend-specific overrides.""" + from nemo_platform_plugin.commands import ( + _add_explain_command, + _add_run_command, + _add_submit_command, + ) + from nemo_platform_plugin.scheduler import NemoJobScheduler + + app = typer.Typer(name=self.name, help=self.cli_help, no_args_is_help=True) + scheduler = NemoJobScheduler() + _add_run_command(app, self.job_cls, scheduler) + _add_submit_command(app, self.job_cls, scheduler) + _add_explain_command(app, self.job_cls, scheduler) + self.apply_cli_overrides(app) + return app + + def get_authz_contribution(self) -> AuthzContribution: + """Register the backend job routes with the platform authorization policy.""" + return authz_for_workspace_job_collection( + api_area="customization", + collection_suffix=f"/{self.name}/jobs", + permission_prefix=f"customization.{self.name}.jobs", + include_healthz=True, + healthz_suffix=f"/{self.name}/healthz", + ) + + def get_sdk_resources(self) -> CustomizationContributorSDKResources | None: + """Return SDK resource classes for ``client.customization.``. + + Overridden per backend to supply the sync/async ``Customization`` + classes (the customization hub composes them). Defaults to ``None`` for a + backend with no Python SDK surface. + """ + return None + + +# Re-exported so subclasses can annotate their override signatures if desired. +GenerateJobName = Callable[[], str] diff --git a/packages/nmp_customization_common/src/nmp/customization_common/contributor/config.py b/packages/nmp_customization_common/src/nmp/customization_common/contributor/config.py new file mode 100644 index 0000000000..b26c2f4165 --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/contributor/config.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Base plugin config + job-id helper for customization contributor plugins.""" + +from __future__ import annotations + +import uuid + +from pydantic_settings import BaseSettings + + +class BaseTrainingPluginConfig(BaseSettings): + """Environment-driven settings shared by customization contributor plugins. + + Subclasses set their own ``model_config`` (``env_prefix``) and may add + backend-specific fields (e.g. automodel's image defaults). + """ + + default_training_execution_profile: str = "gpu" + + +def generate_job_id(prefix: str) -> str: + """Generate a unique job name (``-<12 hex>``) when the submitter omits one.""" + return f"{prefix}-{uuid.uuid4().hex[:12]}" diff --git a/packages/nmp_customization_common/src/nmp/customization_common/contributor/jobs.py b/packages/nmp_customization_common/src/nmp/customization_common/contributor/jobs.py new file mode 100644 index 0000000000..b56c555802 --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/contributor/jobs.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Base remote-submit training job for customization backends. + +Both backends submit a 4-step ``PlatformJobSpec`` (download → train → upload → +model-entity) executed on the platform GPU cluster. ``to_spec`` and the +Docker-runtime guard are shared here; ``compile`` genuinely diverges (compiler +call convention, schema validation, profile resolution) and stays per-backend. +""" + +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.exceptions import PlatformJobCompilationError +from pydantic import BaseModel + + +def require_docker_runtime(backend_label: str) -> None: + """Refuse to compile when the platform isn't configured for Docker. + + 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( + f"{backend_label} 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( + f"{backend_label} training requires a reachable Docker daemon (platform.runtime: docker).", + ) + + +class BaseSubmitJob(NemoJob): + """Shared submit-only job scaffold. + + Subclasses set the ``NemoJob`` ClassVars (``name``, ``description``, + ``job_collection_path``, ``input_spec_schema``, ``spec_schema``), implement + :meth:`_transform` and :meth:`compile`, and may set :attr:`docker_runtime_label`. + """ + + dependencies: ClassVar[list[str]] = ["entities", "auth", "jobs", "secrets", "files", "models"] + #: Human-readable backend name used in the Docker-runtime guard messages. + docker_runtime_label: ClassVar[str] = "Training" + + @classmethod + async def _transform(cls, job_input: BaseModel, workspace: str, async_sdk: AsyncNeMoPlatform) -> BaseModel: + """Validate platform refs and return the canonical output spec. Per backend.""" + raise NotImplementedError + + @classmethod + async def to_spec( + cls, + input_spec: BaseModel, + workspace: str, + entity_client: object, + async_sdk: object, + is_local: bool, + ) -> BaseModel: + """Validate platform refs, resolve naming, return the canonical spec.""" + del entity_client, is_local + schema = cls.input_spec_schema + if schema is None: + raise PlatformJobCompilationError(f"{cls.__name__} is missing an input_spec_schema.") + job_input = input_spec if isinstance(input_spec, schema) else schema.model_validate(input_spec.model_dump()) + return await cls._transform(job_input, workspace, cast(AsyncNeMoPlatform, async_sdk)) diff --git a/packages/nmp_customization_common/src/nmp/customization_common/contributor/transform.py b/packages/nmp_customization_common/src/nmp/customization_common/contributor/transform.py new file mode 100644 index 0000000000..7ab21ef54c --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/contributor/transform.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Shared output-naming helpers for the input → canonical spec transform. + +Both backends generate the output entity/fileset name the same way when the +submitter omits one: slugified model basename + slugified dataset basename + +a short random suffix. Those helpers live here; the schema-bound assembly of +the canonical output (which fields exist, how the output type is inferred) +stays in each backend's ``transform.py``. +""" + +from __future__ import annotations + +import re +import uuid + +from nmp.common.entities.utils import parse_entity_ref + +_MAX_PREFIX_LEN = 50 +_HEX_LEN = 12 +_NAME_SAFE_RE = re.compile(r"[^a-zA-Z0-9_-]+") + + +def slugify(token: str) -> str: + """Reduce an arbitrary string to a platform-safe name segment. + + Runs of characters outside ``[A-Za-z0-9_-]`` collapse to a single ``-``; + leading/trailing ``-`` are trimmed; an empty result falls back to ``"x"`` + so the segment is never empty. E.g. ``"Meta-Llama-3.1 8B"`` -> ``"Meta-Llama-3-1-8B"``. + """ + cleaned = _NAME_SAFE_RE.sub("-", token).strip("-") + return cleaned or "x" + + +def random_suffix(prefix: str) -> str: + """Append a short random hex suffix to a length-capped prefix.""" + truncated = prefix[:_MAX_PREFIX_LEN].rstrip("-") + return f"{truncated}-{uuid.uuid4().hex[:_HEX_LEN]}" + + +def model_basename(model_ref: str, workspace: str) -> str: + """Slugified entity name of a model ref (handles ``workspace/name`` and ``name``).""" + return slugify(parse_entity_ref(model_ref, workspace).name) + + +def dataset_basename(uri: str) -> str: + """Slugified last segment of a fileset ref. + + Handles an optional protocol prefix (e.g. ``fileset://``) and + ``workspace/name`` form. + """ + cleaned = uri.split("://", 1)[-1] + last = cleaned.rsplit("/", 1)[-1] or cleaned + return slugify(last) + + +def generated_output_name(model_ref: str, dataset_ref: str, workspace: str) -> str: + """Default output entity/fileset name used when the submitter omits one.""" + return random_suffix(f"{model_basename(model_ref, workspace)}-{dataset_basename(dataset_ref)}") diff --git a/packages/nmp_customization_common/src/nmp/customization_common/integrations/__init__.py b/packages/nmp_customization_common/src/nmp/customization_common/integrations/__init__.py new file mode 100644 index 0000000000..7a197b4102 --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/integrations/__init__.py @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Customization training integration compile/runtime helpers.""" + +from nmp.customization_common.integrations.compiler import ( + collect_integration_secret_envs, + warn_incomplete_integrations, +) +from nmp.customization_common.integrations.context import IntegrationRuntimeContext +from nmp.customization_common.integrations.runtime import build_mlflow_config, build_wandb_config + +__all__ = [ + "IntegrationRuntimeContext", + "build_mlflow_config", + "build_wandb_config", + "collect_integration_secret_envs", + "warn_incomplete_integrations", +] diff --git a/packages/nmp_customization_common/src/nmp/customization_common/integrations/compiler.py b/packages/nmp_customization_common/src/nmp/customization_common/integrations/compiler.py new file mode 100644 index 0000000000..d0f5fbf675 --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/integrations/compiler.py @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Compile-time helpers for experiment-tracking integrations.""" + +import logging + +from nemo_platform_plugin.jobs.api_factory import ( + EnvironmentVariable, + EnvironmentVariableFromSecret, +) +from nmp.common.integrations import IntegrationsSpec + +logger = logging.getLogger(__name__) + + +def warn_incomplete_integrations(integrations: IntegrationsSpec | None) -> None: + """Log when integrations are requested but may not activate at runtime.""" + if integrations and integrations.wandb and not integrations.wandb.api_key_secret: + logger.warning( + "integrations.wandb is configured but api_key_secret is missing; " + "W&B will only activate if WANDB_API_KEY is already set in the training container." + ) + if integrations and integrations.mlflow and not integrations.mlflow.tracking_uri: + logger.warning( + "integrations.mlflow is configured but tracking_uri is missing; " + "MLflow will only activate if MLFLOW_TRACKING_URI is already set in the training container." + ) + + +def collect_integration_secret_envs( + integrations: IntegrationsSpec | None, +) -> list[EnvironmentVariable]: + """Collect secret environment variables from integration configs. + + Secrets are propagated via ``PlatformJobStep.environment`` (not step config) + so the Jobs service can resolve secret references at runtime. + """ + if not integrations or not integrations.wandb or not integrations.wandb.api_key_secret: + return [] + + return [ + EnvironmentVariable( + name="WANDB_API_KEY", + from_secret=EnvironmentVariableFromSecret( + name=integrations.wandb.api_key_secret.root, + ), + ), + ] diff --git a/packages/nmp_customization_common/src/nmp/customization_common/integrations/context.py b/packages/nmp_customization_common/src/nmp/customization_common/integrations/context.py new file mode 100644 index 0000000000..404add42b5 --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/integrations/context.py @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Runtime context for building experiment-tracking integration configs.""" + +from dataclasses import dataclass +from typing import Self + +from nmp.common.integrations import IntegrationsSpec, MlflowIntegration, WandbIntegration +from nmp.customization_common.service.context import NMPJobContext + + +@dataclass(frozen=True) +class IntegrationRuntimeContext: + """Inputs shared by W&B and MLflow runtime config builders.""" + + wandb: WandbIntegration | None + mlflow: MlflowIntegration | None + output_name: str + workspace_path: str + model_name: str | None + job_ctx: NMPJobContext + framework: str + + @classmethod + def from_integrations_spec( + cls, + *, + integrations: IntegrationsSpec | None, + output_name: str, + workspace_path: str, + model_name: str | None, + job_ctx: NMPJobContext, + framework: str, + ) -> Self: + """Build context from the canonical job integrations object.""" + return cls( + wandb=integrations.wandb if integrations else None, + mlflow=integrations.mlflow if integrations else None, + output_name=output_name, + workspace_path=workspace_path, + model_name=model_name, + job_ctx=job_ctx, + framework=framework, + ) diff --git a/packages/nmp_customization_common/src/nmp/customization_common/integrations/runtime.py b/packages/nmp_customization_common/src/nmp/customization_common/integrations/runtime.py new file mode 100644 index 0000000000..48e39fadd4 --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/integrations/runtime.py @@ -0,0 +1,151 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Build W&B and MLflow runtime configs for customization training backends.""" + +import logging +import os +from pathlib import Path +from typing import Any + +from nmp.customization_common.integrations.context import IntegrationRuntimeContext + +logger = logging.getLogger(__name__) + + +def _resolve_wandb_dir(workspace_path: str) -> Path: + """Return a W&B run directory outside model artifact upload trees. + + Container jobs write uploadable checkpoints under ``output_model`` (unsloth) + or keep training scratch under ``training`` (automodel). W&B metadata must + not live in those trees — use sibling ``ephemeral/wandb`` instead. + """ + workspace = Path(workspace_path) + if workspace.name in ("output_model", "training"): + return workspace.parent / "ephemeral" / "wandb" + return workspace / "wandb" + + +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*.""" + 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(ctx: IntegrationRuntimeContext) -> dict[str, Any] | None: + """Build MLflow config passed to backend logging setup. + + Run naming strategy (same as W&B): + - ``name`` on input resolves to ``run_name`` in the output dict (defaults to job_id) + - task_id is added to tags for granular execution tracking + + Missing tracking URI disables integration with a warning. + """ + user_config = ctx.mlflow + if not user_config: + return None + + 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": "nemo-platform", + "framework": ctx.framework, + } + if ctx.job_ctx.workspace: + tags["workspace"] = ctx.job_ctx.workspace + if ctx.job_ctx.job_id: + tags["job"] = ctx.job_ctx.job_id + if ctx.job_ctx.task: + tags["task"] = ctx.job_ctx.task + if ctx.model_name: + tags["model_name"] = ctx.model_name + + if user_config.tags: + tags.update(user_config.tags) + if user_config.description: + tags["mlflow.note.content"] = user_config.description + + experiment_name = _resolve_with_fallback( + user_config.experiment_name, + ctx.output_name, + "default-experiment", + field_label="MLflow experiment_name", + ) + run_name = _resolve_with_fallback( + user_config.name, + ctx.job_ctx.job_id, + "default-run", + field_label="MLflow name", + ) + + return { + "tracking_uri": tracking_uri, + "experiment_name": experiment_name, + "run_name": run_name, + "tags": tags, + } + + +def build_wandb_config(ctx: IntegrationRuntimeContext) -> dict[str, Any] | None: + """Build W&B config passed to ``wandb.init()`` by Automodel training. + + See: https://docs.wandb.ai/ref/python/init + """ + user_config = ctx.wandb + if not user_config: + return None + + wandb_api_key = os.environ.get("WANDB_API_KEY") + if not wandb_api_key: + if user_config.base_url: + logger.warning( + "WANDB_API_KEY is not set; attempting W&B with base_url only (%s). " + "This only works when the server allows access without a cloud API key.", + user_config.base_url, + ) + else: + logger.warning("WandB API key is not set and no base_url is provided, skipping WandB integration") + return None + + run_dir = _resolve_wandb_dir(ctx.workspace_path) + + tags: list[str] = ["service:nemo-platform", f"framework:{ctx.framework}"] + if ctx.job_ctx.workspace: + tags.append(f"workspace:{ctx.job_ctx.workspace}") + if ctx.job_ctx.job_id: + tags.append(f"job:{ctx.job_ctx.job_id}") + if ctx.job_ctx.task: + tags.append(f"task:{ctx.job_ctx.task}") + if ctx.model_name: + tags.append(f"model:{ctx.model_name}") + if user_config.tags: + tags.extend(user_config.tags) + + wandb_config: dict[str, Any] = { + "project": _resolve_with_fallback(user_config.project, ctx.output_name, "default-project"), + "name": _resolve_with_fallback(user_config.name, ctx.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: + 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/packages/nmp_customization_common/src/nmp/customization_common/schemas/file_io.py b/packages/nmp_customization_common/src/nmp/customization_common/schemas/file_io.py new file mode 100644 index 0000000000..6f8abd49a5 --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/schemas/file_io.py @@ -0,0 +1,186 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Schemas for the customization file_io task configuration. + +Shared by both the unsloth and automodel backends; each service re-exports these +from its ``app/jobs/file_io/schemas.py`` so existing import paths keep working. +""" + +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 NMPJobContext. + 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: + """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] + # split("/", 1) yields at most 2 parts, so this is the 2-part case. + return parts[0], parts[1] + + @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: object) -> object: + """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!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. + + 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..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.""" + + +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. + + This is a security error raised when user-provided paths like '../..' would + result in file operations 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/packages/nmp_customization_common/src/nmp/customization_common/schemas/model_entity.py b/packages/nmp_customization_common/src/nmp/customization_common/schemas/model_entity.py new file mode 100644 index 0000000000..74c35f7630 --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/schemas/model_entity.py @@ -0,0 +1,99 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Schemas for the model_entity container task configuration. + +Shared by both backends; each service re-exports these from its +``app/jobs/model_entity/schemas.py`` so existing import paths keep working. +""" + +from __future__ import annotations + +from typing import Optional + +from nmp.customization_common.schemas.file_io import FileSetRef +from nmp.customization_common.schemas.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, gt=0, 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 = Field(gt=0) + alpha: int = Field(gt=0) + + +class ModelEntityTaskConfig(BaseModel): + """Configuration for the model_entity task. + + Used when running ``python -m nmp..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/packages/nmp_customization_common/src/nmp/customization_common/schemas/values.py b/packages/nmp_customization_common/src/nmp/customization_common/schemas/values.py new file mode 100644 index 0000000000..8964f2ee6c --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/schemas/values.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Value enums shared by the customization backends. + +Only the enums with identical members across unsloth and automodel live here. +``TrainingType`` stays per-backend (different supported algorithms), and +``CheckpointFormat`` / ``Precision`` are automodel-only. +""" + +from enum import Enum, StrEnum + + +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 (LoRA only) or model (merged / full).""" + + ADAPTER = "adapter" + MODEL = "model" diff --git a/packages/nmp_customization_common/src/nmp/customization_common/sdk/client.py b/packages/nmp_customization_common/src/nmp/customization_common/sdk/client.py new file mode 100644 index 0000000000..ec02f3054a --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/sdk/client.py @@ -0,0 +1,340 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Parametrized customization jobs-collection SDK client. + +Both the unsloth and automodel contributor plugins expose an identical +``client.customization.`` SDK namespace — a jobs collection whose only +backend-specific variable is the route segment (``unsloth`` / ``automodel``). +This module collapses the three previously-duplicated SDK files +(``http_utils`` / ``job_resources`` / ``resources``) into one factory. + +Each plugin keeps a thin ``nemo__plugin/sdk/resources.py`` shim that calls +:func:`make_customization_sdk` and re-exports ``Customization`` / +``AsyncCustomization`` — the symbols the ``nemo-customizer`` SDK hub imports +by string. +""" + +from typing import Any, ClassVar +from urllib.parse import quote, urljoin + +from nemo_platform import AsyncNeMoPlatform, NeMoPlatform +from nemo_platform_plugin.jobs.schemas import PlatformJobStatusResponse +from pydantic import BaseModel + +PlatformClient = NeMoPlatform | AsyncNeMoPlatform + +_API_PREFIX = "/apis/customization" + + +# --------------------------------------------------------------------------- # +# URL / payload helpers +# --------------------------------------------------------------------------- # +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 _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("/")) + + +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, backend: str, workspace: str | None = None) -> str: + """URL for the backend's jobs collection in a workspace.""" + return url(platform, f"v2/workspaces/{{workspace}}/{backend}/jobs", workspace) + + +def job_url(platform: PlatformClient, backend: str, job_name: str, workspace: str | None = None) -> str: + """URL for a single backend job.""" + return _join_url(jobs_collection_url(platform, backend, 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: BaseModel) -> dict[str, Any]: + """Serialize a job creation request body.""" + return {"spec": spec.model_dump(mode="json")} + + +def _job_status_path(root: str, backend: str, workspace: str, job_name: str) -> str: + encoded_workspace = quote(workspace, safe="") + encoded_job = quote(job_name, safe="") + return f"{base_url(root)}/apis/customization/v2/workspaces/{encoded_workspace}/{backend}/jobs/{encoded_job}" + + +# --------------------------------------------------------------------------- # +# Job records / resources +# --------------------------------------------------------------------------- # +class JobRecord(BaseModel): + """Minimal job record returned by the customization jobs API.""" + + name: str + workspace: str + status: str | None = None + spec: dict[str, Any] | None = None + + +class JobResource: + """Sync handle for one submitted job.""" + + def __init__( + self, + backend: str, + job: JobRecord, + http_client: Any, + base_url: str, + workspace: str, + headers: dict[str, str], + ) -> None: + self._backend = backend + 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._backend, self._workspace, self.job.name), + headers=self._headers, + ) + response.raise_for_status() + return PlatformJobStatusResponse.model_validate(response.json()) + + +class AsyncJobResource: + """Async handle for one submitted job.""" + + def __init__( + self, + backend: str, + job: JobRecord, + http_client: Any, + base_url: str, + workspace: str, + headers: dict[str, str], + ) -> None: + self._backend = backend + 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._backend, self._workspace, self.job.name), + headers=self._headers, + ) + response.raise_for_status() + return PlatformJobStatusResponse.model_validate(response.json()) + + +# --------------------------------------------------------------------------- # +# Jobs collection + customization namespaces (parametrized by backend) +# --------------------------------------------------------------------------- # +class _JobsResourceBase: + backend: ClassVar[str] + record_schema: ClassVar[type[JobRecord]] = JobRecord + + _platform: PlatformClient + # Sync subclass holds an httpx.Client, async holds an httpx.AsyncClient; typed + # Any so the shared sync/async method bodies (await vs no-await) both check. + _http_client: Any + + def __init__(self, platform: PlatformClient) -> None: + self._platform = platform + self._http_client = platform._client + + def _healthz_url(self) -> str: + return url( + self._platform, + f"v2/workspaces/{{workspace}}/{self.backend}/healthz", + self._platform.workspace, + ) + + def _new_record(self, payload: Any) -> JobRecord: + return self.record_schema.model_validate(payload) + + +class JobsResource(_JobsResourceBase): + """Sync SDK namespace at ``client.customization..jobs``.""" + + def plugin_status(self) -> dict[str, object]: + """Return contributor health from the customization service.""" + response = self._http_client.get( + self._healthz_url(), + headers=platform_default_headers(self._platform), + ) + response.raise_for_status() + payload = response.json() + if not isinstance(payload, dict): + raise TypeError(f"{self.backend} health response must be a JSON object.") + return {str(key): value for key, value in payload.items()} + + def create( + self, + spec: BaseModel, + workspace: str | None = None, + name: str | None = None, + ) -> JobResource: + """Submit a training job to the platform GPU cluster.""" + body: dict[str, Any] = create_job_payload(spec) + if name is not None: + body["name"] = name + response = self._http_client.post( + jobs_collection_url(self._platform, self.backend, workspace), + json=body, + headers=platform_default_headers(self._platform), + ) + response.raise_for_status() + resolved_ws = resolve_workspace(self._platform, workspace) + return JobResource( + backend=self.backend, + job=self._new_record(response.json()), + http_client=self._http_client, + base_url=base_url(str(self._platform.base_url)), + workspace=resolved_ws, + headers=platform_default_headers(self._platform), + ) + + def get_job_resource(self, job_name: str, workspace: str | None = None) -> JobResource: + """Get a resource handle for an existing job.""" + resolved_ws = resolve_workspace(self._platform, workspace) + response = self._http_client.get( + job_url(self._platform, self.backend, job_name, resolved_ws), + headers=platform_default_headers(self._platform), + ) + response.raise_for_status() + return JobResource( + backend=self.backend, + job=self._new_record(response.json()), + http_client=self._http_client, + base_url=base_url(str(self._platform.base_url)), + workspace=resolved_ws, + headers=platform_default_headers(self._platform), + ) + + +class AsyncJobsResource(_JobsResourceBase): + """Async SDK namespace at ``client.customization..jobs``.""" + + async def plugin_status(self) -> dict[str, object]: + """Return contributor health from the customization service.""" + response = await self._http_client.get( + self._healthz_url(), + headers=platform_default_headers(self._platform), + ) + response.raise_for_status() + payload = response.json() + if not isinstance(payload, dict): + raise TypeError(f"{self.backend} health response must be a JSON object.") + return {str(key): value for key, value in payload.items()} + + async def create( + self, + spec: BaseModel, + workspace: str | None = None, + name: str | None = None, + ) -> AsyncJobResource: + """Submit a training job to the platform GPU cluster.""" + body: dict[str, Any] = create_job_payload(spec) + if name is not None: + body["name"] = name + response = await self._http_client.post( + jobs_collection_url(self._platform, self.backend, workspace), + json=body, + headers=platform_default_headers(self._platform), + ) + response.raise_for_status() + resolved_ws = resolve_workspace(self._platform, workspace) + return AsyncJobResource( + backend=self.backend, + job=self._new_record(response.json()), + http_client=self._http_client, + base_url=base_url(str(self._platform.base_url)), + workspace=resolved_ws, + headers=platform_default_headers(self._platform), + ) + + async def get_job_resource(self, job_name: str, workspace: str | None = None) -> AsyncJobResource: + """Get a resource handle for an existing job.""" + resolved_ws = resolve_workspace(self._platform, workspace) + response = await self._http_client.get( + job_url(self._platform, self.backend, job_name, resolved_ws), + headers=platform_default_headers(self._platform), + ) + response.raise_for_status() + return AsyncJobResource( + backend=self.backend, + job=self._new_record(response.json()), + http_client=self._http_client, + base_url=base_url(str(self._platform.base_url)), + workspace=resolved_ws, + headers=platform_default_headers(self._platform), + ) + + +class _CustomizationBase: + backend: ClassVar[str] + jobs_resource_cls: ClassVar[type[_JobsResourceBase]] + + def __init__(self, platform: PlatformClient) -> None: + self.jobs = self.jobs_resource_cls(platform) + + +def make_customization_sdk( + backend: str, + record_schema: type[JobRecord] = JobRecord, +) -> tuple[type[_CustomizationBase], type[_CustomizationBase]]: + """Build the sync + async ``Customization`` SDK namespace classes. + + Returns a ``(sync_cls, async_cls)`` tuple. Each class takes a platform client + and exposes ``.jobs`` — matching the shape the ``nemo-customizer`` SDK hub + instantiates as ``client.customization.``. + """ + title = backend.capitalize() + + sync_jobs = type( + f"{title}JobsResource", + (JobsResource,), + {"backend": backend, "record_schema": record_schema}, + ) + async_jobs = type( + f"Async{title}JobsResource", + (AsyncJobsResource,), + {"backend": backend, "record_schema": record_schema}, + ) + sync_cls = type( + f"{title}Customization", + (_CustomizationBase,), + {"backend": backend, "jobs_resource_cls": sync_jobs}, + ) + async_cls = type( + f"Async{title}Customization", + (_CustomizationBase,), + {"backend": backend, "jobs_resource_cls": async_jobs}, + ) + return sync_cls, async_cls diff --git a/packages/nmp_customization_common/src/nmp/customization_common/service/constants.py b/packages/nmp_customization_common/src/nmp/customization_common/service/constants.py new file mode 100644 index 0000000000..028eea65c8 --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/service/constants.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Container-path and env-var constants shared by the customization backends. + +The unsloth and automodel services expose the same path layout to the 4-step +container submit pipeline (download -> train -> upload -> model-entity). The +shared subset lives here; each backend's ``app/constants.py`` re-exports these +and adds its own ``SERVICE_NAME`` plus any backend-specific constants. +""" + +from nmp.common.jobs.constants import DEFAULT_JOB_STORAGE_PATH + +# 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 +# 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}" +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/packages/nmp_customization_common/src/nmp/customization_common/service/context.py b/packages/nmp_customization_common/src/nmp/customization_common/service/context.py new file mode 100644 index 0000000000..bc6cee5235 --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/service/context.py @@ -0,0 +1,105 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Job context for customization container task entrypoints. + +Populated from the Job Controller environment variables. Shared by both the +unsloth and automodel services; each service re-exports ``NMPJobContext`` from +its ``app/jobs/context.py`` so existing import paths keep working. +""" + +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.customization_common.service.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" + + +# 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) + + @property + def is_configured(self) -> bool: + """True only when populated from real Job Controller env vars. + + ``from_env`` fills missing identifiers with non-empty placeholder + sentinels (``unknown-job-id`` / ``unknown-step`` / ``unknown-task``). + Callers (e.g. progress reporting) must gate on this rather than a bare + truthiness check, or they issue failing SDK calls / log spam when + running outside a real platform job (local ``run`` paths, tests). + """ + return ( + self.job_id not in ("", DEFAULT_JOB_ID) + and self.step not in ("", DEFAULT_STEP) + and self.task not in ("", DEFAULT_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/packages/nmp_customization_common/src/nmp/customization_common/service/images.py b/packages/nmp_customization_common/src/nmp/customization_common/service/images.py new file mode 100644 index 0000000000..b4d4b53b74 --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/service/images.py @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Shared Docker image resolution for customization job steps. + +Each backend keeps its own image-name constants and ``get_tasks_image`` / +``get_training_image`` (their fallback behavior differs); this module holds the +common registry-resolution logic. +""" + +from __future__ import annotations + +from nemo_platform_plugin.config import get_platform_config +from nemo_platform_plugin.jobs.image import get_qualified_image + + +def resolve_qualified_image(name: str, override: str | None, image_registry: str | None) -> str: + """Resolve a job step image reference. + + Args: + name: Image repository name under the registry (e.g. ``nmp--tasks``). + override: Full image ref (e.g. from ``NMP__TASKS_IMAGE``); returned verbatim when set. + image_registry: Backend ``config.image_registry``; falls back to the platform registry. + + Returns: + Fully qualified image (``{registry}/{name}:{tag}``) unless ``override`` is set. + """ + if override: + return override + + platform_config = get_platform_config() + registry = image_registry or platform_config.image_registry + return get_qualified_image(name, registry=registry) diff --git a/packages/nmp_customization_common/src/nmp/customization_common/service/platform_client.py b/packages/nmp_customization_common/src/nmp/customization_common/service/platform_client.py new file mode 100644 index 0000000000..9a8066bdb2 --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/service/platform_client.py @@ -0,0 +1,51 @@ +# 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. + +Used by each plugin's ``transform.py`` (async, runs inside the FastAPI request +handler / ``to_spec`` flow) to validate that the submitter's ``model`` and +``dataset`` references exist before the job moves on to compile / run. +""" + +from nemo_platform import AsyncNeMoPlatform +from nemo_platform._exceptions import NotFoundError, PermissionDeniedError +from nemo_platform.types.models import ModelEntity +from nmp.common.entities.utils import parse_entity_ref +from nmp.customization_common.schemas.file_io import FileSetRef + + +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.""" + 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/packages/nmp_customization_common/src/nmp/customization_common/tasks/file_io_progress_reporter.py b/packages/nmp_customization_common/src/nmp/customization_common/tasks/file_io_progress_reporter.py new file mode 100644 index 0000000000..c0c19c73ce --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/tasks/file_io_progress_reporter.py @@ -0,0 +1,97 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Progress reporting for the file_io container task. + +Re-exported by each backend's ``tasks/file_io/progress_reporter.py``. +""" + +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.customization_common.schemas.file_io import ProgressReportError +from nmp.customization_common.service.context import NMPJobContext +from nmp.customization_common.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/packages/nmp_customization_common/src/nmp/customization_common/tasks/file_io_utils.py b/packages/nmp_customization_common/src/nmp/customization_common/tasks/file_io_utils.py new file mode 100644 index 0000000000..dba331095e --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/tasks/file_io_utils.py @@ -0,0 +1,181 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Shared fileset path/IO + error-handling helpers for the file_io task. + +Re-exported by each backend's ``tasks/file_io/utils.py``. +""" + +import json +import logging +import os +from collections.abc import Iterator +from contextlib import contextmanager +from dataclasses import dataclass +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.customization_common.schemas.file_io import ( + FileDownloadError, + FileIOTaskConfig, + FileUploadError, + PathTraversalError, + ProgressReportError, +) + +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class LocalFileInfo: + """A local file entry for upload/download progress accounting.""" + + path: str + size: int + + +def list_local_files(src_path: Path) -> list[LocalFileInfo]: + """List files under *src_path* for progress totals. + + Skips individual unreadable or transient paths (e.g. W&B log files deleted + mid-walk) instead of failing the entire listing. + """ + if not src_path.exists(): + logger.warning(f"Failed to list local files. Source path does not exist: {src_path}") + return [] + + def _on_walk_error(err: OSError) -> None: + logger.warning( + f"Skipping inaccessible path during upload listing: {getattr(err, 'filename', src_path)}. Error: {err}" + ) + + try: + if src_path.is_file(): + size = src_path.stat().st_size + logger.info(f"Found 1 file: {src_path.name}") + return [LocalFileInfo(path=src_path.name, size=size)] + + files: list[LocalFileInfo] = [] + for root, _, filenames in os.walk(src_path, onerror=_on_walk_error): + for filename in filenames: + full_path = Path(root) / filename + try: + if not full_path.is_file(): + continue + relative_path = full_path.relative_to(src_path) + files.append( + LocalFileInfo(path=str(relative_path), size=full_path.stat().st_size), + ) + except OSError as e: + logger.warning(f"Skipping unreadable file during upload listing: {full_path}. Error: {e}") + 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 [] + + +@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/packages/nmp_customization_common/src/nmp/customization_common/training/callbacks.py b/packages/nmp_customization_common/src/nmp/customization_common/training/callbacks.py new file mode 100644 index 0000000000..870b64d6ae --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/training/callbacks.py @@ -0,0 +1,129 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Training progress callback shared by the customization backends. + +Composes a :class:`nmp.customization_common.training.progress.JobsServiceProgressReporter` +and provides training-specific methods. 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. + +Backends subclass this and set :attr:`_default_backend`: unsloth stamps a +``backend`` field on each report (``"unsloth"``); automodel leaves it ``None`` so +no ``backend`` key is added (preserving its status-detail shape). Callers may also +pass ``backend`` per call (e.g. unsloth's HF trainer callback). +""" + +import logging +from typing import ClassVar + +from nmp.customization_common.training.progress import JobsServiceProgressReporter + +logger = logging.getLogger(__name__) + + +class TrainingProgressCallback: + """Report training progress to the Jobs service.""" + + #: Backend name stamped on each report when a per-call ``backend`` isn't given. + #: ``None`` means no ``backend`` field is added. + _default_backend: ClassVar[str | None] = None + + 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 _resolve_backend(self, backend: str | None) -> str | None: + return backend if backend is not None else self._default_backend + + 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, *, backend: str | None = None) -> None: + """Report that training has started with schedule information.""" + self._reporter.configure_progress_tracking(max_steps, num_epochs) + details: dict[str, object] = {"step": 0, "max_steps": max_steps, "num_epochs": num_epochs} + resolved = self._resolve_backend(backend) + if resolved is not None: + details["backend"] = resolved + self._reporter.report_running(phase="training", **details) + + def report_train_step( + self, + step: int, + epoch: int, + loss: float, + lr: float | None = None, + grad_norm: float | None = None, + *, + backend: str | None = None, + ) -> None: + """Report training step with metrics.""" + self._train_metrics.append({"step": step, "epoch": epoch, "value": loss}) + details: dict[str, object] = { + "step": step, + "epoch": epoch, + "train_loss": loss, + "lr": lr, + "grad_norm": grad_norm, + "metrics": self._build_metrics_summary(), + } + resolved = self._resolve_backend(backend) + if resolved is not None: + details["backend"] = resolved + self._reporter.report_running(phase="training", **details) + + def report_validation(self, step: int, epoch: int, val_loss: float, *, backend: str | None = None) -> None: + """Report validation results.""" + self._val_metrics.append({"step": step, "epoch": epoch, "value": val_loss}) + details: dict[str, object] = { + "step": step, + "epoch": epoch, + "val_loss": val_loss, + "metrics": self._build_metrics_summary(), + } + resolved = self._resolve_backend(backend) + if resolved is not None: + details["backend"] = resolved + self._reporter.report_running(phase="validation", **details) + + def report_checkpoint_saved( + self, + step: int, + epoch: int, + checkpoint_path: str | None = None, + *, + backend: str | None = None, + ) -> None: + """Report that a checkpoint was saved.""" + details: dict[str, object] = {"step": step, "epoch": epoch, "checkpoint_path": checkpoint_path} + resolved = self._resolve_backend(backend) + if resolved is not None: + details["backend"] = resolved + self._reporter.report_running(phase="checkpoint_saved", **details) + + def report_epoch_end(self, step: int, epoch: int, *, backend: str | None = None) -> None: + """Report that an epoch has completed.""" + details: dict[str, object] = {"step": step, "epoch": epoch} + resolved = self._resolve_backend(backend) + if resolved is not None: + details["backend"] = resolved + self._reporter.report_running(phase="epoch_end", **details) + + def close(self) -> None: + """Clean up resources.""" + self._reporter.close() diff --git a/packages/nmp_customization_common/src/nmp/customization_common/training/progress.py b/packages/nmp_customization_common/src/nmp/customization_common/training/progress.py new file mode 100644 index 0000000000..8ab1381be1 --- /dev/null +++ b/packages/nmp_customization_common/src/nmp/customization_common/training/progress.py @@ -0,0 +1,112 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""High-level progress reporting for training tasks. + +Provides progress reporting to the Jobs service using the NeMo Platform SDK. +``JobsServiceProgressReporter`` handles high-level phase reporting for the +training runner; backends subclass it (or instantiate it directly) supplying +their own ``service_name`` so the task SDK resolves the right credentials. + +For training-specific metrics (loss, validation, checkpoints) see the +``TrainingProgressCallback`` which composes this reporter. +""" + +import logging +import os +from typing import Any, cast + +from nmp.common.sdk_factory import get_task_sdk +from nmp.customization_common.service.context import NMPJobContext + +logger = logging.getLogger(__name__) + + +class JobsServiceProgressReporter: + """Reports high-level progress to the Jobs service.""" + + def __init__(self, job_ctx: NMPJobContext, service_name: str): + 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 + + # Gate on real job context, not bare truthiness: from_env() fills missing + # identifiers with non-empty sentinel defaults, which would otherwise + # enable reporting (and failing SDK calls) outside a real job run. + self._enabled = self._is_main_rank and self._job_ctx.is_configured + + 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 + # Clamp to 100: step can exceed max_steps (e.g. resumed/over-run), and + # downstream progress consumers expect a bounded percentage. + return min(100, 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, # ty: ignore[invalid-argument-type] + 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 = cast(dict[str, Any], (task.status_details or {}).get("metrics", {}) or {}) + 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/plugins/nemo-automodel/src/nemo_automodel_plugin/jobs/__init__.py b/packages/nmp_customization_common/src/nmp/customization_common/version.py similarity index 60% rename from plugins/nemo-automodel/src/nemo_automodel_plugin/jobs/__init__.py rename to packages/nmp_customization_common/src/nmp/customization_common/version.py index e5725ea5a4..5325534b5a 100644 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/jobs/__init__.py +++ b/packages/nmp_customization_common/src/nmp/customization_common/version.py @@ -1,2 +1,6 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 + +"""Version marker for the nmp-customization-common shared library.""" + +__version__ = "0.1.0" diff --git a/packages/nmp_customization_common/tests/integrations/test_compiler.py b/packages/nmp_customization_common/tests/integrations/test_compiler.py new file mode 100644 index 0000000000..c4d65b48cb --- /dev/null +++ b/packages/nmp_customization_common/tests/integrations/test_compiler.py @@ -0,0 +1,68 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from nmp.common.api.common import SecretRef +from nmp.common.integrations import IntegrationsSpec, WandbIntegration +from nmp.customization_common.integrations import collect_integration_secret_envs, warn_incomplete_integrations + + +class TestCollectIntegrationSecretEnvs: + def test_no_integrations(self) -> None: + assert collect_integration_secret_envs(None) == [] + + def test_wandb_without_secret(self) -> None: + integrations = IntegrationsSpec.model_validate({"wandb": {"project": "my-project"}}) + assert collect_integration_secret_envs(integrations) == [] + + def test_wandb_with_secret(self) -> None: + integrations = IntegrationsSpec( + wandb=WandbIntegration( + project="my-project", + api_key_secret=SecretRef("my-wandb-secret"), + ), + ) + result = collect_integration_secret_envs(integrations) + assert len(result) == 1 + assert result[0] == { + "name": "WANDB_API_KEY", + "from_secret": {"name": "my-wandb-secret"}, + } + + def test_wandb_with_workspace_qualified_secret(self) -> None: + integrations = IntegrationsSpec( + wandb=WandbIntegration(api_key_secret=SecretRef("my-workspace/my-wandb-secret")), + ) + result = collect_integration_secret_envs(integrations) + assert result[0] == { + "name": "WANDB_API_KEY", + "from_secret": {"name": "my-workspace/my-wandb-secret"}, + } + + def test_mlflow_only_no_secrets(self) -> None: + integrations = IntegrationsSpec.model_validate({"mlflow": {"experiment_name": "my-experiment"}}) + assert collect_integration_secret_envs(integrations) == [] + + +class TestWarnIncompleteIntegrations: + def test_warns_when_wandb_missing_secret( + self, + caplog: pytest.LogCaptureFixture, + ) -> None: + integrations = IntegrationsSpec.model_validate({"wandb": {"project": "my-project"}}) + caplog.set_level("WARNING") + + warn_incomplete_integrations(integrations) + + assert "api_key_secret is missing" in caplog.text + + def test_warns_when_mlflow_missing_tracking_uri( + self, + caplog: pytest.LogCaptureFixture, + ) -> None: + integrations = IntegrationsSpec.model_validate({"mlflow": {"experiment_name": "exp"}}) + caplog.set_level("WARNING") + + warn_incomplete_integrations(integrations) + + assert "tracking_uri is missing" in caplog.text diff --git a/packages/nmp_customization_common/tests/integrations/test_runtime.py b/packages/nmp_customization_common/tests/integrations/test_runtime.py new file mode 100644 index 0000000000..3f094859bd --- /dev/null +++ b/packages/nmp_customization_common/tests/integrations/test_runtime.py @@ -0,0 +1,210 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from pathlib import Path + +import pytest +from nmp.common.integrations import IntegrationsSpec, MlflowIntegration, WandbIntegration +from nmp.customization_common.integrations import IntegrationRuntimeContext, build_mlflow_config, build_wandb_config +from nmp.customization_common.service.context import NMPJobContext + + +@pytest.fixture +def job_ctx(tmp_path: Path) -> NMPJobContext: + return NMPJobContext( + workspace="test-workspace", + job_id="job-123", + attempt_id="attempt-1", + step="training", + task="task-abc123", + jobs_url="http://jobs.example.com", + files_url="http://files.example.com", + storage_path=tmp_path / "job-storage", + config_path=tmp_path / "config.json", + ) + + +def _runtime_ctx( + job_ctx: NMPJobContext, + *, + wandb: WandbIntegration | None = None, + mlflow: MlflowIntegration | None = None, + output_name: str = "output-model-name", + workspace_path: str = "/workspace", + model_name: str = "meta/llama-test", + framework: str = "automodel", +) -> IntegrationRuntimeContext: + return IntegrationRuntimeContext( + wandb=wandb, + mlflow=mlflow, + output_name=output_name, + workspace_path=workspace_path, + model_name=model_name, + job_ctx=job_ctx, + framework=framework, + ) + + +class TestBuildMlflowConfig: + def test_returns_none_without_mlflow(self, job_ctx: NMPJobContext) -> None: + assert build_mlflow_config(_runtime_ctx(job_ctx)) is None + + def test_missing_tracking_uri_warns_and_disables( + self, + job_ctx: NMPJobContext, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, + ) -> None: + ctx = _runtime_ctx( + job_ctx, + mlflow=MlflowIntegration(experiment_name="exp-no-uri"), + ) + monkeypatch.delenv("MLFLOW_TRACKING_URI", raising=False) + caplog.set_level("WARNING") + + assert build_mlflow_config(ctx) is None + assert "MLflow integration is configured but no tracking URI is set" in caplog.text + + def test_config_tracking_uri_takes_precedence( + self, + job_ctx: NMPJobContext, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + ctx = _runtime_ctx( + job_ctx, + mlflow=MlflowIntegration( + tracking_uri="http://config-mlflow.example.com:5000", + experiment_name="configured-experiment", + tags={"user_tag": "user_value"}, + description="run-description", + ), + ) + monkeypatch.setenv("MLFLOW_TRACKING_URI", "http://env-mlflow.example.com:5000") + + result = build_mlflow_config(ctx) + + assert result is not None + assert result["tracking_uri"] == "http://config-mlflow.example.com:5000" + assert result["experiment_name"] == "configured-experiment" + assert result["run_name"] == "job-123" + assert result["tags"]["service"] == "nemo-platform" + assert result["tags"]["framework"] == "automodel" + assert result["tags"]["workspace"] == "test-workspace" + assert result["tags"]["job"] == "job-123" + assert result["tags"]["task"] == "task-abc123" + assert result["tags"]["model_name"] == "meta/llama-test" + assert result["tags"]["user_tag"] == "user_value" + assert result["tags"]["mlflow.note.content"] == "run-description" + + def test_from_integrations_spec_factory(self, job_ctx: NMPJobContext, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("MLFLOW_TRACKING_URI", "http://env-mlflow.example.com:5000") + integrations = IntegrationsSpec.model_validate({"mlflow": {"experiment_name": "configured-experiment"}}) + ctx = IntegrationRuntimeContext.from_integrations_spec( + integrations=integrations, + output_name="output-model-name", + workspace_path="/workspace", + model_name="meta/llama-test", + job_ctx=job_ctx, + framework="automodel", + ) + result = build_mlflow_config(ctx) + assert result is not None + assert result["tracking_uri"] == "http://env-mlflow.example.com:5000" + + +class TestBuildWandbConfig: + def test_returns_none_without_wandb(self, job_ctx: NMPJobContext) -> None: + assert build_wandb_config(_runtime_ctx(job_ctx)) is None + + def test_missing_api_key_and_base_url_warns_and_disables( + self, + job_ctx: NMPJobContext, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, + ) -> None: + ctx = _runtime_ctx(job_ctx, wandb=WandbIntegration(project="proj")) + monkeypatch.delenv("WANDB_API_KEY", raising=False) + caplog.set_level("WARNING") + + assert build_wandb_config(ctx) is None + assert "WandB API key is not set" in caplog.text + + def test_base_url_without_api_key_warns_and_activates( + self, + job_ctx: NMPJobContext, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, + ) -> None: + monkeypatch.delenv("WANDB_API_KEY", raising=False) + ctx = _runtime_ctx( + job_ctx, + wandb=WandbIntegration(project="proj", base_url="https://wandb.internal"), + ) + caplog.set_level("WARNING") + + result = build_wandb_config(ctx) + + assert result is not None + assert result["settings"]["base_url"] == "https://wandb.internal" + assert "base_url only" in caplog.text + + def test_builds_full_config( + self, + job_ctx: NMPJobContext, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv("WANDB_API_KEY", "test-api-key") + ctx = _runtime_ctx( + job_ctx, + wandb=WandbIntegration( + project="my-project", + name="my-run", + entity="my-team", + tags=["tag-a"], + notes="notes", + ), + workspace_path="/tmp/workspace", + ) + + result = build_wandb_config(ctx) + + assert result is not None + assert result["project"] == "my-project" + assert result["name"] == "my-run" + assert result["entity"] == "my-team" + assert result["notes"] == "notes" + assert result["dir"] == "/tmp/workspace/wandb" + + def test_wandb_dir_uses_ephemeral_for_output_model( + self, + job_ctx: NMPJobContext, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv("WANDB_API_KEY", "test-api-key") + ctx = _runtime_ctx( + job_ctx, + wandb=WandbIntegration(project="proj"), + workspace_path="/var/run/scratch/job/output_model", + ) + + result = build_wandb_config(ctx) + + assert result is not None + assert result["dir"] == "/var/run/scratch/job/ephemeral/wandb" + + def test_wandb_dir_uses_ephemeral_for_training_workspace( + self, + job_ctx: NMPJobContext, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv("WANDB_API_KEY", "test-api-key") + ctx = _runtime_ctx( + job_ctx, + wandb=WandbIntegration(project="proj"), + workspace_path="/var/run/scratch/job/training", + ) + + result = build_wandb_config(ctx) + + assert result is not None + assert result["dir"] == "/var/run/scratch/job/ephemeral/wandb" diff --git a/packages/nmp_customization_common/tests/tasks/test_file_io_utils.py b/packages/nmp_customization_common/tests/tasks/test_file_io_utils.py new file mode 100644 index 0000000000..7cdb6acf48 --- /dev/null +++ b/packages/nmp_customization_common/tests/tasks/test_file_io_utils.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 nmp.customization_common.tasks.file_io_utils import list_local_files + + +def test_list_local_files_skips_unreadable_entries(tmp_path: Path) -> None: + root = tmp_path / "output_model" + root.mkdir() + (root / "adapter_model.safetensors").write_bytes(b"x" * 8) + wandb_dir = root / "wandb" / "run-1" / "logs" + wandb_dir.mkdir(parents=True) + (wandb_dir / "debug.log").write_text("ok") + broken = wandb_dir / "debug-core.log" + broken.symlink_to(root / "missing-target") + + files = list_local_files(root) + + paths = {f.path for f in files} + assert "adapter_model.safetensors" in paths + assert "wandb/run-1/logs/debug.log" in paths + assert "wandb/run-1/logs/debug-core.log" not in paths + assert len(files) == 2 + + +def test_list_local_files_single_file(tmp_path: Path) -> None: + file_path = tmp_path / "weights.bin" + file_path.write_bytes(b"abc") + + files = list_local_files(file_path) + + assert len(files) == 1 + assert files[0].path == "weights.bin" + assert files[0].size == 3 diff --git a/plugins/nemo-automodel/README.md b/plugins/nemo-automodel/README.md index 3c6df3a557..f4b831d3c0 100644 --- a/plugins/nemo-automodel/README.md +++ b/plugins/nemo-automodel/README.md @@ -28,3 +28,5 @@ 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`. + +Optional `integrations` (W&B / MLflow) use the shared `IntegrationsSpec` from `nmp.common.integrations`. Example: `plugins/nemo-automodel/tests/fixtures/integrations_wandb_mlflow.json`. Field reference: customizer skill `references/hyperparameters.md` § **Integrations (automodel + unsloth)**. diff --git a/plugins/nemo-automodel/pyproject.toml b/plugins/nemo-automodel/pyproject.toml index 126816f9df..f40bc9e1c5 100644 --- a/plugins/nemo-automodel/pyproject.toml +++ b/plugins/nemo-automodel/pyproject.toml @@ -8,6 +8,7 @@ dependencies = [ "nemo-platform-plugin", "nemo-platform", "nmp-automodel", + "nmp-customization-common", "pydantic>=2.10.6", "pydantic-settings>=2.6.1", "typer>=0.12.5", @@ -31,6 +32,7 @@ nemo-platform-plugin = { workspace = true } nemo-platform = { workspace = true } nmp-automodel = { workspace = true } nemo-customizer-plugin = { workspace = true } +nmp-customization-common = { workspace = true } [dependency-groups] dev = [ 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 9229fea42c..a06ca16eb6 100644 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/inputs.py +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/cli/inputs.py @@ -1,17 +1,26 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""CLI overrides: submit accepts a job JSON file instead of ``--spec``.""" +"""CLI overrides for the Automodel contributor. + +The override machinery is shared in :mod:`nmp.customization_common.cli.overrides`; this +module supplies the Automodel specifics: the ``AutomodelJobInput`` schema (via +``load_job_json``), the ``JOB_JSON`` help text, and the run-disabled message. +""" import json -from collections.abc import Callable from pathlib import Path import typer +from nmp.customization_common.cli.overrides import apply_job_cli_overrides from nemo_automodel_plugin.schema import AutomodelJobInput _JOB_JSON_HELP = "Path to Automodel job JSON (AutomodelJobInput schema)." +_RUN_DISABLED_MESSAGE = ( + "Automodel does not support local run. Submit to the platform API instead:\n" + " nemo customization automodel submit -w " +) def load_job_json(path: Path) -> str: @@ -23,74 +32,9 @@ def load_job_json(path: Path) -> str: 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]: - 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 command.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, - ) + apply_job_cli_overrides( + group, + load_job_json=load_job_json, + job_json_help=_JOB_JSON_HELP, + run_disabled_message=_RUN_DISABLED_MESSAGE, + ) diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/config.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/config.py index c54a2a1dcd..4ee30ba6cb 100644 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/config.py +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/config.py @@ -5,16 +5,15 @@ from __future__ import annotations -from pydantic_settings import BaseSettings, SettingsConfigDict +from nmp.customization_common.contributor.config import BaseTrainingPluginConfig, generate_job_id +from pydantic_settings import SettingsConfigDict -class AutomodelPluginConfig(BaseSettings): +class AutomodelPluginConfig(BaseTrainingPluginConfig): """Environment-driven Automodel plugin settings.""" model_config = SettingsConfigDict(env_prefix="NMP_AUTOMODEL_", extra="ignore") - default_training_execution_profile: str = "gpu" - def get_config() -> AutomodelPluginConfig: return AutomodelPluginConfig() @@ -22,6 +21,4 @@ def get_config() -> AutomodelPluginConfig: def generate_automodel_id() -> str: """Generate a job name when the submitter omits ``name``.""" - import uuid - - return f"automodel-{uuid.uuid4().hex[:12]}" + return generate_job_id("automodel") diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/contributor.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/contributor.py index ebe0b5c299..9c01217b1d 100644 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/contributor.py +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/contributor.py @@ -1,92 +1,42 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Automodel customization contributor.""" +"""Automodel customization contributor. + +Shared shape lives in :class:`nmp.customization_common.contributor.base.BaseContributor`; +this subclass supplies the backend-specific values + the SDK resource classes the +customization hub composes under ``client.customization.automodel``. +""" 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.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 +from nmp.customization_common.contributor.base import BaseContributor -from nemo_automodel_plugin.config import generate_automodel_id, get_config +from nemo_automodel_plugin.config import AutomodelPluginConfig, generate_automodel_id, get_config from nemo_automodel_plugin.jobs.jobs import AutomodelJob -class AutomodelContributor: - """Registers Automodel routes under the customization router.""" +class AutomodelContributor(BaseContributor): + """Registers Automodel routes/CLI 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() + job_cls: ClassVar[type] = AutomodelJob + cli_help: ClassVar[str] = "Automodel training jobs (SFT, distillation)." + jobs_router_description: ClassVar[str] = "Automodel training jobs." - @router.get("/healthz") - async def healthz() -> dict[str, str]: - return {"backend": self.name, "status": "ok"} + generate_job_name = staticmethod(generate_automodel_id) - 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 + def _get_config(self) -> AutomodelPluginConfig: + return get_config() + def apply_cli_overrides(self, app: typer.Typer) -> None: 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", - ) def get_sdk_resources(self) -> CustomizationContributorSDKResources: from nemo_automodel_plugin.sdk.resources import AsyncAutomodelCustomization, AutomodelCustomization diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/jobs/jobs.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/jobs/jobs.py index e23e7c4785..926622cf41 100644 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/jobs/jobs.py +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/jobs/jobs.py @@ -1,41 +1,30 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Automodel training job (NemoJob).""" +"""Automodel training job (NemoJob). + +Shared scaffold (``to_spec`` + the Docker-runtime guard) lives in +:class:`nmp.customization_common.contributor.jobs.BaseSubmitJob`; ``compile`` stays here +because it validates for training and resolves the execution profile from the +schema (automodel-specific). +""" from __future__ import annotations from typing import ClassVar, cast +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 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 nmp.customization_common.contributor.jobs import BaseSubmitJob, require_docker_runtime 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): +class AutomodelJob(BaseSubmitJob): """GPU Automodel fine-tuning job under the customization router.""" name: ClassVar[str] = "automodel.jobs" @@ -43,23 +32,11 @@ class AutomodelJob(NemoJob): 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"] + docker_runtime_label: ClassVar[str] = "Automodel" @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)) + async def _transform(cls, job_input: BaseModel, workspace: str, async_sdk: AsyncNeMoPlatform) -> AutomodelJobOutput: + return await transform_input_to_output(cast(AutomodelJobInput, job_input), workspace, async_sdk) @classmethod async def compile( @@ -72,7 +49,8 @@ async def compile( profile: str | None = None, options: dict | None = None, ) -> PlatformJobSpec: - _require_docker_runtime() + del entity_client, options + require_docker_runtime(cls.docker_runtime_label) canonical = ( spec if isinstance(spec, AutomodelJobOutput) else AutomodelJobOutput.model_validate(spec.model_dump()) ) diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py index 0dc8d79973..55459a40a0 100644 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py @@ -5,10 +5,26 @@ from __future__ import annotations -from typing import Any, Literal, Self +from typing import Literal, Self +from nmp.common.integrations import IntegrationsSpec from pydantic import BaseModel, ConfigDict, Field, model_validator +__all__ = [ + "AutomodelJobInput", + "AutomodelJobOutput", + "BatchSpec", + "DatasetSpec", + "LoRAParams", + "OptimizerSpec", + "OutputRequest", + "OutputResponse", + "ParallelismSpec", + "ScheduleSpec", + "TrainingSpec", + "ValidationError", +] + class ValidationError(ValueError): """Raised when automodel job input validation fails.""" @@ -106,21 +122,6 @@ class OutputResponse(BaseModel): 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.""" 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 deleted file mode 100644 index adf4b7ffc9..0000000000 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/http_utils.py +++ /dev/null @@ -1,64 +0,0 @@ -# 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 deleted file mode 100644 index 7832f87a86..0000000000 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/job_resources.py +++ /dev/null @@ -1,86 +0,0 @@ -# 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 index 63e08a9266..0dec6dc090 100644 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/resources.py +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/sdk/resources.py @@ -1,164 +1,17 @@ # 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``).""" +"""Automodel contributor SDK resources (composed by ``nemo-customizer-plugin``). -from __future__ import annotations +Thin shim over the shared :func:`nmp.customization_common.sdk.client.make_customization_sdk` +factory. The ``AutomodelCustomization`` / ``AsyncAutomodelCustomization`` symbols below +are imported by string by the ``nemo-customizer`` SDK hub and must not move. +""" -from typing import Any +from nmp.customization_common.sdk.client import make_customization_sdk -from nemo_platform import AsyncNeMoPlatform, NeMoPlatform +AutomodelCustomization, AsyncAutomodelCustomization = make_customization_sdk("automodel") -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) +# Jobs-resource classes re-exported for ``sdk/__init__.py`` and backward compatibility. +AutomodelJobsResource = AutomodelCustomization.jobs_resource_cls +AsyncAutomodelJobsResource = AsyncAutomodelCustomization.jobs_resource_cls diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/transform.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/transform.py index be0a0b6c7b..8cfa949a13 100644 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/transform.py +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/transform.py @@ -5,11 +5,10 @@ from __future__ import annotations -import uuid from typing import TYPE_CHECKING -from nemo_platform_plugin.refs import parse_entity_ref from nmp.automodel.platform_client import check_dataset_access, fetch_model_entity +from nmp.customization_common.contributor.transform import generated_output_name from nemo_automodel_plugin.schema import ( AutomodelJobInput, @@ -20,25 +19,6 @@ 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: @@ -67,12 +47,10 @@ async def transform_input_to_output( "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}") + out_name = generated_output_name(input_spec.model, input_spec.dataset.training, workspace) fileset = out_name else: out_name = input_spec.output.name diff --git a/plugins/nemo-automodel/tests/fixtures/integrations_wandb_mlflow.json b/plugins/nemo-automodel/tests/fixtures/integrations_wandb_mlflow.json new file mode 100644 index 0000000000..9362be3fc4 --- /dev/null +++ b/plugins/nemo-automodel/tests/fixtures/integrations_wandb_mlflow.json @@ -0,0 +1,50 @@ +{ + "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" + }, + "integrations": { + "wandb": { + "project": "my-project", + "name": "run-001", + "entity": "my-team", + "tags": ["sft", "llama"], + "notes": "Experiment notes", + "base_url": "https://wandb.internal", + "api_key_secret": "default/wandb-api-key" + }, + "mlflow": { + "experiment_name": "llama-finetuning", + "name": "run-001", + "tracking_uri": "http://mlflow:5000", + "tags": { + "team": "nlp" + }, + "description": "SFT experiment" + } + } +} diff --git a/plugins/nemo-automodel/tests/test_contract_job_inputs.py b/plugins/nemo-automodel/tests/test_contract_job_inputs.py new file mode 100644 index 0000000000..4395f1cf8b --- /dev/null +++ b/plugins/nemo-automodel/tests/test_contract_job_inputs.py @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Contract fixtures for submit-time AutomodelJobInput JSON.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest +from nemo_automodel_plugin.schema import AutomodelJobInput + +FIXTURES_DIR = Path(__file__).resolve().parent / "fixtures" + + +@pytest.mark.parametrize( + "fixture_name", + ["integrations_wandb_mlflow.json"], +) +def test_contract_job_input_validates(fixture_name: str) -> None: + path = FIXTURES_DIR / fixture_name + spec = AutomodelJobInput.model_validate(json.loads(path.read_text())) + assert spec.integrations is not None + assert spec.integrations.wandb is not None + assert spec.integrations.wandb.project == "my-project" + assert spec.integrations.wandb.name == "run-001" + assert spec.integrations.wandb.api_key_secret is not None + assert spec.integrations.wandb.api_key_secret.root == "default/wandb-api-key" + assert spec.integrations.mlflow is not None + assert spec.integrations.mlflow.tracking_uri == "http://mlflow:5000" + assert spec.integrations.mlflow.name == "run-001" 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 572f864b62..9d7943e63e 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 @@ -588,6 +588,7 @@ Use the user's platform URL in `NEMO_BASE_URL` when they overrode it; omit the e | 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. | +| W&B not syncing / no `[launcher]` secret lines / `WandbCallback requires wandb` / wandb 401 | `references/troubleshooting.md` § **W&B / integrations not working** (jobs-launcher build, secret update, unsloth image). Setup: `references/integrations-setup.md`. | For other terminal errors, keep the same header template; put remediation detail in **Notes** or a short **Next steps** section as appropriate. @@ -601,8 +602,12 @@ For other terminal errors, keep the same header template; put remediation detail | 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, 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 | `nemo customization automodel explain` / `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`**. +| Live JSON schema | `uv run nemo customization automodel explain` / `uv run nemo customization unsloth explain` | +| Job JSON fixture (automodel, minimal) | `plugins/nemo-automodel/tests/fixtures/qwen3_0.6b_sft_lora.json` (ignore `max_steps` for real runs) | +| Job JSON fixture (unsloth, minimal) | `plugins/nemo-unsloth/tests/fixtures/minimal_unsloth_sft.json` (ignore `max_steps` for real runs) | +| Job JSON fixture (integrations, both backends) | `plugins/nemo-automodel/tests/fixtures/integrations_wandb_mlflow.json`, `plugins/nemo-unsloth/tests/fixtures/integrations_wandb_mlflow.json` | +| Automodel compile-path contract configs | `services/automodel/tests/contract/input_configs/` → YAML in `output_configs/` (legacy `TrainingStepConfig` shape, not submit JSON) | +| W&B / MLflow field reference | `references/hyperparameters.md` § **Integrations (automodel + unsloth)** | +| W&B secret + MLflow local server + jobs-launcher | `references/integrations-setup.md` | + +Related: `plugins/nemo-automodel/README.md`, `plugins/nemo-unsloth/README.md`, `plugins/nemo-customizer/docs/CUSTOMIZATION.md`, skills **`nemo-files`**, **`nemo-status`**, **`nemo-secrets`**. 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 2f025a4023..15f495eaf0 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 @@ -13,6 +13,54 @@ Both schemas use `extra="forbid"` — unknown keys raise validation errors. Fiel --- +## Integrations (automodel + unsloth) + +Both backends accept the same `integrations` object on job JSON (`IntegrationsSpec` in `nmp.common.integrations`). A non-null backend block **requests** that integration; the training runtime **activates** it only when credentials/URIs are available (W&B needs `WANDB_API_KEY`, MLflow needs a tracking URI). Omit the field or set a backend to `null` to disable. There is no `enabled` flag and no `report_to` on input — `report_to` is derived at runtime from activated backends. The compiler logs a warning when W&B is requested without `api_key_secret` or MLflow without `tracking_uri`. + +```json +"integrations": { + "wandb": { + "project": "my-project", + "name": "run-001", + "entity": "my-team", + "tags": ["sft", "llama"], + "notes": "Experiment notes", + "base_url": "https://wandb.internal", + "api_key_secret": "default/wandb-api-key" + }, + "mlflow": { + "experiment_name": "llama-finetuning", + "name": "run-001", + "tracking_uri": "http://mlflow:5000", + "tags": { "team": "nlp" }, + "description": "SFT experiment" + } +} +``` + +| Field | Notes | +|-------|-------| +| `wandb` | Non-null requests W&B (requires `WANDB_API_KEY` at runtime). | +| `wandb.project` | W&B project; defaults to `output.name` at runtime if unset. | +| `wandb.name` | W&B run name; defaults to job ID. Legacy `run_name` is accepted with a deprecation warning. | +| `wandb.entity` | W&B team or username. | +| `wandb.tags` / `wandb.notes` | Optional run metadata. | +| `wandb.base_url` | Self-hosted W&B server URL. Without `api_key_secret`, W&B may still activate when `base_url` is set **and** the server allows access without a cloud API key — a compile-time warning is logged. | +| `wandb.api_key_secret` | Platform secret ref (`secret_name` or `workspace/secret_name`). The compiler injects `WANDB_API_KEY` into the training step environment. | +| `mlflow` | Non-null requests MLflow (requires tracking URI at runtime). | +| `mlflow.tracking_uri` | MLflow tracking server; can also come from `MLFLOW_TRACKING_URI` in the container. | +| `mlflow.experiment_name` | Defaults to `output.name` if unset. | +| `mlflow.name` | MLflow run name; defaults to job ID. Legacy `run_name` is accepted with a deprecation warning. | +| `mlflow.tags` / `mlflow.description` | Optional run metadata. | + +Set `"integrations": null` or omit the field when tracking is not needed. Contract examples: `plugins/nemo-automodel/tests/fixtures/integrations_wandb_mlflow.json`, `plugins/nemo-unsloth/tests/fixtures/integrations_wandb_mlflow.json`. + +**Local setup (MLflow server, `docker0` tracking URI, jobs-launcher, W&B secret):** `references/integrations-setup.md`. + +**Unsloth note:** HuggingFace `TrainingArguments.run_name` is shared by W&B and MLflow. When both backends are active, `wandb.name` wins if set; otherwise `mlflow.name` is used. If both names are set to different values, a runtime warning is logged and W&B's name is used. + +--- + # 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"`). @@ -175,12 +223,7 @@ Example: 1 node, 2 GPUs, TP=1 → DP=2 → GBS must be a multiple of `2 × micro ### Automodel `integrations` (optional) -```json -"integrations": { - "wandb": { "enabled": true, "project": "my-project", "api_key_secret": "wandb-api-key" }, - "mlflow": null -} -``` +See **Integrations (automodel + unsloth)** above. --- @@ -336,7 +379,7 @@ Unsloth is **submit-only, single-GPU inside the training container**. There is n | `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` | +| `integrations` | Optional W&B / MLflow (same shape as automodel) | | `output` | Output entity name, optional description, **`save_method`** (controls what's persisted) | Full template (every section, defaults inline): @@ -421,7 +464,7 @@ Full template (every section, defaults inline): | `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. +**Mutex:** `load_in_4bit` xor `load_in_8bit`. Both quantization flags are also **incompatible with `training.finetuning_type: "all_weights"`** — full SFT must use a non-quantized base. ### `dataset` @@ -440,7 +483,7 @@ See `references/dataset-formats.md` § Unsloth for row-shape rules. | Field | Default | Notes | |-------|---------|-------| | `training_type` | `"sft"` | Only `"sft"` is implemented today. | -| `finetuning_type` | `"lora"` | `"lora"` (adapter; default) or `"full"` (full SFT — heavy, no quantization). | +| `finetuning_type` | `"lora"` | `"lora"` (adapter; default) or `"all_weights"` (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. | @@ -456,7 +499,7 @@ See `references/dataset-formats.md` § Unsloth for row-shape rules. | `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"`. +`lora` is auto-filled with these defaults when `finetuning_type: "lora"` and the user omits the block. Must be `null` / omitted when `finetuning_type: "all_weights"`. ### Unsloth `schedule` @@ -502,21 +545,7 @@ See `references/dataset-formats.md` § Unsloth for row-shape rules. ### Unsloth `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 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. +See **Integrations (automodel + unsloth)** above. ### `output` diff --git a/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/integrations-setup.md b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/integrations-setup.md new file mode 100644 index 0000000000..9927d26c17 --- /dev/null +++ b/plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/integrations-setup.md @@ -0,0 +1,130 @@ +# Integrations setup (W&B + MLflow, local / Docker platform) + +Use this when job JSON includes `integrations.wandb` and/or `integrations.mlflow` on a **local or single-node Docker** NeMo Platform (`platform.runtime: docker`). Field reference: `hyperparameters.md` § **Integrations (automodel + unsloth)**. + +## MLflow — local tracking server + +Run MLflow on the **platform host** (the machine whose Docker daemon runs training containers): + +```bash +docker run -d \ + --name mlflow \ + --restart unless-stopped \ + -p 5001:5000 \ + -v mlflow-data:/mlflow \ + ghcr.io/mlflow/mlflow:v2.18.0 \ + mlflow server \ + --host 0.0.0.0 \ + --port 5000 \ + --backend-store-uri sqlite:///mlflow/mlflow.db \ + --default-artifact-root /mlflow/artifacts +``` + +UI: `http://:5001` + +### `tracking_uri` for training containers + +Training steps run in Docker. Set `integrations.mlflow.tracking_uri` to an address the **container** can reach — not `localhost` inside the container. + +1. Resolve the host bridge IP (often `docker0`): + +```bash +export DOCKER_HOST_IP=$(ip -4 addr show docker0 | awk '/inet / {print $2}' | cut -d/ -f1) +echo "$DOCKER_HOST_IP" +``` + +1. Use that IP with the **published** host port (`5001` in the command above): + +```json +"mlflow": { + "experiment_name": "customizer-integration", + "name": "my-run", + "tracking_uri": "http://${DOCKER_HOST_IP}:5001" +} +``` + +Substitute `${DOCKER_HOST_IP}` with the value from step 1 (JSON does not expand shell variables). With `job_container_network: host` on the GPU execution profile, the host's LAN IP or `docker0` may both work — use whichever you verified from a running training container. + +## jobs-launcher — required for `WANDB_API_KEY` injection + +W&B (and other `from_secret` env vars) are injected by **jobs-launcher** before the training entrypoint runs. If launcher is missing or misconfigured, training starts without `WANDB_API_KEY` even when `integrations.wandb.api_key_secret` is set. + +On the **platform host**, from the nemo-platform git root: + +```bash +cd services/core/jobs/jobs-launcher +./build-manual.sh linux amd64 +cd ../../../.. +``` + +Point platform config at the built binary (absolute path), e.g. in `~/.nemo/config.yaml` under the Docker jobs executor: + +```yaml +jobs: + executors: + docker: + launcher_tool_path: /path/to/nemo-platform/services/core/jobs/jobs-launcher/jobs-launcher +``` + +Restart platform services after changing launcher path: + +```bash +uv run nemo services restart +``` + +Successful injection appears in training logs: + +```text +[launcher] Successfully fetched secret wandb-api-key and mapped to WANDB_API_KEY +``` + +## W&B — platform secret + +Job JSON references the secret by name: + +```json +"wandb": { + "project": "customizer-integration", + "name": "my-run", + "entity": "Nemo-automodel", + "api_key_secret": "default/wandb-api-key" +} +``` + +`default/wandb-api-key` means workspace `default`, secret name `wandb-api-key`. + +Store the API key in the **platform** secret store. A local `wandb login` cache on your laptop is **not** used by training containers. + +```bash +export NEMO_BASE_URL=http://:8080 # omit when using default localhost +cd /path/to/nemo-platform + +# Create (first time) +uv run nemo secrets create wandb-api-key \ + --value "$WANDB_API_KEY" \ + --workspace default + +# Update (replace placeholder or rotated key) +uv run nemo secrets update wandb-api-key \ + --value "$WANDB_API_KEY" \ + --workspace default +``` + +Prefer piping when the key has special characters: + +```bash +printf '%s' "$WANDB_API_KEY" | uv run nemo secrets update wandb-api-key --from-file - --workspace default +``` + +Get a key from https://wandb.ai/authorize (User settings → API keys). + +## Unsloth image note + +`nmp-unsloth-training` must include the `[integrations]` extra (`wandb`, `mlflow-skinny`) or HF `WandbCallback` / MLflow callbacks fail at trainer init. Rebuild and set `NMP_UNSLOTH_TRAINING_IMAGE` on the platform host after Dockerfile changes. + +## Verify end-to-end + +1. Submit a job with both integrations (fixtures: `plugins/nemo-automodel/tests/fixtures/integrations_wandb_mlflow.json`, `plugins/nemo-unsloth/tests/fixtures/integrations_wandb_mlflow.json`). +2. Training logs: launcher secret fetch, `wandb: Syncing run …`, MLflow run under the configured experiment. +3. W&B UI: `https://wandb.ai//` +4. MLflow UI: `http://:5001` 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 cd8a0d5df4..098a30d7ee 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,9 +1,17 @@ # Troubleshooting -Read this file when submit fails, jobs fail on images, the platform is unreachable, or the user asks for Unsloth. +Read this file when submit fails, jobs fail on images, the platform is unreachable, W&B/MLflow integrations fail, or the user asks for Unsloth. Resolve the CLI first per **Pre-flight — CLI resolution** in `SKILL.md` (`nemo` on `PATH`, else `uv run nemo`, else route to **nemo-setup**). Example commands below use `nemo …`. +## Prerequisites + +Before working through any section below, confirm: + +- **NeMo CLI available** — `nemo` on `PATH`, otherwise `uv run nemo` from the nemo-platform repo root (see **Pre-flight — CLI resolution** in `SKILL.md`). +- **Platform base URL configured** — via the CLI context, `--base-url`, or `$NMP_BASE_URL`; defaults to `http://localhost:8080`. +- **Workspace access** — authenticated (`nemo auth login`) with access to the target workspace. + ## 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`. Auth is not required when the cluster has authentication disabled (`nemo auth status`); on 401/403 see **Authentication** in `SKILL.md`. @@ -157,6 +165,50 @@ nemo customization submit /tmp/job.json --workspace default [--profile 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. +## W&B / integrations not working + +Job JSON has `integrations.wandb` (and/or `integrations.mlflow`) but tracking fails or never starts. Full setup: `references/integrations-setup.md`. + +| Symptom / log excerpt | Likely cause | Fix | +|-----------------------|--------------|-----| +| Training logs **omit** `[launcher]` lines; entrypoint is the training module directly (e.g. `Running main process: /opt/venv/bin/python [-m nmp.unsloth.tasks.training]` with **no** preceding `Fetching secret wandb-api-key`) | **jobs-launcher** binary missing or `launcher_tool_path` wrong — secrets are never injected | Build launcher on the **platform host**, set absolute `launcher_tool_path`, restart services. See § **jobs-launcher missing** below and `integrations-setup.md` § **jobs-launcher**. | +| `wandb: ERROR` / HTTP 401 / `permission denied` after launcher lines present | Platform secret `wandb-api-key` has wrong or placeholder value; local `wandb login` cache is **not** used | `uv run nemo secrets update wandb-api-key --value "$WANDB_API_KEY" --workspace default` (or `--from-file -`). Re-submit. | +| `RuntimeError: WandbCallback requires wandb to be installed` (unsloth) | `nmp-unsloth-training` image lacks `wandb` | Rebuild image with `nmp-unsloth[integrations]` extra; set `NMP_UNSLOTH_TRAINING_IMAGE`; restart platform. See **Missing training images**. | +| Compile/submit warning: `integrations.wandb is configured but api_key_secret is missing` | Job JSON has `wandb` block without `api_key_secret` | Add `"api_key_secret": "default/wandb-api-key"` (or your secret ref). | +| MLflow run never appears; W&B works | `tracking_uri` unreachable from container (`localhost`, wrong port) | Use `docker0` host IP + published port (e.g. `http://${DOCKER_HOST_IP}:5001`). See `integrations-setup.md` § **`tracking_uri`**. | + +### jobs-launcher missing (W&B secret not injected) + +The Docker executor wraps the training entrypoint with **jobs-launcher** only when `launcher_tool_path` points to a built binary on the platform host. If the path is missing or still the default stub, the container runs training **without** secret injection — `WANDB_API_KEY` never reaches the process even when `integrations.wandb.api_key_secret` is set in job JSON. + +**Working** — launcher fetched the secret before training: + +```text +[launcher] 2026/06/11 22:03:03 Fetching secret wandb-api-key from workspace default... +[launcher] 2026/06/11 22:03:03 Successfully fetched secret wandb-api-key and mapped to WANDB_API_KEY +[launcher] 2026/06/11 22:03:03 Injected 1 secret(s) as environment variables +[launcher] 2026/06/11 22:03:03 Running main process: /opt/venv/bin/python [-m nmp.unsloth.tasks.training] +... +wandb: Syncing run my-run +``` + +**Broken** — no launcher wrapper (typical when binary was never built or path is wrong): + +```text +2026-06-11 21:47:42,848 - __main__ - INFO - Container: CUDA_VISIBLE_DEVICES=0 +... +# No wandb: Syncing run … — W&B never initialized because WANDB_API_KEY was not injected +``` + +On the platform host: + +```bash +cd /path/to/nemo-platform/services/core/jobs/jobs-launcher +./build-manual.sh linux amd64 +``` + +Set `jobs.executors.docker.launcher_tool_path` in `~/.nemo/config.yaml` to the **absolute** path of the built `jobs-launcher` binary, then `uv run nemo services restart`. Re-submit with a fresh `output.name`. + ## Unsloth submit errors | Error / symptom | Cause | Fix | diff --git a/plugins/nemo-unsloth/README.md b/plugins/nemo-unsloth/README.md index 8fd9468be2..cde3b32675 100644 --- a/plugins/nemo-unsloth/README.md +++ b/plugins/nemo-unsloth/README.md @@ -60,12 +60,12 @@ The container image targets the same compute capabilities NVIDIA's stock `pytorc - `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`. +- `training: TrainingSpec` — `training_type`, `finetuning_type` (`lora` or `all_weights`), `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`. +- `integrations: IntegrationsSpec | None` — optional W&B / MLflow (`nmp.common.integrations`). Request by presence; `api_key_secret` carries a secret *reference* that the jobs launcher resolves into `WANDB_API_KEY` in the training container **at runtime** (compile only records the reference). Example: `plugins/nemo-unsloth/tests/fixtures/integrations_wandb_mlflow.json`. - `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. diff --git a/plugins/nemo-unsloth/pyproject.toml b/plugins/nemo-unsloth/pyproject.toml index c5969f1c93..954444b202 100644 --- a/plugins/nemo-unsloth/pyproject.toml +++ b/plugins/nemo-unsloth/pyproject.toml @@ -8,6 +8,7 @@ dependencies = [ "nemo-platform-plugin", "nemo-platform", "nmp-unsloth", + "nmp-customization-common", "pydantic>=2.10.6", "pydantic-settings>=2.6.1", "typer>=0.12.5", @@ -36,6 +37,7 @@ nemo-platform-plugin = { workspace = true } nemo-platform = { workspace = true } nemo-customizer-plugin = { workspace = true } nmp-unsloth = { workspace = true } +nmp-customization-common = { workspace = true } [dependency-groups] dev = [ 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 20904723d0..3e1a71d4b2 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/inputs.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/cli/inputs.py @@ -3,30 +3,24 @@ """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). +The override machinery is shared in :mod:`nmp.customization_common.cli.overrides`; this +module supplies the Unsloth specifics: the ``UnslothJobInput`` schema (via +``load_job_json``), the ``JOB_JSON`` help text, and the run-disabled message. """ import json -from collections.abc import Callable from pathlib import Path import typer +from nmp.customization_common.cli.overrides import apply_job_cli_overrides from nemo_unsloth_plugin.schema import UnslothJobInput _JOB_JSON_HELP = "Path to Unsloth job JSON (UnslothJobInput schema)." +_RUN_DISABLED_MESSAGE = ( + "Unsloth does not support local run. Submit to the platform API instead:\n" + " nemo customization unsloth submit -w " +) def load_job_json(path: Path) -> str: @@ -37,89 +31,10 @@ def load_job_json(path: Path) -> str: 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]: - 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 command.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") - - @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, - ) + """Flat ``unsloth`` CLI: ``submit JOB.json``; ``run`` is disabled.""" + apply_job_cli_overrides( + group, + load_job_json=load_job_json, + job_json_help=_JOB_JSON_HELP, + run_disabled_message=_RUN_DISABLED_MESSAGE, + ) diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/config.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/config.py index 6ae3f12e58..6718418887 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/config.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/config.py @@ -5,12 +5,11 @@ from __future__ import annotations -import uuid +from nmp.customization_common.contributor.config import BaseTrainingPluginConfig, generate_job_id +from pydantic_settings import SettingsConfigDict -from pydantic_settings import BaseSettings, SettingsConfigDict - -class UnslothPluginConfig(BaseSettings): +class UnslothPluginConfig(BaseTrainingPluginConfig): """Environment-driven Unsloth plugin settings. All fields are optional. The only knob the contributor actually @@ -21,8 +20,6 @@ class UnslothPluginConfig(BaseSettings): model_config = SettingsConfigDict(env_prefix="NMP_UNSLOTH_", extra="ignore") - default_training_execution_profile: str = "gpu" - def get_config() -> UnslothPluginConfig: return UnslothPluginConfig() @@ -30,4 +27,4 @@ def get_config() -> UnslothPluginConfig: def generate_unsloth_id() -> str: """Generate a job name when the submitter omits ``name``.""" - return f"unsloth-{uuid.uuid4().hex[:12]}" + return generate_job_id("unsloth") diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/contributor.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/contributor.py index 8cc7d47483..36776c4dbd 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/contributor.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/contributor.py @@ -11,6 +11,8 @@ class at startup and: - adds :meth:`get_cli` under ``nemo customization unsloth`` - merges :meth:`get_authz_contribution` into the platform authz policy - composes :meth:`get_sdk_resources` under ``client.customization.unsloth`` + +The shared shape lives in :class:`nmp.customization_common.contributor.base.BaseContributor`. """ from __future__ import annotations @@ -18,100 +20,30 @@ class at startup and: 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.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 +from nmp.customization_common.contributor.base import BaseContributor -from nemo_unsloth_plugin.config import generate_unsloth_id, get_config +from nemo_unsloth_plugin.config import UnslothPluginConfig, generate_unsloth_id, get_config from nemo_unsloth_plugin.jobs.jobs import UnslothJob -class UnslothContributor: - """Registers Unsloth routes/CLI under the customization router.""" +class UnslothContributor(BaseContributor): + """Registers Unsloth routes/CLI under the customization router (SFT only, container submit).""" 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, - ) + job_cls: ClassVar[type] = UnslothJob + cli_help: ClassVar[str] = "Unsloth GPU fine-tuning (container submit). SFT only." + jobs_router_description: ClassVar[str] = "Unsloth GPU fine-tuning jobs (container submit)." - 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 + generate_job_name = staticmethod(generate_unsloth_id) + def _get_config(self) -> UnslothPluginConfig: + return get_config() + + def apply_cli_overrides(self, app: typer.Typer) -> None: 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", - ) def get_sdk_resources(self) -> CustomizationContributorSDKResources: from nemo_unsloth_plugin.sdk.resources import AsyncUnslothCustomization, UnslothCustomization diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/jobs/__init__.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/jobs/__init__.py deleted file mode 100644 index e5725ea5a4..0000000000 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/jobs/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# 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 index 43b4081ff8..2ccc6f1e75 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/jobs/jobs.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/jobs/jobs.py @@ -4,22 +4,11 @@ """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`. +→ train → upload → model-entity) on the platform's GPU cluster. -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. +Shared scaffold (``to_spec`` + the Docker-runtime guard) lives in +:class:`nmp.customization_common.contributor.jobs.BaseSubmitJob`; ``compile`` stays here +because the compiler call convention and profile resolution are backend-specific. """ from __future__ import annotations @@ -27,77 +16,30 @@ 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 nemo_unsloth_plugin.schema import UnslothJobInput +from nemo_unsloth_plugin.transform import transform_input_to_output +from nmp.customization_common.contributor.jobs import BaseSubmitJob, require_docker_runtime 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 - - -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`. - """ +class UnslothJob(BaseSubmitJob): + """GPU Unsloth fine-tuning job under the customization router (submit-only).""" 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"] + docker_runtime_label: ClassVar[str] = "Unsloth" @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), - ) + async def _transform(cls, job_input: BaseModel, workspace: str, async_sdk: AsyncNeMoPlatform) -> UnslothJobOutput: + return await transform_input_to_output(cast(UnslothJobInput, job_input), workspace, async_sdk) @classmethod async def compile( @@ -112,28 +54,12 @@ async def compile( ) -> 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. + Unsloth's :class:`HardwareSpec` does not expose an ``execution_profile`` + field, so resolution is ``profile`` arg → + ``unsloth_config.default_training_execution_profile``. """ del entity_client, options - _require_docker_runtime() + require_docker_runtime(cls.docker_runtime_label) canonical = spec if isinstance(spec, UnslothJobOutput) else UnslothJobOutput.model_validate(spec.model_dump()) execution_profile = profile or unsloth_config.default_training_execution_profile diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/schema.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/schema.py index 81c805acf2..54104b5ef7 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/schema.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/schema.py @@ -23,12 +23,12 @@ from typing import Literal, Self +from nmp.common.integrations import IntegrationsSpec from nmp.unsloth.schemas import ( BatchSpec, DatasetSpec, DeploymentParams, HardwareSpec, - IntegrationsSpec, LoRAParams, ModelLoadSpec, OptimizerSpec, @@ -37,7 +37,6 @@ ToolCallParams, TrainingSpec, UnslothJobOutput, - WandbIntegration, ) from pydantic import BaseModel, ConfigDict, Field, model_validator @@ -46,7 +45,6 @@ "DatasetSpec", "DeploymentParams", "HardwareSpec", - "IntegrationsSpec", "LoRAParams", "ModelLoadSpec", "OptimizerSpec", @@ -57,7 +55,6 @@ "TrainingSpec", "UnslothJobInput", "UnslothJobOutput", - "WandbIntegration", ] @@ -98,23 +95,20 @@ class UnslothJobInput(BaseModel): @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") + # Schedule: epochs defaults to 1; max_steps (when set) caps training. Both may be + # present — the trainer honours max_steps as an override (consistent with Automodel). # 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": + # all-weights (full) FT cannot quantize + if self.training.finetuning_type == "all_weights": 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; " + "training.finetuning_type='all_weights' 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'") + raise ValueError("training.lora must be unset when training.finetuning_type='all_weights'") # auto-fill LoRA when implied but not provided if self.training.finetuning_type == "lora" and self.training.lora is None: self.training.lora = LoRAParams() 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 deleted file mode 100644 index 7cb5f99491..0000000000 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/http_utils.py +++ /dev/null @@ -1,64 +0,0 @@ -# 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 deleted file mode 100644 index 699b14b4cd..0000000000 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/job_resources.py +++ /dev/null @@ -1,86 +0,0 @@ -# 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.""" - -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 index 8e1bc1e045..85c87b6528 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/resources.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/sdk/resources.py @@ -1,159 +1,17 @@ # 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``).""" +"""Unsloth contributor SDK resources (composed by ``nemo-customizer-plugin``). -from typing import Any +Thin shim over the shared :func:`nmp.customization_common.sdk.client.make_customization_sdk` +factory. The ``UnslothCustomization`` / ``AsyncUnslothCustomization`` symbols below +are imported by string by the ``nemo-customizer`` SDK hub and must not move. +""" -from nemo_platform import AsyncNeMoPlatform, NeMoPlatform +from nmp.customization_common.sdk.client import make_customization_sdk -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, -) +UnslothCustomization, AsyncUnslothCustomization = make_customization_sdk("unsloth") - -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 to the platform GPU cluster.""" - 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) +# Jobs-resource classes re-exported for ``sdk/__init__.py`` and backward compatibility. +UnslothJobsResource = UnslothCustomization.jobs_resource_cls +AsyncUnslothJobsResource = AsyncUnslothCustomization.jobs_resource_cls diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py index 835eb545a8..55b30d7a9e 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py @@ -17,11 +17,9 @@ from __future__ import annotations -import re -import uuid from typing import TYPE_CHECKING -from nemo_platform_plugin.refs import parse_entity_ref +from nmp.customization_common.contributor.transform import generated_output_name from nmp.unsloth.platform_client import check_dataset_access, fetch_model_entity from nmp.unsloth.schemas import OutputResponse, UnslothJobOutput @@ -30,32 +28,6 @@ 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).""" @@ -102,9 +74,7 @@ async def transform_input_to_output( 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}") + out_name = generated_output_name(input_spec.model.name, input_spec.dataset.path, workspace) else: out_name = output_request.name diff --git a/plugins/nemo-unsloth/tests/fixtures/integrations_wandb_mlflow.json b/plugins/nemo-unsloth/tests/fixtures/integrations_wandb_mlflow.json new file mode 100644 index 0000000000..50e8dc1fb9 --- /dev/null +++ b/plugins/nemo-unsloth/tests/fixtures/integrations_wandb_mlflow.json @@ -0,0 +1,35 @@ +{ + "name": "qwen-integrations-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 + }, + "integrations": { + "wandb": { + "project": "my-project", + "name": "run-001", + "entity": "my-team", + "tags": ["sft", "llama"], + "notes": "Experiment notes", + "base_url": "https://wandb.internal", + "api_key_secret": "default/wandb-api-key" + }, + "mlflow": { + "experiment_name": "llama-finetuning", + "name": "run-001", + "tracking_uri": "http://mlflow:5000", + "tags": { + "team": "nlp" + }, + "description": "SFT experiment" + } + } +} diff --git a/plugins/nemo-unsloth/tests/test_contract_job_inputs.py b/plugins/nemo-unsloth/tests/test_contract_job_inputs.py new file mode 100644 index 0000000000..570ff3c8ae --- /dev/null +++ b/plugins/nemo-unsloth/tests/test_contract_job_inputs.py @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Contract fixtures for submit-time UnslothJobInput JSON.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest +from nemo_unsloth_plugin.schema import UnslothJobInput + +FIXTURES_DIR = Path(__file__).resolve().parent / "fixtures" + + +@pytest.mark.parametrize( + "fixture_name", + ["integrations_wandb_mlflow.json"], +) +def test_contract_job_input_validates(fixture_name: str) -> None: + path = FIXTURES_DIR / fixture_name + spec = UnslothJobInput.model_validate(json.loads(path.read_text())) + assert spec.integrations is not None + assert spec.integrations.wandb is not None + assert spec.integrations.wandb.project == "my-project" + assert spec.integrations.wandb.name == "run-001" + assert spec.integrations.wandb.api_key_secret is not None + assert spec.integrations.wandb.api_key_secret.root == "default/wandb-api-key" + assert spec.integrations.mlflow is not None + assert spec.integrations.mlflow.tracking_uri == "http://mlflow:5000" + assert spec.integrations.mlflow.name == "run-001" diff --git a/plugins/nemo-unsloth/tests/test_jobs.py b/plugins/nemo-unsloth/tests/test_jobs.py index 60d52d9cd6..b9158d82c1 100644 --- a/plugins/nemo-unsloth/tests/test_jobs.py +++ b/plugins/nemo-unsloth/tests/test_jobs.py @@ -88,7 +88,7 @@ def test_compile_delegates_to_service_compiler(self) -> None: ) with ( - patch("nemo_unsloth_plugin.jobs.jobs._require_docker_runtime"), + 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), @@ -121,7 +121,7 @@ def test_compile_delegates_to_service_compiler(self) -> None: 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.require_docker_runtime"), patch( "nemo_unsloth_plugin.jobs.jobs.platform_job_config_compiler", new=AsyncMock(return_value=SimpleNamespace(steps=[])), @@ -146,7 +146,7 @@ def test_compile_rejects_non_docker_runtime(self) -> None: # 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", + "nemo_unsloth_plugin.jobs.jobs.require_docker_runtime", side_effect=PlatformJobCompilationError("not docker"), ): with pytest.raises(PlatformJobCompilationError, match="not docker"): diff --git a/plugins/nemo-unsloth/tests/test_schema.py b/plugins/nemo-unsloth/tests/test_schema.py index 06cf9c1043..5d014fa9bc 100644 --- a/plugins/nemo-unsloth/tests/test_schema.py +++ b/plugins/nemo-unsloth/tests/test_schema.py @@ -105,18 +105,22 @@ def test_model_required(self) -> None: UnslothJobInput.model_validate(payload) -class TestScheduleMutex: - def test_neither_epochs_nor_max_steps_rejected(self) -> None: +class TestSchedule: + def test_empty_schedule_defaults_to_one_epoch(self) -> None: + # Consistent with Automodel: epochs defaults to 1 (no epochs/max_steps mutex). payload = _minimal_payload() payload["schedule"] = {} - with pytest.raises(ValidationError, match="schedule.epochs or schedule.max_steps"): - UnslothJobInput.model_validate(payload) + spec = UnslothJobInput.model_validate(payload) + assert spec.schedule.epochs == 1 + assert spec.schedule.max_steps is None - def test_both_epochs_and_max_steps_rejected(self) -> None: + def test_epochs_and_max_steps_both_allowed(self) -> None: + # max_steps caps/overrides epochs at train time; the two are not mutually exclusive. payload = _minimal_payload() - payload["schedule"] = {"epochs": 1, "max_steps": 60} - with pytest.raises(ValidationError, match="mutually exclusive"): - UnslothJobInput.model_validate(payload) + payload["schedule"] = {"epochs": 3, "max_steps": 60} + spec = UnslothJobInput.model_validate(payload) + assert spec.schedule.epochs == 3 + assert spec.schedule.max_steps == 60 def test_either_one_is_fine(self) -> None: for sched in ({"epochs": 3}, {"max_steps": 60}): @@ -138,25 +142,25 @@ def test_4bit_and_8bit_rejected(self) -> None: UnslothJobInput.model_validate(payload) -class TestFullFinetuneRules: - def test_full_ft_rejects_4bit(self) -> None: +class TestAllWeightsFinetuneRules: + def test_all_weights_ft_rejects_4bit(self) -> None: payload = _minimal_payload() - payload["training"] = {"finetuning_type": "full"} + payload["training"] = {"finetuning_type": "all_weights"} # 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: + def test_all_weights_ft_rejects_lora_block(self) -> None: payload = _minimal_payload() payload["model"]["load_in_4bit"] = False - payload["training"] = {"finetuning_type": "full", "lora": {"rank": 8}} + payload["training"] = {"finetuning_type": "all_weights", "lora": {"rank": 8}} with pytest.raises(ValidationError, match="training.lora must be unset"): UnslothJobInput.model_validate(payload) - def test_full_ft_clean(self) -> None: + def test_all_weights_ft_clean(self) -> None: payload = _minimal_payload() payload["model"]["load_in_4bit"] = False - payload["training"] = {"finetuning_type": "full"} + payload["training"] = {"finetuning_type": "all_weights"} spec = UnslothJobInput.model_validate(payload) assert spec.training.lora is None @@ -175,10 +179,10 @@ def test_merged_save_with_lora_ok(self) -> None: payload["output"] = {"save_method": "merged_16bit"} UnslothJobInput.model_validate(payload) - def test_merged_save_with_full_rejected(self) -> None: + def test_merged_save_with_all_weights_rejected(self) -> None: payload = _minimal_payload() payload["model"]["load_in_4bit"] = False - payload["training"] = {"finetuning_type": "full"} + payload["training"] = {"finetuning_type": "all_weights"} payload["output"] = {"save_method": "merged_16bit"} with pytest.raises(ValidationError, match="only valid for training.finetuning_type='lora'"): UnslothJobInput.model_validate(payload) diff --git a/pyproject.toml b/pyproject.toml index 27458e38bc..ff5597f7ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -370,6 +370,7 @@ nemo-automodel-plugin = { workspace = true } nemo-unsloth-plugin = { workspace = true } nmp-automodel = { workspace = true } nmp-unsloth = { workspace = true } +nmp-customization-common = { workspace = true } [tool.uv.workspace] @@ -379,6 +380,7 @@ members = [ "packages/nemo_platform", "packages/filesets", "packages/nmp_common", + "packages/nmp_customization_common", "packages/nmp_platform_runner", "packages/garak_api/", "packages/nmp_testing", diff --git a/services/automodel/pyproject.toml b/services/automodel/pyproject.toml index fbc3eed955..431486fbf3 100644 --- a/services/automodel/pyproject.toml +++ b/services/automodel/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.11,<3.14" dependencies = [ "nmp-common", + "nmp-customization-common", "nemo-platform-sdk", "pydantic>=2.10.6", "pydantic-settings>=2.6.1", @@ -32,6 +33,7 @@ packages = ["src/nmp"] [tool.uv.sources] nmp-common = { workspace = true } +nmp-customization-common = { workspace = true } nemo-platform-sdk = { workspace = true } [tool.pytest.ini_options] diff --git a/services/automodel/src/nmp/automodel/adapter.py b/services/automodel/src/nmp/automodel/adapter.py index 27491354f7..3fbcb69a5d 100644 --- a/services/automodel/src/nmp/automodel/adapter.py +++ b/services/automodel/src/nmp/automodel/adapter.py @@ -10,14 +10,12 @@ from nmp.automodel.api.v2.jobs.schemas import ( CustomizationJobOutput, DistillationTraining, - IntegrationParams, LoRAParams, OutputResponse, ParallelismParams, SFTTraining, - WandBParams, ) -from nmp.common.api.common import SecretRef +from nmp.common.integrations import IntegrationsSpec from pydantic import BaseModel @@ -85,19 +83,11 @@ def _build_training_block(spec: dict[str, Any]) -> SFTTraining | DistillationTra return SFTTraining(**common) -def _build_integrations(spec: dict[str, Any]) -> IntegrationParams | None: +def _build_integrations(spec: dict[str, Any]) -> IntegrationsSpec | 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")) + return IntegrationsSpec.model_validate(raw) def automodel_spec_to_compiler_output(spec: dict[str, Any] | BaseModel) -> CustomizationJobOutput: diff --git a/services/automodel/src/nmp/automodel/api/__init__.py b/services/automodel/src/nmp/automodel/api/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/services/automodel/src/nmp/automodel/api/v2/__init__.py b/services/automodel/src/nmp/automodel/api/v2/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/services/automodel/src/nmp/automodel/api/v2/jobs/__init__.py b/services/automodel/src/nmp/automodel/api/v2/jobs/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/services/automodel/src/nmp/automodel/api/v2/jobs/schemas.py b/services/automodel/src/nmp/automodel/api/v2/jobs/schemas.py index 31a115816a..5ac657bdae 100644 --- a/services/automodel/src/nmp/automodel/api/v2/jobs/schemas.py +++ b/services/automodel/src/nmp/automodel/api/v2/jobs/schemas.py @@ -7,11 +7,11 @@ 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 nmp.common.integrations import IntegrationsSpec from pydantic import AfterValidator, BaseModel, ConfigDict, Discriminator, Field, model_validator # Important!!! Do not import Pydantic models from this file into tasks. @@ -323,93 +323,6 @@ def _peft_not_yet_supported(self) -> Self: 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 # ============================================================ @@ -521,7 +434,7 @@ class _CustomizationJobBase(BaseModel): 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( + integrations: Optional[IntegrationsSpec] = Field( default=None, description="Third-party integrations (e.g., Weights & Biases, MLflow).", ) diff --git a/services/automodel/src/nmp/automodel/app/constants.py b/services/automodel/src/nmp/automodel/app/constants.py index 083498adaf..44ce6323fa 100644 --- a/services/automodel/src/nmp/automodel/app/constants.py +++ b/services/automodel/src/nmp/automodel/app/constants.py @@ -2,29 +2,49 @@ # SPDX-License-Identifier: Apache-2.0 from nmp.common.jobs.constants import DEFAULT_JOB_STORAGE_PATH +from nmp.customization_common.service.constants import ( + DEFAULT_DATASET_OUTPUT_DIR_NAME, + DEFAULT_DATASET_PATH, + DEFAULT_MODEL_OUTPUT_DIR_NAME, + DEFAULT_MODEL_PATH, + DEFAULT_OUTPUT_MODEL_DIR_NAME, + DEFAULT_OUTPUT_MODEL_PATH, + NMP_FILES_URL_ENVVAR, + NMP_JOBS_URL_ENVVAR, +) + +__all__ = [ + "DEFAULT_DATASET_OUTPUT_DIR_NAME", + "DEFAULT_DATASET_PATH", + "DEFAULT_MODEL_OUTPUT_DIR_NAME", + "DEFAULT_MODEL_PATH", + "DEFAULT_OUTPUT_MODEL_DIR_NAME", + "DEFAULT_OUTPUT_MODEL_PATH", + "DEFAULT_SEED", + "DEFAULT_TEACHER_MODEL_DIR_NAME", + "DEFAULT_TEACHER_MODEL_PATH", + "DEFAULT_TRAINING_OUTPUT_DIR_NAME", + "DEFAULT_TRAINING_OUTPUT_PATH", + "DEFAULT_TRAINING_RESULT_FILE_NAME", + "NMP_FILES_URL_ENVVAR", + "NMP_JOBS_URL_ENVVAR", + "SERVICE_NAME", + "V4_MODEL_FOR_CAUSAL_LM_MAPPING_NAMES", +] 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" +# Relative directory names unique to automodel (teacher model + training outputs). 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 diff --git a/services/automodel/src/nmp/automodel/app/jobs/__init__.py b/services/automodel/src/nmp/automodel/app/jobs/__init__.py deleted file mode 100644 index e5725ea5a4..0000000000 --- a/services/automodel/src/nmp/automodel/app/jobs/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# 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/context.py b/services/automodel/src/nmp/automodel/app/jobs/context.py index 4987dfe6b7..09d236df24 100644 --- a/services/automodel/src/nmp/automodel/app/jobs/context.py +++ b/services/automodel/src/nmp/automodel/app/jobs/context.py @@ -1,82 +1,24 @@ # 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, +"""Job context for automodel container task entrypoints. + +Re-exports the shared :class:`nmp.customization_common.service.context.NMPJobContext` +so existing ``nmp.automodel.app.jobs.context`` import paths keep working. +""" + +from nmp.customization_common.service.context import ( + DEFAULT_ATTEMPT_ID, + DEFAULT_JOB_ID, + DEFAULT_STEP, + DEFAULT_TASK, + NMPJobContext, ) -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) - ), - ) +__all__ = [ + "DEFAULT_ATTEMPT_ID", + "DEFAULT_JOB_ID", + "DEFAULT_STEP", + "DEFAULT_TASK", + "NMPJobContext", +] 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 index c6a214fcc7..87cafe40b9 100644 --- a/services/automodel/src/nmp/automodel/app/jobs/file_io/schemas.py +++ b/services/automodel/src/nmp/automodel/app/jobs/file_io/schemas.py @@ -1,182 +1,44 @@ # 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 +"""Schemas for the automodel file_io task configuration. + +Re-exports the shared :mod:`nmp.customization_common.schemas.file_io` so existing +``nmp.automodel.app.jobs.file_io.schemas`` import paths keep working. +""" + +from nmp.customization_common.schemas.file_io import ( + FILESET_PROTOCOL, + DownloadItem, + DownloadStats, + FileDownloadError, + FileIOTaskConfig, + FileSetRef, + FileStats, + FileUploadError, + PathTraversalError, + ProgressReportError, + TaskCompilationError, + TaskPhase, + TaskStatus, + UploadItem, + UploadStats, +) + +__all__ = [ + "FILESET_PROTOCOL", + "DownloadItem", + "DownloadStats", + "FileDownloadError", + "FileIOTaskConfig", + "FileSetRef", + "FileStats", + "FileUploadError", + "PathTraversalError", + "ProgressReportError", + "TaskCompilationError", + "TaskPhase", + "TaskStatus", + "UploadItem", + "UploadStats", +] 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 index b2cd122d23..f621ce9679 100644 --- a/services/automodel/src/nmp/automodel/app/jobs/model_entity/schemas.py +++ b/services/automodel/src/nmp/automodel/app/jobs/model_entity/schemas.py @@ -1,106 +1,23 @@ # 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 +"""Schemas for the automodel model_entity task configuration. + +Re-exports the shared :mod:`nmp.customization_common.schemas.model_entity`. +""" + +from nmp.customization_common.schemas.model_entity import ( + DeploymentParameters, + ModelEntityCreationError, + ModelEntityTaskConfig, + PEFTConfig, + ToolCallConfig, +) + +__all__ = [ + "DeploymentParameters", + "ModelEntityCreationError", + "ModelEntityTaskConfig", + "PEFTConfig", + "ToolCallConfig", +] 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 5533baae6e..9eb1120cca 100644 --- a/services/automodel/src/nmp/automodel/app/jobs/training/compiler.py +++ b/services/automodel/src/nmp/automodel/app/jobs/training/compiler.py @@ -10,7 +10,6 @@ ContainerSpec, DistributedGPUExecutionProviderSpec, EnvironmentVariable, - EnvironmentVariableFromSecret, GPUExecutionProviderSpec, PlatformJobStep, ResourcesSpec, @@ -21,8 +20,6 @@ CustomizationJobOutput, DistillationTraining, LoRAParams, - MLflowParams, - WandBParams, ) from nmp.automodel.app.constants import ( DEFAULT_DATASET_PATH, @@ -33,15 +30,17 @@ 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.model_utils import is_embedding_model +from nmp.customization_common.integrations import ( + collect_integration_secret_envs, + warn_incomplete_integrations, +) logger = logging.getLogger(__name__) @@ -176,7 +175,7 @@ def compile_training_step( expert_parallel_size=p.expert_parallel_size, sequence_parallel=p.sequence_parallel, ), - integrations=_translate_integrations(job_spec), + integrations=job_spec.integrations, output_model=job_spec.output.name, ) @@ -214,7 +213,8 @@ def compile_training_step( ), ) - secret_envs = _collect_integration_secret_envs(job_spec) + warn_incomplete_integrations(job_spec.integrations) + secret_envs = collect_integration_secret_envs(job_spec.integrations) return PlatformJobStep( name="training", @@ -316,69 +316,6 @@ def _translate_lora_config(api_lora: LoRAParams, me: ModelEntity) -> LoRAConfig: 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. diff --git a/services/automodel/src/nmp/automodel/app/jobs/training/schemas.py b/services/automodel/src/nmp/automodel/app/jobs/training/schemas.py index 93d8ae7f45..032bfbb759 100644 --- a/services/automodel/src/nmp/automodel/app/jobs/training/schemas.py +++ b/services/automodel/src/nmp/automodel/app/jobs/training/schemas.py @@ -10,6 +10,7 @@ DEFAULT_TRAINING_OUTPUT_PATH, ) from nmp.automodel.entities.values import CheckpointFormat, FinetuningType, Precision, TrainingType +from nmp.common.integrations import IntegrationsSpec from pydantic import BaseModel, Field @@ -157,27 +158,6 @@ class EmbeddingConfig(BaseModel): 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.""" @@ -223,10 +203,6 @@ class ParallelismConfig(BaseModel): 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 @@ -235,7 +211,7 @@ class IntegrationsConfig(BaseModel): batch: BatchConfig optimizer: OptimizerConfig parallelism: ParallelismConfig - integrations: IntegrationsConfig = Field(default_factory=IntegrationsConfig) + integrations: IntegrationsSpec | None = None # === Output Paths === output_model: str # Set at compile-time from CustomizationJobOutput diff --git a/services/automodel/src/nmp/automodel/entities/values.py b/services/automodel/src/nmp/automodel/entities/values.py index b236ac9b4b..42f0d9ff9b 100644 --- a/services/automodel/src/nmp/automodel/entities/values.py +++ b/services/automodel/src/nmp/automodel/entities/values.py @@ -1,9 +1,18 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Value types for the Customizer service.""" +"""Value types for the Customizer service. -from enum import Enum, StrEnum +``FinetuningType`` and ``OutputNameType`` are shared with unsloth via +:mod:`nmp.customization_common.schemas.values`. ``TrainingType`` (4 algorithms), +``CheckpointFormat`` and ``Precision`` are automodel-specific. +""" + +from enum import Enum + +from nmp.customization_common.schemas.values import FinetuningType, OutputNameType + +__all__ = ["CheckpointFormat", "FinetuningType", "OutputNameType", "Precision", "TrainingType"] class CheckpointFormat(str, Enum): @@ -79,18 +88,3 @@ class TrainingType(str, Enum): 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 index efb29034ca..7dfd71ab87 100644 --- a/services/automodel/src/nmp/automodel/images.py +++ b/services/automodel/src/nmp/automodel/images.py @@ -5,9 +5,8 @@ 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 +from nmp.customization_common.service.images import resolve_qualified_image BASE_IMAGE_NAME = "nmp-automodel-base" TASKS_IMAGE_NAME = "nmp-automodel-tasks" @@ -20,21 +19,8 @@ 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 - - platform_config = get_platform_config() - registry = config.image_registry or platform_config.image_registry - return get_qualified_image(name, registry=registry) + """Resolve a job step image reference (see ``resolve_qualified_image``).""" + return resolve_qualified_image(name, override, config.image_registry) def get_tasks_image() -> str: diff --git a/services/automodel/src/nmp/automodel/platform_client.py b/services/automodel/src/nmp/automodel/platform_client.py index d55672d1ab..c0d67fa2a8 100644 --- a/services/automodel/src/nmp/automodel/platform_client.py +++ b/services/automodel/src/nmp/automodel/platform_client.py @@ -1,39 +1,12 @@ # 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 helpers for resolving model/dataset references against the platform. +Re-exports the shared :mod:`nmp.customization_common.service.platform_client` so existing +``nmp.automodel.platform_client`` import paths keep working. +""" -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 +from nmp.customization_common.service.platform_client import check_dataset_access, fetch_model_entity - -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 +__all__ = ["check_dataset_access", "fetch_model_entity"] diff --git a/services/automodel/src/nmp/automodel/tasks/file_io/callbacks.py b/services/automodel/src/nmp/automodel/tasks/file_io/callbacks.py index 51a2a18046..017b41a322 100644 --- a/services/automodel/src/nmp/automodel/tasks/file_io/callbacks.py +++ b/services/automodel/src/nmp/automodel/tasks/file_io/callbacks.py @@ -4,7 +4,6 @@ """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 @@ -15,6 +14,7 @@ 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 +from nmp.customization_common.tasks.file_io_utils import list_local_files as _list_local_files logger = logging.getLogger(__name__) @@ -264,46 +264,8 @@ def __init__( @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 [] + """List all files from a local path (see shared ``list_local_files``).""" + return [FileInfo(path=f.path, size=f.size) for f in _list_local_files(src_path)] @abstractmethod def branched(self, source_path: str, dest_path: str, **kwargs: Any) -> "BaseSingleFileCallback": 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 index 00fa660118..9af222e3b1 100644 --- a/services/automodel/src/nmp/automodel/tasks/file_io/progress_reporter.py +++ b/services/automodel/src/nmp/automodel/tasks/file_io/progress_reporter.py @@ -1,93 +1,15 @@ # 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 +"""Progress reporting for the automodel file_io task. -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 +Re-exports the shared :mod:`nmp.customization_common.tasks.file_io_progress_reporter`. +""" -logger = logging.getLogger(__name__) +from nmp.customization_common.tasks.file_io_progress_reporter import ( + JobsServiceProgressReporter, + NoOpProgressReporter, + ProgressReporter, +) - -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() +__all__ = ["JobsServiceProgressReporter", "NoOpProgressReporter", "ProgressReporter"] 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 5cd23fa6ce..d8ff35ad07 100644 --- a/services/automodel/src/nmp/automodel/tasks/file_io/run.py +++ b/services/automodel/src/nmp/automodel/tasks/file_io/run.py @@ -88,6 +88,10 @@ httpx.TimeoutException, httpx.ConnectError, httpx.ReadTimeout, + # Connection dropped mid-transfer (CDN/proxy closed the socket before the + # full body arrived). Common on large multi-GB model shards; safe to retry. + httpx.RemoteProtocolError, + httpx.ReadError, ) diff --git a/services/automodel/src/nmp/automodel/tasks/file_io/utils.py b/services/automodel/src/nmp/automodel/tasks/file_io/utils.py index e809105248..e685a70758 100644 --- a/services/automodel/src/nmp/automodel/tasks/file_io/utils.py +++ b/services/automodel/src/nmp/automodel/tasks/file_io/utils.py @@ -1,184 +1,23 @@ # 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 +"""Fileset path/IO + error-handling helpers for the automodel file_io task. -import httpx +Re-exports the shared :mod:`nmp.customization_common.tasks.file_io_utils`. +""" -# https://docs.nvidia.com/nemo/microservices/latest/pysdk/index.html#handling-errors -from nemo_platform import ( - APIConnectionError, - APIStatusError, - APITimeoutError, - AuthenticationError, - PermissionDeniedError, +from nmp.customization_common.tasks.file_io_utils import ( + filesystem_sdk_error_handler, + get_config, + sdk_error_handler, + validate_safe_path, + validate_storage_path, ) -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 +__all__ = [ + "filesystem_sdk_error_handler", + "get_config", + "sdk_error_handler", + "validate_safe_path", + "validate_storage_path", +] diff --git a/services/automodel/src/nmp/automodel/tasks/training/__init__.py b/services/automodel/src/nmp/automodel/tasks/training/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/services/automodel/src/nmp/automodel/tasks/training/backends/callbacks.py b/services/automodel/src/nmp/automodel/tasks/training/backends/callbacks.py index 04c7b40c2f..fc740eddfc 100644 --- a/services/automodel/src/nmp/automodel/tasks/training/backends/callbacks.py +++ b/services/automodel/src/nmp/automodel/tasks/training/backends/callbacks.py @@ -1,94 +1,14 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -import logging +"""Training progress callbacks for Automodel Jobs-service reporting. -from nmp.automodel.tasks.training.progress import JobsServiceProgressReporter +Re-exports the shared +:class:`nmp.customization_common.training.callbacks.TrainingProgressCallback`. +Automodel does not stamp a ``backend`` field (``_default_backend`` is ``None``), +preserving its existing ``status_details`` shape. +""" -logger = logging.getLogger(__name__) +from nmp.customization_common.training.callbacks import TrainingProgressCallback - -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() +__all__ = ["TrainingProgressCallback"] diff --git a/services/automodel/src/nmp/automodel/tasks/training/integrations.py b/services/automodel/src/nmp/automodel/tasks/training/integrations.py index f3610c9d13..c97d3bad91 100644 --- a/services/automodel/src/nmp/automodel/tasks/training/integrations.py +++ b/services/automodel/src/nmp/automodel/tasks/training/integrations.py @@ -1,40 +1,37 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-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.""" +"""Bridge Automodel ``TrainingStepConfig`` to shared integration runtime helpers.""" -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 +from nmp.automodel.app.jobs.training.schemas import TrainingStepConfig +from nmp.customization_common.integrations import ( + IntegrationRuntimeContext, +) +from nmp.customization_common.integrations import ( + build_mlflow_config as _build_mlflow_config, +) +from nmp.customization_common.integrations import ( + build_wandb_config as _build_wandb_config, +) + + +def integration_context_from_training_step( + customizer_config: TrainingStepConfig, + job_ctx: NMPJobContext, + framework: str, +) -> IntegrationRuntimeContext: + """Map compiled training-step config to shared integration runtime context.""" + return IntegrationRuntimeContext.from_integrations_spec( + integrations=customizer_config.integrations, + output_name=customizer_config.output_model, + workspace_path=customizer_config.workspace_path, + model_name=customizer_config.model.name, + job_ctx=job_ctx, + framework=framework, + ) def build_mlflow_config( @@ -42,127 +39,18 @@ def build_mlflow_config( 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", + """Build MLflow config for Automodel training.""" + return _build_mlflow_config( + integration_context_from_training_step(customizer_config, job_ctx, framework), ) - 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 + """Build W&B config for Automodel training.""" + return _build_wandb_config( + integration_context_from_training_step(customizer_config, job_ctx, framework), + ) diff --git a/services/automodel/src/nmp/automodel/tasks/training/progress.py b/services/automodel/src/nmp/automodel/tasks/training/progress.py index 0ede065112..b586e09117 100644 --- a/services/automodel/src/nmp/automodel/tasks/training/progress.py +++ b/services/automodel/src/nmp/automodel/tasks/training/progress.py @@ -1,173 +1,25 @@ # 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. +"""Progress reporting for Automodel training tasks. -For training-specific metrics (loss, validation, checkpoints), see -the `TrainingProgressCallback` in the automodel backend which composes -this reporter and provides training-specific methods. +Thin subclass of the shared +:class:`nmp.customization_common.training.progress.JobsServiceProgressReporter` +that bakes in the automodel ``SERVICE_NAME`` so callers keep the +``JobsServiceProgressReporter(job_ctx)`` constructor. """ -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__) +from nmp.customization_common.service.context import NMPJobContext +from nmp.customization_common.training.progress import ( + JobsServiceProgressReporter as _BaseJobsServiceProgressReporter, +) +__all__ = ["JobsServiceProgressReporter"] -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. - """ +class JobsServiceProgressReporter(_BaseJobsServiceProgressReporter): + """Automodel training progress reporter (binds the automodel service name).""" 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() + super().__init__(job_ctx, service_name=SERVICE_NAME) diff --git a/services/automodel/src/nmp/automodel/tasks/training/schemas.py b/services/automodel/src/nmp/automodel/tasks/training/schemas.py index 4a5d493ee5..f691295c39 100644 --- a/services/automodel/src/nmp/automodel/tasks/training/schemas.py +++ b/services/automodel/src/nmp/automodel/tasks/training/schemas.py @@ -7,13 +7,11 @@ EmbeddingConfig, GPUInfo, LoRAConfig, - MLflowConfig, ModelConfig, OptimizerType, TrainingMetrics, TrainingResult, TrainingStepConfig, - WandBConfig, ) from nmp.automodel.entities.values import ( CheckpointFormat, @@ -32,11 +30,9 @@ "EmbeddingConfig", "GPUInfo", "LoRAConfig", - "MLflowConfig", "ModelConfig", "OptimizerType", "TrainingMetrics", "TrainingResult", "TrainingStepConfig", - "WandBConfig", ] diff --git a/services/automodel/tests/contract/README.md b/services/automodel/tests/contract/README.md index 77bdbe318a..a7dc8c9fac 100644 --- a/services/automodel/tests/contract/README.md +++ b/services/automodel/tests/contract/README.md @@ -34,6 +34,9 @@ Located in `./sample-datasets/`: ## Directory Layout +Submit-time job JSON (including W&B / MLflow integrations) lives in +`plugins/nemo-automodel/tests/fixtures/`. + Input configs are grouped by model so the generation script downloads each model only once: ``` diff --git a/services/automodel/tests/test_adapter.py b/services/automodel/tests/test_adapter.py index dfd1f44b45..50bd44e0d0 100644 --- a/services/automodel/tests/test_adapter.py +++ b/services/automodel/tests/test_adapter.py @@ -1,8 +1,10 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +from nemo_automodel_plugin.schema import AutomodelJobOutput from nmp.automodel.adapter import automodel_spec_to_compiler_output from nmp.automodel.api.v2.jobs.schemas import DistillationTraining, SFTTraining +from nmp.customization_common.integrations import collect_integration_secret_envs def test_adapter_sft() -> None: @@ -33,3 +35,92 @@ def test_adapter_distillation() -> None: ) assert isinstance(spec.training, DistillationTraining) assert spec.training.teacher_model == "meta/teacher" + + +def test_adapter_no_integrations() -> 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 spec.integrations is None + assert collect_integration_secret_envs(spec.integrations) == [] + + +def test_adapter_integrations_full_round_trip() -> None: + spec = automodel_spec_to_compiler_output( + { + "model": "meta/llama", + "dataset": {"training": "default/train"}, + "training": {"training_type": "sft", "finetuning_type": "lora"}, + "output": {"name": "my-output", "type": "adapter", "fileset": "out-fs"}, + "integrations": { + "wandb": { + "project": "my-project", + "name": "run-001", + "entity": "my-team", + "tags": ["sft", "llama"], + "notes": "experiment notes", + "base_url": "https://wandb.internal", + "api_key_secret": "default/wandb-key", + }, + "mlflow": { + "experiment_name": "exp-1", + "name": "run-001", + "tracking_uri": "http://mlflow:5000", + "tags": {"team": "nlp"}, + "description": "SFT run", + }, + }, + }, + ) + + assert spec.integrations is not None + wandb = spec.integrations.wandb + assert wandb is not None + assert wandb.project == "my-project" + assert wandb.name == "run-001" + assert wandb.entity == "my-team" + assert wandb.tags == ["sft", "llama"] + assert wandb.notes == "experiment notes" + assert wandb.base_url == "https://wandb.internal" + assert wandb.api_key_secret is not None + assert wandb.api_key_secret.root == "default/wandb-key" + + mlflow = spec.integrations.mlflow + assert mlflow is not None + assert mlflow.experiment_name == "exp-1" + assert mlflow.name == "run-001" + assert mlflow.tracking_uri == "http://mlflow:5000" + assert mlflow.tags == {"team": "nlp"} + assert mlflow.description == "SFT run" + + secret_envs = collect_integration_secret_envs(spec.integrations) + assert secret_envs == [ + {"name": "WANDB_API_KEY", "from_secret": {"name": "default/wandb-key"}}, + ] + + +def test_adapter_integrations_from_automodel_job_output() -> None: + job_output = AutomodelJobOutput.model_validate( + { + "model": "meta/llama", + "dataset": {"training": "default/train"}, + "training": {"training_type": "sft", "finetuning_type": "lora"}, + "schedule": {"epochs": 1}, + "batch": {"global_batch_size": 8, "micro_batch_size": 1}, + "optimizer": {"learning_rate": 1e-4}, + "parallelism": {"num_nodes": 1, "num_gpus_per_node": 1}, + "output": {"name": "out", "type": "adapter", "fileset": "out-fs"}, + "integrations": {"wandb": {"project": "plugin-project"}}, + }, + ) + + spec = automodel_spec_to_compiler_output(job_output) + + assert spec.integrations is not None + assert spec.integrations.wandb is not None + assert spec.integrations.wandb.project == "plugin-project" diff --git a/services/automodel/tests/test_images.py b/services/automodel/tests/test_images.py index 64e05d4147..8782e0d079 100644 --- a/services/automodel/tests/test_images.py +++ b/services/automodel/tests/test_images.py @@ -5,6 +5,7 @@ import nemo_platform_plugin.jobs.image as platform_image import nmp.automodel.images as automodel_images +import nmp.customization_common.service.images as shared_images import pytest from nmp.automodel.config import AutomodelConfig from nmp.automodel.images import ( @@ -19,7 +20,7 @@ @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(shared_images, "get_platform_config", lambda: config) monkeypatch.setattr(platform_image, "get_platform_config", lambda: config) return config diff --git a/services/automodel/tests/test_integrations_compiler.py b/services/automodel/tests/test_integrations_compiler.py new file mode 100644 index 0000000000..ba385d4f33 --- /dev/null +++ b/services/automodel/tests/test_integrations_compiler.py @@ -0,0 +1,83 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from datetime import datetime + +import pytest +from nemo_platform.types.models.model_entity import ModelEntity +from nmp.automodel.api.v2.jobs.schemas import ( + CustomizationJobOutput, + LoRAParams, + OutputResponse, + SFTTraining, +) +from nmp.automodel.app.jobs.training.compiler import compile_training_step +from nmp.common.entities.utils import get_random_id +from nmp.common.integrations import IntegrationsSpec + + +def _make_model_entity() -> ModelEntity: + return ModelEntity( + id=get_random_id("model"), + workspace="default", + name="test-target", + fileset="default/base-model", + trust_remote_code=False, + finetuning_type=None, + created_at=datetime.now(), + updated_at=datetime.now(), + ) + + +def _job_spec_with_integrations() -> CustomizationJobOutput: + return CustomizationJobOutput( + model="default/test-target", + dataset="default/train", + 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"), + integrations=IntegrationsSpec.model_validate( + { + "wandb": { + "project": "my-project", + "api_key_secret": "default/wandb-key", + }, + }, + ), + ) + + +def test_compile_training_step_injects_wandb_secret() -> None: + step = compile_training_step(_job_spec_with_integrations(), base_env=[], me=_make_model_entity()) + + env = step["environment"] if isinstance(step, dict) else step.environment + assert {"name": "WANDB_API_KEY", "from_secret": {"name": "default/wandb-key"}} in env + + +def test_compile_training_step_no_integrations() -> None: + spec = _job_spec_with_integrations().model_copy(update={"integrations": None}) + step = compile_training_step(spec, base_env=[], me=_make_model_entity()) + + env = step["environment"] if isinstance(step, dict) else step.environment + secret_envs = [item for item in env if item.get("name") == "WANDB_API_KEY"] + assert secret_envs == [] + + +def test_compile_training_step_warns_on_incomplete_wandb( + caplog: pytest.LogCaptureFixture, +) -> None: + spec = _job_spec_with_integrations().model_copy( + update={ + "integrations": IntegrationsSpec.model_validate({"wandb": {"project": "my-project"}}), + }, + ) + caplog.set_level("WARNING") + + compile_training_step(spec, base_env=[], me=_make_model_entity()) + + assert "api_key_secret is missing" in caplog.text diff --git a/services/unsloth/pyproject.toml b/services/unsloth/pyproject.toml index 1ff9ae9e8d..9ccba21b72 100644 --- a/services/unsloth/pyproject.toml +++ b/services/unsloth/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.11,<3.14" dependencies = [ "nmp-common", + "nmp-customization-common", "nemo-platform-sdk", "pydantic>=2.10.6", "pydantic-settings>=2.6.1", @@ -29,6 +30,12 @@ dependencies = [ # command (per unsloth's README), so this extra is mainly for non-container # callers that want a single-package handle. unsloth = ["unsloth[huggingface]"] +# HF Trainer callbacks for integrations.wandb / integrations.mlflow (report_to). +# Container image installs this extra; keep out of core deps so compile/API stay lean. +integrations = [ + "wandb>=0.25.1", + "mlflow-skinny", +] [project.scripts] # Container entrypoints. Each script matches the automodel name pattern for parity. @@ -45,6 +52,7 @@ packages = ["src/nmp"] [tool.uv.sources] nmp-common = { workspace = true } +nmp-customization-common = { workspace = true } nemo-platform-sdk = { workspace = true } [dependency-groups] diff --git a/services/unsloth/src/nmp/unsloth/app/__init__.py b/services/unsloth/src/nmp/unsloth/app/__init__.py deleted file mode 100644 index e5725ea5a4..0000000000 --- a/services/unsloth/src/nmp/unsloth/app/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# 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 index ecb3cfa3da..a40a0d8347 100644 --- a/services/unsloth/src/nmp/unsloth/app/constants.py +++ b/services/unsloth/src/nmp/unsloth/app/constants.py @@ -1,28 +1,38 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Shared constants for the unsloth container job pipeline. +"""Constants for the unsloth container job pipeline. -These mirror the ``services/automodel`` constants so the unsloth service -exposes the same path layout to the 4-step container submit pipeline. +Shared container-path/env constants (including the validation-dataset path) come +from :mod:`nmp.customization_common.service.constants`; this module only adds the unsloth +``SERVICE_NAME``. """ -from nmp.common.jobs.constants import DEFAULT_JOB_STORAGE_PATH +from nmp.customization_common.service.constants import ( + DEFAULT_DATASET_OUTPUT_DIR_NAME, + DEFAULT_DATASET_PATH, + DEFAULT_MODEL_OUTPUT_DIR_NAME, + DEFAULT_MODEL_PATH, + DEFAULT_OUTPUT_MODEL_DIR_NAME, + DEFAULT_OUTPUT_MODEL_PATH, + DEFAULT_VALIDATION_DATASET_OUTPUT_DIR_NAME, + DEFAULT_VALIDATION_DATASET_PATH, + NMP_FILES_URL_ENVVAR, + NMP_JOBS_URL_ENVVAR, +) -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_VALIDATION_DATASET_OUTPUT_DIR_NAME = "validation_dataset" -DEFAULT_OUTPUT_MODEL_DIR_NAME = "output_model" +__all__ = [ + "DEFAULT_DATASET_OUTPUT_DIR_NAME", + "DEFAULT_DATASET_PATH", + "DEFAULT_MODEL_OUTPUT_DIR_NAME", + "DEFAULT_MODEL_PATH", + "DEFAULT_OUTPUT_MODEL_DIR_NAME", + "DEFAULT_OUTPUT_MODEL_PATH", + "DEFAULT_VALIDATION_DATASET_OUTPUT_DIR_NAME", + "DEFAULT_VALIDATION_DATASET_PATH", + "NMP_FILES_URL_ENVVAR", + "NMP_JOBS_URL_ENVVAR", + "SERVICE_NAME", +] -# 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}" -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" +SERVICE_NAME = "unsloth" diff --git a/services/unsloth/src/nmp/unsloth/app/jobs/__init__.py b/services/unsloth/src/nmp/unsloth/app/jobs/__init__.py deleted file mode 100644 index e5725ea5a4..0000000000 --- a/services/unsloth/src/nmp/unsloth/app/jobs/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# 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/context.py b/services/unsloth/src/nmp/unsloth/app/jobs/context.py index 388366fd48..ffdb753c9e 100644 --- a/services/unsloth/src/nmp/unsloth/app/jobs/context.py +++ b/services/unsloth/src/nmp/unsloth/app/jobs/context.py @@ -3,87 +3,22 @@ """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. +Re-exports the shared :class:`nmp.customization_common.service.context.NMPJobContext` +so existing ``nmp.unsloth.app.jobs.context`` import paths keep working. """ -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.customization_common.service.context import ( + DEFAULT_ATTEMPT_ID, + DEFAULT_JOB_ID, + DEFAULT_STEP, + DEFAULT_TASK, + NMPJobContext, ) -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) - ), - ) +__all__ = [ + "DEFAULT_ATTEMPT_ID", + "DEFAULT_JOB_ID", + "DEFAULT_STEP", + "DEFAULT_TASK", + "NMPJobContext", +] 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 deleted file mode 100644 index e5725ea5a4..0000000000 --- a/services/unsloth/src/nmp/unsloth/app/jobs/file_io/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# 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 index 641a9ea69e..0d00b36ece 100644 --- a/services/unsloth/src/nmp/unsloth/app/jobs/file_io/schemas.py +++ b/services/unsloth/src/nmp/unsloth/app/jobs/file_io/schemas.py @@ -3,173 +3,42 @@ """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. +Re-exports the shared :mod:`nmp.customization_common.schemas.file_io` so existing +``nmp.unsloth.app.jobs.file_io.schemas`` import paths keep working. """ -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 +from nmp.customization_common.schemas.file_io import ( + FILESET_PROTOCOL, + DownloadItem, + DownloadStats, + FileDownloadError, + FileIOTaskConfig, + FileSetRef, + FileStats, + FileUploadError, + PathTraversalError, + ProgressReportError, + TaskCompilationError, + TaskPhase, + TaskStatus, + UploadItem, + UploadStats, +) + +__all__ = [ + "FILESET_PROTOCOL", + "DownloadItem", + "DownloadStats", + "FileDownloadError", + "FileIOTaskConfig", + "FileSetRef", + "FileStats", + "FileUploadError", + "PathTraversalError", + "ProgressReportError", + "TaskCompilationError", + "TaskPhase", + "TaskStatus", + "UploadItem", + "UploadStats", +] 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 deleted file mode 100644 index e5725ea5a4..0000000000 --- a/services/unsloth/src/nmp/unsloth/app/jobs/model_entity/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# 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 index cd7f7f52f9..170b8bf7c6 100644 --- a/services/unsloth/src/nmp/unsloth/app/jobs/model_entity/schemas.py +++ b/services/unsloth/src/nmp/unsloth/app/jobs/model_entity/schemas.py @@ -3,108 +3,21 @@ """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. +Re-exports the shared :mod:`nmp.customization_common.schemas.model_entity`. """ -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.""" +from nmp.customization_common.schemas.model_entity import ( + DeploymentParameters, + ModelEntityCreationError, + ModelEntityTaskConfig, + PEFTConfig, + ToolCallConfig, +) + +__all__ = [ + "DeploymentParameters", + "ModelEntityCreationError", + "ModelEntityTaskConfig", + "PEFTConfig", + "ToolCallConfig", +] diff --git a/services/unsloth/src/nmp/unsloth/app/jobs/training/__init__.py b/services/unsloth/src/nmp/unsloth/app/jobs/training/__init__.py deleted file mode 100644 index e5725ea5a4..0000000000 --- a/services/unsloth/src/nmp/unsloth/app/jobs/training/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# 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 index 9439c175f9..13e120c00e 100644 --- a/services/unsloth/src/nmp/unsloth/app/jobs/training/compiler.py +++ b/services/unsloth/src/nmp/unsloth/app/jobs/training/compiler.py @@ -19,6 +19,10 @@ PlatformJobStep, ResourcesSpec, ) +from nmp.customization_common.integrations import ( + collect_integration_secret_envs, + warn_incomplete_integrations, +) from nmp.unsloth.app.constants import ( DEFAULT_DATASET_PATH, DEFAULT_MODEL_PATH, @@ -76,9 +80,10 @@ def compile_training_step( if profile is not None: executor["profile"] = profile + warn_incomplete_integrations(job_spec.integrations) return PlatformJobStep( name="training", executor=executor, - environment=base_env, + environment=[*base_env, *collect_integration_secret_envs(job_spec.integrations)], config=step_config.model_dump(mode="json"), ) diff --git a/services/unsloth/src/nmp/unsloth/entities/__init__.py b/services/unsloth/src/nmp/unsloth/entities/__init__.py deleted file mode 100644 index e5725ea5a4..0000000000 --- a/services/unsloth/src/nmp/unsloth/entities/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# 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 index 4ade32a64b..0abdd54de8 100644 --- a/services/unsloth/src/nmp/unsloth/entities/values.py +++ b/services/unsloth/src/nmp/unsloth/entities/values.py @@ -3,38 +3,19 @@ """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. +``FinetuningType`` and ``OutputNameType`` are shared with automodel via +:mod:`nmp.customization_common.schemas.values`. ``TrainingType`` is unsloth-specific: +the unsloth backend supports SFT only. """ -from enum import Enum, StrEnum +from enum import Enum + +from nmp.customization_common.schemas.values import FinetuningType, OutputNameType + +__all__ = ["FinetuningType", "OutputNameType", "TrainingType"] 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 index 137ec231fd..b96c189b11 100644 --- a/services/unsloth/src/nmp/unsloth/images.py +++ b/services/unsloth/src/nmp/unsloth/images.py @@ -7,16 +7,15 @@ :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. +Unsloth ships a **single** image, ``nmp-unsloth-training``, used by all four +steps (file_io, model_entity, training) — the CPU task steps reuse the training +image rather than a separate ``nmp-unsloth-tasks`` build. Override the whole +image via ``NMP_UNSLOTH_TRAINING_IMAGE``. """ 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.customization_common.service.images import resolve_qualified_image from nmp.unsloth.config import config BASE_IMAGE_NAME = "nmp-unsloth-base" @@ -30,34 +29,16 @@ 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 - - platform_config = get_platform_config() - registry = config.image_registry or platform_config.image_registry - return get_qualified_image(name, registry=registry) + """Resolve a job step image reference (see ``resolve_qualified_image``).""" + return resolve_qualified_image(name, override, config.image_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. + Unsloth ships a single image, so the CPU task steps reuse the + ``nmp-unsloth-training`` image rather than a separate tasks image. """ - if config.tasks_image: - return get_unsloth_qualified_image(TASKS_IMAGE_NAME, config.tasks_image) return get_training_image() diff --git a/services/unsloth/src/nmp/unsloth/integrations/__init__.py b/services/unsloth/src/nmp/unsloth/integrations/__init__.py new file mode 100644 index 0000000000..bbd21d7806 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/integrations/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Unsloth-specific experiment-tracking helpers.""" + +from nmp.unsloth.integrations.hf_bridge import apply_integrations_to_sft_config + +__all__ = ["apply_integrations_to_sft_config"] diff --git a/services/unsloth/src/nmp/unsloth/integrations/hf_bridge.py b/services/unsloth/src/nmp/unsloth/integrations/hf_bridge.py new file mode 100644 index 0000000000..ba60bf8c64 --- /dev/null +++ b/services/unsloth/src/nmp/unsloth/integrations/hf_bridge.py @@ -0,0 +1,98 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Map shared integration configs onto HuggingFace ``SFTConfig`` + env vars.""" + +import json +import logging +from pathlib import Path +from typing import Any + +from nmp.common.integrations import IntegrationsSpec +from nmp.customization_common.integrations import ( + IntegrationRuntimeContext, + build_mlflow_config, + build_wandb_config, +) +from nmp.customization_common.service.context import NMPJobContext + +logger = logging.getLogger(__name__) + + +def apply_integrations_to_sft_config( + *, + integrations: IntegrationsSpec | None, + job_ctx: NMPJobContext, + output_name: str, + workspace_path: Path | str, + model_name: str | None, +) -> tuple[list[str], dict[str, Any], dict[str, str]]: + """Apply integrations to HF Trainer and return ``(report_to, sft_kwargs, env)``. + + Uses the shared runtime builders so Unsloth gets the same defaults and + validation as Automodel. Backends are included in ``report_to`` only when + the shared builder activates them (e.g. W&B requires ``WANDB_API_KEY``). + + HuggingFace ``TrainingArguments.run_name`` is shared by W&B and MLflow + callbacks. When both integrations are active, ``wandb.name`` wins if set; + otherwise ``mlflow.name`` is used. + + Environment variables are returned for the caller to apply — this function + does not mutate ``os.environ``. + """ + ctx = IntegrationRuntimeContext.from_integrations_spec( + integrations=integrations, + output_name=output_name, + workspace_path=str(workspace_path), + model_name=model_name, + job_ctx=job_ctx, + framework="unsloth", + ) + + report_to: list[str] = [] + sft_kwargs: dict[str, Any] = {} + env: dict[str, str] = {} + + wandb_config = build_wandb_config(ctx) + mlflow_config = build_mlflow_config(ctx) + + if integrations and integrations.wandb and integrations.mlflow: + wandb_name = integrations.wandb.name + mlflow_name = integrations.mlflow.name + if wandb_name and mlflow_name and wandb_name != mlflow_name: + logger.warning( + "integrations.wandb.name (%s) and integrations.mlflow.name (%s) differ; " + "HuggingFace TrainingArguments.run_name can only hold one value — using wandb.name.", + wandb_name, + mlflow_name, + ) + + if wandb_config: + report_to.append("wandb") + if project := wandb_config.get("project"): + env["WANDB_PROJECT"] = project + if entity := wandb_config.get("entity"): + env["WANDB_ENTITY"] = entity + if notes := wandb_config.get("notes"): + env["WANDB_NOTES"] = notes + if tags := wandb_config.get("tags"): + env["WANDB_TAGS"] = ",".join(tags) + if base_url := (wandb_config.get("settings") or {}).get("base_url"): + env["WANDB_BASE_URL"] = base_url + if wandb_dir := wandb_config.get("dir"): + env["WANDB_DIR"] = wandb_dir + + if mlflow_config: + report_to.append("mlflow") + env["MLFLOW_TRACKING_URI"] = mlflow_config["tracking_uri"] + if experiment_name := mlflow_config.get("experiment_name"): + env["MLFLOW_EXPERIMENT_NAME"] = experiment_name + if tags := mlflow_config.get("tags"): + env["MLFLOW_TAGS"] = json.dumps(tags) + + if wandb_config and wandb_config.get("name"): + sft_kwargs["run_name"] = wandb_config["name"] + elif mlflow_config and mlflow_config.get("run_name"): + sft_kwargs["run_name"] = mlflow_config["run_name"] + + return report_to or ["none"], sft_kwargs, env diff --git a/services/unsloth/src/nmp/unsloth/platform_client.py b/services/unsloth/src/nmp/unsloth/platform_client.py index 0e1284ca77..7ec7865ac1 100644 --- a/services/unsloth/src/nmp/unsloth/platform_client.py +++ b/services/unsloth/src/nmp/unsloth/platform_client.py @@ -3,78 +3,10 @@ """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. +Re-exports the shared :mod:`nmp.customization_common.service.platform_client` so existing +``nmp.unsloth.platform_client`` import paths keep working. """ -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. +from nmp.customization_common.service.platform_client import check_dataset_access, fetch_model_entity - 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 +__all__ = ["check_dataset_access", "fetch_model_entity"] diff --git a/services/unsloth/src/nmp/unsloth/schemas.py b/services/unsloth/src/nmp/unsloth/schemas.py index 33afbed22d..a9529e88c2 100644 --- a/services/unsloth/src/nmp/unsloth/schemas.py +++ b/services/unsloth/src/nmp/unsloth/schemas.py @@ -31,6 +31,7 @@ from typing import Literal +from nmp.common.integrations import IntegrationsSpec from pydantic import BaseModel, ConfigDict, Field @@ -93,7 +94,7 @@ class TrainingSpec(BaseModel): model_config = ConfigDict(extra="forbid") training_type: Literal["sft"] = "sft" - finetuning_type: Literal["lora", "full"] = "lora" + finetuning_type: Literal["lora", "all_weights"] = "lora" lora: LoRAParams | None = Field( default=None, description="Required when finetuning_type='lora'. Auto-filled with defaults if omitted.", @@ -139,7 +140,9 @@ class ScheduleSpec(BaseModel): model_config = ConfigDict(extra="forbid") - epochs: int | None = Field(default=None, gt=0) + # Consistent with Automodel: train for ``epochs`` (default 1) unless ``max_steps`` + # is set, in which case the trainer caps training at that many steps. + epochs: int = Field(default=1, 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) @@ -195,25 +198,6 @@ class HardwareSpec(BaseModel): ) -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 - # training container environment themselves. - - -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. Output naming is resolved during ``to_spec``. diff --git a/services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py b/services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py index 0ef1a32f10..ffb36875a8 100644 --- a/services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py +++ b/services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py @@ -4,7 +4,6 @@ """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 @@ -13,6 +12,7 @@ from fsspec.callbacks import Callback, TqdmCallback from nmp.common.jobs.schemas import PlatformJobStatus +from nmp.customization_common.tasks.file_io_utils import list_local_files as _list_local_files from nmp.unsloth.app.jobs.file_io.schemas import DownloadStats, TaskPhase, UploadStats from nmp.unsloth.tasks.file_io.progress_reporter import ProgressReporter @@ -157,32 +157,8 @@ def __init__( @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 [] + """List all files under *src_path* (see shared ``list_local_files``).""" + return [FileInfo(path=f.path, size=f.size) for f in _list_local_files(src_path)] @abstractmethod def branched(self, source_path: str, dest_path: str, **kwargs: Any) -> "BaseSingleFileCallback": 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 index 3536b470d3..3c50534d26 100644 --- a/services/unsloth/src/nmp/unsloth/tasks/file_io/progress_reporter.py +++ b/services/unsloth/src/nmp/unsloth/tasks/file_io/progress_reporter.py @@ -1,93 +1,15 @@ # 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 +"""Progress reporting for the unsloth file_io task. -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 +Re-exports the shared :mod:`nmp.customization_common.tasks.file_io_progress_reporter`. +""" -logger = logging.getLogger(__name__) +from nmp.customization_common.tasks.file_io_progress_reporter import ( + JobsServiceProgressReporter, + NoOpProgressReporter, + ProgressReporter, +) - -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() +__all__ = ["JobsServiceProgressReporter", "NoOpProgressReporter", "ProgressReporter"] 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 4a9f4acaf4..8b6346385d 100644 --- a/services/unsloth/src/nmp/unsloth/tasks/file_io/run.py +++ b/services/unsloth/src/nmp/unsloth/tasks/file_io/run.py @@ -90,6 +90,10 @@ httpx.TimeoutException, httpx.ConnectError, httpx.ReadTimeout, + # Connection dropped mid-transfer (CDN/proxy closed the socket before the + # full body arrived). Common on large multi-GB model shards; safe to retry. + httpx.RemoteProtocolError, + httpx.ReadError, ) diff --git a/services/unsloth/src/nmp/unsloth/tasks/file_io/utils.py b/services/unsloth/src/nmp/unsloth/tasks/file_io/utils.py index 9bd25d4b36..1bdcb3b9f8 100644 --- a/services/unsloth/src/nmp/unsloth/tasks/file_io/utils.py +++ b/services/unsloth/src/nmp/unsloth/tasks/file_io/utils.py @@ -1,125 +1,23 @@ # 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 +"""Fileset path/IO + error-handling helpers for the unsloth file_io task. -import httpx +Re-exports the shared :mod:`nmp.customization_common.tasks.file_io_utils`. +""" -# https://docs.nvidia.com/nemo/microservices/latest/pysdk/index.html#handling-errors -from nemo_platform import ( - APIConnectionError, - APIStatusError, - APITimeoutError, - AuthenticationError, - PermissionDeniedError, +from nmp.customization_common.tasks.file_io_utils import ( + filesystem_sdk_error_handler, + get_config, + sdk_error_handler, + validate_safe_path, + validate_storage_path, ) -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 +__all__ = [ + "filesystem_sdk_error_handler", + "get_config", + "sdk_error_handler", + "validate_safe_path", + "validate_storage_path", +] diff --git a/services/unsloth/src/nmp/unsloth/tasks/training/backends/callbacks.py b/services/unsloth/src/nmp/unsloth/tasks/training/backends/callbacks.py index 4a3da9e631..b55c538a53 100644 --- a/services/unsloth/src/nmp/unsloth/tasks/training/backends/callbacks.py +++ b/services/unsloth/src/nmp/unsloth/tasks/training/backends/callbacks.py @@ -1,109 +1,23 @@ # 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.""" +"""Training progress callbacks for Unsloth Jobs-service reporting. -import logging +Thin subclass of the shared +:class:`nmp.customization_common.training.callbacks.TrainingProgressCallback` +that stamps ``backend="unsloth"`` on each report by default. +""" -from nmp.unsloth.tasks.training.progress import JobsServiceProgressReporter +from typing import ClassVar -logger = logging.getLogger(__name__) +from nmp.customization_common.training.callbacks import ( + TrainingProgressCallback as _BaseTrainingProgressCallback, +) +__all__ = ["TrainingProgressCallback"] -class TrainingProgressCallback: - """Report Unsloth training progress to the Jobs service. - Metric accumulation matches Automodel: ``train_loss`` and ``val_loss`` - time series are included under ``metrics`` on every update. - """ +class TrainingProgressCallback(_BaseTrainingProgressCallback): + """Report Unsloth training progress to the Jobs service.""" - 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]]]: - return { - "train_loss": list(self._train_metrics), - "val_loss": list(self._val_metrics), - } - - 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, - backend=backend, - ) - - def report_train_step( - self, - step: int, - epoch: int, - loss: float, - lr: float | None = None, - grad_norm: float | None = None, - *, - backend: str = "unsloth", - ) -> None: - 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, - backend=backend, - metrics=self._build_metrics_summary(), - ) - - 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, - *, - 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, *, backend: str = "unsloth") -> None: - self._reporter.report_running(phase="epoch_end", step=step, epoch=epoch, backend=backend) - - def close(self) -> None: - self._reporter.close() + _default_backend: ClassVar[str | None] = "unsloth" 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 a8d379cf9a..efbe14aee7 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 @@ -21,11 +21,15 @@ import logging import os +from dataclasses import replace from typing import Any, Literal from nemo_platform_plugin.job_context import JobContext +from nmp.unsloth.app.jobs.context import NMPJobContext +from nmp.unsloth.integrations import apply_integrations_to_sft_config from nmp.unsloth.schemas import UnslothJobOutput from nmp.unsloth.tasks.training.backends.callbacks import TrainingProgressCallback +from nmp.unsloth.tasks.training.progress import JobsServiceProgressReporter logger = logging.getLogger(__name__) @@ -171,9 +175,9 @@ def train_sft( use_gradient_checkpointing=gc_value, max_seq_length=spec.model.max_seq_length, ) - # Full FT: leave `model` as-is. `from_pretrained` already returned the + # All-weights 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'`. + # rejected by the spec validator for `finetuning_type='all_weights'`. # ── Dataset ──────────────────────────────────────────────────────── resolved_train_path = dataset_path or spec.dataset.path @@ -203,6 +207,23 @@ def train_sft( bf16 = spec.hardware.precision == "bf16" fp16 = spec.hardware.precision == "fp16" + # Prefer identifiers from the passed JobContext; the in-process UnslothJob.run() + # path may not have the Job Controller env vars set. Fall back to env-derived + # values for fields JobContext doesn't carry (step/task). + job_ctx = NMPJobContext.from_env() + if ctx.job_id: + job_ctx = replace(job_ctx, job_id=ctx.job_id, workspace=ctx.workspace) + + report_to, integration_kwargs, integration_env = apply_integrations_to_sft_config( + integrations=spec.integrations, + job_ctx=job_ctx, + output_name=spec.output.name, + workspace_path=output_dir, + model_name=spec.model.name, + ) + for key, value in integration_env.items(): + os.environ[key] = value + args_kwargs: dict[str, Any] = { "output_dir": str(output_dir), "per_device_train_batch_size": spec.batch.per_device_train_batch_size, @@ -216,9 +237,7 @@ def train_sft( "seed": spec.schedule.seed, "bf16": bf16, "fp16": fp16, - "report_to": list( - spec.integrations.report_to if spec.integrations is not None else ["none"], - ), + "report_to": list(report_to), # 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, @@ -226,8 +245,8 @@ def train_sft( } 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 + # epochs always set (defaults to 1); max_steps, when present, caps/overrides it (trl semantics). + 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: @@ -251,13 +270,7 @@ def train_sft( 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 - # 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_kwargs.update(integration_kwargs) args = SFTConfig(**args_kwargs) @@ -297,9 +310,6 @@ def train_sft( 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())) diff --git a/services/unsloth/src/nmp/unsloth/tasks/training/progress.py b/services/unsloth/src/nmp/unsloth/tasks/training/progress.py index 0a4f596ef1..75f151fec2 100644 --- a/services/unsloth/src/nmp/unsloth/tasks/training/progress.py +++ b/services/unsloth/src/nmp/unsloth/tasks/training/progress.py @@ -3,104 +3,23 @@ """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.). +Thin subclass of the shared +:class:`nmp.customization_common.training.progress.JobsServiceProgressReporter` +that bakes in the unsloth ``SERVICE_NAME`` so callers keep the +``JobsServiceProgressReporter(job_ctx)`` constructor. """ -import logging -import os -from typing import Any - -from nmp.common.sdk_factory import get_task_sdk +from nmp.customization_common.service.context import NMPJobContext +from nmp.customization_common.training.progress import ( + JobsServiceProgressReporter as _BaseJobsServiceProgressReporter, +) from nmp.unsloth.app.constants import SERVICE_NAME -from nmp.unsloth.app.jobs.context import NMPJobContext -logger = logging.getLogger(__name__) +__all__ = ["JobsServiceProgressReporter"] -class JobsServiceProgressReporter: - """Reports high-level progress to the Jobs service.""" +class JobsServiceProgressReporter(_BaseJobsServiceProgressReporter): + """Unsloth training progress reporter (binds the unsloth service name).""" 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() + super().__init__(job_ctx, service_name=SERVICE_NAME) diff --git a/services/unsloth/tests/test_file_io.py b/services/unsloth/tests/test_file_io.py index b1a7531c6c..93343ec5ec 100644 --- a/services/unsloth/tests/test_file_io.py +++ b/services/unsloth/tests/test_file_io.py @@ -275,7 +275,7 @@ def test_extracts_canonical_fields(self) -> None: load_in_8bit=False, ), dataset=DatasetSpec(path="/data/sample.jsonl"), - training=TrainingSpec(finetuning_type="full", lora=None), + training=TrainingSpec(finetuning_type="all_weights", lora=None), output=OutputResponse( name="qwen-out", type="model", @@ -287,7 +287,7 @@ def test_extracts_canonical_fields(self) -> None: meta = build_output_metadata(spec) assert meta == { "model": "Qwen/Qwen3-0.6B", - "finetuning_type": "full", + "finetuning_type": "all_weights", "save_method": "lora", "output_type": "model", } diff --git a/services/unsloth/tests/test_images.py b/services/unsloth/tests/test_images.py index 0b2c84d68c..1b1b640566 100644 --- a/services/unsloth/tests/test_images.py +++ b/services/unsloth/tests/test_images.py @@ -4,6 +4,7 @@ from types import SimpleNamespace import nemo_platform_plugin.jobs.image as platform_image +import nmp.customization_common.service.images as shared_images import nmp.unsloth.images as unsloth_images import pytest from nmp.unsloth.config import UnslothConfig @@ -19,7 +20,7 @@ @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(shared_images, "get_platform_config", lambda: config) monkeypatch.setattr(platform_image, "get_platform_config", lambda: config) return config @@ -32,7 +33,7 @@ def test_default_unsloth_images_use_platform_registry(monkeypatch, platform_conf 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 == expected_training # unsloth ships one image: tasks reuse the training image assert TASKS_IMAGE_NAME.count("/") == 0 # single repo segment, no nested paths @@ -49,13 +50,16 @@ def test_unsloth_image_registry_override(monkeypatch, platform_config): ) -def test_unsloth_full_image_override(monkeypatch, platform_config): +def test_unsloth_training_image_override_used_for_all_steps(monkeypatch, platform_config): + # A single training-image override drives both the training step and the + # CPU task steps, since unsloth reuses the training image for tasks. monkeypatch.setattr( unsloth_images, "config", UnslothConfig( - tasks_image="my-registry/nemo-platform-dev/nmp-unsloth-tasks:dev", + training_image="my-registry/nemo-platform-dev/nmp-unsloth-training:dev", ), ) - assert get_tasks_image() == "my-registry/nemo-platform-dev/nmp-unsloth-tasks:dev" + assert get_training_image() == "my-registry/nemo-platform-dev/nmp-unsloth-training:dev" + assert get_tasks_image() == "my-registry/nemo-platform-dev/nmp-unsloth-training:dev" diff --git a/services/unsloth/tests/test_integrations_compiler.py b/services/unsloth/tests/test_integrations_compiler.py new file mode 100644 index 0000000000..85b408f225 --- /dev/null +++ b/services/unsloth/tests/test_integrations_compiler.py @@ -0,0 +1,66 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from nmp.unsloth.app.jobs.training.compiler import compile_training_step +from nmp.unsloth.schemas import ( + DatasetSpec, + LoRAParams, + ModelLoadSpec, + OutputResponse, + TrainingSpec, + UnslothJobOutput, +) + + +def _job_spec_with_integrations() -> UnslothJobOutput: + return UnslothJobOutput( + model=ModelLoadSpec(name="default/base"), + dataset=DatasetSpec(path="default/train"), + training=TrainingSpec(lora=LoRAParams()), + output=OutputResponse( + name="out", + type="adapter", + save_method="lora", + fileset="out", + ), + integrations={ + "wandb": { + "project": "my-project", + "api_key_secret": "default/wandb-key", + }, + }, + ) + + +def test_compile_training_step_injects_wandb_secret() -> None: + step = compile_training_step(_job_spec_with_integrations(), base_env=[]) + + assert step["environment"] == [ + {"name": "WANDB_API_KEY", "from_secret": {"name": "default/wandb-key"}}, + ] + + +def test_compile_training_step_no_integrations() -> None: + spec = _job_spec_with_integrations() + spec = spec.model_copy(update={"integrations": None}) + step = compile_training_step(spec, base_env=[]) + + assert step["environment"] == [] + + +def test_compile_training_step_warns_on_incomplete_wandb( + caplog: pytest.LogCaptureFixture, +) -> None: + from nmp.common.integrations import IntegrationsSpec + + spec = _job_spec_with_integrations().model_copy( + update={ + "integrations": IntegrationsSpec.model_validate({"wandb": {"project": "my-project"}}), + }, + ) + caplog.set_level("WARNING") + + compile_training_step(spec, base_env=[]) + + assert "api_key_secret is missing" in caplog.text diff --git a/services/unsloth/tests/test_integrations_hf_bridge.py b/services/unsloth/tests/test_integrations_hf_bridge.py new file mode 100644 index 0000000000..3e7d55ecd3 --- /dev/null +++ b/services/unsloth/tests/test_integrations_hf_bridge.py @@ -0,0 +1,204 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import json +from pathlib import Path + +import pytest +from nmp.common.integrations import IntegrationsSpec +from nmp.customization_common.service.context import NMPJobContext +from nmp.unsloth.integrations.hf_bridge import apply_integrations_to_sft_config + + +@pytest.fixture +def job_ctx(tmp_path: Path) -> NMPJobContext: + return NMPJobContext( + workspace="test-workspace", + job_id="job-123", + attempt_id="attempt-1", + step="training", + task="task-abc123", + jobs_url="http://jobs.example.com", + files_url="http://files.example.com", + storage_path=tmp_path / "job-storage", + config_path=tmp_path / "config.json", + ) + + +class TestApplyIntegrationsToSftConfig: + def test_none_integrations(self, job_ctx: NMPJobContext, tmp_path: Path) -> None: + report_to, kwargs, env = apply_integrations_to_sft_config( + integrations=None, + job_ctx=job_ctx, + output_name="out", + workspace_path=tmp_path, + model_name="meta/llama", + ) + assert report_to == ["none"] + assert kwargs == {} + assert env == {} + + def test_wandb_only_when_api_key_present( + self, + job_ctx: NMPJobContext, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv("WANDB_API_KEY", "test-key") + integrations = IntegrationsSpec.model_validate( + { + "wandb": { + "project": "my-project", + "name": "run-001", + "entity": "my-team", + "notes": "notes", + "tags": ["sft"], + }, + }, + ) + + report_to, kwargs, env = apply_integrations_to_sft_config( + integrations=integrations, + job_ctx=job_ctx, + output_name="my-output", + workspace_path=tmp_path, + model_name="meta/llama", + ) + + assert report_to == ["wandb"] + assert kwargs == {"run_name": "run-001"} + assert env["WANDB_PROJECT"] == "my-project" + assert env["WANDB_ENTITY"] == "my-team" + assert env["WANDB_NOTES"] == "notes" + assert "service:nemo-platform" in env["WANDB_TAGS"] + assert env["WANDB_DIR"] == str(tmp_path / "wandb") + assert "MLFLOW_RUN_NAME" not in env + + def test_wandb_dir_outside_output_model_upload_tree( + self, + job_ctx: NMPJobContext, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv("WANDB_API_KEY", "test-key") + integrations = IntegrationsSpec.model_validate({"wandb": {"project": "my-project"}}) + output_model = Path("/var/run/scratch/job/output_model") + + _, _, env = apply_integrations_to_sft_config( + integrations=integrations, + job_ctx=job_ctx, + output_name="my-output", + workspace_path=output_model, + model_name="meta/llama", + ) + + assert env["WANDB_DIR"] == "/var/run/scratch/job/ephemeral/wandb" + + def test_wandb_skipped_without_api_key( + self, + job_ctx: NMPJobContext, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.delenv("WANDB_API_KEY", raising=False) + integrations = IntegrationsSpec.model_validate({"wandb": {"project": "my-project"}}) + + report_to, kwargs, env = apply_integrations_to_sft_config( + integrations=integrations, + job_ctx=job_ctx, + output_name="my-output", + workspace_path=tmp_path, + model_name="meta/llama", + ) + + assert report_to == ["none"] + assert kwargs == {} + assert env == {} + + def test_mlflow_only_sets_training_run_name( + self, + job_ctx: NMPJobContext, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.delenv("MLFLOW_TRACKING_URI", raising=False) + integrations = IntegrationsSpec.model_validate( + { + "mlflow": { + "tracking_uri": "http://mlflow:5000", + "experiment_name": "exp-1", + "name": "run-001", + "tags": {"team": "nlp"}, + }, + }, + ) + + report_to, kwargs, env = apply_integrations_to_sft_config( + integrations=integrations, + job_ctx=job_ctx, + output_name="my-output", + workspace_path=tmp_path, + model_name="meta/llama", + ) + + assert report_to == ["mlflow"] + assert kwargs == {"run_name": "run-001"} + assert env["MLFLOW_TRACKING_URI"] == "http://mlflow:5000" + assert env["MLFLOW_EXPERIMENT_NAME"] == "exp-1" + assert "MLFLOW_RUN_NAME" not in env + tags = json.loads(env["MLFLOW_TAGS"]) + assert tags["service"] == "nemo-platform" + assert tags["team"] == "nlp" + + def test_both_backends_wandb_name_wins( + self, + job_ctx: NMPJobContext, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv("WANDB_API_KEY", "test-key") + integrations = IntegrationsSpec.model_validate( + { + "wandb": {"project": "p", "name": "w-run"}, + "mlflow": {"tracking_uri": "http://mlflow:5000", "name": "m-run"}, + }, + ) + + report_to, kwargs, env = apply_integrations_to_sft_config( + integrations=integrations, + job_ctx=job_ctx, + output_name="my-output", + workspace_path=tmp_path, + model_name="meta/llama", + ) + + assert report_to == ["wandb", "mlflow"] + assert kwargs == {"run_name": "w-run"} + assert env["WANDB_PROJECT"] == "p" + assert env["MLFLOW_TRACKING_URI"] == "http://mlflow:5000" + + def test_conflicting_run_names_logs_warning( + self, + job_ctx: NMPJobContext, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, + ) -> None: + monkeypatch.setenv("WANDB_API_KEY", "test-key") + integrations = IntegrationsSpec.model_validate( + { + "wandb": {"project": "p", "name": "w-run"}, + "mlflow": {"tracking_uri": "http://mlflow:5000", "name": "m-run"}, + }, + ) + caplog.set_level("WARNING") + + _, kwargs, _ = apply_integrations_to_sft_config( + integrations=integrations, + job_ctx=job_ctx, + output_name="my-output", + workspace_path=tmp_path, + model_name="meta/llama", + ) + + assert kwargs == {"run_name": "w-run"} + assert "differ" in caplog.text diff --git a/services/unsloth/tests/test_progress.py b/services/unsloth/tests/test_progress.py index b239db9165..01d9ffab84 100644 --- a/services/unsloth/tests/test_progress.py +++ b/services/unsloth/tests/test_progress.py @@ -22,7 +22,7 @@ def test_progress_reporter_calls_sdk_create_or_update() -> None: ) mock_sdk = MagicMock() - with patch("nmp.unsloth.tasks.training.progress.get_task_sdk", return_value=mock_sdk): + with patch("nmp.customization_common.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") diff --git a/services/unsloth/tests/test_schemas.py b/services/unsloth/tests/test_schemas.py index 554a4b4dc0..ec515c1e9c 100644 --- a/services/unsloth/tests/test_schemas.py +++ b/services/unsloth/tests/test_schemas.py @@ -73,11 +73,9 @@ def test_model_name_required(self) -> None: 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. + # Consistent with Automodel: epochs defaults to 1; max_steps (when set) overrides it. sched = ScheduleSpec() - assert sched.epochs is None + assert sched.epochs == 1 assert sched.max_steps is None def test_training_defaults(self) -> None: diff --git a/uv.lock b/uv.lock index 9fe0d7074c..c57ed6d935 100644 --- a/uv.lock +++ b/uv.lock @@ -49,6 +49,7 @@ members = [ "nmp-build-tools", "nmp-common", "nmp-core-mcp", + "nmp-customization-common", "nmp-dev-mcp", "nmp-entities", "nmp-files", @@ -3941,6 +3942,7 @@ 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 = "nmp-customization-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 = "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')" }, @@ -3961,6 +3963,7 @@ 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 = "nmp-customization-common", editable = "packages/nmp_customization_common" }, { name = "pydantic", specifier = ">=2.10.6" }, { name = "pydantic-settings", specifier = ">=2.6.1" }, { name = "typer", specifier = ">=0.12.5" }, @@ -5732,6 +5735,7 @@ 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-customization-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-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')" }, @@ -5752,6 +5756,7 @@ dev = [ requires-dist = [ { name = "nemo-platform", editable = "packages/nemo_platform" }, { name = "nemo-platform-plugin", editable = "packages/nemo_platform_plugin" }, + { name = "nmp-customization-common", editable = "packages/nmp_customization_common" }, { name = "nmp-unsloth", editable = "services/unsloth" }, { name = "pydantic", specifier = ">=2.10.6" }, { name = "pydantic-settings", specifier = ">=2.6.1" }, @@ -6335,6 +6340,7 @@ dependencies = [ { 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 = "nmp-customization-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')" }, @@ -6354,6 +6360,7 @@ requires-dist = [ { name = "jsonschema", specifier = ">=4.23.0" }, { name = "nemo-platform-sdk", editable = "sdk/python/nemo-platform" }, { name = "nmp-common", editable = "packages/nmp_common" }, + { name = "nmp-customization-common", editable = "packages/nmp_customization_common" }, { name = "pydantic", specifier = ">=2.10.6" }, { name = "pydantic-settings", specifier = ">=2.6.1" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.4" }, @@ -6470,6 +6477,43 @@ requires-dist = [ { name = "nmp-entities", editable = "services/core/entities" }, ] +[[package]] +name = "nmp-customization-common" +version = "0.1.0" +source = { editable = "packages/nmp_customization_common" } +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-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nemo-platform-sdk", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "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.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-plugin", editable = "packages/nemo_platform_plugin" }, + { 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" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-asyncio", specifier = ">=0.25.3" }, +] + [[package]] name = "nmp-dev-mcp" version = "0.1.0" @@ -7181,12 +7225,17 @@ 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 = "nmp-customization-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] +integrations = [ + { name = "mlflow-skinny", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "wandb", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] 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')" }, ] @@ -7200,14 +7249,17 @@ dev = [ [package.metadata] requires-dist = [ { name = "httpx", specifier = ">=0.27.0" }, + { name = "mlflow-skinny", marker = "extra == 'integrations'" }, { name = "nemo-platform-sdk", editable = "sdk/python/nemo-platform" }, { name = "nmp-common", editable = "packages/nmp_common" }, + { name = "nmp-customization-common", editable = "packages/nmp_customization_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'" }, + { name = "wandb", marker = "extra == 'integrations'", specifier = ">=0.25.1" }, ] -provides-extras = ["unsloth"] +provides-extras = ["unsloth", "integrations"] [package.metadata.requires-dev] dev = [ @@ -10758,6 +10810,31 @@ 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" }, ] +[[package]] +name = "wandb" +version = "0.27.2" +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 = "gitpython", marker = "(platform_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 = "protobuf", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pydantic", extra = ["email"], marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "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 = "sentry-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/14/a2/53ca062f430178e3af48ebc137396481d0ee885fb94a554c0df464cd8afa/wandb-0.27.2.tar.gz", hash = "sha256:c81ff93ab63f4dabc5a27b90ac3d12310fbfa6a14ca99201626921c99b2845be", size = 40300451, upload-time = "2026-06-06T01:47:02.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/95/18d3625558667b459d91c19630f7cecfbc133f87f5b144a7fb755e473e8c/wandb-0.27.2-py3-none-macosx_12_0_arm64.whl", hash = "sha256:978400b3c4b7d97e927c32264453da5e4a0040a3468d5b77a00d9c480613f370", size = 23990048, upload-time = "2026-06-06T01:46:38.902Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a3/f9fe31ca72b4f5854d1e488403d6310783127a6b7e267c28577e9bd51b43/wandb-0.27.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e0779592410215a2762063c3585d3dcad73c7dca9cb6d63c4dcc1588267c1392", size = 24554366, upload-time = "2026-06-06T01:46:44.57Z" }, + { url = "https://files.pythonhosted.org/packages/76/e2/7a5064aba235ddb855b8c2250e07e6187fcc8382332e237e545d4de094ee/wandb-0.27.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:55bfebf4d382116a8e9610848cadc0de50d406bacd3d0a390d12dabde196f009", size = 26380293, upload-time = "2026-06-06T01:46:47.487Z" }, + { url = "https://files.pythonhosted.org/packages/ed/4c/0c845edac5ff0fd0930e881bec2569f2e2af2a4fc873249855600546eee0/wandb-0.27.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:566aa2fcd67d2a23c08713da75e9daf82f30f7136af76763ef1d7db3d901d940", size = 24728823, upload-time = "2026-06-06T01:46:49.887Z" }, + { url = "https://files.pythonhosted.org/packages/e1/7a/a6f7a02a0e6bf73e163b61caca03aaba3452836a02dbe2b64f9e1a3c6afc/wandb-0.27.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:41158181fc5b691438b3d04fee0a8c061e3f1f407a3258096afbebfe1db24e72", size = 26691957, upload-time = "2026-06-06T01:46:52.384Z" }, +] + [[package]] name = "wasmtime" version = "43.0.0" From 061668381cf7e0dc23d8e267304f6147771fa1af Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Fri, 12 Jun 2026 14:27:33 -0600 Subject: [PATCH 2/4] Remove unnecessary re-export shims Signed-off-by: Sam Oluwalana --- .../customization_common/schemas/file_io.py | 3 +- .../schemas/model_entity.py | 3 +- .../customization_common/service/context.py | 3 +- .../tasks/file_io_progress_reporter.py | 5 +- .../tasks/file_io_utils.py | 5 +- .../src/nemo_automodel_plugin/transform.py | 2 +- .../src/nemo_unsloth_plugin/transform.py | 2 +- .../src/nmp/automodel/app/jobs/compiler.py | 32 ++++++------ .../src/nmp/automodel/app/jobs/context.py | 24 --------- .../nmp/automodel/app/jobs/file_io/schemas.py | 44 ---------------- .../app/jobs/model_entity/__init__.py | 2 +- .../app/jobs/model_entity/schemas.py | 23 --------- .../src/nmp/automodel/entities/validators.py | 2 +- .../src/nmp/automodel/platform_client.py | 12 ----- .../nmp/automodel/tasks/file_io/callbacks.py | 4 +- .../tasks/file_io/progress_reporter.py | 15 ------ .../src/nmp/automodel/tasks/file_io/run.py | 26 +++++----- .../src/nmp/automodel/tasks/file_io/utils.py | 23 --------- .../nmp/automodel/tasks/model_entity/run.py | 8 +-- .../nmp/automodel/tasks/progress_reporter.py | 12 ----- .../tasks/training/backends/backend.py | 2 +- .../tasks/training/backends/callbacks.py | 14 ------ .../tasks/training/backends/config.py | 2 +- .../tasks/training/backends/finetune.py | 4 +- .../automodel/tasks/training/integrations.py | 2 +- .../nmp/automodel/tasks/training/runner.py | 2 +- .../tests/contract/generate_configs.py | 2 +- .../tasks/training/backends/test_callbacks.py | 2 +- services/automodel/tests/test_job_context.py | 14 +++--- .../automodel/tests/test_platform_client.py | 2 +- .../automodel/tests/test_progress_reporter.py | 4 +- .../src/nmp/unsloth/app/jobs/compiler.py | 20 ++++---- .../src/nmp/unsloth/app/jobs/context.py | 24 --------- .../nmp/unsloth/app/jobs/file_io/schemas.py | 44 ---------------- .../unsloth/app/jobs/model_entity/schemas.py | 23 --------- .../src/nmp/unsloth/integrations/__init__.py | 8 --- .../src/nmp/unsloth/platform_client.py | 12 ----- .../nmp/unsloth/tasks/file_io/callbacks.py | 4 +- .../tasks/file_io/progress_reporter.py | 15 ------ .../src/nmp/unsloth/tasks/file_io/run.py | 22 ++++---- .../src/nmp/unsloth/tasks/file_io/utils.py | 23 --------- .../src/nmp/unsloth/tasks/model_entity/run.py | 6 +-- .../nmp/unsloth/tasks/progress_reporter.py | 12 ----- .../tasks/training/backends/unsloth_sft.py | 4 +- services/unsloth/tests/test_file_io.py | 28 +++++------ services/unsloth/tests/test_model_entity.py | 50 +++++++++++-------- services/unsloth/tests/test_progress.py | 2 +- 47 files changed, 134 insertions(+), 463 deletions(-) delete mode 100644 services/automodel/src/nmp/automodel/app/jobs/context.py delete mode 100644 services/automodel/src/nmp/automodel/app/jobs/file_io/schemas.py delete mode 100644 services/automodel/src/nmp/automodel/app/jobs/model_entity/schemas.py delete mode 100644 services/automodel/src/nmp/automodel/platform_client.py delete mode 100644 services/automodel/src/nmp/automodel/tasks/file_io/progress_reporter.py delete mode 100644 services/automodel/src/nmp/automodel/tasks/file_io/utils.py delete mode 100644 services/automodel/src/nmp/automodel/tasks/progress_reporter.py delete mode 100644 services/automodel/src/nmp/automodel/tasks/training/backends/callbacks.py delete mode 100644 services/unsloth/src/nmp/unsloth/app/jobs/context.py delete mode 100644 services/unsloth/src/nmp/unsloth/app/jobs/file_io/schemas.py delete mode 100644 services/unsloth/src/nmp/unsloth/app/jobs/model_entity/schemas.py delete mode 100644 services/unsloth/src/nmp/unsloth/integrations/__init__.py delete mode 100644 services/unsloth/src/nmp/unsloth/platform_client.py delete mode 100644 services/unsloth/src/nmp/unsloth/tasks/file_io/progress_reporter.py delete mode 100644 services/unsloth/src/nmp/unsloth/tasks/file_io/utils.py delete mode 100644 services/unsloth/src/nmp/unsloth/tasks/progress_reporter.py diff --git a/packages/nmp_customization_common/src/nmp/customization_common/schemas/file_io.py b/packages/nmp_customization_common/src/nmp/customization_common/schemas/file_io.py index 6f8abd49a5..8a8095b4c8 100644 --- a/packages/nmp_customization_common/src/nmp/customization_common/schemas/file_io.py +++ b/packages/nmp_customization_common/src/nmp/customization_common/schemas/file_io.py @@ -3,8 +3,7 @@ """Schemas for the customization file_io task configuration. -Shared by both the unsloth and automodel backends; each service re-exports these -from its ``app/jobs/file_io/schemas.py`` so existing import paths keep working. +Shared by both the unsloth and automodel backends. """ from __future__ import annotations diff --git a/packages/nmp_customization_common/src/nmp/customization_common/schemas/model_entity.py b/packages/nmp_customization_common/src/nmp/customization_common/schemas/model_entity.py index 74c35f7630..b062728a56 100644 --- a/packages/nmp_customization_common/src/nmp/customization_common/schemas/model_entity.py +++ b/packages/nmp_customization_common/src/nmp/customization_common/schemas/model_entity.py @@ -3,8 +3,7 @@ """Schemas for the model_entity container task configuration. -Shared by both backends; each service re-exports these from its -``app/jobs/model_entity/schemas.py`` so existing import paths keep working. +Shared by both the unsloth and automodel backends. """ from __future__ import annotations diff --git a/packages/nmp_customization_common/src/nmp/customization_common/service/context.py b/packages/nmp_customization_common/src/nmp/customization_common/service/context.py index bc6cee5235..adad780d50 100644 --- a/packages/nmp_customization_common/src/nmp/customization_common/service/context.py +++ b/packages/nmp_customization_common/src/nmp/customization_common/service/context.py @@ -4,8 +4,7 @@ """Job context for customization container task entrypoints. Populated from the Job Controller environment variables. Shared by both the -unsloth and automodel services; each service re-exports ``NMPJobContext`` from -its ``app/jobs/context.py`` so existing import paths keep working. +unsloth and automodel backends. """ import os diff --git a/packages/nmp_customization_common/src/nmp/customization_common/tasks/file_io_progress_reporter.py b/packages/nmp_customization_common/src/nmp/customization_common/tasks/file_io_progress_reporter.py index c0c19c73ce..afb4c3963c 100644 --- a/packages/nmp_customization_common/src/nmp/customization_common/tasks/file_io_progress_reporter.py +++ b/packages/nmp_customization_common/src/nmp/customization_common/tasks/file_io_progress_reporter.py @@ -1,10 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Progress reporting for the file_io container task. - -Re-exported by each backend's ``tasks/file_io/progress_reporter.py``. -""" +"""Progress reporting for the file_io container task.""" import logging from typing import Any, Protocol diff --git a/packages/nmp_customization_common/src/nmp/customization_common/tasks/file_io_utils.py b/packages/nmp_customization_common/src/nmp/customization_common/tasks/file_io_utils.py index dba331095e..32331342a2 100644 --- a/packages/nmp_customization_common/src/nmp/customization_common/tasks/file_io_utils.py +++ b/packages/nmp_customization_common/src/nmp/customization_common/tasks/file_io_utils.py @@ -1,10 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Shared fileset path/IO + error-handling helpers for the file_io task. - -Re-exported by each backend's ``tasks/file_io/utils.py``. -""" +"""Shared fileset path/IO + error-handling helpers for the file_io task.""" import json import logging diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/transform.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/transform.py index 8cfa949a13..d9bf668f05 100644 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/transform.py +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/transform.py @@ -7,8 +7,8 @@ from typing import TYPE_CHECKING -from nmp.automodel.platform_client import check_dataset_access, fetch_model_entity from nmp.customization_common.contributor.transform import generated_output_name +from nmp.customization_common.service.platform_client import check_dataset_access, fetch_model_entity from nemo_automodel_plugin.schema import ( AutomodelJobInput, diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py index 55b30d7a9e..2f7a8fe751 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/transform.py @@ -20,7 +20,7 @@ from typing import TYPE_CHECKING from nmp.customization_common.contributor.transform import generated_output_name -from nmp.unsloth.platform_client import check_dataset_access, fetch_model_entity +from nmp.customization_common.service.platform_client import check_dataset_access, fetch_model_entity from nmp.unsloth.schemas import OutputResponse, UnslothJobOutput from nemo_unsloth_plugin.schema import OutputRequest, UnslothJobInput diff --git a/services/automodel/src/nmp/automodel/app/jobs/compiler.py b/services/automodel/src/nmp/automodel/app/jobs/compiler.py index 5b3b0625f9..047b73798d 100644 --- a/services/automodel/src/nmp/automodel/app/jobs/compiler.py +++ b/services/automodel/src/nmp/automodel/app/jobs/compiler.py @@ -30,21 +30,6 @@ 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, @@ -53,11 +38,26 @@ 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.constants import DEFAULT_JOB_STORAGE_PATH, PERSISTENT_JOB_STORAGE_PATH_ENVVAR from nmp.common.jobs.exceptions import PlatformJobCompilationError +from nmp.customization_common.schemas.file_io import ( + DownloadItem, + FileIOTaskConfig, + FileSetRef, + UploadItem, +) +from nmp.customization_common.schemas.model_entity import ( + DeploymentParameters as ModelEntityDeploymentParameters, +) +from nmp.customization_common.schemas.model_entity import ( + ModelEntityTaskConfig, +) +from nmp.customization_common.schemas.model_entity import ( + PEFTConfig as ModelEntityPEFTConfig, +) +from nmp.customization_common.service.platform_client import fetch_model_entity logger = logging.getLogger(__name__) diff --git a/services/automodel/src/nmp/automodel/app/jobs/context.py b/services/automodel/src/nmp/automodel/app/jobs/context.py deleted file mode 100644 index 09d236df24..0000000000 --- a/services/automodel/src/nmp/automodel/app/jobs/context.py +++ /dev/null @@ -1,24 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Job context for automodel container task entrypoints. - -Re-exports the shared :class:`nmp.customization_common.service.context.NMPJobContext` -so existing ``nmp.automodel.app.jobs.context`` import paths keep working. -""" - -from nmp.customization_common.service.context import ( - DEFAULT_ATTEMPT_ID, - DEFAULT_JOB_ID, - DEFAULT_STEP, - DEFAULT_TASK, - NMPJobContext, -) - -__all__ = [ - "DEFAULT_ATTEMPT_ID", - "DEFAULT_JOB_ID", - "DEFAULT_STEP", - "DEFAULT_TASK", - "NMPJobContext", -] 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 deleted file mode 100644 index 87cafe40b9..0000000000 --- a/services/automodel/src/nmp/automodel/app/jobs/file_io/schemas.py +++ /dev/null @@ -1,44 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Schemas for the automodel file_io task configuration. - -Re-exports the shared :mod:`nmp.customization_common.schemas.file_io` so existing -``nmp.automodel.app.jobs.file_io.schemas`` import paths keep working. -""" - -from nmp.customization_common.schemas.file_io import ( - FILESET_PROTOCOL, - DownloadItem, - DownloadStats, - FileDownloadError, - FileIOTaskConfig, - FileSetRef, - FileStats, - FileUploadError, - PathTraversalError, - ProgressReportError, - TaskCompilationError, - TaskPhase, - TaskStatus, - UploadItem, - UploadStats, -) - -__all__ = [ - "FILESET_PROTOCOL", - "DownloadItem", - "DownloadStats", - "FileDownloadError", - "FileIOTaskConfig", - "FileSetRef", - "FileStats", - "FileUploadError", - "PathTraversalError", - "ProgressReportError", - "TaskCompilationError", - "TaskPhase", - "TaskStatus", - "UploadItem", - "UploadStats", -] 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 index c5ddfda4d4..1b17cac997 100644 --- a/services/automodel/src/nmp/automodel/app/jobs/model_entity/__init__.py +++ b/services/automodel/src/nmp/automodel/app/jobs/model_entity/__init__.py @@ -3,7 +3,7 @@ """Model entity job configuration.""" -from .schemas import ModelEntityCreationError, ModelEntityTaskConfig +from nmp.customization_common.schemas.model_entity import ModelEntityCreationError, ModelEntityTaskConfig __all__ = [ "ModelEntityCreationError", 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 deleted file mode 100644 index f621ce9679..0000000000 --- a/services/automodel/src/nmp/automodel/app/jobs/model_entity/schemas.py +++ /dev/null @@ -1,23 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Schemas for the automodel model_entity task configuration. - -Re-exports the shared :mod:`nmp.customization_common.schemas.model_entity`. -""" - -from nmp.customization_common.schemas.model_entity import ( - DeploymentParameters, - ModelEntityCreationError, - ModelEntityTaskConfig, - PEFTConfig, - ToolCallConfig, -) - -__all__ = [ - "DeploymentParameters", - "ModelEntityCreationError", - "ModelEntityTaskConfig", - "PEFTConfig", - "ToolCallConfig", -] diff --git a/services/automodel/src/nmp/automodel/entities/validators.py b/services/automodel/src/nmp/automodel/entities/validators.py index b4c9705450..1edb6040ca 100644 --- a/services/automodel/src/nmp/automodel/entities/validators.py +++ b/services/automodel/src/nmp/automodel/entities/validators.py @@ -6,8 +6,8 @@ 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 +from nmp.customization_common.schemas.file_io import FILESET_PROTOCOL, FileSetRef _NAME_REGEX = re.compile(REGEX_WORD_CHARACTER_DOT_DASH) _UNSUPPORTED_PROTOCOLS = ("hf://", "ngc://", "s3://", "gs://") diff --git a/services/automodel/src/nmp/automodel/platform_client.py b/services/automodel/src/nmp/automodel/platform_client.py deleted file mode 100644 index c0d67fa2a8..0000000000 --- a/services/automodel/src/nmp/automodel/platform_client.py +++ /dev/null @@ -1,12 +0,0 @@ -# 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. - -Re-exports the shared :mod:`nmp.customization_common.service.platform_client` so existing -``nmp.automodel.platform_client`` import paths keep working. -""" - -from nmp.customization_common.service.platform_client import check_dataset_access, fetch_model_entity - -__all__ = ["check_dataset_access", "fetch_model_entity"] diff --git a/services/automodel/src/nmp/automodel/tasks/file_io/callbacks.py b/services/automodel/src/nmp/automodel/tasks/file_io/callbacks.py index 017b41a322..0668b5fd97 100644 --- a/services/automodel/src/nmp/automodel/tasks/file_io/callbacks.py +++ b/services/automodel/src/nmp/automodel/tasks/file_io/callbacks.py @@ -11,9 +11,9 @@ 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 +from nmp.customization_common.schemas.file_io import DownloadStats, TaskPhase, UploadStats +from nmp.customization_common.tasks.file_io_progress_reporter import ProgressReporter from nmp.customization_common.tasks.file_io_utils import list_local_files as _list_local_files logger = logging.getLogger(__name__) 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 deleted file mode 100644 index 9af222e3b1..0000000000 --- a/services/automodel/src/nmp/automodel/tasks/file_io/progress_reporter.py +++ /dev/null @@ -1,15 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Progress reporting for the automodel file_io task. - -Re-exports the shared :mod:`nmp.customization_common.tasks.file_io_progress_reporter`. -""" - -from nmp.customization_common.tasks.file_io_progress_reporter import ( - JobsServiceProgressReporter, - NoOpProgressReporter, - ProgressReporter, -) - -__all__ = ["JobsServiceProgressReporter", "NoOpProgressReporter", "ProgressReporter"] 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 d8ff35ad07..031197cdca 100644 --- a/services/automodel/src/nmp/automodel/tasks/file_io/run.py +++ b/services/automodel/src/nmp/automodel/tasks/file_io/run.py @@ -30,8 +30,16 @@ ) 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 ( +from nmp.automodel.tasks.file_io.callbacks import ( + CompositeCallback, + FileDownloadProgressCallback, + FileUploadProgressCallback, + TqdmPerFileDownloadCallback, + TqdmPerFileUploadCallback, +) +from nmp.common.jobs.schemas import PlatformJobStatus +from nmp.common.sdk_factory import get_task_sdk +from nmp.customization_common.schemas.file_io import ( DownloadItem, DownloadStats, FileDownloadError, @@ -42,23 +50,15 @@ 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 ( +from nmp.customization_common.service.context import NMPJobContext +from nmp.customization_common.tasks.file_io_progress_reporter import JobsServiceProgressReporter, ProgressReporter +from nmp.customization_common.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__) diff --git a/services/automodel/src/nmp/automodel/tasks/file_io/utils.py b/services/automodel/src/nmp/automodel/tasks/file_io/utils.py deleted file mode 100644 index e685a70758..0000000000 --- a/services/automodel/src/nmp/automodel/tasks/file_io/utils.py +++ /dev/null @@ -1,23 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Fileset path/IO + error-handling helpers for the automodel file_io task. - -Re-exports the shared :mod:`nmp.customization_common.tasks.file_io_utils`. -""" - -from nmp.customization_common.tasks.file_io_utils import ( - filesystem_sdk_error_handler, - get_config, - sdk_error_handler, - validate_safe_path, - validate_storage_path, -) - -__all__ = [ - "filesystem_sdk_error_handler", - "get_config", - "sdk_error_handler", - "validate_safe_path", - "validate_storage_path", -] 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 8009c979f7..b50b604766 100644 --- a/services/automodel/src/nmp/automodel/tasks/model_entity/run.py +++ b/services/automodel/src/nmp/automodel/tasks/model_entity/run.py @@ -37,14 +37,14 @@ 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 ( +from nmp.automodel.entities.values import FinetuningType +from nmp.common.sdk_factory import get_task_sdk +from nmp.customization_common.schemas.model_entity import ( DeploymentParameters, ModelEntityCreationError, ModelEntityTaskConfig, ) -from nmp.automodel.entities.values import FinetuningType -from nmp.common.sdk_factory import get_task_sdk +from nmp.customization_common.service.context import NMPJobContext from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential logger = logging.getLogger(__name__) diff --git a/services/automodel/src/nmp/automodel/tasks/progress_reporter.py b/services/automodel/src/nmp/automodel/tasks/progress_reporter.py deleted file mode 100644 index 82bb236165..0000000000 --- a/services/automodel/src/nmp/automodel/tasks/progress_reporter.py +++ /dev/null @@ -1,12 +0,0 @@ -# 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/backends/backend.py b/services/automodel/src/nmp/automodel/tasks/training/backends/backend.py index d5fba7bb1a..901b8a644d 100644 --- a/services/automodel/src/nmp/automodel/tasks/training/backends/backend.py +++ b/services/automodel/src/nmp/automodel/tasks/training/backends/backend.py @@ -10,7 +10,6 @@ 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, @@ -24,6 +23,7 @@ TrainingStepConfig, ) from nmp.automodel.tasks.training.utils import generate_torchrun_flags_from_env +from nmp.customization_common.service.context import NMPJobContext from .checkpoints import ModelType, find_best_checkpoint, process_checkpoint from .config import compile_automodel_config diff --git a/services/automodel/src/nmp/automodel/tasks/training/backends/callbacks.py b/services/automodel/src/nmp/automodel/tasks/training/backends/callbacks.py deleted file mode 100644 index fc740eddfc..0000000000 --- a/services/automodel/src/nmp/automodel/tasks/training/backends/callbacks.py +++ /dev/null @@ -1,14 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Training progress callbacks for Automodel Jobs-service reporting. - -Re-exports the shared -:class:`nmp.customization_common.training.callbacks.TrainingProgressCallback`. -Automodel does not stamp a ``backend`` field (``_default_backend`` is ``None``), -preserving its existing ``status_details`` shape. -""" - -from nmp.customization_common.training.callbacks import TrainingProgressCallback - -__all__ = ["TrainingProgressCallback"] diff --git a/services/automodel/src/nmp/automodel/tasks/training/backends/config.py b/services/automodel/src/nmp/automodel/tasks/training/backends/config.py index b51fba84e3..7b7852a99f 100644 --- a/services/automodel/src/nmp/automodel/tasks/training/backends/config.py +++ b/services/automodel/src/nmp/automodel/tasks/training/backends/config.py @@ -15,7 +15,6 @@ 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, @@ -40,6 +39,7 @@ calculate_optimal_pack_size, estimate_dataset_sequence_lengths, ) +from nmp.customization_common.service.context import NMPJobContext logger = logging.getLogger(__name__) 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..d292255dec 100644 --- a/services/automodel/src/nmp/automodel/tasks/training/backends/finetune.py +++ b/services/automodel/src/nmp/automodel/tasks/training/backends/finetune.py @@ -18,9 +18,9 @@ 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 +from nmp.customization_common.service.context import NMPJobContext +from nmp.customization_common.training.callbacks import TrainingProgressCallback logger = logging.getLogger(__name__) diff --git a/services/automodel/src/nmp/automodel/tasks/training/integrations.py b/services/automodel/src/nmp/automodel/tasks/training/integrations.py index c97d3bad91..c0d115802e 100644 --- a/services/automodel/src/nmp/automodel/tasks/training/integrations.py +++ b/services/automodel/src/nmp/automodel/tasks/training/integrations.py @@ -5,7 +5,6 @@ from typing import Any -from nmp.automodel.app.jobs.context import NMPJobContext from nmp.automodel.app.jobs.training.schemas import TrainingStepConfig from nmp.customization_common.integrations import ( IntegrationRuntimeContext, @@ -16,6 +15,7 @@ from nmp.customization_common.integrations import ( build_wandb_config as _build_wandb_config, ) +from nmp.customization_common.service.context import NMPJobContext def integration_context_from_training_step( diff --git a/services/automodel/src/nmp/automodel/tasks/training/runner.py b/services/automodel/src/nmp/automodel/tasks/training/runner.py index 2d893dde19..c81988ea6f 100644 --- a/services/automodel/src/nmp/automodel/tasks/training/runner.py +++ b/services/automodel/src/nmp/automodel/tasks/training/runner.py @@ -18,7 +18,7 @@ import yaml from nmp.automodel.app.constants import DEFAULT_TRAINING_RESULT_FILE_NAME -from nmp.automodel.app.jobs.context import NMPJobContext +from nmp.customization_common.service.context import NMPJobContext from .backends.backend import AUTOMODEL_CONFIG_FILENAME, AutomodelBackend from .distributed import DistributedContext diff --git a/services/automodel/tests/contract/generate_configs.py b/services/automodel/tests/contract/generate_configs.py index 1b352003ad..b4b3787cbd 100644 --- a/services/automodel/tests/contract/generate_configs.py +++ b/services/automodel/tests/contract/generate_configs.py @@ -55,9 +55,9 @@ sys.path.insert(0, "/app/services/automodel/src") 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 +from nmp.customization_common.service.context import NMPJobContext # noqa: E402 INPUT_DIR = SCRIPT_DIR / "input_configs" OUTPUT_DIR = SCRIPT_DIR / "output_configs" diff --git a/services/automodel/tests/tasks/training/backends/test_callbacks.py b/services/automodel/tests/tasks/training/backends/test_callbacks.py index ea3627fe76..71dcb278ca 100644 --- a/services/automodel/tests/tasks/training/backends/test_callbacks.py +++ b/services/automodel/tests/tasks/training/backends/test_callbacks.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock -from nmp.automodel.tasks.training.backends.callbacks import TrainingProgressCallback +from nmp.customization_common.training.callbacks import TrainingProgressCallback class TestTrainingProgressCallback: diff --git a/services/automodel/tests/test_job_context.py b/services/automodel/tests/test_job_context.py index 55efea6a11..82c9c929d4 100644 --- a/services/automodel/tests/test_job_context.py +++ b/services/automodel/tests/test_job_context.py @@ -7,13 +7,6 @@ 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, @@ -25,6 +18,13 @@ NEMO_JOB_WORKSPACE_ENVVAR, PERSISTENT_JOB_STORAGE_PATH_ENVVAR, ) +from nmp.customization_common.service.context import ( + DEFAULT_ATTEMPT_ID, + DEFAULT_JOB_ID, + DEFAULT_STEP, + DEFAULT_TASK, + NMPJobContext, +) class TestNMPJobContextFromEnv: diff --git a/services/automodel/tests/test_platform_client.py b/services/automodel/tests/test_platform_client.py index c33b52cab2..75d5b0eec9 100644 --- a/services/automodel/tests/test_platform_client.py +++ b/services/automodel/tests/test_platform_client.py @@ -1,7 +1,7 @@ # 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 +from nmp.customization_common.schemas.file_io import FileSetRef def test_fileset_ref_parse() -> None: diff --git a/services/automodel/tests/test_progress_reporter.py b/services/automodel/tests/test_progress_reporter.py index ab7fc77894..90b20ec372 100644 --- a/services/automodel/tests/test_progress_reporter.py +++ b/services/automodel/tests/test_progress_reporter.py @@ -7,9 +7,9 @@ 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 +from nmp.customization_common.service.context import NMPJobContext +from nmp.customization_common.tasks.file_io_progress_reporter import JobsServiceProgressReporter def test_progress_reporter_calls_sdk_create_or_update() -> None: diff --git a/services/unsloth/src/nmp/unsloth/app/jobs/compiler.py b/services/unsloth/src/nmp/unsloth/app/jobs/compiler.py index b4b3083986..5ce588af7b 100644 --- a/services/unsloth/src/nmp/unsloth/app/jobs/compiler.py +++ b/services/unsloth/src/nmp/unsloth/app/jobs/compiler.py @@ -30,27 +30,27 @@ ) 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, - DEFAULT_VALIDATION_DATASET_PATH, -) -from nmp.unsloth.app.jobs.file_io.schemas import ( +from nmp.customization_common.schemas.file_io import ( DownloadItem, FileIOTaskConfig, FileSetRef, UploadItem, ) -from nmp.unsloth.app.jobs.model_entity.schemas import ( +from nmp.customization_common.schemas.model_entity import ( DeploymentParameters as ModelEntityDeploymentParameters, ) -from nmp.unsloth.app.jobs.model_entity.schemas import ModelEntityTaskConfig, PEFTConfig +from nmp.customization_common.schemas.model_entity import ModelEntityTaskConfig, PEFTConfig +from nmp.customization_common.service.platform_client import fetch_model_entity +from nmp.unsloth.app.constants import ( + DEFAULT_DATASET_PATH, + DEFAULT_MODEL_PATH, + DEFAULT_OUTPUT_MODEL_PATH, + DEFAULT_VALIDATION_DATASET_PATH, +) 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__) diff --git a/services/unsloth/src/nmp/unsloth/app/jobs/context.py b/services/unsloth/src/nmp/unsloth/app/jobs/context.py deleted file mode 100644 index ffdb753c9e..0000000000 --- a/services/unsloth/src/nmp/unsloth/app/jobs/context.py +++ /dev/null @@ -1,24 +0,0 @@ -# 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. - -Re-exports the shared :class:`nmp.customization_common.service.context.NMPJobContext` -so existing ``nmp.unsloth.app.jobs.context`` import paths keep working. -""" - -from nmp.customization_common.service.context import ( - DEFAULT_ATTEMPT_ID, - DEFAULT_JOB_ID, - DEFAULT_STEP, - DEFAULT_TASK, - NMPJobContext, -) - -__all__ = [ - "DEFAULT_ATTEMPT_ID", - "DEFAULT_JOB_ID", - "DEFAULT_STEP", - "DEFAULT_TASK", - "NMPJobContext", -] 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 deleted file mode 100644 index 0d00b36ece..0000000000 --- a/services/unsloth/src/nmp/unsloth/app/jobs/file_io/schemas.py +++ /dev/null @@ -1,44 +0,0 @@ -# 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. - -Re-exports the shared :mod:`nmp.customization_common.schemas.file_io` so existing -``nmp.unsloth.app.jobs.file_io.schemas`` import paths keep working. -""" - -from nmp.customization_common.schemas.file_io import ( - FILESET_PROTOCOL, - DownloadItem, - DownloadStats, - FileDownloadError, - FileIOTaskConfig, - FileSetRef, - FileStats, - FileUploadError, - PathTraversalError, - ProgressReportError, - TaskCompilationError, - TaskPhase, - TaskStatus, - UploadItem, - UploadStats, -) - -__all__ = [ - "FILESET_PROTOCOL", - "DownloadItem", - "DownloadStats", - "FileDownloadError", - "FileIOTaskConfig", - "FileSetRef", - "FileStats", - "FileUploadError", - "PathTraversalError", - "ProgressReportError", - "TaskCompilationError", - "TaskPhase", - "TaskStatus", - "UploadItem", - "UploadStats", -] 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 deleted file mode 100644 index 170b8bf7c6..0000000000 --- a/services/unsloth/src/nmp/unsloth/app/jobs/model_entity/schemas.py +++ /dev/null @@ -1,23 +0,0 @@ -# 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. - -Re-exports the shared :mod:`nmp.customization_common.schemas.model_entity`. -""" - -from nmp.customization_common.schemas.model_entity import ( - DeploymentParameters, - ModelEntityCreationError, - ModelEntityTaskConfig, - PEFTConfig, - ToolCallConfig, -) - -__all__ = [ - "DeploymentParameters", - "ModelEntityCreationError", - "ModelEntityTaskConfig", - "PEFTConfig", - "ToolCallConfig", -] diff --git a/services/unsloth/src/nmp/unsloth/integrations/__init__.py b/services/unsloth/src/nmp/unsloth/integrations/__init__.py deleted file mode 100644 index bbd21d7806..0000000000 --- a/services/unsloth/src/nmp/unsloth/integrations/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Unsloth-specific experiment-tracking helpers.""" - -from nmp.unsloth.integrations.hf_bridge import apply_integrations_to_sft_config - -__all__ = ["apply_integrations_to_sft_config"] diff --git a/services/unsloth/src/nmp/unsloth/platform_client.py b/services/unsloth/src/nmp/unsloth/platform_client.py deleted file mode 100644 index 7ec7865ac1..0000000000 --- a/services/unsloth/src/nmp/unsloth/platform_client.py +++ /dev/null @@ -1,12 +0,0 @@ -# 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. - -Re-exports the shared :mod:`nmp.customization_common.service.platform_client` so existing -``nmp.unsloth.platform_client`` import paths keep working. -""" - -from nmp.customization_common.service.platform_client import check_dataset_access, fetch_model_entity - -__all__ = ["check_dataset_access", "fetch_model_entity"] diff --git a/services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py b/services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py index ffb36875a8..e7ccc27538 100644 --- a/services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py +++ b/services/unsloth/src/nmp/unsloth/tasks/file_io/callbacks.py @@ -12,9 +12,9 @@ from fsspec.callbacks import Callback, TqdmCallback from nmp.common.jobs.schemas import PlatformJobStatus +from nmp.customization_common.schemas.file_io import DownloadStats, TaskPhase, UploadStats +from nmp.customization_common.tasks.file_io_progress_reporter import ProgressReporter from nmp.customization_common.tasks.file_io_utils import list_local_files as _list_local_files -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__) 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 deleted file mode 100644 index 3c50534d26..0000000000 --- a/services/unsloth/src/nmp/unsloth/tasks/file_io/progress_reporter.py +++ /dev/null @@ -1,15 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Progress reporting for the unsloth file_io task. - -Re-exports the shared :mod:`nmp.customization_common.tasks.file_io_progress_reporter`. -""" - -from nmp.customization_common.tasks.file_io_progress_reporter import ( - JobsServiceProgressReporter, - NoOpProgressReporter, - ProgressReporter, -) - -__all__ = ["JobsServiceProgressReporter", "NoOpProgressReporter", "ProgressReporter"] 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 8b6346385d..67bb0d093a 100644 --- a/services/unsloth/src/nmp/unsloth/tasks/file_io/run.py +++ b/services/unsloth/src/nmp/unsloth/tasks/file_io/run.py @@ -31,9 +31,7 @@ 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 ( +from nmp.customization_common.schemas.file_io import ( DownloadItem, DownloadStats, FileDownloadError, @@ -44,6 +42,16 @@ UploadItem, UploadStats, ) +from nmp.customization_common.service.context import NMPJobContext +from nmp.customization_common.tasks.file_io_progress_reporter import JobsServiceProgressReporter, ProgressReporter +from nmp.customization_common.tasks.file_io_utils import ( + filesystem_sdk_error_handler, + get_config, + sdk_error_handler, + validate_safe_path, + validate_storage_path, +) +from nmp.unsloth.app.constants import SERVICE_NAME from nmp.unsloth.tasks.file_io.callbacks import ( CompositeCallback, FileDownloadProgressCallback, @@ -51,14 +59,6 @@ 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__) diff --git a/services/unsloth/src/nmp/unsloth/tasks/file_io/utils.py b/services/unsloth/src/nmp/unsloth/tasks/file_io/utils.py deleted file mode 100644 index 1bdcb3b9f8..0000000000 --- a/services/unsloth/src/nmp/unsloth/tasks/file_io/utils.py +++ /dev/null @@ -1,23 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Fileset path/IO + error-handling helpers for the unsloth file_io task. - -Re-exports the shared :mod:`nmp.customization_common.tasks.file_io_utils`. -""" - -from nmp.customization_common.tasks.file_io_utils import ( - filesystem_sdk_error_handler, - get_config, - sdk_error_handler, - validate_safe_path, - validate_storage_path, -) - -__all__ = [ - "filesystem_sdk_error_handler", - "get_config", - "sdk_error_handler", - "validate_safe_path", - "validate_storage_path", -] 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 c3fe0f54eb..355c5cb089 100644 --- a/services/unsloth/src/nmp/unsloth/tasks/model_entity/run.py +++ b/services/unsloth/src/nmp/unsloth/tasks/model_entity/run.py @@ -38,13 +38,13 @@ 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 ( +from nmp.customization_common.schemas.model_entity import ( DeploymentParameters, ModelEntityCreationError, ModelEntityTaskConfig, ) +from nmp.customization_common.service.context import NMPJobContext +from nmp.unsloth.app.constants import SERVICE_NAME from nmp.unsloth.entities.values import FinetuningType from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential diff --git a/services/unsloth/src/nmp/unsloth/tasks/progress_reporter.py b/services/unsloth/src/nmp/unsloth/tasks/progress_reporter.py deleted file mode 100644 index 462cb59d9d..0000000000 --- a/services/unsloth/src/nmp/unsloth/tasks/progress_reporter.py +++ /dev/null @@ -1,12 +0,0 @@ -# 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/backends/unsloth_sft.py b/services/unsloth/src/nmp/unsloth/tasks/training/backends/unsloth_sft.py index efbe14aee7..6795925beb 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 @@ -25,8 +25,8 @@ from typing import Any, Literal from nemo_platform_plugin.job_context import JobContext -from nmp.unsloth.app.jobs.context import NMPJobContext -from nmp.unsloth.integrations import apply_integrations_to_sft_config +from nmp.customization_common.service.context import NMPJobContext +from nmp.unsloth.integrations.hf_bridge import apply_integrations_to_sft_config from nmp.unsloth.schemas import UnslothJobOutput from nmp.unsloth.tasks.training.backends.callbacks import TrainingProgressCallback from nmp.unsloth.tasks.training.progress import JobsServiceProgressReporter diff --git a/services/unsloth/tests/test_file_io.py b/services/unsloth/tests/test_file_io.py index 93343ec5ec..c11963afa7 100644 --- a/services/unsloth/tests/test_file_io.py +++ b/services/unsloth/tests/test_file_io.py @@ -26,8 +26,8 @@ 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.customization_common.service.context import NMPJobContext + from nmp.customization_common.tasks.file_io_progress_reporter import NoOpProgressReporter from nmp.unsloth.tasks.file_io.run import FileIORunner job_ctx = NMPJobContext( @@ -94,7 +94,7 @@ def _make_dir(tmp_path: Path) -> Path: class TestCreateFileset: def test_creates_fileset_with_service_source_and_metadata(self) -> None: - from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + from nmp.customization_common.schemas.file_io import FileSetRef sdk = _make_sdk() runner = _make_runner(sdk) @@ -111,7 +111,7 @@ def test_creates_fileset_with_service_source_and_metadata(self) -> None: assert call.kwargs["metadata"] == metadata def test_conflict_patches_metadata_on_existing(self) -> None: - from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + from nmp.customization_common.schemas.file_io import FileSetRef sdk = _make_sdk(conflict_on_create=True) runner = _make_runner(sdk) @@ -126,7 +126,7 @@ def test_conflict_patches_metadata_on_existing(self) -> None: 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 + from nmp.customization_common.schemas.file_io import FileSetRef sdk = _make_sdk(conflict_on_create=True) runner = _make_runner(sdk) @@ -140,7 +140,7 @@ def test_update_failure_is_warning_not_fatal( self, caplog: pytest.LogCaptureFixture, ) -> None: - from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + from nmp.customization_common.schemas.file_io import FileSetRef sdk = _make_sdk(conflict_on_create=True) sdk.files.filesets.update.side_effect = RuntimeError("backend down") @@ -160,7 +160,7 @@ def test_update_failure_is_warning_not_fatal( class TestUploadFileset: def test_directory_uploads_with_trailing_slash(self, tmp_path: Path) -> None: - from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + from nmp.customization_common.schemas.file_io import FileSetRef sdk = _make_sdk() runner = _make_runner(sdk) @@ -178,7 +178,7 @@ def test_directory_uploads_with_trailing_slash(self, tmp_path: Path) -> None: 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 + from nmp.customization_common.schemas.file_io import FileSetRef sdk = _make_sdk() runner = _make_runner(sdk) @@ -193,7 +193,7 @@ def test_single_file_uploads_to_basename(self, tmp_path: Path) -> None: 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 + from nmp.customization_common.schemas.file_io import FileSetRef, FileUploadError sdk = _make_sdk() sdk.files.upload.side_effect = RuntimeError("upload broke") @@ -212,7 +212,7 @@ def test_upload_failure_propagates_as_file_upload_error(self, tmp_path: Path) -> class TestDownloadFileset: def test_lists_then_downloads(self, tmp_path: Path) -> None: - from nmp.unsloth.app.jobs.file_io.schemas import FileSetRef + from nmp.customization_common.schemas.file_io import FileSetRef sdk = _make_sdk() # files.list returns an object with .data (a list of FilesetFile-ish objects). @@ -237,7 +237,7 @@ def test_lists_then_downloads(self, tmp_path: Path) -> None: 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 + from nmp.customization_common.schemas.file_io import FileSetRef sdk = _make_sdk() sdk.files.list.return_value = types.SimpleNamespace(data=[]) @@ -300,14 +300,14 @@ def test_extracts_canonical_fields(self) -> None: class TestValidateSafePath: def test_safe_path_resolves(self, tmp_path: Path) -> None: - from nmp.unsloth.tasks.file_io.utils import validate_safe_path + from nmp.customization_common.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 + from nmp.customization_common.schemas.file_io import PathTraversalError + from nmp.customization_common.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_model_entity.py b/services/unsloth/tests/test_model_entity.py index 75acc1cbbd..aedf1115f7 100644 --- a/services/unsloth/tests/test_model_entity.py +++ b/services/unsloth/tests/test_model_entity.py @@ -22,7 +22,7 @@ def _make_job_ctx(workspace: str = "default"): - from nmp.unsloth.app.jobs.context import NMPJobContext + from nmp.customization_common.service.context import NMPJobContext return NMPJobContext( workspace=workspace, @@ -106,8 +106,8 @@ def test_caps_length_below_60_and_strips_trailing_hyphen(self) -> None: 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 + from nmp.customization_common.schemas.file_io import FileSetRef + from nmp.customization_common.schemas.model_entity import ModelEntityTaskConfig sdk = _make_sdk() sdk.models.retrieve.return_value = _model_entity(name="base-model") @@ -133,8 +133,8 @@ def test_creates_model_entity_for_full_sft(self) -> None: 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 + from nmp.customization_common.schemas.file_io import FileSetRef + from nmp.customization_common.schemas.model_entity import ModelEntityTaskConfig sdk = _make_sdk() sdk.models.retrieve.return_value = _model_entity(name="base-model") @@ -158,8 +158,8 @@ def test_conflict_falls_back_to_update(self) -> None: 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 + from nmp.customization_common.schemas.file_io import FileSetRef + from nmp.customization_common.schemas.model_entity import ModelEntityCreationError, ModelEntityTaskConfig sdk = _make_sdk() sdk.files.filesets.retrieve.side_effect = RuntimeError("fileset missing") @@ -182,8 +182,8 @@ def test_missing_fileset_raises_creation_error(self) -> None: 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.customization_common.schemas.file_io import FileSetRef + from nmp.customization_common.schemas.model_entity import ModelEntityTaskConfig, PEFTConfig from nmp.unsloth.entities.values import FinetuningType sdk = _make_sdk() @@ -207,8 +207,8 @@ def test_creates_adapter_for_lora(self) -> None: 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.customization_common.schemas.file_io import FileSetRef + from nmp.customization_common.schemas.model_entity import ModelEntityTaskConfig, PEFTConfig from nmp.unsloth.entities.values import FinetuningType sdk = _make_sdk() @@ -237,8 +237,8 @@ def test_adapter_conflict_falls_back_to_update(self) -> None: 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 + from nmp.customization_common.schemas.file_io import FileSetRef + from nmp.customization_common.schemas.model_entity import ModelEntityTaskConfig sdk = _make_sdk() runner = _make_runner(sdk) @@ -257,8 +257,8 @@ def test_no_deployment_config_returns_early(self) -> None: 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 + from nmp.customization_common.schemas.file_io import FileSetRef + from nmp.customization_common.schemas.model_entity import DeploymentParameters, ModelEntityTaskConfig sdk = _make_sdk() sdk.inference.deployment_configs.create.return_value = types.SimpleNamespace( @@ -291,8 +291,8 @@ def test_inline_params_creates_config_then_deployment(self) -> None: 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 + from nmp.customization_common.schemas.file_io import FileSetRef + from nmp.customization_common.schemas.model_entity import ModelEntityTaskConfig sdk = _make_sdk() sdk.inference.deployment_configs.retrieve.return_value = types.SimpleNamespace( @@ -329,8 +329,12 @@ def test_string_ref_resolves_existing_config(self) -> None: 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.customization_common.schemas.file_io import FileSetRef + from nmp.customization_common.schemas.model_entity import ( + DeploymentParameters, + ModelEntityTaskConfig, + PEFTConfig, + ) from nmp.unsloth.entities.values import FinetuningType sdk = _make_sdk() @@ -360,8 +364,12 @@ 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.customization_common.schemas.file_io import FileSetRef + from nmp.customization_common.schemas.model_entity import ( + DeploymentParameters, + ModelEntityTaskConfig, + PEFTConfig, + ) from nmp.unsloth.entities.values import FinetuningType sdk = _make_sdk() diff --git a/services/unsloth/tests/test_progress.py b/services/unsloth/tests/test_progress.py index 01d9ffab84..3ccd80c6b3 100644 --- a/services/unsloth/tests/test_progress.py +++ b/services/unsloth/tests/test_progress.py @@ -4,7 +4,7 @@ from pathlib import Path from unittest.mock import MagicMock, patch -from nmp.unsloth.app.jobs.context import NMPJobContext +from nmp.customization_common.service.context import NMPJobContext from nmp.unsloth.tasks.training.progress import JobsServiceProgressReporter From 5f6a7e54cc0c6990590089ab558fa4c7421c14da Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Fri, 12 Jun 2026 14:58:36 -0600 Subject: [PATCH 3/4] Fix code to not import from nmp common in plugins Signed-off-by: Sam Oluwalana --- .../integrations/__init__.py | 4 +- .../integrations/schemas.py | 4 +- .../src/nemo_platform_plugin/schema.py | 16 +- .../tests/test_integrations_schemas.py} | 2 +- .../integrations/compiler.py | 2 +- .../integrations/context.py | 2 +- .../tests/integrations/test_compiler.py | 4 +- .../tests/integrations/test_runtime.py | 2 +- plugins/nemo-automodel/README.md | 2 +- .../src/nemo_automodel_plugin/schema.py | 2 +- .../references/hyperparameters.md | 2 +- plugins/nemo-unsloth/README.md | 2 +- .../src/nemo_unsloth_plugin/schema.py | 2 +- services/automodel/pyproject.toml | 2 + .../automodel/src/nmp/automodel/adapter.py | 2 +- .../src/nmp/automodel/api/v2/jobs/schemas.py | 2 +- .../automodel/app/jobs/training/schemas.py | 2 +- .../tests/test_integrations_compiler.py | 2 +- .../spans/test_chat_completions_ingest.py | 4 +- services/unsloth/pyproject.toml | 2 + .../src/nmp/unsloth/integrations/hf_bridge.py | 2 +- services/unsloth/src/nmp/unsloth/schemas.py | 2 +- .../tests/test_integrations_compiler.py | 2 +- .../tests/test_integrations_hf_bridge.py | 2 +- uv.lock | 472 ++++++++++++++++++ 25 files changed, 517 insertions(+), 25 deletions(-) rename packages/{nmp_common/src/nmp/common => nemo_platform_plugin/src/nemo_platform_plugin}/integrations/__init__.py (55%) rename packages/{nmp_common/src/nmp/common => nemo_platform_plugin/src/nemo_platform_plugin}/integrations/schemas.py (98%) rename packages/{nmp_common/tests/integrations/test_schemas.py => nemo_platform_plugin/tests/test_integrations_schemas.py} (97%) diff --git a/packages/nmp_common/src/nmp/common/integrations/__init__.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/integrations/__init__.py similarity index 55% rename from packages/nmp_common/src/nmp/common/integrations/__init__.py rename to packages/nemo_platform_plugin/src/nemo_platform_plugin/integrations/__init__.py index a99c01cc2f..a6909780b2 100644 --- a/packages/nmp_common/src/nmp/common/integrations/__init__.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/integrations/__init__.py @@ -1,9 +1,9 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Shared experiment-tracking integration schemas.""" +"""Shared experiment-tracking integration schemas for platform plugins.""" -from nmp.common.integrations.schemas import IntegrationsSpec, MlflowIntegration, WandbIntegration +from nemo_platform_plugin.integrations.schemas import IntegrationsSpec, MlflowIntegration, WandbIntegration __all__ = [ "IntegrationsSpec", diff --git a/packages/nmp_common/src/nmp/common/integrations/schemas.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/integrations/schemas.py similarity index 98% rename from packages/nmp_common/src/nmp/common/integrations/schemas.py rename to packages/nemo_platform_plugin/src/nemo_platform_plugin/integrations/schemas.py index ca678d28a0..5edaa682b8 100644 --- a/packages/nmp_common/src/nmp/common/integrations/schemas.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/integrations/schemas.py @@ -1,14 +1,14 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Experiment-tracking integrations schema shared across platform services and plugins.""" +"""Experiment-tracking integrations schema shared across platform plugins and services.""" from __future__ import annotations import warnings from typing import Self -from nmp.common.api.common import SecretRef +from nemo_platform_plugin.schema import SecretRef from pydantic import BaseModel, ConfigDict, Field, model_validator diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/schema.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/schema.py index 317a22a2fe..072c7e4e44 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/schema.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/schema.py @@ -74,7 +74,7 @@ class WidgetStatsResponse(BaseModel): from datetime import datetime from typing import Any, Generic, Optional, TypeVar -from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, RootModel, model_validator __all__ = [ "DatetimeFilter", @@ -83,6 +83,7 @@ class WidgetStatsResponse(BaseModel): "NemoListResponse", "Page", "PaginationData", + "SecretRef", "StringFilter", "Value", ] @@ -100,6 +101,19 @@ class Value(BaseModel): model_config = {"arbitrary_types_allowed": True, "protected_namespaces": ()} +class SecretRef(RootModel): + """Reference to a platform secret by name.""" + + root: str = Field( + description="Reference to a secret. Format: 'secret_name' (uses request workspace) or 'workspace/secret_name' (explicit workspace).", + pattern=r"^[a-z0-9_-]+(/[a-z0-9_-]+)?$", + examples=[ + "my-secret", + "my-workspace/my-secret", + ], + ) + + class PaginationData(Value): page: int = Field(description="The current page number.") page_size: int = Field(description="The page size used for the query.") diff --git a/packages/nmp_common/tests/integrations/test_schemas.py b/packages/nemo_platform_plugin/tests/test_integrations_schemas.py similarity index 97% rename from packages/nmp_common/tests/integrations/test_schemas.py rename to packages/nemo_platform_plugin/tests/test_integrations_schemas.py index be94f11fa2..8b73c38d67 100644 --- a/packages/nmp_common/tests/integrations/test_schemas.py +++ b/packages/nemo_platform_plugin/tests/test_integrations_schemas.py @@ -4,7 +4,7 @@ import warnings import pytest -from nmp.common.integrations import IntegrationsSpec, MlflowIntegration, WandbIntegration +from nemo_platform_plugin.integrations import IntegrationsSpec, MlflowIntegration, WandbIntegration from pydantic import ValidationError diff --git a/packages/nmp_customization_common/src/nmp/customization_common/integrations/compiler.py b/packages/nmp_customization_common/src/nmp/customization_common/integrations/compiler.py index d0f5fbf675..03231665f1 100644 --- a/packages/nmp_customization_common/src/nmp/customization_common/integrations/compiler.py +++ b/packages/nmp_customization_common/src/nmp/customization_common/integrations/compiler.py @@ -5,11 +5,11 @@ import logging +from nemo_platform_plugin.integrations import IntegrationsSpec from nemo_platform_plugin.jobs.api_factory import ( EnvironmentVariable, EnvironmentVariableFromSecret, ) -from nmp.common.integrations import IntegrationsSpec logger = logging.getLogger(__name__) diff --git a/packages/nmp_customization_common/src/nmp/customization_common/integrations/context.py b/packages/nmp_customization_common/src/nmp/customization_common/integrations/context.py index 404add42b5..8bf185cbf4 100644 --- a/packages/nmp_customization_common/src/nmp/customization_common/integrations/context.py +++ b/packages/nmp_customization_common/src/nmp/customization_common/integrations/context.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from typing import Self -from nmp.common.integrations import IntegrationsSpec, MlflowIntegration, WandbIntegration +from nemo_platform_plugin.integrations import IntegrationsSpec, MlflowIntegration, WandbIntegration from nmp.customization_common.service.context import NMPJobContext diff --git a/packages/nmp_customization_common/tests/integrations/test_compiler.py b/packages/nmp_customization_common/tests/integrations/test_compiler.py index c4d65b48cb..45f231b4bc 100644 --- a/packages/nmp_customization_common/tests/integrations/test_compiler.py +++ b/packages/nmp_customization_common/tests/integrations/test_compiler.py @@ -2,8 +2,8 @@ # SPDX-License-Identifier: Apache-2.0 import pytest -from nmp.common.api.common import SecretRef -from nmp.common.integrations import IntegrationsSpec, WandbIntegration +from nemo_platform_plugin.integrations import IntegrationsSpec, WandbIntegration +from nemo_platform_plugin.schema import SecretRef from nmp.customization_common.integrations import collect_integration_secret_envs, warn_incomplete_integrations diff --git a/packages/nmp_customization_common/tests/integrations/test_runtime.py b/packages/nmp_customization_common/tests/integrations/test_runtime.py index 3f094859bd..522ffa2149 100644 --- a/packages/nmp_customization_common/tests/integrations/test_runtime.py +++ b/packages/nmp_customization_common/tests/integrations/test_runtime.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest -from nmp.common.integrations import IntegrationsSpec, MlflowIntegration, WandbIntegration +from nemo_platform_plugin.integrations import IntegrationsSpec, MlflowIntegration, WandbIntegration from nmp.customization_common.integrations import IntegrationRuntimeContext, build_mlflow_config, build_wandb_config from nmp.customization_common.service.context import NMPJobContext diff --git a/plugins/nemo-automodel/README.md b/plugins/nemo-automodel/README.md index f4b831d3c0..cd571c42fb 100644 --- a/plugins/nemo-automodel/README.md +++ b/plugins/nemo-automodel/README.md @@ -29,4 +29,4 @@ Other customization backends may still use `nemo customization jobs su Job JSON uses the simplified `AutomodelJobInput` schema (see `nemo_automodel_plugin/schema.py`). Submit posts to `/apis/customization/v2/workspaces/{workspace}/automodel/jobs`. -Optional `integrations` (W&B / MLflow) use the shared `IntegrationsSpec` from `nmp.common.integrations`. Example: `plugins/nemo-automodel/tests/fixtures/integrations_wandb_mlflow.json`. Field reference: customizer skill `references/hyperparameters.md` § **Integrations (automodel + unsloth)**. +Optional `integrations` (W&B / MLflow) use the shared `IntegrationsSpec` from `nemo_platform_plugin.integrations`. Example: `plugins/nemo-automodel/tests/fixtures/integrations_wandb_mlflow.json`. Field reference: customizer skill `references/hyperparameters.md` § **Integrations (automodel + unsloth)**. diff --git a/plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py b/plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py index 55459a40a0..e09c9a4889 100644 --- a/plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py +++ b/plugins/nemo-automodel/src/nemo_automodel_plugin/schema.py @@ -7,7 +7,7 @@ from typing import Literal, Self -from nmp.common.integrations import IntegrationsSpec +from nemo_platform_plugin.integrations import IntegrationsSpec from pydantic import BaseModel, ConfigDict, Field, model_validator __all__ = [ 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 15f495eaf0..768b0f7d3d 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 @@ -15,7 +15,7 @@ Both schemas use `extra="forbid"` — unknown keys raise validation errors. Fiel ## Integrations (automodel + unsloth) -Both backends accept the same `integrations` object on job JSON (`IntegrationsSpec` in `nmp.common.integrations`). A non-null backend block **requests** that integration; the training runtime **activates** it only when credentials/URIs are available (W&B needs `WANDB_API_KEY`, MLflow needs a tracking URI). Omit the field or set a backend to `null` to disable. There is no `enabled` flag and no `report_to` on input — `report_to` is derived at runtime from activated backends. The compiler logs a warning when W&B is requested without `api_key_secret` or MLflow without `tracking_uri`. +Both backends accept the same `integrations` object on job JSON (`IntegrationsSpec` in `nemo_platform_plugin.integrations`). A non-null backend block **requests** that integration; the training runtime **activates** it only when credentials/URIs are available (W&B needs `WANDB_API_KEY`, MLflow needs a tracking URI). Omit the field or set a backend to `null` to disable. There is no `enabled` flag and no `report_to` on input — `report_to` is derived at runtime from activated backends. The compiler logs a warning when W&B is requested without `api_key_secret` or MLflow without `tracking_uri`. ```json "integrations": { diff --git a/plugins/nemo-unsloth/README.md b/plugins/nemo-unsloth/README.md index cde3b32675..235da26535 100644 --- a/plugins/nemo-unsloth/README.md +++ b/plugins/nemo-unsloth/README.md @@ -65,7 +65,7 @@ The container image targets the same compute capabilities NVIDIA's stock `pytorc - `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` — optional W&B / MLflow (`nmp.common.integrations`). Request by presence; `api_key_secret` carries a secret *reference* that the jobs launcher resolves into `WANDB_API_KEY` in the training container **at runtime** (compile only records the reference). Example: `plugins/nemo-unsloth/tests/fixtures/integrations_wandb_mlflow.json`. +- `integrations: IntegrationsSpec | None` — optional W&B / MLflow (`nemo_platform_plugin.integrations`). Request by presence; `api_key_secret` carries a secret *reference* that the jobs launcher resolves into `WANDB_API_KEY` in the training container **at runtime** (compile only records the reference). Example: `plugins/nemo-unsloth/tests/fixtures/integrations_wandb_mlflow.json`. - `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. diff --git a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/schema.py b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/schema.py index 54104b5ef7..187b861348 100644 --- a/plugins/nemo-unsloth/src/nemo_unsloth_plugin/schema.py +++ b/plugins/nemo-unsloth/src/nemo_unsloth_plugin/schema.py @@ -23,7 +23,7 @@ from typing import Literal, Self -from nmp.common.integrations import IntegrationsSpec +from nemo_platform_plugin.integrations import IntegrationsSpec from nmp.unsloth.schemas import ( BatchSpec, DatasetSpec, diff --git a/services/automodel/pyproject.toml b/services/automodel/pyproject.toml index 431486fbf3..e8e3829181 100644 --- a/services/automodel/pyproject.toml +++ b/services/automodel/pyproject.toml @@ -7,6 +7,7 @@ requires-python = ">=3.11,<3.14" dependencies = [ "nmp-common", "nmp-customization-common", + "nemo-platform-plugin", "nemo-platform-sdk", "pydantic>=2.10.6", "pydantic-settings>=2.6.1", @@ -34,6 +35,7 @@ packages = ["src/nmp"] [tool.uv.sources] nmp-common = { workspace = true } nmp-customization-common = { workspace = true } +nemo-platform-plugin = { workspace = true } nemo-platform-sdk = { workspace = true } [tool.pytest.ini_options] diff --git a/services/automodel/src/nmp/automodel/adapter.py b/services/automodel/src/nmp/automodel/adapter.py index 3fbcb69a5d..fe2dccd491 100644 --- a/services/automodel/src/nmp/automodel/adapter.py +++ b/services/automodel/src/nmp/automodel/adapter.py @@ -7,6 +7,7 @@ from typing import Any, Literal +from nemo_platform_plugin.integrations import IntegrationsSpec from nmp.automodel.api.v2.jobs.schemas import ( CustomizationJobOutput, DistillationTraining, @@ -15,7 +16,6 @@ ParallelismParams, SFTTraining, ) -from nmp.common.integrations import IntegrationsSpec from pydantic import BaseModel diff --git a/services/automodel/src/nmp/automodel/api/v2/jobs/schemas.py b/services/automodel/src/nmp/automodel/api/v2/jobs/schemas.py index 5ac657bdae..620781ba8b 100644 --- a/services/automodel/src/nmp/automodel/api/v2/jobs/schemas.py +++ b/services/automodel/src/nmp/automodel/api/v2/jobs/schemas.py @@ -5,13 +5,13 @@ from typing import Annotated, Any, Dict, Literal, Optional, Self, Union +from nemo_platform_plugin.integrations import IntegrationsSpec from nmp.automodel.entities.validators import validate_fileset_uri from nmp.automodel.entities.values import FinetuningType, OutputNameType, Precision from nmp.common.entities.constants import ( MAX_LENGTH_255, REGEX_WORD_CHARACTER_DOT_DASH, ) -from nmp.common.integrations import IntegrationsSpec from pydantic import AfterValidator, BaseModel, ConfigDict, Discriminator, Field, model_validator # Important!!! Do not import Pydantic models from this file into tasks. diff --git a/services/automodel/src/nmp/automodel/app/jobs/training/schemas.py b/services/automodel/src/nmp/automodel/app/jobs/training/schemas.py index 032bfbb759..6add112c70 100644 --- a/services/automodel/src/nmp/automodel/app/jobs/training/schemas.py +++ b/services/automodel/src/nmp/automodel/app/jobs/training/schemas.py @@ -4,13 +4,13 @@ from enum import Enum from typing import Optional +from nemo_platform_plugin.integrations import IntegrationsSpec 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 nmp.common.integrations import IntegrationsSpec from pydantic import BaseModel, Field diff --git a/services/automodel/tests/test_integrations_compiler.py b/services/automodel/tests/test_integrations_compiler.py index ba385d4f33..8982a53416 100644 --- a/services/automodel/tests/test_integrations_compiler.py +++ b/services/automodel/tests/test_integrations_compiler.py @@ -5,6 +5,7 @@ import pytest from nemo_platform.types.models.model_entity import ModelEntity +from nemo_platform_plugin.integrations import IntegrationsSpec from nmp.automodel.api.v2.jobs.schemas import ( CustomizationJobOutput, LoRAParams, @@ -13,7 +14,6 @@ ) from nmp.automodel.app.jobs.training.compiler import compile_training_step from nmp.common.entities.utils import get_random_id -from nmp.common.integrations import IntegrationsSpec def _make_model_entity() -> ModelEntity: diff --git a/services/intake/tests/integration/spans/test_chat_completions_ingest.py b/services/intake/tests/integration/spans/test_chat_completions_ingest.py index 70ba7b1f11..af5a87a2cc 100644 --- a/services/intake/tests/integration/spans/test_chat_completions_ingest.py +++ b/services/intake/tests/integration/spans/test_chat_completions_ingest.py @@ -6,6 +6,7 @@ from __future__ import annotations import json +from datetime import datetime, timezone from decimal import Decimal from typing import Any @@ -50,7 +51,8 @@ def _openai_response(**overrides: Any) -> dict[str, Any]: response = { "id": "chatcmpl-test-abc123", "object": "chat.completion", - "created": 1778698885, + # Keep within the spans list default 30-day started_at lookback. + "created": int(datetime.now(timezone.utc).timestamp()), "model": "gpt-4o-mini-2024-08-06", "choices": [ { diff --git a/services/unsloth/pyproject.toml b/services/unsloth/pyproject.toml index 9ccba21b72..810bd052ec 100644 --- a/services/unsloth/pyproject.toml +++ b/services/unsloth/pyproject.toml @@ -7,6 +7,7 @@ requires-python = ">=3.11,<3.14" dependencies = [ "nmp-common", "nmp-customization-common", + "nemo-platform-plugin", "nemo-platform-sdk", "pydantic>=2.10.6", "pydantic-settings>=2.6.1", @@ -53,6 +54,7 @@ packages = ["src/nmp"] [tool.uv.sources] nmp-common = { workspace = true } nmp-customization-common = { workspace = true } +nemo-platform-plugin = { workspace = true } nemo-platform-sdk = { workspace = true } [dependency-groups] diff --git a/services/unsloth/src/nmp/unsloth/integrations/hf_bridge.py b/services/unsloth/src/nmp/unsloth/integrations/hf_bridge.py index ba60bf8c64..c3ebc1a012 100644 --- a/services/unsloth/src/nmp/unsloth/integrations/hf_bridge.py +++ b/services/unsloth/src/nmp/unsloth/integrations/hf_bridge.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import Any -from nmp.common.integrations import IntegrationsSpec +from nemo_platform_plugin.integrations import IntegrationsSpec from nmp.customization_common.integrations import ( IntegrationRuntimeContext, build_mlflow_config, diff --git a/services/unsloth/src/nmp/unsloth/schemas.py b/services/unsloth/src/nmp/unsloth/schemas.py index a9529e88c2..fed73da296 100644 --- a/services/unsloth/src/nmp/unsloth/schemas.py +++ b/services/unsloth/src/nmp/unsloth/schemas.py @@ -31,7 +31,7 @@ from typing import Literal -from nmp.common.integrations import IntegrationsSpec +from nemo_platform_plugin.integrations import IntegrationsSpec from pydantic import BaseModel, ConfigDict, Field diff --git a/services/unsloth/tests/test_integrations_compiler.py b/services/unsloth/tests/test_integrations_compiler.py index 85b408f225..fb957cf76c 100644 --- a/services/unsloth/tests/test_integrations_compiler.py +++ b/services/unsloth/tests/test_integrations_compiler.py @@ -52,7 +52,7 @@ def test_compile_training_step_no_integrations() -> None: def test_compile_training_step_warns_on_incomplete_wandb( caplog: pytest.LogCaptureFixture, ) -> None: - from nmp.common.integrations import IntegrationsSpec + from nemo_platform_plugin.integrations import IntegrationsSpec spec = _job_spec_with_integrations().model_copy( update={ diff --git a/services/unsloth/tests/test_integrations_hf_bridge.py b/services/unsloth/tests/test_integrations_hf_bridge.py index 3e7d55ecd3..5e66407ad9 100644 --- a/services/unsloth/tests/test_integrations_hf_bridge.py +++ b/services/unsloth/tests/test_integrations_hf_bridge.py @@ -5,7 +5,7 @@ from pathlib import Path import pytest -from nmp.common.integrations import IntegrationsSpec +from nemo_platform_plugin.integrations import IntegrationsSpec from nmp.customization_common.service.context import NMPJobContext from nmp.unsloth.integrations.hf_bridge import apply_integrations_to_sft_config diff --git a/uv.lock b/uv.lock index c57ed6d935..53526e630a 100644 --- a/uv.lock +++ b/uv.lock @@ -246,20 +246,44 @@ 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/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" }, + { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" }, { 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/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" }, { 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/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" }, + { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" }, { 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/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, { 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/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, { 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/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, { 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/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, { 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/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, { 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/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, { 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" }, ] @@ -772,16 +796,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" }, @@ -813,18 +843,42 @@ sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b0 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/a1/5f/2574f0f09f3c3bc1b2f992e20bce6546cb1f17e111c5be07308dc5427956/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d", size = 217363, upload-time = "2026-03-15T18:50:28.601Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d1/0ae20ad77bc949ddd39b51bf383b6ca932f2916074c95cad34ae465ab71f/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de", size = 212994, upload-time = "2026-03-15T18:50:30.102Z" }, { 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/25/3c/8a18fc411f085b82303cfb7154eed5bd49c77035eb7608d049468b53f87c/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c", size = 191673, upload-time = "2026-03-15T18:50:33.433Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a7/11cfe61d6c5c5c7438d6ba40919d0306ed83c9ab957f3d4da2277ff67836/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc", size = 201120, upload-time = "2026-03-15T18:50:35.105Z" }, { 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/28/70/039796160b48b18ed466fde0af84c1b090c4e288fae26cd674ad04a2d703/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef", size = 192516, upload-time = "2026-03-15T18:50:38.228Z" }, + { url = "https://files.pythonhosted.org/packages/ff/34/c56f3223393d6ff3124b9e78f7de738047c2d6bc40a4f16ac0c9d7a1cb3c/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398", size = 218795, upload-time = "2026-03-15T18:50:39.664Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3b/ce2d4f86c5282191a041fdc5a4ce18f1c6bd40a5bd1f74cf8625f08d51c1/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e", size = 201833, upload-time = "2026-03-15T18:50:41.552Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9b/b6a9f76b0fd7c5b5ec58b228ff7e85095370282150f0bd50b3126f5506d6/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed", size = 213920, upload-time = "2026-03-15T18:50:43.33Z" }, { 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/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, + { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, + { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, + { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, + { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, ] @@ -889,18 +943,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" }, @@ -951,22 +1017,38 @@ 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/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, { 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/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, { 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/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] @@ -989,8 +1071,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 +1083,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" }, @@ -1596,23 +1684,43 @@ sdist = { url = "https://files.pythonhosted.org/packages/dd/00/dab9ca274cf1fde19 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/bc/dd/1d346cdfcd3064f6c435eff90a8d7cf0021487e3681453bdd681b9488d81/fastar-0.9.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:52f96a3d4cfbe4f06b376706fa0562f3a1d2329bc37168119af0e47e1ac21cab", size = 759627, upload-time = "2026-03-20T14:24:01.984Z" }, + { url = "https://files.pythonhosted.org/packages/02/a1/e91eb7ae1e41c0d3ead86dc199beb13a0b80101e2948d66adeb578b09e60/fastar-0.9.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57e9b94e485713c79bb259f7ecff1213527d05e9aa43a157c3fbc88812cf163e", size = 926211, upload-time = "2026-03-20T14:24:15.218Z" }, + { url = "https://files.pythonhosted.org/packages/9b/63/9fea9604e7aecc2f062f0df5729f74712d81615a1b18fa6a1a13106184fa/fastar-0.9.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb06d0a0cc3cf52a9c07559bb16ab99eb75afe0b3d5ce68f5c299569460851ac", size = 818748, upload-time = "2026-03-20T14:24:40.765Z" }, { 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/92/05/f33cc3f5f96ffb7d81a7f06c9239d4eea584527292a030a73d3218148f41/fastar-0.9.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:24b13fc4ef3f1e3c9cc2dcf07ad9445900db9d3ce09b73021547a55994d0407f", size = 886569, upload-time = "2026-03-20T14:24:27.567Z" }, { 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/6a/ee/04cf9374e5e6a82ddc87073d684c1fa7a9ca368bf85c2786535b1bfc38a9/fastar-0.9.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a79c53c3003958dca88a7ec3dd805bf9c2fb2a659110039f44571d57e329e3d4", size = 1036738, upload-time = "2026-03-20T14:25:57.551Z" }, { 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/b8/96/086116ad46e3b98f6c217919d680e619f2857ffa6b5cc0d7e46e4f214b83/fastar-0.9.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75c70be3a7da3ff9342f64c15ec3749c13ef56bc28e69075d82d03768532a8d0", size = 758000, upload-time = "2026-03-20T14:24:03.471Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e6/ea642ea61eea98d609343080399a296a9ff132bd0492a6638d6e0d9e41a7/fastar-0.9.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a734506b071d2a8844771fe735fbd6d67dd0eec80eef5f189bbe763ebe7a0b8", size = 923647, upload-time = "2026-03-20T14:24:16.875Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3e/53874aad61e4a664af555a2aa7a52fe46cfadd423db0e592fa0cfe0fa668/fastar-0.9.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8eac084ab215aaf65fa406c9b9da1ac4e697c3d3a1a183e09c488e555802f62d", size = 816528, upload-time = "2026-03-20T14:24:42.048Z" }, { url = "https://files.pythonhosted.org/packages/41/df/d663214d35380b07a24a796c48d7d7d4dc3a28ec0756edbcb7e2a81dc572/fastar-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb62e2369834fb23d26327157f0a2dbec40b230c709fa85b1ce96cf010e6fbf", size = 819050, upload-time = "2026-03-20T14:25:08.352Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5a/455b53f11527568100ba6d5847635430645bad62d676f0bae4173fc85c90/fastar-0.9.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:f2f399fffb74bcd9e9d4507e253ace2430b5ccf61000596bda41e90414bcf4f2", size = 885257, upload-time = "2026-03-20T14:24:28.86Z" }, { url = "https://files.pythonhosted.org/packages/4f/dd/0a8ea7b910293b07f8c82ef4e6451262ccf2a6f2020e880f184dc4abd6c2/fastar-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87006c8770dfc558aefe927590bbcdaf9648ca4472a9ee6d10dfb7c0bda4ce5b", size = 968135, upload-time = "2026-03-20T14:25:45.614Z" }, + { url = "https://files.pythonhosted.org/packages/6b/cb/5c7e9231d6ba00e225623947068db09ddd4e401800b0afaf39eece14bfee/fastar-0.9.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d012644421d669d9746157193f4eafd371e8ae56ff7aef97612a4922418664c", size = 1034940, upload-time = "2026-03-20T14:25:58.893Z" }, { url = "https://files.pythonhosted.org/packages/8b/53/6ddda28545b428d54c42f341d797046467c689616a36eae9a43ba56f2545/fastar-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:59bc500d7b6bdaf2ffb2b632bc6b0f97ddfb3bb7d31b54d61ceb00b5698d6484", size = 1025314, upload-time = "2026-03-20T14:26:24.624Z" }, { url = "https://files.pythonhosted.org/packages/77/52/f3b06867e5ca8d5b2c1c15a1563415e0037b5831f2058ee72b03960296d9/fastar-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f07c6bdeedfeb30ef459f21fa9ab06e2b6727f7e7653176d3abb7a85f447c400", size = 627615, upload-time = "2026-03-20T14:25:21.608Z" }, { url = "https://files.pythonhosted.org/packages/3f/54/e2e1b4c8512d670373047e5e585b1d1ff9ffd722b0a17647d22c9c9bd248/fastar-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:108bb46c080ca152bb331f1e0576177d36e9badba51b1d5724d2823542e0dd1f", size = 760246, upload-time = "2026-03-20T14:23:51.964Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7d/1e283dd8dbb3647049594bb477bdc053045c6fff2d3f06386d2dcacce7aa/fastar-0.9.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d17d311cfbb559154ba940972b6d07a3a7ac221a2a01208f119ad03495f01d32", size = 757024, upload-time = "2026-03-20T14:24:04.69Z" }, + { url = "https://files.pythonhosted.org/packages/87/ac/82d3cb64d318ce16c5d1a26a40b8aa570fcc9b23684221aece838c4cbada/fastar-0.9.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2ef34e7088f308e73460e1b8d9b0479a743f679816782a80db6ae87ee68714a", size = 921630, upload-time = "2026-03-20T14:24:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b8/3e7892f1a25a1a2054a20de6c846c0794b8fa361e5b9d3d00915b41e97bd/fastar-0.9.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c93bf4732d0dd6adae4a8b3bbebe19af76ee1072b7688bf39c5a1d120425a772", size = 815791, upload-time = "2026-03-20T14:24:43.28Z" }, { url = "https://files.pythonhosted.org/packages/db/5e/8fcc662db1fd0985f4f8a54e79276416565a0d1fcb8da66665b2061ead30/fastar-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a67b061b1099cf3b8b6234dd3605fa16f5078ab6b51c8d77ad7a5d11c3cf834", size = 818980, upload-time = "2026-03-20T14:25:09.545Z" }, + { url = "https://files.pythonhosted.org/packages/68/ed/37291fbd6c9b5b0905712da6191bdfc25a7dc236efbf130e3a1a7d1b9440/fastar-0.9.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:912efe3121dc1f3c05940cfa1c6b09b8868d702d24566506aa1d0d96e429923a", size = 884578, upload-time = "2026-03-20T14:24:30.584Z" }, { url = "https://files.pythonhosted.org/packages/94/19/7b3b7af978ae4f012664781554716d67549ab19ddbcb6e6d1adc04d7a5e7/fastar-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2394980cc126a3263e115600bc4ff9e7320cddde83c99fc334ab530be5b7166e", size = 967790, upload-time = "2026-03-20T14:25:46.975Z" }, + { url = "https://files.pythonhosted.org/packages/e6/38/4cce2a8e529a7d3e99e427c9bbcccd7013ff6b3ba295613e6f1c573c9e6c/fastar-0.9.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d0aff74ea98642784c941d3cd8c35943258d4b9626157858901c5b181683339b", size = 1033892, upload-time = "2026-03-20T14:26:00.22Z" }, { url = "https://files.pythonhosted.org/packages/10/4f/6ec0c123c15bbcb9a9b82e979dc81273789ebbfbb4a2b41a1a6941577c94/fastar-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c9bd8879ebf05aa247e60e454bb7568cbdd44f016b8c58e31e5398039403e61d", size = 1025768, upload-time = "2026-03-20T14:26:25.957Z" }, { 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/c8/f2/121c5432bb152da68fc466a0d0206d66383a40a2f9beff5583d9277aceee/fastar-0.9.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2a9a49f9217f4f60f9ba23fdd1f7f3f04fed97391145eb9460ec83ca0b4bd33", size = 762897, upload-time = "2026-03-20T14:24:11.932Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/88d3a603b997063e032f94cc0fff74031d76903f38cc30416a400395df03/fastar-0.9.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d860e82a531e9cc67e7f500a299bffbe6e93d80bbf48401fd8f452a0c58f28", size = 927024, upload-time = "2026-03-20T14:24:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/a6/17/d6dc778c45b0c7d9a279706d7a5d62122dab0a7a0cb39aac6f5ef42f13f6/fastar-0.9.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3feede2d72ec0782b5ccc18568f36cbe33816be396551aa47b3e1b73c322cdd2", size = 821265, upload-time = "2026-03-20T14:24:50.407Z" }, { 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/52/90/c354969770d21d1b07c9281b5e23052392c288d22984a1917d30940e86cb/fastar-0.9.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:40b8c08df809e5e58d1839ccb37bafe4485deb6ee56bb7c5f0cbb72d701eb965", size = 888886, upload-time = "2026-03-20T14:24:38.229Z" }, { 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/8d/88/f7e28100fa7ff4a26a3493ad7a5d45d70f6de858c05f5c34aca3570c5839/fastar-0.9.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:7bf6958bb6f94e5ec522e4a255b8e940d3561ad973f0be5dde6115b5a0854af5", size = 1039106, upload-time = "2026-03-20T14:26:07.686Z" }, { 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" }, ] @@ -1798,25 +1906,49 @@ 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" }, ] @@ -1935,16 +2067,22 @@ sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd9 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/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", size = 605046, upload-time = "2026-02-20T21:02:45.234Z" }, { 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/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, @@ -1959,16 +2097,19 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/db/1d56e5f5823257b291962d6c0ce106146c6447f405b60b234c4f222a7cde/grpcio-1.80.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:dfab85db094068ff42e2a3563f60ab3dddcc9d6488a35abf0132daec13209c8a", size = 6055009, upload-time = "2026-03-30T08:46:46.265Z" }, { 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/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" }, { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" }, { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" }, { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" }, @@ -2020,13 +2161,19 @@ sdist = { url = "https://files.pythonhosted.org/packages/1a/eb/8fc64f40388c29ce8 wheels = [ { url = "https://files.pythonhosted.org/packages/ea/2e/3d60b1a9e9f29a2152aa66c823bf5e399ae7be3fef310ff0de86779c5d2d/hf_transfer-0.1.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ebc4ab9023414880c8b1d3c38174d1c9989eb5022d37e814fa91a3060123eb0", size = 1343558, upload-time = "2025-01-07T10:04:42.313Z" }, { url = "https://files.pythonhosted.org/packages/fb/38/130a5ac3747f104033591bcac1c961cb1faadfdc91704f59b09c0b465ff2/hf_transfer-0.1.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8674026f21ed369aa2a0a4b46000aca850fc44cd2b54af33a172ce5325b4fc82", size = 3726676, upload-time = "2025-01-07T10:04:11.539Z" }, + { url = "https://files.pythonhosted.org/packages/15/a1/f4e27c5ad17aac616ae0849e2aede5aae31db8267a948c6b3eeb9fd96446/hf_transfer-0.1.9-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a736dfbb2c84f5a2c975478ad200c0c8bfcb58a25a35db402678fb87ce17fa4", size = 3062920, upload-time = "2025-01-07T10:04:16.297Z" }, + { url = "https://files.pythonhosted.org/packages/50/d0/2b213eb1ea8b1252ccaf1a6c804d0aba03fea38aae4124df6a3acb70511a/hf_transfer-0.1.9-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c7fc1b85f4d0f76e452765d7648c9f4bfd0aedb9ced2ae1ebfece2d8cfaf8e2", size = 3398837, upload-time = "2025-01-07T10:04:22.778Z" }, { url = "https://files.pythonhosted.org/packages/8c/8a/79dbce9006e0bd6b74516f97451a7b7c64dbbb426df15d901dd438cfeee3/hf_transfer-0.1.9-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d991376f0eac70a60f0cbc95602aa708a6f7c8617f28b4945c1431d67b8e3c8", size = 3546986, upload-time = "2025-01-07T10:04:36.415Z" }, { url = "https://files.pythonhosted.org/packages/a9/f7/9ac239b6ee6fe0bad130325d987a93ea58c4118e50479f0786f1733b37e8/hf_transfer-0.1.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e6ac4eddcd99575ed3735ed911ddf9d1697e2bd13aa3f0ad7e3904dd4863842e", size = 4071715, upload-time = "2025-01-07T10:04:53.224Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a3/0ed697279f5eeb7a40f279bd783cf50e6d0b91f24120dcf66ef2cf8822b4/hf_transfer-0.1.9-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:57fd9880da1ee0f47250f735f791fab788f0aa1ee36afc49f761349869c8b4d9", size = 3388081, upload-time = "2025-01-07T10:04:57.818Z" }, { url = "https://files.pythonhosted.org/packages/45/07/6661e43fbee09594a8a5e9bb778107d95fe38dac4c653982afe03d32bd4d/hf_transfer-0.1.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a5b366d34cd449fe9b20ef25941e6eef0460a2f74e7389f02e673e1f88ebd538", size = 3690551, upload-time = "2025-01-07T10:05:09.238Z" }, { url = "https://files.pythonhosted.org/packages/41/ba/8d9fd9f1083525edfcb389c93738c802f3559cb749324090d7109c8bf4c2/hf_transfer-0.1.9-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:8669dbcc7a3e2e8d61d42cd24da9c50d57770bd74b445c65123291ca842a7e7a", size = 1348126, upload-time = "2025-01-07T10:04:45.712Z" }, { url = "https://files.pythonhosted.org/packages/8e/a2/cd7885bc9959421065a6fae0fe67b6c55becdeda4e69b873e52976f9a9f0/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fd0167c4407a3bc4cdd0307e65ada2294ec04f1813d8a69a5243e379b22e9d8", size = 3728604, upload-time = "2025-01-07T10:04:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2e/a072cf196edfeda3310c9a5ade0a0fdd785e6154b3ce24fc738c818da2a7/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee8b10afedcb75f71091bcc197c526a6ebf5c58bbbadb34fdeee6160f55f619f", size = 3064995, upload-time = "2025-01-07T10:04:18.663Z" }, + { url = "https://files.pythonhosted.org/packages/29/63/b560d39651a56603d64f1a0212d0472a44cbd965db2fa62b99d99cb981bf/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc6bd19e1cc177c66bdef15ef8636ad3bde79d5a4f608c158021153b4573509d", size = 3400839, upload-time = "2025-01-07T10:04:26.122Z" }, { url = "https://files.pythonhosted.org/packages/d6/d8/f87ea6f42456254b48915970ed98e993110521e9263472840174d32c880d/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdca9bfb89e6f8f281890cc61a8aff2d3cecaff7e1a4d275574d96ca70098557", size = 3552664, upload-time = "2025-01-07T10:04:40.123Z" }, { url = "https://files.pythonhosted.org/packages/d6/56/1267c39b65fc8f4e2113b36297320f102718bf5799b544a6cbe22013aa1d/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:89a23f58b7b7effbc047b8ca286f131b17728c99a9f972723323003ffd1bb916", size = 4073732, upload-time = "2025-01-07T10:04:55.624Z" }, + { url = "https://files.pythonhosted.org/packages/82/1a/9c748befbe3decf7cb415e34f8a0c3789a0a9c55910dea73d581e48c0ce5/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:dc7fff1345980d6c0ebb92c811d24afa4b98b3e07ed070c8e38cc91fd80478c5", size = 3390096, upload-time = "2025-01-07T10:04:59.98Z" }, { url = "https://files.pythonhosted.org/packages/e7/6e/e597b04f753f1b09e6893075d53a82a30c13855cbaa791402695b01e369f/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d2fde99d502093ade3ab1b53f80da18480e9902aa960dab7f74fb1b9e5bc5746", size = 3695243, upload-time = "2025-01-07T10:05:11.411Z" }, ] @@ -2411,16 +2558,25 @@ sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b5 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/84/34/6e8d412e60ff06b186040e77da5f83bc158e9735759fcae65b37d681f28b/jiter-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2", size = 371028, upload-time = "2025-05-18T19:03:30.292Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d9/9ee86173aae4576c35a2f50ae930d2ccb4c4c236f6cb9353267aa1d626b7/jiter-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61", size = 491083, upload-time = "2025-05-18T19:03:31.654Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2c/f955de55e74771493ac9e188b0f731524c6a995dffdcb8c255b89c6fb74b/jiter-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db", size = 388821, upload-time = "2025-05-18T19:03:33.184Z" }, { 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/75/d0/bb6b4f209a77190ce10ea8d7e50bf3725fc16d3372d0a9f11985a2b23eff/jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026", size = 369670, upload-time = "2025-05-18T19:03:49.334Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f5/a61787da9b8847a601e6827fbc42ecb12be2c925ced3252c8ffcb56afcaf/jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c", size = 489057, upload-time = "2025-05-18T19:03:50.66Z" }, + { url = "https://files.pythonhosted.org/packages/12/e4/6f906272810a7b21406c760a53aadbe52e99ee070fc5c0cb191e316de30b/jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959", size = 389372, upload-time = "2025-05-18T19:03:51.98Z" }, { url = "https://files.pythonhosted.org/packages/e2/ba/77013b0b8ba904bf3762f11e0129b8928bff7f978a81838dfcc958ad5728/jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a", size = 352038, upload-time = "2025-05-18T19:03:53.703Z" }, { url = "https://files.pythonhosted.org/packages/c0/72/0d6b7e31fc17a8fdce76164884edef0698ba556b8eb0af9546ae1a06b91d/jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea", size = 523557, upload-time = "2025-05-18T19:03:56.386Z" }, { url = "https://files.pythonhosted.org/packages/2f/09/bc1661fbbcbeb6244bd2904ff3a06f340aa77a2b94e5a7373fd165960ea3/jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b", size = 514202, upload-time = "2025-05-18T19:03:57.675Z" }, { url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947, upload-time = "2025-05-18T19:04:03.347Z" }, { url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618, upload-time = "2025-05-18T19:04:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829, upload-time = "2025-05-18T19:04:06.912Z" }, + { url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034, upload-time = "2025-05-18T19:04:08.222Z" }, + { url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529, upload-time = "2025-05-18T19:04:09.566Z" }, { url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671, upload-time = "2025-05-18T19:04:10.98Z" }, { url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989, upload-time = "2025-05-18T19:04:14.261Z" }, { url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495, upload-time = "2025-05-18T19:04:15.603Z" }, @@ -2505,23 +2661,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" }, ] @@ -3188,21 +3360,37 @@ wheels = [ { 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/b3/cb/bf035dedbdf7fab49411aa52e4236f3445e98d38647d85419e6c0d2806b9/lxml-6.1.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:23a5dc68e08ed13331d61815c08f260f46b4a60fdd1640bbeb82cf89a9d90289", size = 4659643, upload-time = "2026-04-18T04:32:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/22be31f33727a5e4c7b01b0a874503026e50329b259d3587e0b923cf964b/lxml-6.1.0-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f15401d8d3dbf239e23c818afc10c7207f7b95f9a307e092122b6f86dd43209a", size = 5265963, upload-time = "2026-04-18T04:32:41.881Z" }, { 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/d3/c3/3f034fec1594c331a6dbf9491238fdcc9d66f68cc529e109ec75b97197e1/lxml-6.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0d082495c5fcf426e425a6e28daaba1fcb6d8f854a4ff01effb1f1f381203eb9", size = 4712703, upload-time = "2026-04-18T04:32:47.16Z" }, + { url = "https://files.pythonhosted.org/packages/12/16/0b83fccc158218aca75a7aa33e97441df737950734246b9fffa39301603d/lxml-6.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e3c4f84b24a1fcba435157d111c4b755099c6ff00a3daee1ad281817de75ed11", size = 5252745, upload-time = "2026-04-18T04:32:50.427Z" }, { 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/e3/38/a0db9be8f38ad6043ab9429487c128dd1d30f07956ef43040402f8da49e8/lxml-6.1.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485", size = 5630827, upload-time = "2026-04-18T04:33:04.036Z" }, { url = "https://files.pythonhosted.org/packages/31/ba/3c13d3fc24b7cacf675f808a3a1baabf43a30d0cd24c98f94548e9aa58eb/lxml-6.1.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814", size = 5240445, upload-time = "2026-04-18T04:33:06.87Z" }, + { url = "https://files.pythonhosted.org/packages/7e/01/1da87c7b587c38d0cbe77a01aae3b9c1c49ed47d76918ef3db8fc151b1ca/lxml-6.1.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad", size = 4694949, upload-time = "2026-04-18T04:33:11.628Z" }, + { url = "https://files.pythonhosted.org/packages/a1/88/7db0fe66d5aaf128443ee1623dec3db1576f3e4c17751ec0ef5866468590/lxml-6.1.0-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54", size = 5243901, upload-time = "2026-04-18T04:33:13.95Z" }, { url = "https://files.pythonhosted.org/packages/00/a8/1346726af7d1f6fca1f11223ba34001462b0a3660416986d37641708d57c/lxml-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d", size = 5048054, upload-time = "2026-04-18T04:33:16.965Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b7/85057012f035d1a0c87e02f8c723ca3c3e6e0728bcf4cb62080b21b1c1e3/lxml-6.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69", size = 4777324, upload-time = "2026-04-18T04:33:19.832Z" }, + { url = "https://files.pythonhosted.org/packages/75/6c/ad2f94a91073ef570f33718040e8e160d5fb93331cf1ab3ca1323f939e2d/lxml-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d", size = 5645702, upload-time = "2026-04-18T04:33:22.436Z" }, + { url = "https://files.pythonhosted.org/packages/3b/89/0bb6c0bd549c19004c60eea9dc554dd78fd647b72314ef25d460e0d208c6/lxml-6.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5", size = 5232901, upload-time = "2026-04-18T04:33:26.21Z" }, { url = "https://files.pythonhosted.org/packages/a1/d9/d609a11fb567da9399f525193e2b49847b5a409cdebe737f06a8b7126bdc/lxml-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d", size = 5261333, upload-time = "2026-04-18T04:33:28.984Z" }, { url = "https://files.pythonhosted.org/packages/08/03/69347590f1cf4a6d5a4944bb6099e6d37f334784f16062234e1f892fdb1d/lxml-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45", size = 8559689, upload-time = "2026-04-18T04:31:57.785Z" }, { url = "https://files.pythonhosted.org/packages/f5/54/92ad98a94ac318dc4f97aaac22ff8d1b94212b2ae8af5b6e9b354bf825f7/lxml-6.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2", size = 4923489, upload-time = "2026-04-18T04:33:31.401Z" }, { url = "https://files.pythonhosted.org/packages/15/3b/a20aecfab42bdf4f9b390590d345857ad3ffd7c51988d1c89c53a0c73faf/lxml-6.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491", size = 5082162, upload-time = "2026-04-18T04:33:34.262Z" }, { url = "https://files.pythonhosted.org/packages/45/26/2cdb3d281ac1bd175603e290cbe4bad6eff127c0f8de90bafd6f8548f0fd/lxml-6.1.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc", size = 4993247, upload-time = "2026-04-18T04:33:36.674Z" }, + { url = "https://files.pythonhosted.org/packages/f6/05/d735aef963740022a08185c84821f689fc903acb3d50326e6b1e9886cc22/lxml-6.1.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e", size = 5613042, upload-time = "2026-04-18T04:33:39.205Z" }, { url = "https://files.pythonhosted.org/packages/ee/b8/ead7c10efff731738c72e59ed6eb5791854879fbed7ae98781a12006263a/lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2", size = 5228304, upload-time = "2026-04-18T04:33:41.647Z" }, + { url = "https://files.pythonhosted.org/packages/89/54/40d9403d7c2775fa7301d3ddd3464689bfe9ba71acc17dfff777071b4fdc/lxml-6.1.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe", size = 4700209, upload-time = "2026-04-18T04:33:47.552Z" }, + { url = "https://files.pythonhosted.org/packages/85/b2/bbdcc2cf45dfc7dfffef4fd97e5c47b15919b6a365247d95d6f684ef5e82/lxml-6.1.0-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88", size = 5232365, upload-time = "2026-04-18T04:33:50.249Z" }, { url = "https://files.pythonhosted.org/packages/48/5a/b06875665e53aaba7127611a7bed3b7b9658e20b22bc2dd217a0b7ab0091/lxml-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181", size = 5043654, upload-time = "2026-04-18T04:33:52.71Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9c/e71a069d09641c1a7abeb30e693f828c7c90a41cbe3d650b2d734d876f85/lxml-6.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24", size = 4769326, upload-time = "2026-04-18T04:33:55.244Z" }, + { url = "https://files.pythonhosted.org/packages/cc/06/7a9cd84b3d4ed79adf35f874750abb697dec0b4a81a836037b36e47c091a/lxml-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e", size = 5635879, upload-time = "2026-04-18T04:33:58.509Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f0/9d57916befc1e54c451712c7ee48e9e74e80ae4d03bdce49914e0aee42cd/lxml-6.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495", size = 5224048, upload-time = "2026-04-18T04:34:00.943Z" }, { url = "https://files.pythonhosted.org/packages/99/75/90c4eefda0c08c92221fe0753db2d6699a4c628f76ff4465ec20dea84cc1/lxml-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33", size = 5250241, upload-time = "2026-04-18T04:34:03.365Z" }, { 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" }, @@ -3272,22 +3460,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" }, ] @@ -3524,13 +3720,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" }, @@ -3539,7 +3743,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" }, ] @@ -3611,26 +3819,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" }, ] @@ -6338,6 +6570,7 @@ 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-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-platform-sdk", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "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-customization-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')" }, @@ -6358,6 +6591,7 @@ requires-dist = [ { name = "aiofiles", specifier = ">=24.1.0" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "jsonschema", specifier = ">=4.23.0" }, + { name = "nemo-platform-plugin", editable = "packages/nemo_platform_plugin" }, { name = "nemo-platform-sdk", editable = "sdk/python/nemo-platform" }, { name = "nmp-common", editable = "packages/nmp_common" }, { name = "nmp-customization-common", editable = "packages/nmp_customization_common" }, @@ -7223,6 +7457,7 @@ 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-plugin", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "nemo-platform-sdk", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "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-customization-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')" }, @@ -7250,6 +7485,7 @@ dev = [ requires-dist = [ { name = "httpx", specifier = ">=0.27.0" }, { name = "mlflow-skinny", marker = "extra == 'integrations'" }, + { name = "nemo-platform-plugin", editable = "packages/nemo_platform_plugin" }, { name = "nemo-platform-sdk", editable = "sdk/python/nemo-platform" }, { name = "nmp-common", editable = "packages/nmp_common" }, { name = "nmp-customization-common", editable = "packages/nmp_customization_common" }, @@ -8058,20 +8294,32 @@ 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/08/4a/2025a60ff3f5c8522060cda46612d9b1efa653de66ed2908591d8d82f22d/orjson-3.11.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eda5b8b6be91d3f26efb7dc6e5e68ee805bc5617f65a328587b35255f138bf4", size = 130483, upload-time = "2026-03-31T16:14:54.605Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f2/a8238e7734de7cb589fed319857a8025d509c89dc52fdcc88f39c6d03d5a/orjson-3.11.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d8b5231de76c528a46b57010bbd83fb51e056aa0220a372fd5065e978406f1c", size = 146819, upload-time = "2026-03-31T16:14:57.548Z" }, + { url = "https://files.pythonhosted.org/packages/db/10/dbf1e2a3cafea673b1b4350e371877b759060d6018a998643b7040e5de48/orjson-3.11.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58a4a208a6fbfdb7a7327b8f201c6014f189f721fd55d047cafc4157af1bc62a", size = 132846, upload-time = "2026-03-31T16:14:58.91Z" }, { 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/5c/cc/2f78ea241d52b717d2efc38878615fe80425bf2beb6e68c984dde257a766/orjson-3.11.8-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6", size = 423845, upload-time = "2026-03-31T16:15:03.703Z" }, { 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/52/4b/5500f76f0eece84226e0689cb48dcde081104c2fa6e2483d17ca13685ffb/orjson-3.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813", size = 130368, upload-time = "2026-03-31T16:15:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/56/7c/ba7cb871cba1bcd5cd02ee34f98d894c6cea96353ad87466e5aef2429c60/orjson-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546", size = 146877, upload-time = "2026-03-31T16:15:19.833Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/eb9c25fc1386696c6a342cd361c306452c75e0b55e86ad602dd4827a7fd7/orjson-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506", size = 132837, upload-time = "2026-03-31T16:15:21.282Z" }, { url = "https://files.pythonhosted.org/packages/37/87/5ddeb7fc1fbd9004aeccab08426f34c81a5b4c25c7061281862b015fce2b/orjson-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f", size = 133624, upload-time = "2026-03-31T16:15:22.641Z" }, { url = "https://files.pythonhosted.org/packages/22/09/90048793db94ee4b2fcec4ac8e5ddb077367637d6650be896b3494b79bb7/orjson-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e", size = 141904, upload-time = "2026-03-31T16:15:24.435Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cf/eb284847487821a5d415e54149a6449ba9bfc5872ce63ab7be41b8ec401c/orjson-3.11.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb", size = 423742, upload-time = "2026-03-31T16:15:26.155Z" }, { url = "https://files.pythonhosted.org/packages/b3/6d/37c2589ba864e582ffe7611643314785c6afb1f83c701654ef05daa8fcc7/orjson-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25", size = 136485, upload-time = "2026-03-31T16:15:29.749Z" }, { url = "https://files.pythonhosted.org/packages/66/7f/95fba509bb2305fab0073558f1e8c3a2ec4b2afe58ed9fcb7d3b8beafe94/orjson-3.11.8-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc", size = 229180, upload-time = "2026-03-31T16:15:36.426Z" }, { url = "https://files.pythonhosted.org/packages/f6/9d/b237215c743ca073697d759b5503abd2cb8a0d7b9c9e21f524bcf176ab66/orjson-3.11.8-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559", size = 128754, upload-time = "2026-03-31T16:15:38.049Z" }, { url = "https://files.pythonhosted.org/packages/42/3d/27d65b6d11e63f133781425f132807aef793ed25075fec686fc8e46dd528/orjson-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623", size = 131877, upload-time = "2026-03-31T16:15:39.484Z" }, + { url = "https://files.pythonhosted.org/packages/dd/cc/faee30cd8f00421999e40ef0eba7332e3a625ce91a58200a2f52c7fef235/orjson-3.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c", size = 130361, upload-time = "2026-03-31T16:15:41.274Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7c/ca3a3525aa32ff636ebb1778e77e3587b016ab2edb1b618b36ba96f8f2c0/orjson-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55", size = 146862, upload-time = "2026-03-31T16:15:44.341Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0c/18a9d7f18b5edd37344d1fd5be17e94dc652c67826ab749c6e5948a78112/orjson-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137", size = 132847, upload-time = "2026-03-31T16:15:46.368Z" }, { url = "https://files.pythonhosted.org/packages/23/91/7e722f352ad67ca573cee44de2a58fb810d0f4eb4e33276c6a557979fd8a/orjson-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53", size = 133637, upload-time = "2026-03-31T16:15:48.123Z" }, { url = "https://files.pythonhosted.org/packages/af/04/32845ce13ac5bd1046ddb02ac9432ba856cc35f6d74dde95864fe0ad5523/orjson-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e", size = 141906, upload-time = "2026-03-31T16:15:49.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/5e/c551387ddf2d7106d9039369862245c85738b828844d13b99ccb8d61fd06/orjson-3.11.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6", size = 423722, upload-time = "2026-03-31T16:15:51.176Z" }, { url = "https://files.pythonhosted.org/packages/18/6d/0dce10b9f6643fdc59d99333871a38fa5a769d8e2fc34a18e5d2bfdee900/orjson-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b", size = 136460, upload-time = "2026-03-31T16:15:54.431Z" }, ] @@ -8083,18 +8331,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" }, ] @@ -8417,26 +8671,46 @@ 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/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, { 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/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, { 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/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] @@ -8449,6 +8723,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" }, ] @@ -8477,18 +8752,30 @@ sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba42464 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/ce/24/e18339c407a13c72b336e0d9013fbbbde77b6fd13e853979019a1269519c/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", size = 4468007, upload-time = "2025-10-10T11:11:24.831Z" }, { 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/0d/61/4aa89eeb6d751f05178a13da95516c036e27468c5d4d2509bb1e15341c81/psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", size = 3981881, upload-time = "2025-10-30T02:55:07.332Z" }, { 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/84/74/4defcac9d002bca5709951b975173c8c2fa968e1a95dc713f61b3a8d3b6a/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", size = 3296039, upload-time = "2025-10-10T11:11:40.432Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c2/782a3c64403d8ce35b5c50e1b684412cf94f171dc18111be8c976abd2de1/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", size = 3043477, upload-time = "2025-10-30T02:55:11.182Z" }, { 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/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, + { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, + { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" }, { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" }, { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, ] @@ -8543,21 +8830,27 @@ sdist = { url = "https://files.pythonhosted.org/packages/8e/63/4fbc14810c32d2a88 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/f1/f5/b79249c787c59b9ce2c5d007c0a0dc0fc1ecccfcf98a546c131cca55899e/py_rust_stemmers-0.1.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07b3b8582313ef8a7f544acf2c887f27c3dd48c5ddca028fa0f498de7380e24f", size = 315238, upload-time = "2025-02-19T13:55:13.39Z" }, { 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/a0/ee/ed09ce6fde1eefe50aa13a8a8533aa7ebe3cc096d1a43155cc71ba28d298/py_rust_stemmers-0.1.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0ae0540453843bc36937abb54fdbc0d5d60b51ef47aa9667afd05af9248e09eb", size = 575581, upload-time = "2025-02-19T13:55:19.669Z" }, { 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/a9/08/62e97652d359b75335486f4da134a6f1c281f38bd3169ed6ecfb276448c3/py_rust_stemmers-0.1.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a979c3f4ff7ad94a0d4cf566ca7bfecebb59e66488cc158e64485cf0c9a7879f", size = 315237, upload-time = "2025-02-19T13:55:28.116Z" }, { url = "https://files.pythonhosted.org/packages/1c/b9/fc0278432f288d2be4ee4d5cc80fd8013d604506b9b0503e8b8cae4ba1c3/py_rust_stemmers-0.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c3593d895453fa06bf70a7b76d6f00d06def0f91fc253fe4260920650c5e078", size = 324419, upload-time = "2025-02-19T13:55:29.211Z" }, { url = "https://files.pythonhosted.org/packages/6b/5b/74e96eaf622fe07e83c5c389d101540e305e25f76a6d0d6fb3d9e0506db8/py_rust_stemmers-0.1.5-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:96ccc7fd042ffc3f7f082f2223bb7082ed1423aa6b43d5d89ab23e321936c045", size = 324792, upload-time = "2025-02-19T13:55:30.948Z" }, { url = "https://files.pythonhosted.org/packages/4f/f7/b76816d7d67166e9313915ad486c21d9e7da0ac02703e14375bb1cb64b5a/py_rust_stemmers-0.1.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef18cfced2c9c676e0d7d172ba61c3fab2aa6969db64cc8f5ca33a7759efbefe", size = 488014, upload-time = "2025-02-19T13:55:32.066Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ed/7d9bed02f78d85527501f86a867cd5002d97deb791b9a6b1b45b00100010/py_rust_stemmers-0.1.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:541d4b5aa911381e3d37ec483abb6a2cf2351b4f16d5e8d77f9aa2722956662a", size = 575582, upload-time = "2025-02-19T13:55:34.005Z" }, { url = "https://files.pythonhosted.org/packages/93/40/eafd1b33688e8e8ae946d1ef25c4dc93f5b685bd104b9c5573405d7e1d30/py_rust_stemmers-0.1.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ffd946a36e9ac17ca96821963663012e04bc0ee94d21e8b5ae034721070b436c", size = 493267, upload-time = "2025-02-19T13:55:35.294Z" }, { url = "https://files.pythonhosted.org/packages/ed/be/0465dcb3a709ee243d464e89231e3da580017f34279d6304de291d65ccb0/py_rust_stemmers-0.1.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4e308fc7687901f0c73603203869908f3156fa9c17c4ba010a7fcc98a7a1c5f2", size = 272019, upload-time = "2025-02-19T13:55:39.183Z" }, { url = "https://files.pythonhosted.org/packages/ab/b6/76ca5b1f30cba36835938b5d9abee0c130c81833d51b9006264afdf8df3c/py_rust_stemmers-0.1.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9efc4da5e734bdd00612e7506de3d0c9b7abc4b89d192742a0569d0d1fe749", size = 310545, upload-time = "2025-02-19T13:55:40.339Z" }, + { url = "https://files.pythonhosted.org/packages/56/8f/5be87618cea2fe2e70e74115a20724802bfd06f11c7c43514b8288eb6514/py_rust_stemmers-0.1.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc2cc8d2b36bc05b8b06506199ac63d437360ae38caefd98cd19e479d35afd42", size = 315236, upload-time = "2025-02-19T13:55:41.55Z" }, { url = "https://files.pythonhosted.org/packages/00/02/ea86a316aee0f0a9d1449ad4dbffff38f4cf0a9a31045168ae8b95d8bdf8/py_rust_stemmers-0.1.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a231dc6f0b2a5f12a080dfc7abd9e6a4ea0909290b10fd0a4620e5a0f52c3d17", size = 324419, upload-time = "2025-02-19T13:55:42.693Z" }, { url = "https://files.pythonhosted.org/packages/2a/fd/1612c22545dcc0abe2f30fc08f30a2332f2224dd536fa1508444a9ca0e39/py_rust_stemmers-0.1.5-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5845709d48afc8b29e248f42f92431155a3d8df9ba30418301c49c6072b181b0", size = 324794, upload-time = "2025-02-19T13:55:43.896Z" }, { url = "https://files.pythonhosted.org/packages/66/18/8a547584d7edac9e7ac9c7bdc53228d6f751c0f70a317093a77c386c8ddc/py_rust_stemmers-0.1.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e48bfd5e3ce9d223bfb9e634dc1425cf93ee57eef6f56aa9a7120ada3990d4be", size = 488014, upload-time = "2025-02-19T13:55:45.088Z" }, + { url = "https://files.pythonhosted.org/packages/3b/87/4619c395b325e26048a6e28a365afed754614788ba1f49b2eefb07621a03/py_rust_stemmers-0.1.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:35d32f6e7bdf6fd90e981765e32293a8be74def807147dea9fdc1f65d6ce382f", size = 575582, upload-time = "2025-02-19T13:55:46.436Z" }, { url = "https://files.pythonhosted.org/packages/98/6e/214f1a889142b7df6d716e7f3fea6c41e87bd6c29046aa57e175d452b104/py_rust_stemmers-0.1.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:191ea8bf922c984631ffa20bf02ef0ad7eec0465baeaed3852779e8f97c7e7a3", size = 493269, upload-time = "2025-02-19T13:55:49.057Z" }, ] @@ -8659,18 +8952,30 @@ sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd2 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/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, { 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/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, { 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/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, { url = "https://files.pythonhosted.org/packages/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" }, @@ -8681,6 +8986,7 @@ wheels = [ { 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/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, { 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" }, ] @@ -9057,16 +9363,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" }, @@ -9080,11 +9389,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" }, @@ -9172,26 +9483,50 @@ 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" }, ] @@ -9328,23 +9663,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" }, ] @@ -9377,27 +9728,47 @@ sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b3 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/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, { url = "https://files.pythonhosted.org/packages/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/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, { url = "https://files.pythonhosted.org/packages/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" }, ] @@ -9417,10 +9788,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" }, ] @@ -9461,8 +9838,13 @@ sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd2 wheels = [ { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" }, { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" }, + { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" }, { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" }, { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" }, + { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" }, { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" }, ] @@ -10154,8 +10536,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" }, ] @@ -10392,10 +10778,15 @@ 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" }, ] @@ -10732,11 +11123,16 @@ sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062c wheels = [ { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, + { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, { 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/ee/6b/cf342ba8a898f1de024be0243fac67c025cad530c79ea7f89c4ce718891a/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da2234387b45fde40b0fedfee64a0ba591caeea9c48c7698ab6e2d85c7991533", size = 343711, upload-time = "2026-02-20T22:50:43.965Z" }, + { url = "https://files.pythonhosted.org/packages/b3/20/049418d094d396dfa6606b30af925cc68a6670c3b9103b23e6990f84b589/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50fffc2827348c1e48972eed3d1c698959e63f9d030aa5dd82ba451113158a62", size = 476731, upload-time = "2026-02-20T22:50:30.589Z" }, { 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" }, ] @@ -10862,6 +11258,10 @@ 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" }, ] @@ -10876,21 +11276,33 @@ sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d 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/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, { 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/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, @@ -11056,23 +11468,39 @@ sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6 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/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, { 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/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, { 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/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, + { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, + { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, { 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" }, @@ -11110,26 +11538,58 @@ 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/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, { 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/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, { 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/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, + { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, { 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/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, ] @@ -11151,23 +11611,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 f69c05c28e156411ec84f27ee33cd3dc003f0c07 Mon Sep 17 00:00:00 2001 From: Sam Oluwalana Date: Fri, 12 Jun 2026 16:38:07 -0600 Subject: [PATCH 4/4] Fix lora sidecar image Signed-off-by: Sam Oluwalana --- .../skills/nemo-customizer/SKILL.md | 23 +++++- .../references/troubleshooting.md | 74 ++++++++++++++++++- .../tests/integration/conftest.py | 2 +- .../controllers/backends/adapter_sidecar.py | 24 ++++++ .../backends/docker/creation_reconciler.py | 11 ++- .../k8s_nim_operator/nimservice_compiler.py | 14 ++-- .../core/models/tests/integration/conftest.py | 2 +- .../integration/test_models_controller.py | 2 +- .../unit/controllers/test_docker_backend.py | 2 + .../controllers/test_nimservice_compiler.py | 12 ++- 10 files changed, 140 insertions(+), 26 deletions(-) create mode 100644 services/core/models/src/nmp/core/models/controllers/backends/adapter_sidecar.py 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 9d7943e63e..b907063734 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,17 @@ When auth **is** enabled on the connected platform, API calls need credentials: Use `admin@example.com` unless the user specifies another email. Run `nemo auth status` after login to confirm. +## HuggingFace token (gated models) + +Gated HF repos (Llama, Gemma, Mistral instruct, …) need a platform secret (convention: **`hf-token`**) referenced as **`token_secret`** on the **model fileset** — not in job JSON (unlike W&B's `api_key_secret`). The Files service does **not** read your local `~/.cache/huggingface` or shell `HF_TOKEN`. + +| Model access | Action | +|--------------|--------| +| Public (e.g. `Qwen/Qwen3-1.7B`) | Skip; omit `token_secret` on the fileset | +| Gated / private HF repo | Before model fileset creation or job submit: `nemo secrets list --workspace default` and confirm `hf-token` exists. If missing, **ask the user** for their HF token and **stop** — do not create the fileset or submit until wired up. | + +Full create/update commands, fileset `token_secret`, license acceptance, and download-phase errors: `references/troubleshooting.md` § **Gated HuggingFace models**. + ## Plugin pick 1. Run `nemo jobs list-execution-profiles -f json` (login first only if auth is enabled — see **Authentication**; see `references/troubleshooting.md` for parsing). @@ -129,6 +140,7 @@ Training never runs inside the `nemo` CLI process. After `submit`, the platform' - **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 `docker/unsloth/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**. +- **Gated HuggingFace models** (Llama, Gemma, …) — confirm `hf-token` + fileset `token_secret` before submit; download fails with `Failed to access upstream storage` / 502 when missing. See **HuggingFace token (gated models)** and `references/troubleshooting.md` § **Gated HuggingFace models**. ## Workflow @@ -142,7 +154,8 @@ Common steps then **branch by plugin pick**: - [ ] 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) +- [ ] Gated HF base model? → confirm `hf-token` exists; ask user and stop if missing (see HuggingFace token + troubleshooting § Gated HuggingFace models) +- [ ] Create HF weights fileset + model entity if missing (--exist-ok; gated repos need `token_secret` on fileset — see troubleshooting) # automodel branch (submit → Docker GPU job) - [ ] Write /tmp/job.json (batch sizing for ≥48 GB GPU; else Defaults table) @@ -180,7 +193,7 @@ nemo files upload /tmp/train-data/train.jsonl "$DATASET" --workspace default --r nemo files list "$DATASET" --workspace default ``` -**2. Model** — skip if entity exists (`nemo models list --workspace default`). +**2. Model** — skip if entity exists (`nemo models list --workspace default`). For **gated** HF repos, complete **HuggingFace token (gated models)** first — see `references/troubleshooting.md` § **Gated HuggingFace models** for `token_secret` on the fileset. ```bash WEIGHTS= # e.g. qwen3-1.7b @@ -194,6 +207,8 @@ nemo models create "$MODEL_ENTITY" --workspace default --exist-ok \ --input-data '{"name":"'"$MODEL_ENTITY"'","fileset":"default/'"$WEIGHTS"'","custom_fields":{"hf_model_id":"'"$HF_REPO"'"}}' ``` +For gated repos, add `"token_secret":"hf-token"` to the `--storage` JSON (after creating the secret). See troubleshooting § **Gated HuggingFace models**. + **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 @@ -588,6 +603,7 @@ Use the user's platform URL in `NEMO_BASE_URL` when they overrode it; omit the e | 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. | +| Download fails / `Failed to access upstream storage` / 502 on gated HF model | `references/troubleshooting.md` § **Gated HuggingFace models** — create/update `hf-token`, add `token_secret` to fileset, confirm HF license, re-submit. | | W&B not syncing / no `[launcher]` secret lines / `WandbCallback requires wandb` / wandb 401 | `references/troubleshooting.md` § **W&B / integrations not working** (jobs-launcher build, secret update, unsloth image). Setup: `references/integrations-setup.md`. | For other terminal errors, keep the same header template; put remediation detail in **Notes** or a short **Next steps** section as appropriate. @@ -601,7 +617,7 @@ For other terminal errors, keep the same header template; put remediation detail | 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, submit failure, container images, missing image on remote platform, CLI, connection errors | `references/troubleshooting.md` (§ **Parsing CLI JSON** for `2>&1` / `json.load`) | +| Backend choice, execution profiles, submit failure, container images, missing image on remote platform, gated HF auth / download 502, CLI, connection errors | `references/troubleshooting.md` (§ **Parsing CLI JSON** for `2>&1` / `json.load`; § **Gated HuggingFace models** for `hf-token`) | | Live JSON schema | `uv run nemo customization automodel explain` / `uv run nemo customization unsloth explain` | | Job JSON fixture (automodel, minimal) | `plugins/nemo-automodel/tests/fixtures/qwen3_0.6b_sft_lora.json` (ignore `max_steps` for real runs) | | Job JSON fixture (unsloth, minimal) | `plugins/nemo-unsloth/tests/fixtures/minimal_unsloth_sft.json` (ignore `max_steps` for real runs) | @@ -609,5 +625,6 @@ For other terminal errors, keep the same header template; put remediation detail | Automodel compile-path contract configs | `services/automodel/tests/contract/input_configs/` → YAML in `output_configs/` (legacy `TrainingStepConfig` shape, not submit JSON) | | W&B / MLflow field reference | `references/hyperparameters.md` § **Integrations (automodel + unsloth)** | | W&B secret + MLflow local server + jobs-launcher | `references/integrations-setup.md` | +| Gated HF model auth (`hf-token`, fileset `token_secret`) | `references/troubleshooting.md` § **Gated HuggingFace models** | Related: `plugins/nemo-automodel/README.md`, `plugins/nemo-unsloth/README.md`, `plugins/nemo-customizer/docs/CUSTOMIZATION.md`, skills **`nemo-files`**, **`nemo-status`**, **`nemo-secrets`**. 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 098a30d7ee..b0f799f91f 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,6 @@ # Troubleshooting -Read this file when submit fails, jobs fail on images, the platform is unreachable, W&B/MLflow integrations fail, or the user asks for Unsloth. +Read this file when submit fails, jobs fail on images, the platform is unreachable, W&B/MLflow integrations fail, gated HuggingFace model download fails, or the user asks for Unsloth. Resolve the CLI first per **Pre-flight — CLI resolution** in `SKILL.md` (`nemo` on `PATH`, else `uv run nemo`, else route to **nemo-setup**). Example commands below use `nemo …`. @@ -104,6 +104,75 @@ Same rule for `nemo jobs list-execution-profiles -f json`: parse stdout only; us - **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`). +## Gated HuggingFace models + +Gated or private HuggingFace repos (e.g. Llama, Gemma) require a **platform secret** and **`token_secret`** on the model fileset. The Files service does **not** use your local `~/.cache/huggingface` or shell `HF_TOKEN`. Unlike W&B, the HF token is **not** set in job JSON — it is wired on the **model fileset** storage config. + +| Symptom / log excerpt | Likely cause | Fix | +|-----------------------|--------------|-----| +| Job fails in **download** step; `Failed to access upstream storage`; `InternalServerError` 502; `Verify that the referenced credentials are valid` | Missing/stale `hf-token` secret, or fileset created without `token_secret` | Steps below — then re-submit | +| Secret exists but download still fails | User has not **accepted the model license** on huggingface.co for that repo | Accept license with the same HF account as the token, then re-submit | +| Public model (e.g. `Qwen/Qwen3-1.7B`) | No secret needed | Omit `token_secret` on the fileset | + +**Convention:** secret name `hf-token` in workspace `default`. Any valid secret name works if referenced consistently in `token_secret`. + +### 1. Check whether the secret exists + +```bash +nemo secrets list --workspace default +``` + +If `hf-token` is missing, **ask the user** for a HuggingFace token with **Read** access (https://huggingface.co/settings/tokens). They must also accept the model license on the model's HF page. + +### 2. Create or update the secret + +```bash +HF_SECRET=hf-token +printf '%s' "$HF_TOKEN" | nemo secrets create "$HF_SECRET" \ + --workspace default \ + --from-file - + +# Or update if it already exists but is stale: +printf '%s' "$HF_TOKEN" | nemo secrets update "$HF_SECRET" \ + --workspace default \ + --from-file - +``` + +### 3. Create or update the model fileset with `token_secret` + +**New fileset (gated repo):** + +```bash +WEIGHTS= +HF_REPO= # e.g. google/gemma-2-2b-it +HF_SECRET=hf-token + +nemo files filesets create "$WEIGHTS" --workspace default --purpose model --exist-ok \ + --storage '{"type":"huggingface","repo_id":"'"$HF_REPO"'","repo_type":"model","revision":"main","token_secret":"'"$HF_SECRET"'"}' +``` + +**Existing fileset missing `token_secret`:** + +```bash +nemo files filesets update "$WEIGHTS" --workspace default \ + --input-data '{"storage":{"type":"huggingface","repo_id":"'"$HF_REPO"'","repo_type":"model","revision":"main","token_secret":"'"$HF_SECRET"'"}}' +``` + +Then create or reuse the model entity pointing at `default/`. + +**Public repos** — omit `token_secret`: + +```bash +nemo files filesets create "$WEIGHTS" --workspace default --purpose model --exist-ok \ + --storage '{"type":"huggingface","repo_id":"'"$HF_REPO"'","repo_type":"model","revision":"main"}' +``` + +### 4. Re-submit + +After secret + fileset are wired, re-submit the same job JSON (use a fresh `output.name` if a prior partial run already registered an adapter). + +**Note:** `files-hf-token` in platform config is **internal** service-to-service auth between Models and Files — it is **not** your HuggingFace Hub token. Do not confuse the two. + ## 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. @@ -230,7 +299,8 @@ Shared: |--------|---------| | 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"}'` | +| Create HF weights fileset (public) | `nemo files filesets create --workspace default --purpose model --exist-ok --storage '{"type":"huggingface","repo_id":"","repo_type":"model","revision":"main"}'` | +| Create HF weights fileset (gated) | Same as above plus `"token_secret":"hf-token"` — see § **Gated HuggingFace models** | | 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 ''` | diff --git a/services/core/inference-gateway/tests/integration/conftest.py b/services/core/inference-gateway/tests/integration/conftest.py index cf79a13bc4..0375476a63 100644 --- a/services/core/inference-gateway/tests/integration/conftest.py +++ b/services/core/inference-gateway/tests/integration/conftest.py @@ -286,7 +286,7 @@ def controller_with_docker_and_igw( from nemo_platform_plugin.jobs.image import get_qualified_image as real_get_qualified_image def patched_get_qualified_image(name: str, tag=None, registry=None): - if name in ["nmp-core", "nmp-api"]: + if name in ["nmp-automodel-tasks"]: return mock_sidecar_image return real_get_qualified_image(name, tag=tag, registry=registry) diff --git a/services/core/models/src/nmp/core/models/controllers/backends/adapter_sidecar.py b/services/core/models/src/nmp/core/models/controllers/backends/adapter_sidecar.py new file mode 100644 index 0000000000..bf1d7fc0df --- /dev/null +++ b/services/core/models/src/nmp/core/models/controllers/backends/adapter_sidecar.py @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""LoRA adapter sidecar image and launch contract for model deployments.""" + +from nemo_platform_plugin.jobs.image import get_qualified_image + +# Reuse the automodel tasks image (nmp-models + nemo-platform-sdk) instead of nmp-api. +ADAPTER_SIDECAR_IMAGE_NAME = "nmp-automodel-tasks" + +# nmp-automodel-tasks ENTRYPOINT is python; invoke the adapters controller directly. +ADAPTER_SIDECAR_PYTHON = "/opt/venv/bin/python" +ADAPTER_SIDECAR_MODULE = "nmp.core.models.sidecars.adapters.main" + +ADAPTER_SIDECAR_DOCKER_ENTRYPOINT = [ADAPTER_SIDECAR_PYTHON] +ADAPTER_SIDECAR_DOCKER_COMMAND = ["-m", ADAPTER_SIDECAR_MODULE] + +# K8s container.command overrides the image entrypoint — pass the full argv. +ADAPTER_SIDECAR_K8S_COMMAND = [ADAPTER_SIDECAR_PYTHON, "-m", ADAPTER_SIDECAR_MODULE] + + +def get_adapter_sidecar_image() -> str: + """Return the qualified Docker image ref for the LoRA adapter sidecar.""" + return get_qualified_image(ADAPTER_SIDECAR_IMAGE_NAME) diff --git a/services/core/models/src/nmp/core/models/controllers/backends/docker/creation_reconciler.py b/services/core/models/src/nmp/core/models/controllers/backends/docker/creation_reconciler.py index 784338959e..a2e4b50d94 100644 --- a/services/core/models/src/nmp/core/models/controllers/backends/docker/creation_reconciler.py +++ b/services/core/models/src/nmp/core/models/controllers/backends/docker/creation_reconciler.py @@ -27,7 +27,6 @@ from nemo_platform.types.inference.model_deployment import ModelDeployment from nemo_platform.types.inference.model_deployment_config import ModelDeploymentConfig from nemo_platform.types.models.model_entity import ModelEntity -from nemo_platform_plugin.jobs.image import get_qualified_image from nmp.common.config import get_auth_config, get_platform_config from nmp.common.config.base import LOOPBACK_ADDRESSES from nmp.common.docker.gpu_pool import DockerGPUPool, GPUAllocationError @@ -35,6 +34,11 @@ from nmp.core.models.app import ModelWeightsType, get_model_weights_type, is_multi_llm_image, parse_model_name_revision from nmp.core.models.app.constants import MODEL_MANAGED_BY_LABEL, MODEL_MANAGED_BY_MODELS_CONTROLLER from nmp.core.models.app.utils import _get_k8s_safe_name +from nmp.core.models.controllers.backends.adapter_sidecar import ( + ADAPTER_SIDECAR_DOCKER_COMMAND, + ADAPTER_SIDECAR_DOCKER_ENTRYPOINT, + get_adapter_sidecar_image, +) from nmp.core.models.controllers.backends.backends import DeploymentStatusUpdate from nmp.core.models.controllers.backends.common import DeploymentConfigView, deployment_config_view from nmp.core.models.controllers.backends.docker import vllm_compiler @@ -1082,7 +1086,7 @@ async def cleanup_and_error(status_message: str, error_details: dict) -> tuple[D if view.lora_enabled: cfg = get_platform_config() - image = get_qualified_image("nmp-api") + image = get_adapter_sidecar_image() sidecar_envs = cfg.to_shared_envvars() sidecar_envs.update(env_vars) # The adapters sidecar is engine-agnostic: it downloads enabled LoRA @@ -1105,7 +1109,8 @@ async def cleanup_and_error(status_message: str, error_details: dict) -> tuple[D "image": image, "name": f"{container_name}-sidecar", "environment": sidecar_envs, - "command": ["--sidecars", "adapters", "--port", "60830"], + "entrypoint": ADAPTER_SIDECAR_DOCKER_ENTRYPOINT, + "command": ADAPTER_SIDECAR_DOCKER_COMMAND, "detach": True, "volumes": { state.volume_name: {"bind": "/model-store", "mode": "rw"}, diff --git a/services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/nimservice_compiler.py b/services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/nimservice_compiler.py index e8066a07b5..550ecae99b 100644 --- a/services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/nimservice_compiler.py +++ b/services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/nimservice_compiler.py @@ -14,6 +14,10 @@ from nmp.core.models.app import is_multi_llm_image, parse_model_name_revision from nmp.core.models.app.constants import MODEL_MANAGED_BY_LABEL, MODEL_MANAGED_BY_MODELS_CONTROLLER from nmp.core.models.app.utils import _get_k8s_safe_name +from nmp.core.models.controllers.backends.adapter_sidecar import ( + ADAPTER_SIDECAR_IMAGE_NAME, + ADAPTER_SIDECAR_K8S_COMMAND, +) from nmp.core.models.controllers.backends.common import DeploymentConfigView, deployment_config_view from nmp.core.models.controllers.backends.k8s_nim_operator.config import K8sNimOperatorConfig from nmp.core.models.controllers.backends.k8s_nim_operator.types.nimcache import ( @@ -362,18 +366,12 @@ def compile_nimservice( ContainerSpec( name=_get_k8s_safe_name(resource_name, max_length=63, suffix="-lora-sidecar", name_type="label"), image=Image( - repository=f"{platform_config.image_registry}/nmp-api", + repository=f"{platform_config.image_registry}/{ADAPTER_SIDECAR_IMAGE_NAME}", tag=platform_config.image_tag, pullPolicy="IfNotPresent", pullSecrets=image_pull_secrets if image_pull_secrets else None, ), - command=[ - "nemo", - "services", - "run", - "--sidecars", - "adapters", - ], + command=ADAPTER_SIDECAR_K8S_COMMAND, env=sidecar_env_vars, ) ] diff --git a/services/core/models/tests/integration/conftest.py b/services/core/models/tests/integration/conftest.py index 9837d52d1c..bdb4efcf90 100644 --- a/services/core/models/tests/integration/conftest.py +++ b/services/core/models/tests/integration/conftest.py @@ -323,7 +323,7 @@ def docker_backend_config(worker_id: str, mock_sidecar_image: str) -> dict[str, """Configuration for Docker backend in tests. Uses worker_id from pytest-xdist to allocate unique port ranges per worker. - Depends on mock_sidecar_image so the image get_qualified_image('nmp-core') + Depends on mock_sidecar_image so the image get_qualified_image('nmp-automodel-tasks') exists before any test runs. """ start_port, end_port = get_worker_port_range(worker_id) diff --git a/services/core/models/tests/integration/test_models_controller.py b/services/core/models/tests/integration/test_models_controller.py index 1c2294f3b6..2e1d5d6bb5 100644 --- a/services/core/models/tests/integration/test_models_controller.py +++ b/services/core/models/tests/integration/test_models_controller.py @@ -447,7 +447,7 @@ def controller_with_docker( from nemo_platform_plugin.jobs.image import get_qualified_image as real_get_qualified_image def patched_get_qualified_image(name: str, tag=None, registry=None): - if name in ["nmp-core", "nmp-api"]: + if name in ["nmp-automodel-tasks"]: return mock_sidecar_image return real_get_qualified_image(name, tag=tag, registry=registry) diff --git a/services/core/models/tests/unit/controllers/test_docker_backend.py b/services/core/models/tests/unit/controllers/test_docker_backend.py index 3d97774e71..4ddd8b8329 100644 --- a/services/core/models/tests/unit/controllers/test_docker_backend.py +++ b/services/core/models/tests/unit/controllers/test_docker_backend.py @@ -493,6 +493,8 @@ async def test_docker_backend_create_vllm_lora_sidecar(docker_backend, sample_de # vLLM's filesystem resolver requires the adapter's base_model_name_or_path to # equal vLLM's --model value, so the sidecar is told to rewrite it to /model-store. assert sidecar_env["VLLM_LORA_BASE_MODEL_OVERRIDE"] == "/model-store" + assert sidecar_args["entrypoint"] == ["/opt/venv/bin/python"] + assert sidecar_args["command"] == ["-m", "nmp.core.models.sidecars.adapters.main"] # vLLM's filesystem resolver validates VLLM_LORA_RESOLVER_CACHE_DIR exists at # startup, so the controller pre-creates it in the scratch volume via a busybox # run before launching the vLLM container (otherwise vLLM crash-loops). diff --git a/services/core/models/tests/unit/controllers/test_nimservice_compiler.py b/services/core/models/tests/unit/controllers/test_nimservice_compiler.py index a1ff938078..987fe32ce9 100644 --- a/services/core/models/tests/unit/controllers/test_nimservice_compiler.py +++ b/services/core/models/tests/unit/controllers/test_nimservice_compiler.py @@ -339,7 +339,7 @@ def test_compile_nimservice_sidecar_container_name_truncated_for_long_resource_n assert sidecar_name.endswith("-lora-sidecar") -def test_compile_nimservice_sidecar_command_includes_nemo_platform(backend_config, sample_deployment, full_config): +def test_compile_nimservice_sidecar_command_runs_adapters_module(backend_config, sample_deployment, full_config): """Sidecar command must be full argv for K8s (container.command overrides image ENTRYPOINT).""" nimservice = compile_nimservice( backend_config=backend_config, @@ -353,11 +353,9 @@ def test_compile_nimservice_sidecar_command_includes_nemo_platform(backend_confi assert len(nimservice.spec.sidecarContainers) == 1 sidecar = nimservice.spec.sidecarContainers[0] assert sidecar.command == [ - "nemo", - "services", - "run", - "--sidecars", - "adapters", + "/opt/venv/bin/python", + "-m", + "nmp.core.models.sidecars.adapters.main", ] @@ -388,7 +386,7 @@ def test_compile_nimservice_sidecar_image_uses_platform_registry_and_tag( assert nimservice.spec.sidecarContainers is not None assert len(nimservice.spec.sidecarContainers) == 1 sidecar = nimservice.spec.sidecarContainers[0] - assert sidecar.image.repository == "localhost:5000/nmp-api" + assert sidecar.image.repository == "localhost:5000/nmp-automodel-tasks" assert sidecar.image.tag == "sidecar-tag"