diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ebde877b..d5618365 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -15,11 +15,11 @@ "vscode": { "extensions": [ "hashicorp.terraform", - "ms-python.black-formatter" + "charliermarsh.ruff" ], "settings": { "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter", + "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true } } diff --git a/.gitignore b/.gitignore index 08d564d9..df2b5ed5 100644 --- a/.gitignore +++ b/.gitignore @@ -97,6 +97,9 @@ ENV/ env.bak/ venv.bak/ +# Local config / dev data +config/local_inst_data.json + # mkdocs documentation /site @@ -114,3 +117,6 @@ dmypy.json # terraform **/.terraform/* **/terraform.tfvars + +# Cursor rule files +.cursor/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index b2320b68..e268b813 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -29,6 +29,21 @@ "env": { "ENV_FILE_PATH": "${workspaceFolder}/src/worker/.env" } + }, + { + "name": "pytest (current file)", + "type": "debugpy", + "request": "launch", + "module": "pytest", + "args": [ + "${file}", + "-v", + "-s" + ], + "cwd": "${workspaceFolder}", + "env": { + "ENV_FILE_PATH": "${workspaceFolder}/src/webapp/.env" + } } ], } \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dade4816..9d21e9ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,8 +10,8 @@ To get an overview of the project, please read the [README](README.md) and our [ ## Getting started ### Creating Issues -If you spot a problem, [search if an issue already exists](https://github.com/datakind/sst-app-api/issues). If a related issue doesn't exist, -you can open a new issue using a relevant [issue form](https://github.com/datakind/sst-app-api/issues/new). +If you spot a problem, [search if an issue already exists](https://github.com/datakind/edvise-api/issues). If a related issue doesn't exist, +you can open a new issue using a relevant [issue form](https://github.com/datakind/edvise-api/issues/new). As a general rule, we don’t assign issues to anyone. If you find an issue to work on, you are welcome to open a PR with a fix. @@ -28,7 +28,7 @@ poetry install --no-interaction As many other open source projects, we use the famous [gitflow](https://nvie.com/posts/a-successful-git-branching-model/) to manage our branches. Summary of our git branching model: -- Get all the latest work from the upstream `datakind/sst-app-api` repository +- Get all the latest work from the upstream `datakind/edvise-api` repository (`git checkout main`) - Create a new branch off with a descriptive name (for example: `feature/new-test-macro`, `bugfix/bug-when-uploading-results`). You can @@ -107,7 +107,7 @@ You can type `pytest` to run your tests, no matter which type of test it is. ## Continuous Integration -We use [GitHub Actions](https://github.com/datakind/sst-app-api/actions) +We use [GitHub Actions](https://github.com/datakind/edvise-api/actions) for continuous integration. See [here](https://docs.github.com/en/actions) for GitHub's documentation. diff --git a/README.md b/README.md index 2dfb87ff..7497252e 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ This repo contains: -* [src/webapp/](https://github.com/datakind/sst-app-api/tree/develop/src/webapp): The source code for the SST API (which is called by the SST frontend and by any direct API callers) -* [src/worker/](https://github.com/datakind/sst-app-api/tree/develop/src/worker): The source code for the SFTP Worker (which calls the SST API) +* [src/webapp/](https://github.com/datakind/edvise-api/tree/develop/src/webapp): The source code for the SST API (which is called by the SST frontend and by any direct API callers) +* [src/worker/](https://github.com/datakind/edvise-api/tree/develop/src/worker): The source code for the SFTP Worker (which calls the SST API) * [terraform/] -(https://github.com/datakind/sst-app-api/tree/develop/terraform): The Terraform configuration for the SST API/Frontend and other GCP resources including Cloud SQL setup, networking setup, secrets setup +(https://github.com/datakind/edvise-api/tree/develop/terraform): The Terraform configuration for the SST API/Frontend and other GCP resources including Cloud SQL setup, networking setup, secrets setup * .devcontainer/ and .vscode/: which allow easy setup if you are using VSCode as your IDE. -* [devtools/](https://github.com/datakind/sst-app-api/tree/develop/devtools): is a place to put utility scripts -* .github/: contains mostly copied over files when this directory was forked from the student-success-tool repo, so likely much of it is outdated. The only Github action we've added is the [webapp-and-worker-precommit](https://github.com/datakind/sst-app-api/blob/develop/.github/workflows/webapp-and-worker-precommit.yml) which is run on every push to develop. This action contains a python linter (we use [black](https://black.readthedocs.io/en/stable/)), and automated runs of the unit tests in the src/webapp/ and src/worker/ directories. -* Additionally, [pyproject.toml](https://github.com/datakind/sst-app-api/blob/develop/pyproject.toml) and [uv.lock](https://github.com/datakind/sst-app-api/blob/develop/uv.lock) are important for dependency management. At time of writing, the worker is just skeleton code so there's no separate dependency management. In the long-term consider separating out the dependency management for the two programs. +* [devtools/](https://github.com/datakind/edvise-api/tree/develop/devtools): is a place to put utility scripts +* .github/: contains mostly copied over files when this directory was forked from the student-success-tool repo, so likely much of it is outdated. The only Github action we've added is the [webapp-and-worker-precommit](https://github.com/datakind/edvise-api/blob/develop/.github/workflows/webapp-and-worker-precommit.yml) which is run on every push to develop. This action contains a python linter (we use [black](https://black.readthedocs.io/en/stable/)), and automated runs of the unit tests in the src/webapp/ and src/worker/ directories. +* Additionally, [pyproject.toml](https://github.com/datakind/edvise-api/blob/develop/pyproject.toml) and [uv.lock](https://github.com/datakind/edvise-api/blob/develop/uv.lock) are important for dependency management. At time of writing, the worker is just skeleton code so there's no separate dependency management. In the long-term consider separating out the dependency management for the two programs. NOTE: this repo was forked from the https://github.com/datakind/student-success-tool repo, which means some of the static files (e.g. CONTRIBUTING.md) may be outdated or may include irrelevant information from that repo. Please update those as you see fit. For information about the specific items listed above, defer to the specific readmes in the relevant directory. diff --git a/SECURITY.md b/SECURITY.md index d246af05..716c96b7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,4 +8,4 @@ If we verify a reported security vulnerability, our policy is: ## Reporting a Security Issue -To report any security issues, please [raise an issue](https://github.com/datakind/sst-app-api/issues/new/choose) and select **Security issue* +To report any security issues, please [raise an issue](https://github.com/datakind/edvise-api/issues/new/choose) and select **Security issue* diff --git a/cloudbuild-webapp.yaml b/cloudbuild-webapp.yaml new file mode 100644 index 00000000..bd156202 --- /dev/null +++ b/cloudbuild-webapp.yaml @@ -0,0 +1,55 @@ +# Cloud Build config for webapp (dev-webapp trigger). +# _REGION and _ENVIRONMENT are set by the trigger (Terraform). +steps: + - name: ghcr.io/astral-sh/uv:debian + entrypoint: bash + args: + - -c + - | + set -e + apt-get update && apt-get install -y --no-install-recommends git + uv lock --upgrade-package edvise + - name: gcr.io/cloud-builders/docker + args: + - build + - '-f' + - src/webapp/Dockerfile + - '-t' + - '${_REGION}-docker.pkg.dev/${PROJECT_ID}/edvise-api/webapp:$COMMIT_SHA' + - '-t' + - '${_REGION}-docker.pkg.dev/${PROJECT_ID}/edvise-api/webapp:latest' + - . + - name: gcr.io/cloud-builders/docker + args: + - push + - '${_REGION}-docker.pkg.dev/${PROJECT_ID}/edvise-api/webapp:$COMMIT_SHA' + - name: gcr.io/cloud-builders/docker + args: + - push + - '${_REGION}-docker.pkg.dev/${PROJECT_ID}/edvise-api/webapp:latest' + - name: gcr.io/cloud-builders/gcloud + args: + - run + - deploy + - '${_ENVIRONMENT}-webapp' + - '--image' + - '${_REGION}-docker.pkg.dev/${PROJECT_ID}/edvise-api/webapp:$COMMIT_SHA' + - '--region' + - '${_REGION}' + - name: curlimages/curl + args: + - '-X' + - POST + - '-H' + - 'Content-Type: application/json' + - '-f' + - '-d' + - >- + {"text":"🚀 *$REPO_NAME* deployed · `$BRANCH_NAME` · $TRIGGER_NAME · $BUILD_ID"} + - >- + https://hooks.slack.com/triggers/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 + id: notify-slack +timeout: 600s +options: + logging: CLOUD_LOGGING_ONLY + dynamicSubstitutions: true diff --git a/cloudbuild-worker.yaml b/cloudbuild-worker.yaml new file mode 100644 index 00000000..15c4c30d --- /dev/null +++ b/cloudbuild-worker.yaml @@ -0,0 +1,34 @@ +# Cloud Build config for worker (dev-worker trigger). +# _REGION and _ENVIRONMENT are set by the trigger (Terraform). +steps: + - name: gcr.io/cloud-builders/docker + args: + - build + - '-f' + - src/worker/Dockerfile + - '-t' + - '${_REGION}-docker.pkg.dev/${PROJECT_ID}/edvise-api/worker:$COMMIT_SHA' + - '-t' + - '${_REGION}-docker.pkg.dev/${PROJECT_ID}/edvise-api/worker:latest' + - . + - name: gcr.io/cloud-builders/docker + args: + - push + - '${_REGION}-docker.pkg.dev/${PROJECT_ID}/edvise-api/worker:$COMMIT_SHA' + - name: gcr.io/cloud-builders/docker + args: + - push + - '${_REGION}-docker.pkg.dev/${PROJECT_ID}/edvise-api/worker:latest' + - name: gcr.io/cloud-builders/gcloud + args: + - run + - deploy + - '${_ENVIRONMENT}-worker' + - '--image' + - '${_REGION}-docker.pkg.dev/${PROJECT_ID}/edvise-api/worker:$COMMIT_SHA' + - '--region' + - '${_REGION}' +timeout: 600s +options: + logging: CLOUD_LOGGING_ONLY + dynamicSubstitutions: true diff --git a/config/local_inst_data.example.json b/config/local_inst_data.example.json new file mode 100644 index 00000000..eb23226b --- /dev/null +++ b/config/local_inst_data.example.json @@ -0,0 +1,60 @@ +[ + { + "inst_id": "inst-uuid-here", + "name": "Example institution", + "state": "XX", + "retention_days": null, + "pdp_id": "", + "edvise_id": null, + "batches": [ + { + "batch_id": "batch-uuid-here", + "inst_id": "inst-uuid-here", + "file_names_to_ids": { + "example_course.csv": "file-id-course", + "example_student.csv": "file-id-student" + }, + "name": "example_batch_1", + "created_by": "uploader-uuid-here", + "deleted": false, + "completed": true, + "deletion_request_time": null, + "created_at": "2025-01-15T12:00:00", + "updated_at": "2025-01-15T12:00:00", + "updated_by": "" + } + ], + "files": [ + { + "name": "example_course.csv", + "data_id": "file-id-course", + "batch_ids": ["batch-uuid-here"], + "inst_id": "inst-uuid-here", + "uploader": "uploader-uuid-here", + "source": "MANUAL_UPLOAD", + "schemas": ["COURSE"], + "deleted": false, + "deletion_request_time": null, + "retention_days": null, + "sst_generated": false, + "valid": true, + "uploaded_date": "2025-01-15T11:58:00" + }, + { + "name": "example_student.csv", + "data_id": "file-id-student", + "batch_ids": ["batch-uuid-here"], + "inst_id": "inst-uuid-here", + "uploader": "uploader-uuid-here", + "source": "MANUAL_UPLOAD", + "schemas": ["STUDENT"], + "deleted": false, + "deletion_request_time": null, + "retention_days": null, + "sst_generated": false, + "valid": true, + "uploaded_date": "2025-01-15T11:57:00" + } + ] + } +] diff --git a/pyproject.toml b/pyproject.toml index 81c867a0..eb51b4c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dependencies = [ "databricks-sdk~=0.38.0", "pydantic~=2.10", "fastapi[standard]~=0.115.4", - "google-cloud-storage~=2.18.2", + "google-cloud-storage==2.19.0", "paramiko~=3.5.0", "cloud-sql-python-connector[pymysql]~=1.14.0", "sqlalchemy~=2.0.36", @@ -26,13 +26,16 @@ dependencies = [ "pandas~=2.0", "six~=1.16.0", "thefuzz[speedup]~=0.22.1", - "databricks-sql-connector~=3.5.0", + "databricks-sql-connector[pyarrow]~=4.2.0", "pandera~=0.13", - "mlflow~=2.15.0" + "mlflow~=2.22", + "cachetools", + "types-cachetools", + "edvise~=0.2.1", ] [project.urls] -Repository = "https://github.com/datakind/sst-app-api" +Repository = "https://github.com/datakind/edvise-api" [dependency-groups] dev = [ @@ -50,9 +53,15 @@ dev = [ requires = ["hatchling"] build-backend = "hatchling.build" +[tool.hatch.metadata] +allow-direct-references = true + [tool.uv] default-groups = ["dev"] +[tool.uv.sources] +edvise = { git = "https://github.com/datakind/edvise.git", rev = "develop" } + [tool.ruff] line-length = 88 indent-width = 4 @@ -84,8 +93,11 @@ lines-after-imports = 1 [tool.pytest.ini_options] minversion = "8.0" addopts = ["--verbose", "--import-mode=importlib"] -filterwarnings = ["ignore::DeprecationWarning"] -testpaths = ["tests"] +filterwarnings = [ + "ignore::DeprecationWarning", + "ignore::FutureWarning:pandera", +] +testpaths = ["src"] [tool.mypy] files = ["src"] diff --git a/src/webapp/Dockerfile b/src/webapp/Dockerfile index 92755a6e..fce6fdc2 100644 --- a/src/webapp/Dockerfile +++ b/src/webapp/Dockerfile @@ -13,6 +13,11 @@ WORKDIR /app # Add project files ADD uv.lock pyproject.toml /app/ +# Install git and ca-certificates +RUN apt-get update \ + && apt-get install -y --no-install-recommends git ca-certificates \ + && rm -rf /var/lib/apt/lists/* + # Install dependencies RUN uv sync --frozen --no-install-project diff --git a/src/webapp/README.md b/src/webapp/README.md index 484349fa..04815736 100644 --- a/src/webapp/README.md +++ b/src/webapp/README.md @@ -51,7 +51,7 @@ In the long-term, look into a way to have the API key --> token conversion be ha ## Databases -All data is stored in MySQL databases for dev/staging/prod, these are databases in GCP's Cloud SQL. In the local environment, the database is sqlite. The main file you'll want to look at for database table definitions is [src/webapp/database.py](https://github.com/datakind/sst-app-api/blob/develop/src/webapp/database.py). +All data is stored in MySQL databases for dev/staging/prod, these are databases in GCP's Cloud SQL. In the local environment, the database is sqlite. The main file you'll want to look at for database table definitions is [src/webapp/database.py](https://github.com/datakind/edvise-api/blob/develop/src/webapp/database.py). At time of writing, the databases the API cares about and tracks, are as follows: @@ -112,7 +112,7 @@ Enter into the root directory of the repo. You're now in your virtual env with all your dependencies added. -For all of the following, the steps above are pre-requisites and you should be in the root folder of `sst-app-api/`. +For all of the following, the steps above are pre-requisites and you should be in the root folder of `edvise-api/`. ### Spin up the app locally: @@ -168,3 +168,25 @@ The process to upload a file involves three API calls: ## Local VSCode Debugging From the Run & Debug panel (⇧⌘D on 🍎) you can run the [debug launch config](../../.vscode/launch.json) for the webapp or worker modules. This will allow you to set breakpoints within the source code while the applications are running. + +## Local edvise development override + +Production uses a pinned Git reference for `edvise`. For local development, use an +editable install after syncing the environment. + +1. Clone `edvise` alongside `edvise-api` (so `../edvise` exists). +2. Run `uv sync`. +3. Override locally: `uv pip install -e ../edvise` + +To revert back to the pinned Git dependency, run `uv sync --reinstall-package edvise`. + +## Local institutions (optional) + +You can seed the local database with institution, batch, and file metadata that matches dev or staging (names, UUIDs, batch membership) without checking secrets into Git. + +1. Copy `config/local_inst_data.example.json` to `config/local_inst_data.json`. The latter is gitignored. +2. Edit `local_inst_data.json` to match your needs. Use the example file as the schema: one array element per institution, with `inst_id`, `name`, and optionally `state`, `pdp_id`, `batches`, and `files`. + +If the file is missing, startup skips this step and the default local seed in code still applies. + +**Limitation:** Endpoints that read uploaded CSV (for example EDA) load blobs from GCS under the bucket name `dev_`, not from this JSON. To exercise those flows locally you still need GCP credentials and the corresponding objects in that bucket, or you rely on tests/mocks instead. \ No newline at end of file diff --git a/src/webapp/config.py b/src/webapp/config.py index db6df647..ab2b269f 100644 --- a/src/webapp/config.py +++ b/src/webapp/config.py @@ -51,6 +51,7 @@ "DATABRICKS_HOST_URL": "", # The service account that is used in Databricks to access GCP buckets. "DATABRICKS_SERVICE_ACCOUNT_EMAIL": "", + "GCP_CACHE_BUCKET": "", } diff --git a/src/webapp/database.py b/src/webapp/database.py index 7fe974b0..6700af6c 100644 --- a/src/webapp/database.py +++ b/src/webapp/database.py @@ -1,7 +1,9 @@ """Database configuration.""" +import json import uuid import datetime +from pathlib import Path from typing import Set, List, Any from contextvars import ContextVar import enum @@ -22,6 +24,7 @@ Integer, BigInteger, Index, + CheckConstraint, event, ) from sqlalchemy.orm import ( @@ -60,6 +63,49 @@ class Base(DeclarativeBase): DATETIME_TESTING = datetime.datetime(2024, 12, 26, 19, 37, 59, 753357) +def _setup_test_institutions(session: Session) -> None: + """Load optional local institution display data from config/local_inst_data.json (gitignored).""" + file = Path("config/local_inst_data.json") + if file.exists(): + with open(file) as f: + for inst in json.load(f): + session.merge( + InstTable( + id=uuid.UUID(inst["inst_id"]), + name=inst["name"], + state=inst.get("state"), + pdp_id=inst.get("pdp_id"), + created_at=DATETIME_TESTING, + updated_at=DATETIME_TESTING, + created_by=LOCAL_USER_UUID, + ) + ) + schemas_by_file_id = { + f["data_id"]: f.get("schemas", []) for f in inst.get("files", []) + } + for batch in inst.get("batches", []): + batch_table = BatchTable( + id=uuid.UUID(batch["batch_id"]), + inst_id=uuid.UUID(inst["inst_id"]), + name=batch["name"], + created_at=DATETIME_TESTING, + updated_at=DATETIME_TESTING, + created_by=LOCAL_USER_UUID, + ) + for file_name, file_id in batch["file_names_to_ids"].items(): + batch_table.files.add( + session.merge( + FileTable( + id=uuid.UUID(file_id), + inst_id=uuid.UUID(inst["inst_id"]), + name=file_name, + schemas=schemas_by_file_id.get(file_id, []), + ) + ) # type: ignore + ) + session.merge(batch_table) + + @event.listens_for(Mapper, "before_insert") @event.listens_for(Mapper, "before_update") def validate_string_lengths(mapper, connection, target): @@ -118,6 +164,52 @@ def init_db(env: str) -> None: valid=True, ) ) + # Create test files and batches for LOCAL environment + if env == "LOCAL": + _setup_test_institutions(session) + # Create test files + test_file_1 = FileTable( + id=uuid.UUID("f0bb3a20-6d92-4254-afed-6a72f43c562a"), + inst_id=LOCAL_INST_UUID, + name="test_course_file.csv", + source="MANUAL_UPLOAD", + uploader=LOCAL_USER_UUID, + sst_generated=False, + valid=True, + schemas=["COURSE"], # Using string literal to avoid circular import + created_at=DATETIME_TESTING, + updated_at=DATETIME_TESTING, + ) + test_file_2 = FileTable( + id=uuid.UUID("cb02d06c-2a59-486a-9bdd-d394a4fcb833"), + inst_id=LOCAL_INST_UUID, + name="test_cohort_file.csv", + source="MANUAL_UPLOAD", + uploader=LOCAL_USER_UUID, + sst_generated=False, + valid=True, + schemas=[ + "STUDENT" + ], # Using string literal to avoid circular import + created_at=DATETIME_TESTING, + updated_at=DATETIME_TESTING, + ) + # Create test batch for LOCAL_INST_UUID (using a different ID) + test_batch = BatchTable( + id=uuid.UUID("f0bb3a20-6d92-4254-afed-6a72f43c562b"), + inst_id=LOCAL_INST_UUID, + name="test_batch_1", + created_by=LOCAL_USER_UUID, + created_at=DATETIME_TESTING, + updated_at=DATETIME_TESTING, + ) + # Associate files with batch + test_batch.files.add(test_file_1) + test_batch.files.add(test_file_2) + session.merge(test_file_1) + session.merge(test_file_2) + session.merge(test_batch) + session.commit() except Exception as e: session.rollback() @@ -168,6 +260,14 @@ class InstTable(Base): state: Mapped[str | None] = mapped_column(String(VAR_CHAR_LENGTH), nullable=True) # Only populated for PDP schools. pdp_id: Mapped[str | None] = mapped_column(String(VAR_CHAR_LENGTH), nullable=True) + # Only populated for Edvise schools. + edvise_id: Mapped[str | None] = mapped_column( + String(VAR_CHAR_LENGTH), nullable=True + ) + # Only populated for Legacy schools (any-format uploads). + legacy_id: Mapped[str | None] = mapped_column( + String(VAR_CHAR_LENGTH), nullable=True + ) created_at: Mapped[datetime.datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), @@ -483,7 +583,10 @@ class ModelTable(Base): ) inst: Mapped["InstTable"] = relationship(back_populates="models") - jobs: Mapped[Set["JobTable"]] = relationship(back_populates="model") + jobs: Mapped[Set["JobTable"]] = relationship( + back_populates="model", + passive_deletes=True, + ) name: Mapped[str] = mapped_column(String(VAR_CHAR_STANDARD_LENGTH), nullable=False) # What configuration of schemas are allowed (list of maps e.g. [PDP Course : 1 + PDP Cohort : 1, X_schema :1 + Y_schema: 2]) @@ -548,6 +651,12 @@ class JobTable(Base): String(VAR_CHAR_STANDARD_LENGTH), nullable=True ) completed: Mapped[bool] = mapped_column(nullable=True) + model_version: Mapped[str | None] = mapped_column( + String(VAR_CHAR_STANDARD_LENGTH), nullable=True + ) + model_run_id: Mapped[str | None] = mapped_column( + String(VAR_CHAR_STANDARD_LENGTH), nullable=True + ) class DocType(enum.Enum): @@ -558,9 +667,10 @@ class DocType(enum.Enum): class SchemaRegistryTable(Base): """ Stores versioned schema documents: - - Base schema (doc_type=base, is_pdp=False, inst_id NULL) - - PDP shared extension (doc_type=extension, is_pdp=True, inst_id NULL) - - Custom institution extension (doc_type=extension, is_pdp=False, inst_id=) + - Base schema (doc_type=base, is_pdp=False, is_edvise=False, inst_id NULL) + - PDP shared extension (doc_type=extension, is_pdp=True, is_edvise=False, inst_id NULL) + - Edvise shared extension (doc_type=extension, is_pdp=False, is_edvise=True, inst_id NULL) + - Custom institution extension (doc_type=extension, is_pdp=False, is_edvise=False, inst_id=) Layers can reference a parent (extends_schema_id) that they extend. """ @@ -571,11 +681,12 @@ class SchemaRegistryTable(Base): doc_type: Mapped[DocType] = mapped_column( Enum(DocType, native_enum=False), nullable=False ) - # Nullable: NULL for base and PDP shared extension + # Nullable: NULL for base, PDP shared extension, and Edvise shared extension inst_id: Mapped[uuid.UUID | None] = mapped_column( ForeignKey("inst.id", ondelete="RESTRICT", onupdate="CASCADE"), nullable=True ) is_pdp: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + is_edvise: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) version_label: Mapped[str] = mapped_column( String(VAR_CHAR_STANDARD_LENGTH), nullable=False ) @@ -620,8 +731,12 @@ class SchemaRegistryTable(Base): UniqueConstraint("doc_type", "version_label", name="uq_base_version"), UniqueConstraint("is_pdp", "version_label", name="uq_pdp_version"), UniqueConstraint("inst_id", "version_label", name="uq_inst_version"), + CheckConstraint( + "NOT (is_pdp = 1 AND is_edvise = 1)", name="ck_no_pdp_and_edvise" + ), Index("idx_schema_active_base", "doc_type", "is_active"), Index("idx_schema_active_pdp", "is_pdp", "is_active"), + Index("idx_schema_active_edvise", "is_edvise", "is_active"), Index("idx_schema_active_inst", "inst_id", "is_active"), ) @@ -632,6 +747,8 @@ def namespace(self) -> str: return "base" if self.is_pdp: return "pdp" + if self.is_edvise: + return "edvise" if self.inst_id: return f"inst:{self.inst_id}" return "unknown" @@ -654,7 +771,6 @@ def init_connection_pool_local() -> sqlalchemy.engine.base.Engine: """Creates a local sqlite db for local env testing.""" return sqlalchemy.create_engine( "sqlite://", - echo=True, echo_pool="debug", connect_args={"check_same_thread": False}, poolclass=StaticPool, diff --git a/src/webapp/databricks.py b/src/webapp/databricks.py index 0f9612ec..aa4b6b02 100644 --- a/src/webapp/databricks.py +++ b/src/webapp/databricks.py @@ -12,18 +12,16 @@ StatementState, ) from google.cloud import storage -from .validation_extension import generate_extension_schema +from google.api_core import exceptions as gcs_errors from .config import databricks_vars, gcs_vars from .utilities import databricksify_inst_name, SchemaType -from typing import List, Any, Dict, IO, cast, Optional -from databricks.sdk.errors import DatabricksError -from fastapi import HTTPException - -try: - import tomllib as _toml # Py 3.11+ -except ModuleNotFoundError: - import tomli as _toml # Py ≤ 3.10 -import pandas as pd +from typing import List, Any, Dict, Optional +import requests +import hashlib +import json +import gzip +from cachetools import TTLCache +import threading import re # Setting up logger @@ -34,7 +32,7 @@ MEDALLION_LEVELS = ["silver", "gold", "bronze"] # The name of the deployed pipeline in Databricks. Must match directly. -PDP_INFERENCE_JOB_NAME = "github_sourced_pdp_inference_pipeline" +PDP_INFERENCE_JOB_NAME = "edvise_github_sourced_pdp_inference_pipeline" class DatabricksInferenceRunRequest(BaseModel): @@ -44,7 +42,6 @@ class DatabricksInferenceRunRequest(BaseModel): # Note that the following should be the filepath. filepath_to_type: dict[str, list[SchemaType]] model_name: str - model_type: str = "sklearn" # The email where notifications will get sent. email: str gcp_external_bucket_name: str @@ -75,6 +72,21 @@ def check_types(dict_values: list[list[SchemaType]], file_type: SchemaType) -> b return False +def _sha256_json(obj: Any) -> str: + return hashlib.sha256( + json.dumps( + obj, ensure_ascii=False, separators=(",", ":"), sort_keys=True + ).encode("utf-8") + ).hexdigest() + + +L1_RESP_CACHE_TTL = int("600") # seconds +L1_VER_CACHE_TTL = int("3600") # seconds +L1_RESP_CACHE: Any = TTLCache(maxsize=128, ttl=L1_RESP_CACHE_TTL) +L1_VER_CACHE: Any = TTLCache(maxsize=256, ttl=L1_VER_CACHE_TTL) +_L1_LOCK = threading.RLock() + + # Wrapping the usages in a class makes it easier to unit test via mocks. class DatabricksControl(BaseModel): """Object to manage interfacing with GCS.""" @@ -98,7 +110,17 @@ def setup_new_inst(self, inst_name: str) -> None: db_inst_name = databricksify_inst_name(inst_name) cat_name = databricks_vars["CATALOG_NAME"] for medallion in MEDALLION_LEVELS: - w.schemas.create(name=f"{db_inst_name}_{medallion}", catalog_name=cat_name) + try: + w.schemas.create( + name=f"{db_inst_name}_{medallion}", catalog_name=cat_name + ) + except Exception as e: + LOGGER.exception( + f"Failed to provision schemas in databricks for {db_inst_name}_{medallion}: {e}" + ) + raise ValueError( + f"setup_new_inst(): Failed to provision schemas in databricks for {db_inst_name}_{medallion}: {e}" + ) LOGGER.info( f"Creating medallion level schemas for {db_inst_name} & {medallion}." ) @@ -191,17 +213,18 @@ def run_pdp_inference( ) db_inst_name = databricksify_inst_name(req.inst_name) + pipeline_type = PDP_INFERENCE_JOB_NAME try: - job = next(w.jobs.list(name=PDP_INFERENCE_JOB_NAME), None) + job = next(w.jobs.list(name=pipeline_type), None) if not job or job.job_id is None: raise ValueError( - f"run_pdp_inference(): Job '{PDP_INFERENCE_JOB_NAME}' was not found or has no job_id." + f"run_pdp_inference(): Job '{pipeline_type}' was not found or has no job_id for '{gcs_vars['GCP_SERVICE_ACCOUNT_EMAIL']}' and '{databricks_vars['DATABRICKS_HOST_URL']}'." ) job_id = job.job_id - LOGGER.info(f"Resolved job ID for '{PDP_INFERENCE_JOB_NAME}': {job_id}") + LOGGER.info(f"Resolved job ID for '{pipeline_type}': {job_id}") except Exception as e: - LOGGER.exception(f"Job lookup failed for '{PDP_INFERENCE_JOB_NAME}'.") + LOGGER.exception(f"Job lookup failed for '{pipeline_type}'.") raise ValueError(f"run_pdp_inference(): Failed to find job: {e}") try: @@ -220,7 +243,6 @@ def run_pdp_inference( ], # is this value the same PER environ? dev/staging/prod "gcp_bucket_name": req.gcp_external_bucket_name, "model_name": req.model_name, - "model_type": req.model_type, "notification_email": req.email, }, ) @@ -302,80 +324,232 @@ def fetch_table_data( inst_name: str, table_name: str, warehouse_id: str, - ) -> List[Dict[str, Any]]: + ) -> Any: """ - Executes a SELECT * query on the specified table within the given catalog and schema, - using the provided SQL warehouse. Returns the result as a list of dictionaries. + Execute SELECT * via Databricks SQL Statement Execution API using EXTERNAL_LINKS. + Blocks server-side for up to 30s; if not SUCCEEDED, raises. Downloads presigned + URLs in-memory and returns rows as List[Dict[str, Any]]. """ + w = WorkspaceClient( + host=databricks_vars["DATABRICKS_HOST_URL"], + google_service_account=gcs_vars["GCP_SERVICE_ACCOUNT_EMAIL"], + ) + + bucket_name = databricks_vars["GCP_CACHE_BUCKET"] + schema = databricksify_inst_name(inst_name) + table_fqn = f"`{catalog_name}`.`{schema}_silver`.`{table_name}`" + sql = f"SELECT * FROM {table_fqn}" + + ver_cache_key = f"ver:{table_fqn}" + with _L1_LOCK: + table_version = L1_VER_CACHE.get(ver_cache_key) + + if table_version is None: + ver_sql = f"DESCRIBE HISTORY {table_fqn} LIMIT 1" + ver_resp = w.statement_execution.execute_statement( + warehouse_id=warehouse_id, + statement=ver_sql, + disposition=Disposition.INLINE, + format=Format.JSON_ARRAY, + wait_timeout="30s", + on_wait_timeout=ExecuteStatementRequestOnWaitTimeout.CONTINUE, + ) + + if not ver_resp.status or ver_resp.status.state != StatementState.SUCCEEDED: + raise TimeoutError("DESCRIBE HISTORY did not finish within 30s") + cols = [c.name for c in ver_resp.manifest.schema.columns] # type: ignore + idx = {n: i for i, n in enumerate(cols)} + rows = ver_resp.result.data_array or [] # type: ignore + if not rows or "version" not in idx: + raise ValueError("DESCRIBE HISTORY returned no version") + table_version = str(rows[0][idx["version"]]) + + with _L1_LOCK: + L1_VER_CACHE[ver_cache_key] = table_version + + sql_h = _sha256_json({"sql": sql}) + l1_key = f"v1:{warehouse_id}:{catalog_name}.{schema}.{table_name}:{sql_h}:{table_version}" + + with _L1_LOCK: + cached_records = L1_RESP_CACHE.get(l1_key) + if cached_records is not None: + return cached_records + + try: + object_name = f"{warehouse_id}/{catalog_name}.{schema}.{table_name}/{sql_h}/{table_version}.json.gz" + storage_client = storage.Client() + bucket = storage_client.bucket(bucket_name) + blob = bucket.blob(object_name) + try: + blob.reload() # HEAD for metadata (ETag, etc.) + body = blob.download_as_bytes(raw_download=False) + data = json.loads(body) + if isinstance(data, list): + with _L1_LOCK: + L1_RESP_CACHE[l1_key] = data + return data # cache hit + except gcs_errors.NotFound: + pass + except Exception: + pass + + resp = w.statement_execution.execute_statement( + warehouse_id=warehouse_id, + statement=sql, + disposition=Disposition.EXTERNAL_LINKS, + format=Format.JSON_ARRAY, + wait_timeout="30s", + on_wait_timeout=ExecuteStatementRequestOnWaitTimeout.CONTINUE, + ) + + stmt_id = resp.statement_id + if stmt_id is None: + raise ValueError("Databricks returned a null statement_id") + + # No client-side polling; require SUCCEEDED within 30s. + if (resp.status is None) or (resp.status.state != StatementState.SUCCEEDED): + state = resp.status.state if resp.status else "UNKNOWN" + msg = ( + resp.status.error.message + if (resp.status and resp.status.error) + else "Query not finished within wait_timeout" + ) + raise TimeoutError( + f"Statement {stmt_id} not finished (state={state}): {msg}" + ) + + # Columns (ensure List[str] for type-checkers) + if not ( + resp.manifest and resp.manifest.schema and resp.manifest.schema.columns + ): + raise ValueError("Schema/columns missing (EXTERNAL_LINKS).") + cols: List[str] = [] # type: ignore + for c in resp.manifest.schema.columns: + if c.name is None: + raise ValueError("Encountered a column without a name.") + cols.append(c.name) + + records: Any = [] + + # Helper: consume one chunk-like object (first result or subsequent chunk) + def _consume_chunk(chunk_obj: Any) -> int | None: + links = getattr(chunk_obj, "external_links", None) or [] + for link_obj in links: + url = getattr(link_obj, "external_link", None) + if url is None and isinstance(link_obj, dict): + url = link_obj.get("external_link") + if not url: + continue + # IMPORTANT: do not send Databricks auth header to presigned URLs. + r = requests.get(url, timeout=120) + r.raise_for_status() + rows = r.json() + if not isinstance(rows, list): + raise ValueError( + "Unexpected external link payload (expected JSON array)." + ) + for row in rows: + if not isinstance(row, list): + raise ValueError("Unexpected row shape (expected list).") + records.append(dict(zip(cols, row))) + return getattr(chunk_obj, "next_chunk_index", None) + + # First batch is in resp.result + if not resp.result: + return records + next_idx = _consume_chunk(resp.result) + + # Remaining batches by chunk index + while next_idx is not None: + chunk = w.statement_execution.get_statement_result_chunk_n( + statement_id=stmt_id, + chunk_index=next_idx, + ) + next_idx = _consume_chunk(chunk) + + with _L1_LOCK: + if records: + L1_RESP_CACHE[l1_key] = records + + if bucket_name and object_name and records: + try: + raw = json.dumps( + records, ensure_ascii=False, separators=(",", ":") + ).encode("utf-8") + gz = gzip.compress(raw, compresslevel=6) + storage_client = storage.Client() + bucket = storage_client.bucket(bucket_name) + blob = bucket.blob(object_name) + blob.content_encoding = "gzip" + try: + blob.upload_from_string( + gz, + content_type="application/json", + if_generation_match=0, # write-once; 412 if someone beat us + ) + except gcs_errors.PreconditionFailed: + # Another writer won; fine—object exists now. + pass + except Exception: + # Cache write failures must not impact the request + pass + return records + + def fetch_model_version( + self, catalog_name: str, inst_name: str, model_name: str + ) -> Any: + schema = databricksify_inst_name(inst_name) + model_name_path = f"{catalog_name}.{schema}_gold.{model_name}" + try: w = WorkspaceClient( host=databricks_vars["DATABRICKS_HOST_URL"], google_service_account=gcs_vars["GCP_SERVICE_ACCOUNT_EMAIL"], ) - LOGGER.info("Successfully created Databricks WorkspaceClient.") except Exception as e: LOGGER.exception( "Failed to create Databricks WorkspaceClient with host: %s and service account: %s", databricks_vars["DATABRICKS_HOST_URL"], gcs_vars["GCP_SERVICE_ACCOUNT_EMAIL"], ) - raise ValueError( - f"fetch_table_data(): Workspace client initialization failed: {e}" - ) + raise ValueError(f"setup_new_inst(): Workspace client creation failed: {e}") - # Construct the fully qualified table name - schema_name = databricksify_inst_name(inst_name) - fully_qualified_table = ( - f"`{catalog_name}`.`{schema_name}_silver`.`{table_name}`" + model_versions: Any = list( + w.model_versions.list( + full_name=model_name_path, + ) ) - sql_query = f"SELECT * FROM {fully_qualified_table}" - LOGGER.info(f"Executing SQL: {sql_query}") - try: - # Execute the SQL statement - response = w.statement_execution.execute_statement( - warehouse_id=warehouse_id, - statement=sql_query, - disposition=Disposition.INLINE, # Use Enum member - format=Format.JSON_ARRAY, # Use Enum member - wait_timeout="30s", # Wait up to 30 seconds for execution - on_wait_timeout=ExecuteStatementRequestOnWaitTimeout.CANCEL, # Use Enum member - ) - LOGGER.info("Databricks SQL execution successful.") - except DatabricksError as e: - LOGGER.exception("Databricks API call failed.") - raise ValueError(f"Databricks API call failed: {e}") - - # Check if the query execution was successful - status = response.status - if not status or status.state != StatementState.SUCCEEDED: - error_message = ( - status.error.message - if status and status.error - else "No additional error info." - ) - raise ValueError( - f"Query did not succeed (state={status.state if status else 'None'}): {error_message}" - ) + if not model_versions: + raise ValueError(f"No versions found for model: {model_name_path}") - if ( - not response.manifest - or not response.manifest.schema - or not response.manifest.schema.columns - or not response.result - or not response.result.data_array - ): - raise ValueError("Query succeeded but schema or result data is missing.") + latest_version = max(model_versions, key=lambda v: int(v.version)) - column_names = [str(column.name) for column in response.manifest.schema.columns] - data_rows = response.result.data_array + return latest_version - LOGGER.info( - f"Fetched {len(data_rows)} rows from table: {fully_qualified_table}" - ) + def delete_model(self, catalog_name: str, inst_name: str, model_name: str) -> None: + schema = databricksify_inst_name(inst_name) + model_name_path = f"{catalog_name}.{schema}_gold.{model_name}" + + try: + w = WorkspaceClient( + host=databricks_vars["DATABRICKS_HOST_URL"], + google_service_account=gcs_vars["GCP_SERVICE_ACCOUNT_EMAIL"], + ) + except Exception as e: + LOGGER.exception( + "Failed to create Databricks WorkspaceClient with host: %s and service account: %s", + databricks_vars["DATABRICKS_HOST_URL"], + gcs_vars["GCP_SERVICE_ACCOUNT_EMAIL"], + ) + raise ValueError(f"setup_new_inst(): Workspace client creation failed: {e}") - # Combine column names with corresponding row values - return [dict(zip(column_names, row)) for row in data_rows] + try: + w.registered_models.delete(full_name=model_name_path) + LOGGER.info("Deleted registration model: %s", model_name_path) + except Exception: + LOGGER.exception("Failed to delete registered model: %s", model_name_path) + raise def get_key_for_file( self, mapping: Dict[str, Any], file_name: str @@ -384,7 +558,7 @@ def get_key_for_file( Case-insensitive match of file_name against mapping values. Values may be: - str literal (e.g., "student.csv") → allow optional base suffixes before the ext. - - str regex (e.g., r"^course_.*\.csv$") → re.IGNORECASE fullmatch. + - str regex (e.g., r"^course_.*\\.csv$") → re.IGNORECASE fullmatch. - compiled regex (re.Pattern) → fullmatch, adding IGNORECASE if missing. - list of any of the above. """ @@ -446,110 +620,3 @@ def matches_one(pat: Any) -> bool: return key return None - - def create_custom_schema_extension( - self, - bucket_name: str, - inst_query: Any, - file_name: str, - base_schema: Dict[str, Any], # pass base schema dict in - extension_schema: Optional[dict] = None, # existing extension or None - ) -> Any: - if ( - os.getenv("SST_SKIP_EXT_GEN") == "1" - ): # skip using workspace client for tests - LOGGER.info("SST_SKIP_EXT_GEN=1; skipping Databricks extension generation.") - return None - - # 1) Databricks client - try: - w = WorkspaceClient( - host=databricks_vars["DATABRICKS_HOST_URL"], - google_service_account=gcs_vars["GCP_SERVICE_ACCOUNT_EMAIL"], - ) - LOGGER.info("Successfully created Databricks WorkspaceClient.") - except Exception as e: - LOGGER.exception("WorkspaceClient init failed") - raise ValueError(f"Workspace client initialization failed: {e}") - - # 2) Fetch & parse config.toml to get validation_mapping - try: - inst_name = inst_query[0][0].name - inst_id_raw = inst_query[0][0].id - inst_id = str(inst_id_raw) # be robust if id is not a string - config_volume_path = ( - f"/Volumes/staging_sst_01/" - f"{databricksify_inst_name(inst_name)}_bronze/bronze_volume/config.toml" - ) - LOGGER.info("Attempting to download from %s", config_volume_path) - response = w.files.download(config_volume_path) - stream = cast(IO[bytes], response.contents) - file_bytes = stream.read() - LOGGER.info("Download successful, received %d bytes", len(file_bytes)) - except Exception as e: - LOGGER.exception("Failed to fetch config.toml") - raise HTTPException(500, detail=f"Failed to fetch config: {e}") - - try: - cfg = _toml.loads(file_bytes.decode("utf-8")) - mapping = cfg["webapp"]["validation_mapping"] - except KeyError: - raise HTTPException( - 404, detail="Missing [webapp].validation_mapping in config.toml" - ) - except Exception as e: - LOGGER.exception("Invalid TOML") - raise HTTPException(400, detail=f"Invalid TOML in {file_name}: {e}") - - if not isinstance(mapping, dict): - raise HTTPException( - 400, detail="validation_mapping must be a TOML table (dictionary)" - ) - - key = self.get_key_for_file(mapping, file_name) # e.g., "student" - if key is None: - raise HTTPException( - 404, detail=f"{file_name} not found in {inst_name} validation_mapping" - ) - - key_lc = key.lower() - - # 4) If this model already exists in the provided extension for this institution, skip - if extension_schema is not None: - if not isinstance(extension_schema, dict): - raise HTTPException( - 400, detail="extension_schema must be a dict if provided" - ) - - inst_block = extension_schema.get("institutions", {}).get(inst_id, {}) - data_models = inst_block.get("data_models", {}) - existing_keys_lc = {str(k).lower() for k in data_models.keys()} - - if key_lc in existing_keys_lc: - LOGGER.info( - "Model '%s' already present for institution '%s' — skipping (return None).", - key, - inst_id, - ) - return None # <-- sentinel: do not write - - # 5) Read the unvalidated CSV from GCS - try: - client = storage.Client() - bucket = client.bucket(bucket_name) - blob = bucket.blob(f"unvalidated/{file_name}") - with blob.open("r") as fh: - df = pd.read_csv(fh) - except Exception as e: - LOGGER.exception("Failed to read %s from GCS", file_name) - raise HTTPException(500, detail=f"Failed to read {file_name} from GCS: {e}") - - updated_extension = generate_extension_schema( - df=df, - models=key, # exactly one model - institution_id=inst_id, - base_schema=base_schema, # reference only, not mutated - existing_extension=extension_schema, # may be None - ) - - return updated_extension diff --git a/src/webapp/fixtures/validation_error_snapshots/complete_error_flow.txt b/src/webapp/fixtures/validation_error_snapshots/complete_error_flow.txt new file mode 100644 index 00000000..c3d0b26d --- /dev/null +++ b/src/webapp/fixtures/validation_error_snapshots/complete_error_flow.txt @@ -0,0 +1,9 @@ +Missing required columns: 'Student ID' (Student identifier). These columns must be present in your file. + +Unexpected columns found: 'Extra Col'. Please remove these columns or rename them to match the expected schema. + +Column 'Age' has validation errors: + • Row 2: Value validation failed. Current value: found 'None' + +Column 'Grade' has validation errors: + • Row 1: Value must be one of: A, B, C. Current value: found 'X' diff --git a/src/webapp/fixtures/validation_error_snapshots/extra_columns_ambiguous.txt b/src/webapp/fixtures/validation_error_snapshots/extra_columns_ambiguous.txt new file mode 100644 index 00000000..c9a3d78e --- /dev/null +++ b/src/webapp/fixtures/validation_error_snapshots/extra_columns_ambiguous.txt @@ -0,0 +1 @@ +Unexpected columns found: 'Student ID (and 2 similar)'. Please remove these columns or rename them to match the expected schema. diff --git a/src/webapp/fixtures/validation_error_snapshots/missing_required_columns.txt b/src/webapp/fixtures/validation_error_snapshots/missing_required_columns.txt new file mode 100644 index 00000000..b73b01c0 --- /dev/null +++ b/src/webapp/fixtures/validation_error_snapshots/missing_required_columns.txt @@ -0,0 +1 @@ +Missing required columns: 'Student ID' (Unique student identifier), 'Grade' (Student grade (A-F)), 'Age' (Student age). These columns must be present in your file. diff --git a/src/webapp/fixtures/validation_error_snapshots/mixed_types_multiple_columns.txt b/src/webapp/fixtures/validation_error_snapshots/mixed_types_multiple_columns.txt new file mode 100644 index 00000000..381a8042 --- /dev/null +++ b/src/webapp/fixtures/validation_error_snapshots/mixed_types_multiple_columns.txt @@ -0,0 +1,11 @@ +Column 'Age' has validation errors: + • Row 2: Value validation failed. Current value: found 'None' + +Column 'Grade' has validation errors: + • Row 1: Value must be one of: A, B, C, D, F. Current value: found 'X' + +Column 'Score' has validation errors: + • Row 3: Validation failed for greater_than(0) check. Current value: found '-5' + +Column 'Student ID' has validation errors: + • Row 1: Value must be at least 3 characters long. Current value: found 'AB' diff --git a/src/webapp/fixtures/validation_error_snapshots/multiple_rows_same_column.txt b/src/webapp/fixtures/validation_error_snapshots/multiple_rows_same_column.txt new file mode 100644 index 00000000..e7126f3f --- /dev/null +++ b/src/webapp/fixtures/validation_error_snapshots/multiple_rows_same_column.txt @@ -0,0 +1,5 @@ +Column 'Grade' has validation errors: + • Row 1: Value must be one of: A, B, C, D, F. Current value: found 'X' + • Row 2: Value must be one of: A, B, C, D, F. Current value: found 'Y' + • Row 3: Value must be one of: A, B, C, D, F. Current value: found 'Z' + • Row 4: Value must be one of: A, B, C, D, F. Current value: found 'W' diff --git a/src/webapp/fixtures/validation_error_snapshots/pii_masking_mixed.txt b/src/webapp/fixtures/validation_error_snapshots/pii_masking_mixed.txt new file mode 100644 index 00000000..d4b36a26 --- /dev/null +++ b/src/webapp/fixtures/validation_error_snapshots/pii_masking_mixed.txt @@ -0,0 +1,14 @@ +Column 'Course Name' has validation errors: + • Row 1: Value must be at least 3 characters long. Current value: found 'XY' + +Column 'Email' has validation errors: + • Row 2: Value must be at least 5 characters long. Current value: found 'ca******om' (value masked for privacy) + +Column 'Grade' has validation errors: + • Row 2: Value must be one of: A, B, C. Current value: found 'X' + +Column 'SSN' has validation errors: + • Row 3: Value must be at least 9 characters long. Current value: found 'CA******89' (value masked for privacy) + +Column 'Student Name' has validation errors: + • Row 1: Value must be at least 3 characters long. Current value: found 'CA******23' (value masked for privacy) diff --git a/src/webapp/fixtures/validation_error_snapshots/schema_level_errors.txt b/src/webapp/fixtures/validation_error_snapshots/schema_level_errors.txt new file mode 100644 index 00000000..b56d1d7b --- /dev/null +++ b/src/webapp/fixtures/validation_error_snapshots/schema_level_errors.txt @@ -0,0 +1,3 @@ +File-level validation errors: + • Unknown row: Validation failed for non_empty_dataframe check. Current value: found 'DataFrame is empty' + • Unknown row: Validation failed for row_count check. Current value: found 'Row count mismatch' diff --git a/src/webapp/fixtures/validation_error_snapshots/truncation_many_errors.txt b/src/webapp/fixtures/validation_error_snapshots/truncation_many_errors.txt new file mode 100644 index 00000000..86c840ac --- /dev/null +++ b/src/webapp/fixtures/validation_error_snapshots/truncation_many_errors.txt @@ -0,0 +1,13 @@ +Column 'Value' has validation errors: + • Row 1: Validation failed for greater_than(0) check. Current value: found '-1' + • Row 2: Validation failed for greater_than(0) check. Current value: found '-1' + • Row 3: Validation failed for greater_than(0) check. Current value: found '-1' + • Row 4: Validation failed for greater_than(0) check. Current value: found '-1' + • Row 5: Validation failed for greater_than(0) check. Current value: found '-1' + • Row 6: Validation failed for greater_than(0) check. Current value: found '-1' + • Row 7: Validation failed for greater_than(0) check. Current value: found '-1' + • Row 8: Validation failed for greater_than(0) check. Current value: found '-1' + • Row 9: Validation failed for greater_than(0) check. Current value: found '-1' + • Row 10: Validation failed for greater_than(0) check. Current value: found '-1' + +Column 'Value': 5 additional errors found. Please review all rows for this column. diff --git a/src/webapp/gcsutil.py b/src/webapp/gcsutil.py index b6046daa..02c63286 100644 --- a/src/webapp/gcsutil.py +++ b/src/webapp/gcsutil.py @@ -1,15 +1,19 @@ """Cloud storage related helper functions.""" import datetime +import logging +import os +import tempfile +from typing import Any, Dict, List, Optional + +import pandas as pd from pydantic import BaseModel from google.cloud import storage import google.auth from google.auth.transport import requests from .config import gcs_vars, databricks_vars -from .validation import validate_file_reader -from typing import Any, List, Optional, Dict -import logging +from .validation import validate_file_reader, HardValidationError # Set the logging logging.basicConfig(format="%(asctime)s [%(levelname)s]: %(message)s") @@ -19,11 +23,46 @@ SIGNED_URL_EXPIRY_MIN = 30 +def _unlink_if_exists(path: Optional[str]) -> None: + """Remove a file if path is set; ignore missing file or permission errors.""" + if path is None: + return + try: + os.unlink(path) + except OSError: + pass + + +def _download_blob_to_temp_csv_path(blob: Any, file_name: str) -> str: + """ + Stream GCS blob to a private temp CSV path for validation. + + Raises: + OSError: If download fails (after logging errno/context). Temp file is removed. + """ + fd, csv_path = tempfile.mkstemp(suffix=".csv", prefix="validate_upload_") + os.close(fd) + try: + blob.download_to_filename(csv_path) + except OSError as e: + logger.error( + "GCS download_to_filename failed for %r temp_path=%r errno=%s strerror=%s", + file_name, + csv_path, + e.errno, + e.strerror, + exc_info=True, + ) + _unlink_if_exists(csv_path) + raise + return csv_path + + def rename_file( - bucket_name, - file_name, - new_file_name, -): + bucket_name: str, + file_name: str, + new_file_name: str, +) -> None: """Moves a blob from one bucket to another with a new name.""" storage_client = storage.Client() source_bucket = storage_client.bucket(bucket_name) @@ -323,32 +362,162 @@ def validate_file( allowed_schemas: list[str], base_schema: dict, inst_schema: Optional[Dict[Any, Any]] = None, + institution_id: str = "pdp", + institution_identifier: Optional[str] = None, ) -> List[str]: - """Validate that a file is one of the allowed schemas.""" + """Validate that a file conforms to one of the allowed schemas. + + On success: archives the original to raw/{file_name}, writes the normalized + (canonical columns, coerced dtypes) DataFrame to validated/{file_name}, and + deletes from unvalidated/. Downstream uses validated/ only; raw/ is kept for record. + + Args: + bucket_name: GCS bucket name. + file_name: Blob name under unvalidated/. + allowed_schemas: List of schema/model names allowed. + base_schema: Base schema dict. + inst_schema: Optional extension schema with institutions.* blocks. + institution_id: Key into inst_schema["institutions"]: "edvise", "pdp", + or "legacy" (any-format uploads). Default "pdp". + institution_identifier: Optional institution ID (e.g. UUID). Reserved for + future use; Edvise uses JSON-based validation only (different shape). + + Returns: + List of inferred schema names (e.g. ["STUDENT"]). + + Raises: + ValueError: If file not in unvalidated/, validated/ already exists, or + normalized_df was not returned. + HardValidationError: If validation fails (propagated from validator). + """ + if not file_name or not file_name.strip(): + raise ValueError("file_name is required and must be non-empty.") + if "/" in file_name: + raise ValueError("file_name must not contain '/'.") + if not allowed_schemas: + raise ValueError("allowed_schemas must not be empty.") + client = storage.Client() bucket = client.bucket(bucket_name) blob = bucket.blob(f"unvalidated/{file_name}") - new_blob_name = f"validated/{file_name}" - schems: List[str] = [] + if not blob.exists(): + raise ValueError( + f"File not found: unvalidated/{file_name}. " + "Upload the file to unvalidated/ before validating." + ) + + inferred_schema_names, normalized_df = ( + self._run_validation_and_get_normalized_df( + blob, + file_name, + allowed_schemas, + base_schema, + inst_schema, + institution_id, + institution_identifier, + ) + ) + if normalized_df is None: + raise ValueError( + "Validation succeeded but normalized_df was not returned; " + "cannot write validated output (e.g. empty schema list)." + ) + + validated_blob_name = f"validated/{file_name}" + validated_blob = bucket.blob(validated_blob_name) + if validated_blob.exists(): + raise ValueError(validated_blob_name + ": File already exists.") + + self._archive_raw_and_write_validated(bucket, blob, file_name, normalized_df) + return inferred_schema_names + + def _archive_raw_and_write_validated( + self, + bucket: Any, + blob: Any, + file_name: str, + normalized_df: pd.DataFrame, + ) -> None: + """Copy blob to raw/, write normalized DataFrame to validated/, delete from unvalidated/.""" + raw_blob_name = f"raw/{file_name}" + validated_blob_name = f"validated/{file_name}" + bucket.copy_blob(blob, bucket, raw_blob_name) + logging.debug("Archived original to %s", raw_blob_name) + self._write_dataframe_to_gcs_as_csv(bucket, validated_blob_name, normalized_df) + logging.debug("Wrote normalized data to %s", validated_blob_name) + blob.delete() + logging.debug("Validation complete: validated=normalized, raw=archived") + + def _run_validation_and_get_normalized_df( + self, + blob: Any, + file_name: str, + allowed_schemas: list[str], + base_schema: dict, + inst_schema: Optional[Dict[Any, Any]], + institution_id: str, + institution_identifier: Optional[str], + ) -> tuple[List[str], Any]: + """Run validation on blob content; return inferred schema names and normalized DataFrame.""" + local_csv_path: Optional[str] = None + try: + local_csv_path = _download_blob_to_temp_csv_path(blob, file_name) + result = validate_file_reader( + local_csv_path, + allowed_schemas, + base_schema, + inst_schema, + institution_id=institution_id, + institution_identifier=institution_identifier, + ) + inferred_schema_names = [str(s) for s in result.get("schemas", [])] + logging.debug( + "Validation successful for %s: %s", file_name, inferred_schema_names + ) + return inferred_schema_names, result.get("normalized_df") + except HardValidationError: + raise + except (ValueError, UnicodeError) as e: + logging.exception("Validation failed for %s: %s", file_name, e) + raise + except Exception as e: + # Log any other error with context before re-raising (no silent failures). + logging.exception("Validation failed for %s: %s", file_name, e) + raise + finally: + _unlink_if_exists(local_csv_path) + + def _write_dataframe_to_gcs_as_csv( + self, bucket: Any, blob_name: str, normalized_df: pd.DataFrame + ) -> None: + """Write a DataFrame to GCS as UTF-8 CSV. Used for validated/ output.""" + fd, local_csv_path = tempfile.mkstemp(suffix=".csv", prefix="validated_out_") + os.close(fd) try: - with blob.open("r") as file: - schemas = validate_file_reader( - file, allowed_schemas, base_schema, inst_schema + try: + normalized_df.to_csv( + local_csv_path, + index=False, + encoding="utf-8", + lineterminator="\n", ) - schems = [str(s) for s in schemas.get("schemas", [])] - logging.debug( - f"If you see this file validation was successful {schems}" + except OSError as e: + logger.error( + "to_csv failed for validated blob %r temp_path=%r errno=%s strerror=%s", + blob_name, + local_csv_path, + e.errno, + e.strerror, + exc_info=True, ) - except Exception as e: - blob.delete() - raise e - new_blob = bucket.blob(new_blob_name) - if new_blob.exists(): - raise ValueError(new_blob_name + ": File already exists.") - bucket.copy_blob(blob, bucket, new_blob_name) - blob.delete() - logging.debug("If you see this file validation was complete") - return schems + raise + blob = bucket.blob(blob_name) + blob.upload_from_filename( + local_csv_path, + content_type="text/csv; charset=utf-8", + ) + finally: + _unlink_if_exists(local_csv_path) def get_file_contents(self, bucket_name: str, file_name: str) -> Any: """Returns a file as a bytes object.""" @@ -357,3 +526,28 @@ def get_file_contents(self, bucket_name: str, file_name: str) -> Any: blob = bucket.blob(file_name) res = blob.download_as_bytes() return res + + def read_csv_as_dataframe(self, bucket_name: str, file_name: str) -> Any: + """Read a CSV file from GCS and return as pandas DataFrame. + + Args: + bucket_name: GCS bucket name + file_name: Full blob path (e.g., 'validated/filename.csv') + + Returns: + pandas DataFrame + + Raises: + ValueError: If bucket or file not found + """ + import pandas as pd + + storage_client = storage.Client() + bucket = storage_client.get_bucket(bucket_name) + blob = bucket.blob(file_name) + + if not blob.exists(): + raise ValueError(f"File not found: {file_name}") + + with blob.open("r") as fh: + return pd.read_csv(fh) diff --git a/src/webapp/gcsutil_test.py b/src/webapp/gcsutil_test.py new file mode 100644 index 00000000..df1d4ee6 --- /dev/null +++ b/src/webapp/gcsutil_test.py @@ -0,0 +1,473 @@ +"""Tests for gcsutil.StorageControl validation and normalized/raw archive flow.""" + +import errno +import os +import tempfile +from typing import Any +from unittest.mock import MagicMock, patch + +import pandas as pd +import pytest + +from src.webapp.gcsutil import StorageControl +from src.webapp.validation import HardValidationError + + +# --------------------------------------------------------------------------- # +# validate_file: input validation +# --------------------------------------------------------------------------- # + + +def test_validate_file_raises_on_empty_file_name() -> None: + """Rejects empty file_name with clear ValueError.""" + control = StorageControl() + with pytest.raises(ValueError, match="file_name is required and must be non-empty"): + control.validate_file( + bucket_name="test-bucket", + file_name="", + allowed_schemas=["STUDENT"], + base_schema={}, + ) + + +def test_validate_file_raises_on_whitespace_only_file_name() -> None: + """Rejects whitespace-only file_name.""" + control = StorageControl() + with pytest.raises(ValueError, match="file_name is required and must be non-empty"): + control.validate_file( + bucket_name="test-bucket", + file_name=" ", + allowed_schemas=["STUDENT"], + base_schema={}, + ) + + +def test_validate_file_raises_on_file_name_with_slash() -> None: + """Rejects file_name containing '/'.""" + control = StorageControl() + with pytest.raises(ValueError, match="file_name must not contain"): + control.validate_file( + bucket_name="test-bucket", + file_name="path/to/file.csv", + allowed_schemas=["STUDENT"], + base_schema={}, + ) + + +def test_validate_file_raises_on_empty_allowed_schemas() -> None: + """Rejects empty allowed_schemas.""" + control = StorageControl() + with pytest.raises(ValueError, match="allowed_schemas must not be empty"): + control.validate_file( + bucket_name="test-bucket", + file_name="cohort.csv", + allowed_schemas=[], + base_schema={}, + ) + + +# --------------------------------------------------------------------------- # +# validate_file: blob exists and validated-already-exists +# --------------------------------------------------------------------------- # + + +def test_validate_file_raises_when_unvalidated_blob_not_found() -> None: + """Raises ValueError with clear message when file not in unvalidated/.""" + mock_bucket = MagicMock() + mock_blob = MagicMock() + mock_blob.exists.return_value = False + mock_bucket.blob.return_value = mock_blob + + mock_client = MagicMock() + mock_client.bucket.return_value = mock_bucket + + control = StorageControl() + with patch("src.webapp.gcsutil.storage.Client", return_value=mock_client): + with pytest.raises(ValueError, match="File not found: unvalidated/cohort.csv"): + control.validate_file( + bucket_name="test-bucket", + file_name="cohort.csv", + allowed_schemas=["STUDENT"], + base_schema={}, + ) + + +def test_validate_file_raises_when_normalized_df_none() -> None: + """Raises ValueError when validation returns normalized_df None (e.g. empty schema).""" + mock_bucket = MagicMock() + mock_blob = MagicMock() + mock_blob.exists.return_value = True + mock_bucket.blob.return_value = mock_blob + + mock_client = MagicMock() + mock_client.bucket.return_value = mock_bucket + + control = StorageControl() + with patch("src.webapp.gcsutil.storage.Client", return_value=mock_client): + with patch.object( + control, + "_run_validation_and_get_normalized_df", + return_value=(["STUDENT"], None), + ): + with pytest.raises( + ValueError, + match="normalized_df was not returned", + ): + control.validate_file( + bucket_name="test-bucket", + file_name="cohort.csv", + allowed_schemas=["STUDENT"], + base_schema={}, + ) + + +def test_validate_file_raises_when_validated_blob_already_exists() -> None: + """Raises ValueError when validated/{file_name} already exists.""" + mock_bucket = MagicMock() + mock_unvalidated_blob = MagicMock() + mock_unvalidated_blob.exists.return_value = True + mock_validated_blob = MagicMock() + mock_validated_blob.exists.return_value = True + + def blob_side_effect(name: str) -> Any: + if "unvalidated" in name: + return mock_unvalidated_blob + return mock_validated_blob + + mock_bucket.blob.side_effect = blob_side_effect + + mock_client = MagicMock() + mock_client.bucket.return_value = mock_bucket + + small_df = pd.DataFrame({"a": [1], "b": [2]}) + control = StorageControl() + with patch("src.webapp.gcsutil.storage.Client", return_value=mock_client): + with patch.object( + control, + "_run_validation_and_get_normalized_df", + return_value=(["STUDENT"], small_df), + ): + with pytest.raises( + ValueError, match="validated/cohort.csv: File already exists" + ): + control.validate_file( + bucket_name="test-bucket", + file_name="cohort.csv", + allowed_schemas=["STUDENT"], + base_schema={}, + ) + + +# --------------------------------------------------------------------------- # +# validate_file: success path (archive raw, write validated, delete unvalidated) +# --------------------------------------------------------------------------- # + + +def test_validate_file_success_archives_raw_writes_validated_deletes_unvalidated() -> ( + None +): + """On success: copies to raw/, writes normalized CSV to validated/, deletes unvalidated/.""" + mock_bucket = MagicMock() + mock_unvalidated_blob = MagicMock() + mock_unvalidated_blob.exists.return_value = True + mock_validated_blob = MagicMock() + mock_validated_blob.exists.return_value = False + + def blob_side_effect(name: str) -> Any: + if "unvalidated" in name: + return mock_unvalidated_blob + return mock_validated_blob + + mock_bucket.blob.side_effect = blob_side_effect + + mock_client = MagicMock() + mock_client.bucket.return_value = mock_bucket + + small_df = pd.DataFrame({"col_a": [1, 2], "col_b": ["x", "y"]}) + uploaded_chunks: list[bytes] = [] + + def capture_validated_upload(path: str, **kwargs: Any) -> None: + with open(path, "rb") as f: + uploaded_chunks.append(f.read()) + + mock_validated_blob.upload_from_filename.side_effect = capture_validated_upload + + control = StorageControl() + with patch("src.webapp.gcsutil.storage.Client", return_value=mock_client): + with patch.object( + control, + "_run_validation_and_get_normalized_df", + return_value=(["STUDENT"], small_df), + ): + result = control.validate_file( + bucket_name="test-bucket", + file_name="cohort.csv", + allowed_schemas=["STUDENT"], + base_schema={}, + ) + + assert result == ["STUDENT"] + mock_bucket.copy_blob.assert_called_once_with( + mock_unvalidated_blob, mock_bucket, "raw/cohort.csv" + ) + mock_unvalidated_blob.delete.assert_called_once() + # _write_dataframe_to_gcs_as_csv uploads from a temp file via upload_from_filename + assert mock_bucket.blob.call_count >= 2 + mock_validated_blob.upload_from_filename.assert_called_once() + assert mock_validated_blob.upload_from_filename.call_args.kwargs[ + "content_type" + ] == ("text/csv; charset=utf-8") + assert len(uploaded_chunks) == 1 + uploaded = uploaded_chunks[0] + assert b"col_a,col_b" in uploaded + assert b"1,x" in uploaded + + +# --------------------------------------------------------------------------- # +# validate_file: HardValidationError propagates +# --------------------------------------------------------------------------- # + + +def test_validate_file_propagates_hard_validation_error() -> None: + """HardValidationError from validation is not wrapped and propagates.""" + mock_bucket = MagicMock() + mock_blob = MagicMock() + mock_blob.exists.return_value = True + mock_bucket.blob.return_value = mock_blob + + mock_client = MagicMock() + mock_client.bucket.return_value = mock_bucket + + control = StorageControl() + with patch("src.webapp.gcsutil.storage.Client", return_value=mock_client): + with patch.object( + control, + "_run_validation_and_get_normalized_df", + side_effect=HardValidationError(missing_required=["student_id"]), + ): + with pytest.raises(HardValidationError, match="student_id"): + control.validate_file( + bucket_name="test-bucket", + file_name="cohort.csv", + allowed_schemas=["STUDENT"], + base_schema={}, + ) + + +# --------------------------------------------------------------------------- # +# _run_validation_and_get_normalized_df +# --------------------------------------------------------------------------- # + + +def test_run_validation_and_get_normalized_df_returns_names_and_df() -> None: + """Returns (inferred_schema_names, normalized_df) when validation succeeds.""" + mock_blob = MagicMock() + + def download_to_path(path: str) -> None: + with open(path, "w", encoding="utf-8", newline="") as f: + f.write("foo_col,bar_col\n1,a\n2,b\n") + + mock_blob.download_to_filename.side_effect = download_to_path + + control = StorageControl() + with patch("src.webapp.gcsutil.validate_file_reader") as mock_validate: + mock_validate.return_value = { + "validation_status": "passed", + "schemas": ["STUDENT"], + "normalized_df": pd.DataFrame({"x": [1]}), + } + names, df = control._run_validation_and_get_normalized_df( + mock_blob, + "cohort.csv", + ["STUDENT"], + {}, + None, + "pdp", + None, + ) + assert names == ["STUDENT"] + assert df is not None + assert list(df.columns) == ["x"] + + +def test_run_validation_and_get_normalized_df_propagates_hard_validation_error() -> ( + None +): + """HardValidationError is re-raised without wrapping.""" + mock_blob = MagicMock() + mock_blob.download_to_filename.side_effect = lambda p: open(p, "w").close() + + control = StorageControl() + with patch( + "src.webapp.gcsutil.validate_file_reader", side_effect=HardValidationError() + ): + with pytest.raises(HardValidationError): + control._run_validation_and_get_normalized_df( + mock_blob, "f.csv", ["STUDENT"], {}, None, "pdp", None + ) + + +def test_run_validation_and_get_normalized_df_propagates_value_error() -> None: + """ValueError from validate_file_reader (e.g. encoding) is re-raised.""" + mock_blob = MagicMock() + mock_blob.download_to_filename.side_effect = lambda p: open(p, "w").close() + + control = StorageControl() + with patch( + "src.webapp.gcsutil.validate_file_reader", + side_effect=ValueError("Invalid file format"), + ): + with pytest.raises(ValueError, match="Invalid file format"): + control._run_validation_and_get_normalized_df( + mock_blob, "f.csv", ["STUDENT"], {}, None, "pdp", None + ) + + +def test_run_validation_and_get_normalized_df_propagates_unicode_error() -> None: + """UnicodeError from validate_file_reader (e.g. decode) is re-raised.""" + mock_blob = MagicMock() + mock_blob.download_to_filename.side_effect = lambda p: open(p, "w").close() + + control = StorageControl() + with patch( + "src.webapp.gcsutil.validate_file_reader", + side_effect=UnicodeDecodeError("utf-8", b"x", 0, 1, "invalid"), + ): + with pytest.raises(UnicodeDecodeError): + control._run_validation_and_get_normalized_df( + mock_blob, "f.csv", ["STUDENT"], {}, None, "pdp", None + ) + + +# --------------------------------------------------------------------------- # +# _write_dataframe_to_gcs_as_csv +# --------------------------------------------------------------------------- # + + +def test_write_dataframe_to_gcs_as_csv_uploads_utf8_csv() -> None: + """Writes DataFrame as UTF-8 CSV with correct content_type.""" + mock_blob = MagicMock() + mock_bucket = MagicMock() + mock_bucket.blob.return_value = mock_blob + + df = pd.DataFrame({"A": [1, 2], "B": ["a", "b"]}) + + def assert_csv_at_path(path: str, **kwargs: Any) -> None: + with open(path, "r", encoding="utf-8") as f: + assert f.read().strip() == "A,B\n1,a\n2,b" + + mock_blob.upload_from_filename.side_effect = assert_csv_at_path + + control = StorageControl() + control._write_dataframe_to_gcs_as_csv(mock_bucket, "validated/out.csv", df) + + mock_bucket.blob.assert_called_once_with("validated/out.csv") + mock_blob.upload_from_filename.assert_called_once() + assert mock_blob.upload_from_filename.call_args.kwargs["content_type"] == ( + "text/csv; charset=utf-8" + ) + + +def test_run_validation_download_oserror_unlinks_temp_and_skips_validate() -> None: + """If GCS download fails, temp file is removed and validate_file_reader is not run.""" + fd, real_path = tempfile.mkstemp(suffix=".csv", prefix="test_dl_oserr_") + mock_blob = MagicMock() + mock_blob.download_to_filename.side_effect = OSError( + errno.ENOSPC, "No space left on device" + ) + + control = StorageControl() + with patch("src.webapp.gcsutil.tempfile.mkstemp", return_value=(fd, real_path)): + with patch("src.webapp.gcsutil.validate_file_reader") as mock_validate: + with pytest.raises(OSError, match="No space left"): + control._run_validation_and_get_normalized_df( + mock_blob, + "school_course.csv", + ["STUDENT"], + {}, + None, + "pdp", + None, + ) + mock_validate.assert_not_called() + + assert not os.path.exists(real_path) + mock_blob.download_to_filename.assert_called_once_with(real_path) + + +def test_run_validation_download_oserror_logs_errno() -> None: + """OSError from download_to_filename is logged with errno before re-raise.""" + fd, real_path = tempfile.mkstemp(suffix=".csv", prefix="test_dl_log_") + mock_blob = MagicMock() + mock_blob.download_to_filename.side_effect = OSError( + errno.ENOSPC, "No space left on device" + ) + + control = StorageControl() + with patch("src.webapp.gcsutil.tempfile.mkstemp", return_value=(fd, real_path)): + with patch("src.webapp.gcsutil.logger") as mock_logger: + with pytest.raises(OSError): + control._run_validation_and_get_normalized_df( + mock_blob, + "f.csv", + ["STUDENT"], + {}, + None, + "pdp", + None, + ) + mock_logger.error.assert_called_once() + msg = mock_logger.error.call_args[0][0] + assert "download_to_filename failed" in msg + assert mock_logger.error.call_args[0][3] == errno.ENOSPC + + assert not os.path.exists(real_path) + + +def test_write_dataframe_to_csv_oserror_unlinks_temp() -> None: + """If to_csv fails (e.g. disk full), temp file is removed and upload is not attempted.""" + fd, real_path = tempfile.mkstemp(suffix=".csv", prefix="test_csv_oserr_") + mock_blob = MagicMock() + mock_bucket = MagicMock() + mock_bucket.blob.return_value = mock_blob + + control = StorageControl() + with patch("src.webapp.gcsutil.tempfile.mkstemp", return_value=(fd, real_path)): + with patch.object( + pd.DataFrame, + "to_csv", + side_effect=OSError(errno.ENOSPC, "No space left on device"), + ): + with patch("src.webapp.gcsutil.logger") as mock_logger: + with pytest.raises(OSError, match="No space left"): + control._write_dataframe_to_gcs_as_csv( + mock_bucket, + "validated/out.csv", + pd.DataFrame({"a": [1]}), + ) + mock_logger.error.assert_called_once() + assert "to_csv failed" in mock_logger.error.call_args[0][0] + assert mock_logger.error.call_args[0][3] == errno.ENOSPC + + assert not os.path.exists(real_path) + mock_blob.upload_from_filename.assert_not_called() + + +def test_write_dataframe_upload_failure_still_unlinks_temp() -> None: + """If GCS upload fails after to_csv, the local temp file is still deleted.""" + fd, real_path = tempfile.mkstemp(suffix=".csv", prefix="test_upload_fail_") + mock_blob = MagicMock() + mock_blob.upload_from_filename.side_effect = RuntimeError("upload failed") + mock_bucket = MagicMock() + mock_bucket.blob.return_value = mock_blob + + control = StorageControl() + with patch("src.webapp.gcsutil.tempfile.mkstemp", return_value=(fd, real_path)): + with pytest.raises(RuntimeError, match="upload failed"): + control._write_dataframe_to_gcs_as_csv( + mock_bucket, + "validated/out.csv", + pd.DataFrame({"x": [1]}), + ) + + assert not os.path.exists(real_path) diff --git a/src/webapp/main.py b/src/webapp/main.py index acd8a805..4a5773ff 100644 --- a/src/webapp/main.py +++ b/src/webapp/main.py @@ -1,7 +1,7 @@ """Main file for the SST API.""" import logging -from typing import Any, Annotated +from typing import Any, Annotated, Optional, cast from datetime import timedelta import secrets from fastapi import FastAPI, Depends, HTTPException, status, Security @@ -9,7 +9,7 @@ from pydantic import BaseModel from sqlalchemy.future import select from sqlalchemy import update -from .routers import models, users, data, institutions +from .routers import models, users, data, institutions, front_end_tables from .database import ( setup_db, db_engine, @@ -58,6 +58,7 @@ app.include_router(models.router) app.include_router(users.router) app.include_router(data.router) +app.include_router(front_end_tables.router) class SelfInfo(BaseModel): @@ -95,7 +96,9 @@ def read_root() -> Any: @app.post("/token-from-api-key") async def access_token_from_api_key( sql_session: Annotated[Session, Depends(get_session)], - api_key_enduser_tuple: str = Security(get_api_key), + api_key_enduser_tuple: tuple[str, Optional[str], Optional[str]] = Security( + get_api_key + ), ) -> Token: """Generate a token from an API key.""" local_session.set(sql_session) @@ -110,10 +113,11 @@ async def access_token_from_api_key( ) access_token_expires = timedelta( - minutes=int(env_vars["ACCESS_TOKEN_EXPIRE_MINUTES"]) + minutes=int(cast(str, env_vars["ACCESS_TOKEN_EXPIRE_MINUTES"])) ) access_token = create_access_token( - data={"sub": user.email}, expires_delta=access_token_expires + data={"sub": user.email}, # type: ignore + expires_delta=access_token_expires, # type: ignore ) return Token(access_token=access_token, token_type="bearer") @@ -122,7 +126,7 @@ async def access_token_from_api_key( async def read_cross_inst_users( current_user: Annotated[BaseUser, Depends(get_current_active_user)], sql_session: Annotated[Session, Depends(get_session)], -): +) -> Any: """Get users that don't have institution specifications. (datakinders or people who haven't set their institution yet).""" if not current_user.is_datakinder(): @@ -140,7 +144,7 @@ async def read_cross_inst_users( ) .all() ) - res = [] + res: list = [] if not query_result or len(query_result) == 0: return res @@ -186,7 +190,7 @@ async def set_datakinders( .all() ) - res = [] + res: list = [] if not query_result: return res for elem in query_result: @@ -251,7 +255,7 @@ async def generate_api_key( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Database write of the API key duplicate entries.", ) - return { + return { # type: ignore "access_type": query_result[0][0].access_type, "key": generated_key_value, "inst_id": ( diff --git a/src/webapp/main_test.py b/src/webapp/main_test.py index 9c6e99e3..23f1373f 100644 --- a/src/webapp/main_test.py +++ b/src/webapp/main_test.py @@ -1,6 +1,7 @@ """Test file for the main.py file and constituent API functions.""" import json +from typing import Generator import pytest from fastapi.testclient import TestClient import sqlalchemy @@ -32,8 +33,6 @@ def session_fixture(): """Unit test database setup.""" engine = sqlalchemy.create_engine( "sqlite://", - echo=True, - echo_pool="debug", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) @@ -103,7 +102,9 @@ def session_fixture(): @pytest.fixture(name="client") -def client_fixture(session: sqlalchemy.orm.Session): +def client_fixture( + session: sqlalchemy.orm.Session, +) -> Generator[TestClient, None, None]: """Unit test mocks setup for DATAKINDER type.""" def get_session_override(): @@ -121,7 +122,9 @@ def get_current_active_user_override(): @pytest.fixture(name="user_client") -def user_client_fixture(session: sqlalchemy.orm.Session): +def user_client_fixture( + session: sqlalchemy.orm.Session, +) -> Generator[TestClient, None, None]: """Unit test mocks setup for non-Datakinder type.""" def get_session_override(): @@ -138,13 +141,13 @@ def get_current_active_user_override(): app.dependency_overrides.clear() -def test_get_root(client: TestClient): +def test_get_root(client: TestClient) -> None: """Test GET /.""" response = client.get("/") assert response.status_code == 200 -def test_retrieve_token_gen_from_api_key(client: TestClient): +def test_retrieve_token_gen_from_api_key(client: TestClient) -> None: """Test POST /token-from-api-key.""" response = client.post( "/token-from-api-key", @@ -154,7 +157,7 @@ def test_retrieve_token_gen_from_api_key(client: TestClient): assert response.json()["token_type"] == "bearer" -def test_get_cross_isnt_users(client: TestClient): +def test_get_cross_isnt_users(client: TestClient) -> None: """Test GET /non_inst_users.""" response = client.get("/non-inst-users") assert response.status_code == 200 @@ -176,14 +179,14 @@ def test_get_cross_isnt_users(client: TestClient): ] -def test_set_datakinders(client: TestClient): +def test_set_datakinders(client: TestClient) -> None: """Test POST /datakinders.""" response = client.post("/datakinders", json=["new_dk@example.com"]) assert response.status_code == 200 assert response.json() == ["new_dk@example.com"] -def test_check_self_datakinder(client: TestClient): +def test_check_self_datakinder(client: TestClient) -> None: """Test GET /check_self.""" response = client.get("/check-self") assert response.status_code == 200 @@ -195,7 +198,7 @@ def test_check_self_datakinder(client: TestClient): } -def test_check_self(user_client: TestClient): +def test_check_self(user_client: TestClient) -> None: """Test GET /check_self.""" response = user_client.get("/check-self") assert response.status_code == 200 diff --git a/src/webapp/routers/data.py b/src/webapp/routers/data.py index 36079908..88092ee2 100644 --- a/src/webapp/routers/data.py +++ b/src/webapp/routers/data.py @@ -2,23 +2,24 @@ import uuid from datetime import datetime, date -from databricks.sdk import WorkspaceClient -from typing import Annotated, Any, Dict, List, cast, IO, Optional +from typing import Annotated, Any, Dict, List, Optional, Tuple, Union, cast from pydantic import BaseModel, Field -from fastapi import APIRouter, Depends, HTTPException, status, Response, Query -from fastapi.responses import FileResponse +from fastapi import APIRouter, Depends, HTTPException, status, Response from sqlalchemy import and_, or_ from sqlalchemy.orm import Session from sqlalchemy.future import select import os import logging from sqlalchemy.exc import IntegrityError -from ..config import databricks_vars, env_vars, gcs_vars -import tempfile -import pathlib +import re +from ..validation import HardValidationError +from ..validation_error_formatter import format_validation_error +import pandas as pd +from cachetools import TTLCache from ..utilities import ( has_access_to_inst_or_err, + has_at_most_one_school_type, has_full_data_access_or_err, BaseUser, model_owner_and_higher_or_err, @@ -28,7 +29,6 @@ DataSource, get_external_bucket_name, decode_url_piece, - databricksify_inst_name, ) from ..database import ( @@ -37,6 +37,8 @@ BatchTable, FileTable, InstTable, + JobTable, + ModelTable, SchemaRegistryTable, DocType, ) @@ -45,12 +47,19 @@ from ..gcsdbutils import update_db_from_bucket from ..gcsutil import StorageControl +from ..config import env_vars +from edvise.data_audit.eda import EdaSummary # Set the logging logging.basicConfig(format="%(asctime)s [%(levelname)s]: %(message)s") logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) +# Cache for EDA data - TTL of 10 minutes (600 seconds) +# Cache key format: f"{inst_id}:{batch_id}" +EDA_CACHE_TTL = int(os.getenv("EDA_CACHE_TTL", "600")) # Default 10 minutes +EDA_CACHE: Any = TTLCache(maxsize=64, ttl=EDA_CACHE_TTL) + router = APIRouter( prefix="/institutions", tags=["data"], @@ -471,6 +480,246 @@ def read_batch_info( return {"batches": [batch_info], "files": data_infos} +## EDA (Exploratory Data Analysis) Endpoints +class SummaryStats(BaseModel): + """Summary statistics for the EDA dashboard.""" + + total_students: int + transfer_students: int + avg_year1_gpa_all_students: float + + +class SummaryMetric(BaseModel): + """A named metric with a single value (e.g. for EDA summary stats).""" + + name: str + value: Union[int, float] + + +class GpaSeriesData(BaseModel): + """GPA data series for a chart.""" + + name: str + data: List[Optional[float]] + + +class GpaChartData(BaseModel): + """GPA chart data with cohort years and series.""" + + cohort_years: List[str] + series: List[GpaSeriesData] + min_gpa: Optional[float] = None + + +class TermCountPct(BaseModel): + count: int + percentage: float + name: str + + +class YearTermSummary(BaseModel): + year: str + total: int + terms: List[TermCountPct] + + +class StudentsByCohortTerm(BaseModel): + years: List[str] + by_year: List[YearTermSummary] + + +class TermChartData(BaseModel): + """Chart-ready term data: cohort_years, terms.""" + + cohort_years: List[str] + terms: List[Dict[str, Any]] + + +class EdaDataResponse(BaseModel): + """EDA API response: summary metrics, GPA/time-series data, and category+series blobs.""" + + total_students: Optional[SummaryMetric] = None + transfer_students: Optional[SummaryMetric] = None + avg_year1_gpa_all_students: Optional[SummaryMetric] = None + gpa_by_enrollment_type: Optional[GpaChartData] = None + gpa_by_enrollment_intensity: Optional[GpaChartData] = None + students_by_cohort_term: Optional[StudentsByCohortTerm] = None + course_enrollments: Optional[StudentsByCohortTerm] = None + degree_types: Optional[Dict[str, Any]] = ( + None # { "total": int, "degrees": [{ "count", "percentage", "name" }, ...] } + ) + enrollment_type_by_intensity: Optional[Dict[str, Any]] = None + pell_recipient_status: Optional[Dict[str, Any]] = None + pell_recipient_by_first_gen: Optional[Dict[str, Any]] = None + student_age_by_gender: Optional[Dict[str, Any]] = None + race_by_pell_status: Optional[Dict[str, Any]] = None + + @classmethod + def from_eda_summary(cls, eda: Any) -> "EdaDataResponse": + return cls( + total_students=eda.total_students, + transfer_students=eda.transfer_students, + avg_year1_gpa_all_students=eda.avg_year1_gpa_all_students, + gpa_by_enrollment_type=eda.gpa_by_enrollment_type, + gpa_by_enrollment_intensity=eda.gpa_by_enrollment_intensity, + students_by_cohort_term=eda.students_by_cohort_term, + course_enrollments=eda.course_enrollments, + degree_types=eda.degree_types, + enrollment_type_by_intensity=eda.enrollment_type_by_intensity, + pell_recipient_status=eda.pell_recipient_status, + pell_recipient_by_first_gen=eda.pell_recipient_by_first_gen, + student_age_by_gender=eda.student_age_by_gender, + race_by_pell_status=eda.race_by_pell_status, + ) + + +def read_batch_files_as_dataframes( + inst_id: str, + batch_files: Any, # Set[FileTable] + storage_control: StorageControl, +) -> Dict[str, pd.DataFrame]: + """Read CSV files from a batch and return as DataFrames. + + Args: + inst_id: Institution ID + batch_files: Set of FileTable objects from the batch + storage_control: StorageControl instance for GCS access + + Returns: + Dictionary mapping schema_type -> pandas.DataFrame + + Raises: + HTTPException: If no valid files found + """ + bucket_name = get_external_bucket_name(inst_id) + + # Temporary storage: file_record -> DataFrame + loaded_files: Dict[Any, pd.DataFrame] = {} + missing_files: List[str] = [] + + for file_record in batch_files: + file_name = file_record.name + + # Skip SST-generated output files (only process input files) + if file_record.sst_generated: + logger.debug(f"Skipping SST-generated file: {file_name}") + continue + + df = None + + # Read from GCS + try: + blob_path = f"validated/{file_name}" + df = storage_control.read_csv_as_dataframe(bucket_name, blob_path) + logger.info(f"Loaded {file_name} from GCS ({len(df)} rows)") + except ValueError as e: + logger.warning(f"File not found in GCS: {e}") + missing_files.append(file_name) + except Exception as e: + logger.error(f"Failed to read from GCS: {e}") + missing_files.append(file_name) + + if df is not None: + loaded_files[file_record] = df + + if not loaded_files: + error_msg = f"No valid input files found in batch (checked GCS: {bucket_name}/validated/)" + if missing_files: + error_msg += f". Expected files not found: {', '.join(missing_files[:5])}" + if len(missing_files) > 5: + error_msg += f" (and {len(missing_files) - 5} more)" + error_msg += ( + ". Files must be uploaded and validated before they can be used for EDA." + ) + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=error_msg, + ) + + # Group by schema type and combine DataFrames + schema_dataframes: Dict[str, List[pd.DataFrame]] = {} + for file_record, df in loaded_files.items(): + for schema in file_record.schemas: + if schema not in schema_dataframes: + schema_dataframes[schema] = [] + schema_dataframes[schema].append(df) + + result = {} + for schema, dfs in schema_dataframes.items(): + if len(dfs) == 1: + result[schema] = dfs[0] + else: + result[schema] = pd.concat(dfs, ignore_index=True) + logger.info( + f"Combined {len(dfs)} files for schema {schema} ({len(result[schema])} total rows)" + ) + + return result + + +@router.get("/{inst_id}/batch/{batch_id}/eda", response_model=EdaDataResponse) +def get_eda_data( + inst_id: str, + batch_id: str, + current_user: Annotated[BaseUser, Depends(get_current_active_user)], + sql_session: Annotated[Session, Depends(get_session)], + storage_control: Annotated[StorageControl, Depends(StorageControl)], +) -> Any: + """Returns EDA (Exploratory Data Analysis) data for a specific batch. + + This endpoint provides all the data needed to populate the EDA dashboard, + including summary statistics, GPA charts, enrollment data, and demographic breakdowns. + Analyzes all files in the batch together to provide comprehensive insights. + """ + has_access_to_inst_or_err(inst_id, current_user) + has_full_data_access_or_err(current_user, "EDA data") + local_session.set(sql_session) + + batch_result = ( + local_session.get() + .execute( + select(BatchTable).where( + and_( + BatchTable.id == str_to_uuid(batch_id), + BatchTable.inst_id == str_to_uuid(inst_id), + ) + ) + ) + .scalar_one_or_none() + ) + if batch_result is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Batch not found.", + ) + + cache_key = f"{inst_id}:{batch_id}" + cached_result = EDA_CACHE.get(cache_key) + if cached_result is not None: + logger.debug(f"EDA cache hit for {cache_key}") + return cached_result + logger.debug(f"EDA cache miss for {cache_key}, computing...") + + file_dataframes = read_batch_files_as_dataframes( + inst_id, batch_result.files, storage_control + ) + df_cohort = file_dataframes.get("STUDENT") + if df_cohort is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No STUDENT schema files found in batch for EDA.", + ) + + eda = EdaSummary( + df_cohort=df_cohort, + df_course=file_dataframes.get("COURSE"), + ) + result = EdaDataResponse.from_eda_summary(eda) + EDA_CACHE[cache_key] = result + + return result + + @router.post("/{inst_id}/batch", response_model=BatchInfo) def create_batch( inst_id: str, @@ -502,6 +751,7 @@ def create_batch( ) f_names = [] if not req.file_names else req.file_names f_ids = [] if not req.file_ids else strs_to_uuids(req.file_ids) + print(f"File names: {f_names}, File Ids: {f_ids}") # Check that the files requested for this batch exists. # Only valid non-sst generated files can be added to a batch at creation time. query_result_files = ( @@ -995,184 +1245,257 @@ def download_url_inst_file( ) -def infer_models_from_filename(file_path: str, institution_id: str) -> List[str]: - name = os.path.basename(file_path).lower() - - inferred = set() - if "course" in name: - inferred.add("COURSE") - if "student" in name: - inferred.add("STUDENT") - if "semester" in name: - inferred.add("SEMESTER") - if "cohort" in name: - inferred.add("STUDENT") - if "course" not in name and ("ar" in name or "deidentified" in name): - inferred.add("STUDENT") - - if not inferred: - logging.error( - ValueError( - f"Could not infer model(s) from file name: {name}, filenames sould be descriptive of the kind of data it contains e.g. course, cohort" - ) - ) - inferred.add("UNKNOWN") +class _ValidationState: + _ar_re = re.compile(r"(? Any: - """Helper function for file validation.""" - has_access_to_inst_or_err(inst_id, current_user) - if file_name.find("/") != -1: +BASE_TTL = 300 # seconds; base schema cache TTL +EXT_TTL = 120 # seconds; extension schema cache TTL + + +def _infer_allowed_schemas_from_filename(file_name: str, inst: Any) -> List[str]: + """Infer allowed schema names from file name; legacy may use any name (UNKNOWN). + + Args: + file_name: Name of the file (used for keyword inference). + inst: Institution row (must have legacy_id attr for legacy fallback). + + Returns: + Sorted list of allowed schema names (e.g. ["COURSE"], ["STUDENT"], ["UNKNOWN"]). + + Raises: + HTTPException: 422 if name is non-descriptive and institution is not legacy. + """ + name = os.path.basename(file_name).lower() + has_course = "course" in name + has_semester = "semester" in name + has_student = ( + ("student" in name) + or ("cohort" in name) + or ( + (not has_course) + and (STATE._ar_re.search(name) is not None or "deidentified" in name) + ) + ) + inferred_from_name: set[str] = set() + if has_course: + inferred_from_name.add("COURSE") + if has_student: + inferred_from_name.add("STUDENT") + if has_semester: + inferred_from_name.add("SEMESTER") + if not inferred_from_name: + if getattr(inst, "legacy_id", None): + return ["UNKNOWN"] raise HTTPException( - status_code=422, - detail="File name can't contain '/'.", + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=( + f"Could not infer model(s) from file name: {name}. " + "Filenames should be descriptive (e.g., include 'course', 'cohort', " + "'student', or 'semester')." + ), ) - local_session.set(sql_session) + return sorted(inferred_from_name) - allowed_schemas = None - if not allowed_schemas: - allowed_schemas = infer_models_from_filename(file_name, "pdp") - inferred_schemas: list[str] = [] - # ----------------------- Fetch base schema from DB ------------------------------- - base_schema = ( - local_session.get() - .execute( - select(SchemaRegistryTable.schema_id, SchemaRegistryTable.json_doc) +def _get_validation_base_schema(sess: Session) -> Tuple[Any, Any, float]: + """Return (base_schema_id, base_schema, now) using cache. + + Args: + sess: DB session for schema registry query. + + Returns: + Tuple of (base_schema_id, base_schema dict, current time.monotonic()). + + Raises: + RuntimeError: If no active base schema is registered. + """ + import time + + now = time.monotonic() + base_cache = STATE._base_cache + if now < base_cache["exp"] and base_cache["val"] is not None: + cached = base_cache["val"] + base_schema_id, base_schema = cached # pylint: disable=unpacking-non-sequence + return (base_schema_id, base_schema, now) + row = sess.execute( + select(SchemaRegistryTable.schema_id, SchemaRegistryTable.json_doc) + .where( + SchemaRegistryTable.doc_type == DocType.base, + SchemaRegistryTable.is_active.is_(True), + ) + .limit(1) + ).first() + if row is None: + raise RuntimeError("No active base schema found") + base_schema_id, base_schema = row + base_cache["exp"] = now + BASE_TTL + base_cache["val"] = (base_schema_id, base_schema) + return (base_schema_id, base_schema, now) + + +def _resolve_edvise_schema( + sess: Session, now: float +) -> Tuple[str, Optional[Dict[str, Any]]]: + """Resolve schema namespace and extension for Edvise Schema (ES) institutions.""" + schema_namespace = "edvise" + edvise_exp, edvise_doc = STATE._edvise_cache + if now < edvise_exp and edvise_doc is not None: + inst_schema: Optional[Dict[str, Any]] = edvise_doc + else: + inst_schema = sess.execute( + select(SchemaRegistryTable.json_doc) .where( - SchemaRegistryTable.doc_type == DocType.base, + SchemaRegistryTable.is_edvise.is_(True), SchemaRegistryTable.is_active.is_(True), ) .limit(1) + ).scalar_one_or_none() + STATE._edvise_cache = (now + EXT_TTL, inst_schema) + if inst_schema is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Edvise Schema (ES) not found for institution with edvise_id. " + "Please ensure an active Edvise Schema (ES) extension is registered.", ) - .first() - ) - if base_schema is None: - raise RuntimeError("No active base schema found") + return (schema_namespace, inst_schema) - base_schema_id, base_schema = base_schema - # ----------------------- Fetch inst specific extension schema from DB --------------------- - inst = ( - local_session.get() - .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) - .scalar_one_or_none() - ) - if inst is None: - raise ValueError(f"Institution {inst_id} not found") - if inst.pdp_id: # institution is PDP - inst_schema = ( - local_session.get() - .execute( +def _resolve_pdp_schema( + sess: Session, now: float +) -> Tuple[str, Optional[Dict[str, Any]]]: + """Resolve schema namespace and extension for PDP institutions.""" + schema_namespace = "pdp" + pdp_exp, pdp_doc = STATE._pdp_cache + if now < pdp_exp and pdp_doc is not None: + inst_schema: Optional[Dict[str, Any]] = pdp_doc + else: + inst_schema = cast( + Optional[Dict[str, Any]], + sess.execute( select(SchemaRegistryTable.json_doc) .where( SchemaRegistryTable.is_pdp.is_(True), SchemaRegistryTable.is_active.is_(True), ) .limit(1) - ) - .scalar_one_or_none() + ).scalar_one_or_none(), ) - updated_inst_schema: dict | None = inst_schema - else: # custom (or none) - inst_schema = ( - local_session.get() - .execute( - select(SchemaRegistryTable.json_doc) - .where( - SchemaRegistryTable.inst_id == inst.id, - SchemaRegistryTable.is_active.is_(True), - SchemaRegistryTable.doc_type == DocType.extension, # be explicit - ) - .limit(1) - ) - .scalar_one_or_none() - ) - - dbc = DatabricksControl() - schema_extension = dbc.create_custom_schema_extension( - bucket_name=get_external_bucket_name(inst_id), - inst_query=inst, - file_name=file_name, - base_schema=base_schema, - extension_schema=inst_schema, - ) - - if schema_extension is not None: - updated_inst_schema = schema_extension - try: - new_schema_extension_record = SchemaRegistryTable( - doc_type=DocType.extension, - inst_id=str_to_uuid(inst_id), - is_pdp=False, # type: ignore - version_label="1.0.0", - extends_schema_id=base_schema_id, - json_doc=schema_extension, - is_active=True, - ) - sess = local_session.get() - sess.add(new_schema_extension_record) - sess.flush() - logging.info("Schema record inserted for '%s'", inst_id) - except IntegrityError as e: - sess = local_session.get() - sess.rollback() - logging.warning("IntegrityError: %s", e) - except Exception as e: - sess = local_session.get() - sess.rollback() - logging.error("Unexpected DB error: %s", e) - raise HTTPException( - status_code=500, - detail=f"Unexpected database error while inserting file record: {e}", - ) - else: - logging.info( - "No-op: extension already contains this model for inst %s", inst_id - ) - updated_inst_schema = inst_schema + STATE._pdp_cache = (now + EXT_TTL, inst_schema) + if inst_schema is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="PDP schema not found for institution with pdp_id. " + "Please ensure an active PDP schema extension is registered.", + ) + return (schema_namespace, inst_schema) + + +def _resolve_schema_namespace_and_extension( + sess: Session, + inst: Any, + inst_id: str, + now: float, + allowed_schemas: List[str], + bucket: str, + base_schema: dict, + base_schema_id: Any, + file_name: str, +) -> Tuple[str, Optional[Dict[str, Any]]]: + """Resolve schema_namespace and updated_inst_schema by institution type (edvise/pdp/legacy).""" + pdp_id = getattr(inst, "pdp_id", None) + edvise_id = getattr(inst, "edvise_id", None) + legacy_id = getattr(inst, "legacy_id", None) + if not has_at_most_one_school_type(pdp_id, edvise_id, legacy_id): + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Institution configuration error: cannot have more than one of " + "pdp_id, edvise_id, or legacy_id set", + ) + if edvise_id: + return _resolve_edvise_schema(sess, now) + if pdp_id: + return _resolve_pdp_schema(sess, now) + if legacy_id: + return ("legacy", None) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=( + "Institution configuration error: institution has no pdp_id, edvise_id, " + "or legacy_id; cannot resolve validation schema." + ), + ) + - # ----------------------- File validation logic logic -------------------------------------- +def _run_validation_and_upsert_file_record( + bucket: str, + file_name: str, + allowed_schemas: List[str], + base_schema: dict, + updated_inst_schema: Optional[Dict[str, Any]], + schema_namespace: str, + inst_id: str, + source_str: str, + current_user: BaseUser, + storage_control: StorageControl, + sess: Session, +) -> Dict[str, Any]: + """Run storage validate_file, then upsert file record and return response dict.""" try: inferred_schemas = storage_control.validate_file( - get_external_bucket_name(inst_id), + bucket, file_name, allowed_schemas, base_schema, updated_inst_schema, + institution_id=schema_namespace, + institution_identifier=inst_id if schema_namespace == "edvise" else None, ) - logging.debug( - f"!!!!!!!!!!Inferred Schemas was successful {list(inferred_schemas)}" + except HardValidationError as e: + logging.debug("Inferred Schemas FAILED (hard) %s", e) + try: + formatted_msg = format_validation_error(e) + except Exception as format_err: + logging.warning("Error formatting validation message: %s", format_err) + parts = ["VALIDATION_FAILED"] + if e.missing_required: + parts.append(f"missing_required={e.missing_required}") + if e.extra_columns: + parts.append(f"extra_columns={e.extra_columns}") + if e.schema_errors is not None: + parts.append(f"schema_errors={e.schema_errors}") + formatted_msg = "; ".join(parts) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=formatted_msg ) except Exception as e: - logging.debug(f"!!!!!!!!!!Inferred Schemas FAILED {e}") + logging.debug("Inferred Schemas FAILED (other) %s", e) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="File type is not valid and/or not accepted by this institution: " - + str(e), - ) from e - - existing_file = ( - local_session.get() - .query(FileTable) - .filter_by( - name=file_name, - inst_id=str_to_uuid(inst_id), + detail=f"VALIDATION_ERROR: {type(e).__name__}: {e}", ) + logging.debug("Inferred Schemas success %s", list(inferred_schemas)) + existing_file = ( + sess.query(FileTable) + .filter_by(name=file_name, inst_id=str_to_uuid(inst_id)) .first() ) - + if set(inferred_schemas) != set(allowed_schemas): + logging.info( + "Filename inference %s differs from validator result %s for %s; " + "returning filename-based types to preserve API contract.", + allowed_schemas, + inferred_schemas, + file_name, + ) if existing_file: - logging.info(f"File '{file_name}' already exists for institution {inst_id}.") + logging.info("File '%s' already exists for institution %s.", file_name, inst_id) db_status = f"File '{file_name}' already exists for institution {inst_id}." else: try: @@ -1185,22 +1508,21 @@ def validation_helper( schemas=list(allowed_schemas), valid=True, ) - local_session.get().add(new_file_record) - local_session.get().flush() - logging.info(f"File record inserted for '{file_name}'") + sess.add(new_file_record) + sess.flush() + logging.info("File record inserted for '%s'", file_name) db_status = f"File record inserted for '{file_name}'" except IntegrityError as e: - local_session.get().rollback() - logging.warning(f"IntegrityError: {e}") + sess.rollback() + logging.warning("IntegrityError: %s", e) db_status = "Already exists" except Exception as e: - local_session.get().rollback() - logging.error(f"Unexpected DB error: {e}") + sess.rollback() + logging.error("Unexpected DB error: %s", e) raise HTTPException( status_code=500, detail=f"Unexpected database error while inserting file record: {e}", ) - return { "name": file_name, "inst_id": inst_id, @@ -1210,6 +1532,92 @@ def validation_helper( } +def validation_helper( + source_str: str, + inst_id: str, + file_name: str, + current_user: BaseUser, + storage_control: StorageControl, + sql_session: Session, +) -> Any: + """Run file validation for an institution and upsert the file record. + + Validates file name and institution, infers allowed schemas from filename + (or UNKNOWN for legacy when inference fails), resolves extension schema, + runs storage validation, then upserts the file record. + + Args: + source_str: Source label for the upload (e.g. MANUAL_UPLOAD). + inst_id: Institution UUID (hex string). + file_name: Name of the file (no path separators). + current_user: Authenticated user; must have access to inst_id. + storage_control: StorageControl instance for GCS and validate_file. + sql_session: DB session for institution, schema, and file record. + + Returns: + Dict with name, inst_id, file_types, source, status. + + Raises: + HTTPException: 401 if no access, 404 if institution not found or invalid id, + 422 if file name invalid or non-descriptive (non-legacy), 400 on validation failure. + """ + has_access_to_inst_or_err(inst_id, current_user) + if not file_name or not file_name.strip(): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="File name is required and must be non-empty.", + ) + if "/" in file_name: + raise HTTPException(status_code=422, detail="File name can't contain '/'.") + + local_session.set(sql_session) + sess = local_session.get() + + try: + inst = sess.execute( + select(InstTable).where(InstTable.id == str_to_uuid(inst_id)) + ).scalar_one_or_none() + except (ValueError, TypeError): + logging.warning("Invalid institution id for validation: %s", inst_id) + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Institution not found or invalid identifier.", + ) + if inst is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Institution {inst_id} not found", + ) + + allowed_schemas = _infer_allowed_schemas_from_filename(file_name, inst) + base_schema_id, base_schema, now = _get_validation_base_schema(sess) + bucket = get_external_bucket_name(inst_id) + schema_namespace, updated_inst_schema = _resolve_schema_namespace_and_extension( + sess, + inst, + inst_id, + now, + allowed_schemas, + bucket, + base_schema, + base_schema_id, + file_name, + ) + return _run_validation_and_upsert_file_record( + bucket, + file_name, + allowed_schemas, + base_schema, + updated_inst_schema, + schema_namespace, + inst_id, + source_str, + current_user, + storage_control, + sess, + ) + + @router.post( "/{inst_id}/input/validate-sftp/{file_name:path}", response_model=ValidationResult ) @@ -1272,442 +1680,85 @@ def get_upload_url( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) -## FE Inference Tables - - -# Get SHAP Values for Inference -@router.get("/{inst_id}/inference/top-features/{run_id}") -def get_inference_top_features( +@router.post("/{inst_id}/add-custom-school-job/{job_run_id}") +def add_custom_school_job( inst_id: str, - run_id: str, - current_user: Annotated[BaseUser, Depends(get_current_active_user)], + job_run_id: str, + model_name: str, sql_session: Annotated[Session, Depends(get_session)], -) -> List[dict[str, Any]]: - """Returns data for a specific institution.""" - # raise error at this level instead bc otherwise it's getting wrapped as a 200 - has_access_to_inst_or_err(inst_id, current_user) - local_session.set(sql_session) - query_result = ( - local_session.get() - .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) - .all() - ) - if not query_result or len(query_result) == 0: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Institution not found.", - ) - if len(query_result) > 1: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Institution duplicates found.", - ) - - try: - dbc = DatabricksControl() - rows = dbc.fetch_table_data( - catalog_name=env_vars["CATALOG_NAME"], # type: ignore - inst_name=f"{query_result[0][0].name}", - table_name=f"inference_{run_id}_features_with_most_impact", - warehouse_id=env_vars["SQL_WAREHOUSE_ID"], # type: ignore - ) - - return rows - except ValueError as ve: - # Return a 400 error with the specific message from ValueError - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) - - -# Get Box plot values -@router.get("/{inst_id}/inference/features-boxplot-stat/{run_id}") -def get_inference_feature_boxstats( - inst_id: str, - run_id: str, current_user: Annotated[BaseUser, Depends(get_current_active_user)], - sql_session: Annotated[Session, Depends(get_session)], - feature_name: Optional[str] = Query( - None, description="If provided, filter by this feature name" - ), -) -> List[dict[str, Any]]: - """Returns box-plot stats for an institution/run. If `feature_name` is supplied, - only rows for that feature are returned.""" - # raise error at this level instead bc otherwise it's getting wrapped as a 200 + databricks_control: Annotated[DatabricksControl, Depends(DatabricksControl)], +) -> Any: + """Fill in a JobTable .""" has_access_to_inst_or_err(inst_id, current_user) + has_full_data_access_or_err(current_user, "this model") local_session.set(sql_session) - query_result = ( - local_session.get() - .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) - .all() - ) - if not query_result or len(query_result) == 0: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Institution not found.", - ) - if len(query_result) > 1: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Institution duplicates found.", - ) - - try: - dbc = DatabricksControl() - rows = dbc.fetch_table_data( - catalog_name=env_vars["CATALOG_NAME"], # type: ignore - inst_name=f"{query_result[0][0].name}", - table_name=f"inference_{run_id}_box_plot_table", - warehouse_id=env_vars["SQL_WAREHOUSE_ID"], # type: ignore - ) - if not feature_name: - return rows - - # Helper: extract feature_name from various shapes (top-level or JSON column) - def row_feature_name(row: dict[str, Any]) -> Optional[str]: - # common case: it's a top-level column - if "feature_name" in row and row["feature_name"] is not None: - return str(row["feature_name"]) - # fallback: search any dict-valued column for a 'feature_name' key - for v in row.values(): - if ( - isinstance(v, dict) - and "feature_name" in v - and v["feature_name"] is not None - ): - return str(v["feature_name"]) - return None - - filtered = [r for r in rows if row_feature_name(r) == feature_name] - - if not filtered: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Feature '{feature_name}' not found for run_id '{run_id}'.", - ) - - return filtered - - except ValueError as ve: - # Return a 400 error with the specific message from ValueError - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) - -# Get SHAP Values for Inference -@router.get("/{inst_id}/inference/support-overview/{run_id}") -def get_inference_support_overview( - inst_id: str, - run_id: str, - current_user: Annotated[BaseUser, Depends(get_current_active_user)], - sql_session: Annotated[Session, Depends(get_session)], -) -> List[dict[str, Any]]: - """Returns a signed URL for uploading data to a specific institution.""" - # raise error at this level instead bc otherwise it's getting wrapped as a 200 - has_access_to_inst_or_err(inst_id, current_user) - local_session.set(sql_session) - query_result = ( + model_name = decode_url_piece(model_name) + inst_result = ( local_session.get() - .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) - .all() - ) - if not query_result or len(query_result) == 0: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Institution not found.", - ) - if len(query_result) > 1: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Institution duplicates found.", - ) - - try: - dbc = DatabricksControl() - rows = dbc.fetch_table_data( - catalog_name=env_vars["CATALOG_NAME"], # type: ignore - inst_name=f"{query_result[0][0].name}", - table_name=f"inference_{run_id}_support_overview", - warehouse_id=env_vars["SQL_WAREHOUSE_ID"], # type: ignore + .execute( + select(InstTable).where( + and_( + InstTable.id == str_to_uuid(inst_id), + ) + ) ) - - return rows - except ValueError as ve: - # Return a 400 error with the specific message from ValueError - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) - - -@router.get("/{inst_id}/inference/feature_importance/{run_id}") -def get_inference_feature_importance( - inst_id: str, - run_id: str, - current_user: Annotated[BaseUser, Depends(get_current_active_user)], - sql_session: Annotated[Session, Depends(get_session)], -) -> List[dict[str, Any]]: - """Returns a signed URL for uploading data to a specific institution.""" - # raise error at this level instead bc otherwise it's getting wrapped as a 200 - has_access_to_inst_or_err(inst_id, current_user) - local_session.set(sql_session) - query_result = ( - local_session.get() - .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) .all() ) - if not query_result or len(query_result) == 0: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Institution not found.", - ) - if len(query_result) > 1: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Institution duplicates found.", - ) - try: - dbc = DatabricksControl() - rows = dbc.fetch_table_data( - catalog_name=env_vars["CATALOG_NAME"], # type: ignore - inst_name=f"{query_result[0][0].name}", - table_name=f"inference_{run_id}_shap_feature_importance", - warehouse_id=env_vars["SQL_WAREHOUSE_ID"], # type: ignore - ) - - return rows - except ValueError as ve: - # Return a 400 error with the specific message from ValueError - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) - - -## FE Training Tables - - -@router.get("/{inst_id}/training/feature_importance/{run_id}") -def get_training_feature_importance( - inst_id: str, - run_id: str, - current_user: Annotated[BaseUser, Depends(get_current_active_user)], - sql_session: Annotated[Session, Depends(get_session)], -) -> List[dict[str, Any]]: - """Returns a signed URL for uploading data to a specific institution.""" - # raise error at this level instead bc otherwise it's getting wrapped as a 200 - has_access_to_inst_or_err(inst_id, current_user) - local_session.set(sql_session) query_result = ( local_session.get() - .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) - .all() - ) - if not query_result or len(query_result) == 0: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Institution not found.", - ) - if len(query_result) > 1: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Institution duplicates found.", - ) - - try: - dbc = DatabricksControl() - rows = dbc.fetch_table_data( - catalog_name=env_vars["CATALOG_NAME"], # type: ignore - inst_name=f"{query_result[0][0].name}", - table_name=f"training_{run_id}_shap_feature_importance", - warehouse_id=env_vars["SQL_WAREHOUSE_ID"], # type: ignore + .execute( + select(ModelTable).where( + and_( + ModelTable.name == model_name, + ModelTable.inst_id == str_to_uuid(inst_id), + ) + ) ) - - return rows - except ValueError as ve: - # Return a 400 error with the specific message from ValueError - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) - - -@router.get("/{inst_id}/training/confusion_matrix/{run_id}") -def get_training_confusion_matrix( - inst_id: str, - run_id: str, - current_user: Annotated[BaseUser, Depends(get_current_active_user)], - sql_session: Annotated[Session, Depends(get_session)], -) -> List[dict[str, Any]]: - """Returns a signed URL for uploading data to a specific institution.""" - # raise error at this level instead bc otherwise it's getting wrapped as a 200 - has_access_to_inst_or_err(inst_id, current_user) - local_session.set(sql_session) - query_result = ( - local_session.get() - .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) .all() ) - if not query_result or len(query_result) == 0: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Institution not found.", - ) - if len(query_result) > 1: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Institution duplicates found.", - ) - - try: - dbc = DatabricksControl() - rows = dbc.fetch_table_data( - catalog_name=env_vars["CATALOG_NAME"], # type: ignore - inst_name=f"{query_result[0][0].name}", - table_name=f"training_{run_id}_confusion_matrix", - warehouse_id=env_vars["SQL_WAREHOUSE_ID"], # type: ignore - ) - - return rows - except ValueError as ve: - # Return a 400 error with the specific message from ValueError - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) - -@router.get("/{inst_id}/training/roc_curve/{run_id}") -def get_training_roc_curve( - inst_id: str, - run_id: str, - current_user: Annotated[BaseUser, Depends(get_current_active_user)], - sql_session: Annotated[Session, Depends(get_session)], -) -> List[dict[str, Any]]: - """Returns a signed URL for uploading data to a specific institution.""" - # raise error at this level instead bc otherwise it's getting wrapped as a 200 - has_access_to_inst_or_err(inst_id, current_user) - local_session.set(sql_session) - query_result = ( - local_session.get() - .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) - .all() - ) - if not query_result or len(query_result) == 0: + if not inst_result or not query_result: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail="Institution not found.", - ) - if len(query_result) > 1: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Institution duplicates found.", + detail="Institution or model does not exist.", ) try: - dbc = DatabricksControl() - rows = dbc.fetch_table_data( - catalog_name=env_vars["CATALOG_NAME"], # type: ignore - inst_name=f"{query_result[0][0].name}", - table_name=f"training_{run_id}_roc_curve", - warehouse_id=env_vars["SQL_WAREHOUSE_ID"], # type: ignore - ) - - return rows - except ValueError as ve: - # Return a 400 error with the specific message from ValueError - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) + triggered_timestamp = datetime.now() - -@router.get("/{inst_id}/training/support-overview/{run_id}") -def get_training_support_overview( - inst_id: str, - run_id: str, - current_user: Annotated[BaseUser, Depends(get_current_active_user)], - sql_session: Annotated[Session, Depends(get_session)], -) -> List[dict[str, Any]]: - """Returns a signed URL for uploading data to a specific institution.""" - # raise error at this level instead bc otherwise it's getting wrapped as a 200 - has_access_to_inst_or_err(inst_id, current_user) - local_session.set(sql_session) - query_result = ( - local_session.get() - .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) - .all() - ) - if not query_result or len(query_result) == 0: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Institution not found.", - ) - if len(query_result) > 1: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Institution duplicates found.", + latest_model_version = databricks_control.fetch_model_version( + catalog_name=str(env_vars["CATALOG_NAME"]), + inst_name=inst_result[0][0].name, + model_name=model_name, ) - try: - dbc = DatabricksControl() - rows = dbc.fetch_table_data( - catalog_name=env_vars["CATALOG_NAME"], # type: ignore - inst_name=f"{query_result[0][0].name}", - table_name=f"training_{run_id}_support_overview", - warehouse_id=env_vars["SQL_WAREHOUSE_ID"], # type: ignore + job = JobTable( + id=job_run_id, + triggered_at=triggered_timestamp, + created_by=str_to_uuid(current_user.user_id), + batch_name=f"{model_name}_{triggered_timestamp}", # update later when we figure out how to add batches to custom jobs + output_filename=f"{job_run_id}/inference_output.csv", + model_id=query_result[0][0].id, + output_valid=True, + completed=True, + model_version=latest_model_version.version, + model_run_id=latest_model_version.run_id, ) + local_session.get().add(job) - return rows + return { + "inst_id": inst_id, + "m_name": model_name, + "run_id": job_run_id, + "output_filename": f"{job_run_id}/inference_output.csv", + "model_version": latest_model_version.version, + "model_run_id": latest_model_version.run_id, + "created_by": current_user.user_id, + "triggered_at": triggered_timestamp, + } except ValueError as ve: # Return a 400 error with the specific message from ValueError raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) - - -@router.get("/{inst_id}/training/model-cards/{model_name}") -def get_model_cards( - inst_id: str, - model_name: str, - current_user: Annotated[BaseUser, Depends(get_current_active_user)], - sql_session: Annotated[Session, Depends(get_session)], -) -> FileResponse: - has_access_to_inst_or_err(inst_id, current_user) - local_session.set(sql_session) - query_result = ( - local_session.get() - .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) - .all() - ) - if not query_result or len(query_result) == 0: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Institution not found.", - ) - if len(query_result) > 1: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Institution duplicates found.", - ) - - try: - w = WorkspaceClient( - host=databricks_vars["DATABRICKS_HOST_URL"], - google_service_account=gcs_vars["GCP_SERVICE_ACCOUNT_EMAIL"], - ) - - LOGGER.info("Successfully created Databricks WorkspaceClient.") - except Exception as e: - LOGGER.exception( - "Failed to create Databricks WorkspaceClient with host: %s and service account: %s", - databricks_vars["DATABRICKS_HOST_URL"], - gcs_vars["GCP_SERVICE_ACCOUNT_EMAIL"], - ) - raise ValueError( - f"get_model_cards(): Workspace client initialization failed: {e}" - ) - - try: - volume_path = f"/Volumes/staging_sst_01/{databricksify_inst_name(query_result[0][0].name)}_gold/gold_volume/model_cards/model-card-{model_name}.pdf" - LOGGER.info(f"Attempting to download from {volume_path}") - response = w.files.download(volume_path) - stream = cast(IO[bytes], response.contents) - pdf_bytes = stream.read() - - LOGGER.info("Download successful, received %d bytes", len(pdf_bytes)) - except Exception as e: - LOGGER.exception(f"Failed to fetch model card: {e}") - raise HTTPException(500, detail=f"Failed to fetch model card: {e}") - - # Stream back as FileResponse - tmp = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) - tmp.write(pdf_bytes) - tmp.flush() - - return FileResponse( - tmp.name, - filename=pathlib.Path(tmp.name).name, - media_type="application/pdf", - ) diff --git a/src/webapp/routers/data_test.py b/src/webapp/routers/data_test.py index d1cce3ee..7add80bd 100644 --- a/src/webapp/routers/data_test.py +++ b/src/webapp/routers/data_test.py @@ -1,6 +1,7 @@ """Test file for the data.py file and constituent API functions.""" import uuid +import time from unittest import mock from collections import Counter from fastapi.testclient import TestClient @@ -8,6 +9,7 @@ import pytest import sqlalchemy from sqlalchemy.pool import StaticPool +from sqlalchemy.future import select from ..test_helper import ( USR, USER_VALID_INST_UUID, @@ -27,7 +29,13 @@ get_session, ) from ..utilities import uuid_to_str, get_current_active_user, SchemaType -from .data import router, DataOverview, DataInfo +from .data import ( + router, + DataOverview, + DataInfo, + _infer_allowed_schemas_from_filename, +) +from fastapi import HTTPException from ..gcsutil import StorageControl MOCK_STORAGE = mock.Mock() @@ -103,8 +111,6 @@ def session_fixture(): """Unit test database setup.""" engine = sqlalchemy.create_engine( "sqlite://", - echo=True, - echo_pool="debug", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) @@ -157,6 +163,9 @@ def session_fixture(): InstTable( id=USER_VALID_INST_UUID, name="school_1", + legacy_id="legacy_test", + pdp_id=None, + edvise_id=None, created_at=DATETIME_TESTING, updated_at=DATETIME_TESTING, ), @@ -586,11 +595,11 @@ def test_validate_success_batch(client: TestClient) -> None: response_upload = client.post( "/institutions/" + uuid_to_str(USER_VALID_INST_UUID) - + "/input/validate-upload/file_name.csv", + + "/input/validate-upload/pdp_course_deidentified.csv", ) assert response_upload.status_code == 200 - assert response_upload.json()["name"] == "file_name.csv" - assert response_upload.json()["file_types"] == ["UNKNOWN"] + assert response_upload.json()["name"] == "pdp_course_deidentified.csv" + assert response_upload.json()["file_types"] == ["COURSE"] assert response_upload.json()["inst_id"] == uuid_to_str(USER_VALID_INST_UUID) assert response_upload.json()["source"] == "MANUAL_UPLOAD" @@ -598,7 +607,7 @@ def test_validate_success_batch(client: TestClient) -> None: response_sftp = client.post( "/institutions/" + uuid_to_str(UUID_INVALID) - + "/input/validate-sftp/file_name.csv", + + "/input/validate-sftp/pdp_ar_deidentified.csv", ) assert str(response_sftp) == "" assert ( @@ -609,11 +618,11 @@ def test_validate_success_batch(client: TestClient) -> None: response_sftp = client.post( "/institutions/" + uuid_to_str(USER_VALID_INST_UUID) - + "/input/validate-sftp/file_name.csv", + + "/input/validate-sftp/pdp_ar_deidentified.csv", ) assert response_sftp.status_code == 200 - assert response_sftp.json()["name"] == "file_name.csv" - assert response_sftp.json()["file_types"] == ["UNKNOWN"] + assert response_sftp.json()["name"] == "pdp_ar_deidentified.csv" + assert response_sftp.json()["file_types"] == ["STUDENT"] assert response_sftp.json()["inst_id"] == uuid_to_str(USER_VALID_INST_UUID) assert response_sftp.json()["source"] == "PDP_SFTP" @@ -645,3 +654,1101 @@ def test_validate_failure_batch(client: TestClient) -> None: assert response_sftp.json()["file_types"] == ["COURSE"] assert response_sftp.json()["inst_id"] == uuid_to_str(USER_VALID_INST_UUID) assert response_sftp.json()["source"] == "MANUAL_UPLOAD" + + +def test_get_eda_data_unauthorized(client: TestClient) -> None: + """Test GET /institutions//batch//eda with unauthorized access.""" + response = client.get( + "/institutions/" + + uuid_to_str(UUID_INVALID) + + "/batch/" + + uuid_to_str(BATCH_UUID) + + "/eda" + ) + assert str(response) == "" + assert ( + response.text + == '{"detail":"Not authorized to read this institution\'s resources."}' + ) + + +def test_get_eda_data_batch_not_found(client: TestClient) -> None: + """Test GET /institutions//batch//eda with non-existent batch.""" + fake_batch_uuid = uuid.UUID("00000000-0000-0000-0000-000000000000") + response = client.get( + "/institutions/" + + uuid_to_str(USER_VALID_INST_UUID) + + "/batch/" + + uuid_to_str(fake_batch_uuid) + + "/eda" + ) + assert response.status_code == 404 + assert response.json()["detail"] == "Batch not found." + + +def test_get_eda_data_no_student_files( + client: TestClient, session: sqlalchemy.orm.Session +) -> None: + """Test GET /institutions//batch//eda with batch containing no STUDENT files.""" + # Create a batch with only COURSE files + batch_with_course = BatchTable( + id=uuid.UUID("11111111-1111-1111-1111-111111111111"), + inst_id=USER_VALID_INST_UUID, + name="batch_course_only", + created_by=CREATOR_UUID, + created_at=DATETIME_TESTING, + updated_at=DATETIME_TESTING, + ) + course_file = FileTable( + id=uuid.UUID("22222222-2222-2222-2222-222222222222"), + inst_id=USER_VALID_INST_UUID, + name="course_file.csv", + source="MANUAL_UPLOAD", + batches={batch_with_course}, + created_at=DATETIME_TESTING, + updated_at=DATETIME_TESTING, + sst_generated=False, + valid=True, + schemas=[SchemaType.COURSE], + ) + session.add_all([batch_with_course, course_file]) + session.commit() + + # Mock storage to return empty (no files found) + MOCK_STORAGE.read_csv_as_dataframe.side_effect = ValueError("File not found") + + response = client.get( + "/institutions/" + + uuid_to_str(USER_VALID_INST_UUID) + + "/batch/" + + uuid_to_str(batch_with_course.id) + + "/eda" + ) + assert response.status_code == 404 + # When files can't be loaded from GCS, we get "No valid input files found" + # The "No STUDENT schema files found" error only occurs after files are loaded + assert "No valid input files found" in response.json()["detail"] + + +def test_get_eda_data_success( + client: TestClient, session: sqlalchemy.orm.Session +) -> None: + """Test GET /institutions//batch//eda with valid data.""" + import pandas as pd + + # Create a batch with STUDENT and COURSE files + eda_batch = BatchTable( + id=uuid.UUID("33333333-3333-3333-3333-333333333333"), + inst_id=USER_VALID_INST_UUID, + name="batch_eda_test", + created_by=CREATOR_UUID, + created_at=DATETIME_TESTING, + updated_at=DATETIME_TESTING, + completed=True, + ) + student_file = FileTable( + id=uuid.UUID("44444444-4444-4444-4444-444444444444"), + inst_id=USER_VALID_INST_UUID, + name="student_file.csv", + source="MANUAL_UPLOAD", + batches={eda_batch}, + created_at=DATETIME_TESTING, + updated_at=DATETIME_TESTING, + sst_generated=False, + valid=True, + schemas=[SchemaType.STUDENT], + ) + course_file = FileTable( + id=uuid.UUID("55555555-5555-5555-5555-555555555555"), + inst_id=USER_VALID_INST_UUID, + name="course_file.csv", + source="MANUAL_UPLOAD", + batches={eda_batch}, + created_at=DATETIME_TESTING, + updated_at=DATETIME_TESTING, + sst_generated=False, + valid=True, + schemas=[SchemaType.COURSE], + ) + session.add_all([eda_batch, student_file, course_file]) + session.commit() + + df_cohort = pd.DataFrame( + { + "student_id": ["S001", "S002", "S003", "S001"], + "cohort": ["2020", "2020", "2021", "2021"], + "cohort_term": ["FALL", "FALL", "SPRING", "SPRING"], + "enrollment_type": [ + "FIRST-TIME", + "TRANSFER-IN", + "FIRST-TIME", + "TRANSFER-IN", + ], + "enrollment_intensity_first_term": [ + "Full-Time", + "Part-Time", + "Full-Time", + "Part-Time", + ], + "gpa_group_year_1": [3.5, 3.2, 3.8, 2.9], + "credential_type_sought_year_1": [ + "Bachelor", + "Associate", + "Bachelor", + "Associate", + ], + "pell_status_first_year": ["Y", "N", "Y", "N"], + "first_gen": ["Y", "N", "Y", "N"], + "gender": ["Female", "Male", "Female", "Male"], + "race": ["White", "Black or African American", "Asian", "White"], + "student_age": ["20 - 24", "20 or younger", "Older than 24", "20 - 24"], + } + ) + df_course = pd.DataFrame( + { + "study_id": ["S001", "S002", "S003"], + "cohort": ["2020", "2020", "2021"], + "cohort_term": ["FALL", "FALL", "SPRING"], + } + ) + + # Mock storage to return our test DataFrames + def mock_read_csv(bucket_name: str, blob_path: str) -> pd.DataFrame: + if "student" in blob_path.lower(): + return df_cohort + elif "course" in blob_path.lower(): + return df_course + else: + raise ValueError(f"File not found: {blob_path}") + + MOCK_STORAGE.read_csv_as_dataframe.side_effect = mock_read_csv + + response = client.get( + "/institutions/" + + uuid_to_str(USER_VALID_INST_UUID) + + "/batch/" + + uuid_to_str(eda_batch.id) + + "/eda" + ) + + assert response.status_code == 200 + data = response.json() + + # Check response structure + assert "total_students" in data + assert "transfer_students" in data + assert "avg_year1_gpa_all_students" in data + assert "gpa_by_enrollment_type" in data + assert "gpa_by_enrollment_intensity" in data + assert "students_by_cohort_term" in data + assert "course_enrollments" in data + assert "degree_types" in data + assert "total" in data["degree_types"] + assert "degrees" in data["degree_types"] + assert "enrollment_type_by_intensity" in data + assert "pell_recipient_status" in data + assert "pell_recipient_by_first_gen" in data + assert "student_age_by_gender" in data + assert "race_by_pell_status" in data + + # Check summary stats (each is { name, value }) + assert data["total_students"]["name"] == "Total Students" + assert data["total_students"]["value"] == 3 # unique student_id (S001, S002, S003) + assert data["transfer_students"]["name"] == "Transfer Students" + assert data["transfer_students"]["value"] == 2 # 2 TRANSFER-IN + + # Check GPAs have cohort years + assert "cohort_years" in data["gpa_by_enrollment_type"] + assert len(data["gpa_by_enrollment_type"]["cohort_years"]) == 2 # 2020, 2021 + assert "2020" in data["gpa_by_enrollment_type"]["cohort_years"] + assert "2021" in data["gpa_by_enrollment_type"]["cohort_years"] + + # Check term data structure (degree_types style: years + by_year with total and terms[{ count, percentage, name }]) + assert "years" in data["students_by_cohort_term"] + assert "by_year" in data["students_by_cohort_term"] + assert len(data["students_by_cohort_term"]["years"]) == 2 + by_year = data["students_by_cohort_term"]["by_year"] + assert len(by_year) == 2 + assert "year" in by_year[0] and "total" in by_year[0] and "terms" in by_year[0] + assert all( + "count" in t and "percentage" in t and "name" in t for t in by_year[0]["terms"] + ) + + # Check enrollment type by intensity has categories and series + assert "categories" in data["enrollment_type_by_intensity"] + assert "series" in data["enrollment_type_by_intensity"] + assert len(data["enrollment_type_by_intensity"]["series"]) > 0 + + # Check pell recipients + assert "categories" in data["pell_recipient_by_first_gen"] + assert "series" in data["pell_recipient_by_first_gen"] + + # Check student age by gender structure + assert "categories" in data["student_age_by_gender"] + assert "series" in data["student_age_by_gender"] + + # Check race by pell status structure + assert "categories" in data["race_by_pell_status"] + assert "series" in data["race_by_pell_status"] + + +# ==================== EDVISE VALIDATION TESTS ==================== + +EDVISE_INST_UUID = uuid.UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") +EDVISE_INST_2_UUID = uuid.UUID("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb") +EDVISE_SCHEMA_UUID = uuid.UUID("cccccccc-cccc-cccc-cccc-cccccccccccc") +LEGACY_INST_UUID = uuid.UUID("dddddddd-dddd-dddd-dddd-dddddddddddd") + + +@pytest.fixture(name="legacy_session") +def legacy_session_fixture(): + """Database setup for Legacy (any-format) tests: one institution with legacy_id.""" + engine = sqlalchemy.create_engine( + "sqlite://", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + Base.metadata.create_all(engine) + try: + with sqlalchemy.orm.Session(engine) as session: + session.add_all( + [ + InstTable( + id=LEGACY_INST_UUID, + name="legacy_school", + legacy_id="legacy123", + pdp_id=None, + edvise_id=None, + schemas=["UNKNOWN"], + created_at=DATETIME_TESTING, + updated_at=DATETIME_TESTING, + ), + SchemaRegistryTable( + doc_type=DocType.base, + is_pdp=False, + is_edvise=False, + version_label="1.0.0", + json_doc={"version": "1.0.0", "base": {"data_models": {}}}, + is_active=True, + created_at=DATETIME_TESTING, + ), + ] + ) + session.commit() + yield session + finally: + Base.metadata.drop_all(engine) + + +@pytest.fixture(name="legacy_client") +def legacy_client_fixture( + legacy_session: sqlalchemy.orm.Session, monkeypatch: Any +) -> Any: + """Test client for Legacy institution tests.""" + monkeypatch.setenv("SST_SKIP_EXT_GEN", "1") + + def get_session_override(): + return legacy_session + + def get_current_active_user_override(): + from ..utilities import AccessType, BaseUser + + return BaseUser( + uuid_to_str(USER_UUID), + None, + AccessType.DATAKINDER, + "abc@example.com", + ) + + def storage_control_override(): + return MOCK_STORAGE + + app.include_router(router) + app.dependency_overrides[get_session] = get_session_override + app.dependency_overrides[get_current_active_user] = get_current_active_user_override + app.dependency_overrides[StorageControl] = storage_control_override + + client = TestClient(app) + yield client + app.dependency_overrides.clear() + + +@pytest.fixture(name="edvise_session") +def edvise_session_fixture(): + """Unit test database setup for Edvise Schema (ES) tests.""" + engine = sqlalchemy.create_engine( + "sqlite://", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + Base.metadata.create_all(engine) + + # Mock Edvise Schema (ES) extension (schema type, not product) + edvise_schema_doc = { + "version": "1.0.0", + "institutions": { + "edvise": { + "data_models": { + "student": { + "columns": { + "student_id": {"type": "string", "required": True}, + "cohort": {"type": "string", "required": True}, + } + }, + "course": { + "columns": { + "student_id": {"type": "string", "required": True}, + "course_id": {"type": "string", "required": True}, + } + }, + } + } + }, + } + + try: + with sqlalchemy.orm.Session(engine) as session: + session.add_all( + [ + InstTable( + id=EDVISE_INST_UUID, + name="edvise_school", + edvise_id="edvise123", + pdp_id=None, + created_at=DATETIME_TESTING, + updated_at=DATETIME_TESTING, + ), + InstTable( + id=EDVISE_INST_2_UUID, + name="edvise_school_2", + edvise_id="edvise456", + pdp_id=None, + created_at=DATETIME_TESTING, + updated_at=DATETIME_TESTING, + ), + SchemaRegistryTable( + doc_type=DocType.base, + is_pdp=False, + is_edvise=False, + version_label="1.0.0", + json_doc={"version": "1.0.0", "base": {"data_models": {}}}, + is_active=True, + created_at=DATETIME_TESTING, + ), + SchemaRegistryTable( + doc_type=DocType.extension, + is_pdp=False, + is_edvise=True, + version_label="edvise-1.0.0", + json_doc=edvise_schema_doc, + is_active=True, + created_at=DATETIME_TESTING, + ), + # Note: Edvise extension uses version_label="edvise-1.0.0" to avoid violating + # uq_pdp_version constraint (is_pdp, version_label) in MySQL, which requires + # unique (is_pdp, version_label) combinations across all rows. The base schema + # uses version_label="1.0.0" with is_pdp=False, so Edvise must use a different + # version_label. The JSON doc's "version": "1.0.0" field maintains semantic + # versioning, while the database version_label is prefixed for constraint uniqueness. + ] + ) + session.commit() + yield session + finally: + Base.metadata.drop_all(engine) + + +@pytest.fixture(name="edvise_client") +def edvise_client_fixture( + edvise_session: sqlalchemy.orm.Session, monkeypatch: Any +) -> Any: + """Unit test mocks setup for Edvise Schema (ES) tests.""" + monkeypatch.setenv("SST_SKIP_EXT_GEN", "1") + + def get_session_override(): + return edvise_session + + def get_current_active_user_override(): + # Create DATAKINDER user with access to all institutions (needed for tests + # that access multiple Edvise Schema (ES) institutions) + from ..utilities import AccessType, BaseUser + + return BaseUser( + uuid_to_str(USER_UUID), + None, # DATAKINDER has no specific institution + AccessType.DATAKINDER, + "abc@example.com", + ) + + def storage_control_override(): + return MOCK_STORAGE + + app.include_router(router) + app.dependency_overrides[get_session] = get_session_override + app.dependency_overrides[get_current_active_user] = get_current_active_user_override + app.dependency_overrides[StorageControl] = storage_control_override + + client = TestClient(app) + yield client + app.dependency_overrides.clear() + # Clear Edvise cache between tests + from .data import STATE + + STATE._edvise_cache = (0.0, None) + + +def test_validate_file_with_edvise_schema(edvise_client: TestClient) -> None: + """Test file upload validation uses Edvise Schema (ES) when edvise_id is set.""" + MOCK_STORAGE.validate_file.return_value = ["STUDENT"] + + response = edvise_client.post( + "/institutions/" + + uuid_to_str(EDVISE_INST_UUID) + + "/input/validate-upload/edvise_student_file.csv", + ) + + assert response.status_code == 200 + assert response.json()["name"] == "edvise_student_file.csv" + assert response.json()["file_types"] == ["STUDENT"] + assert response.json()["inst_id"] == uuid_to_str(EDVISE_INST_UUID) + assert response.json()["source"] == "MANUAL_UPLOAD" + + # Verify that validate_file was called with institution_identifier for Edvise Schema (ES) + assert MOCK_STORAGE.validate_file.called + call_kwargs = MOCK_STORAGE.validate_file.call_args.kwargs + assert call_kwargs.get("institution_identifier") == uuid_to_str(EDVISE_INST_UUID) + + +def test_validate_file_with_legacy_schema(legacy_client: TestClient) -> None: + """Test file upload validation uses legacy (any-format) path when legacy_id is set.""" + MOCK_STORAGE.validate_file.return_value = ["STUDENT"] + + response = legacy_client.post( + "/institutions/" + + uuid_to_str(LEGACY_INST_UUID) + + "/input/validate-upload/legacy_student_data.csv", + ) + + assert response.status_code == 200 + assert response.json()["name"] == "legacy_student_data.csv" + assert response.json()["file_types"] == ["STUDENT"] + assert response.json()["inst_id"] == uuid_to_str(LEGACY_INST_UUID) + + assert MOCK_STORAGE.validate_file.called + call_kwargs = MOCK_STORAGE.validate_file.call_args.kwargs + assert call_kwargs.get("institution_id") == "legacy" + + +def test_validate_upload_rejects_empty_file_name(legacy_client: TestClient) -> None: + """Empty or whitespace-only file name returns 422.""" + # Trailing space in path decodes to " " + response = legacy_client.post( + "/institutions/" + uuid_to_str(LEGACY_INST_UUID) + "/input/validate-upload/%20", + ) + assert response.status_code == 422 + assert ( + "required" in response.json()["detail"].lower() + or "non-empty" in response.json()["detail"].lower() + ) + + +def test_validate_upload_invalid_inst_id_returns_404(legacy_client: TestClient) -> None: + """Invalid institution id (non-UUID) returns 404, not 500.""" + response = legacy_client.post( + "/institutions/not-a-valid-uuid/input/validate-upload/foo.csv", + ) + assert response.status_code == 404 + assert ( + "institution" in response.json()["detail"].lower() + or "invalid" in response.json()["detail"].lower() + ) + + +def test_validate_file_legacy_accepts_arbitrary_filename( + legacy_client: TestClient, +) -> None: + """Legacy schools may use any filename; when inference fails, allowed_schemas is UNKNOWN.""" + MOCK_STORAGE.validate_file.return_value = ["UNKNOWN"] + + response = legacy_client.post( + "/institutions/" + + uuid_to_str(LEGACY_INST_UUID) + + "/input/validate-upload/export_2024.csv", + ) + + assert response.status_code == 200 + assert response.json()["name"] == "export_2024.csv" + assert response.json()["file_types"] == ["UNKNOWN"] + assert response.json()["inst_id"] == uuid_to_str(LEGACY_INST_UUID) + + assert MOCK_STORAGE.validate_file.called + # allowed_schemas is passed positionally (args[2]) + assert MOCK_STORAGE.validate_file.call_args.args[2] == ["UNKNOWN"] + assert MOCK_STORAGE.validate_file.call_args.kwargs.get("institution_id") == "legacy" + + +def test_validate_file_legacy_pii_rejection_returns_400( + legacy_client: TestClient, +) -> None: + """When legacy validation raises HardValidationError (e.g. PII columns), API returns 400.""" + from ..validation import HardValidationError + + MOCK_STORAGE.validate_file.side_effect = HardValidationError( + schema_errors="Legacy upload: file contains columns that may contain personally identifiable information (PII).", + failure_cases=["email", "first_name"], + ) + + response = legacy_client.post( + "/institutions/" + + uuid_to_str(LEGACY_INST_UUID) + + "/input/validate-upload/legacy_student_data.csv", + ) + + assert response.status_code == 400 + detail = response.json()["detail"] + assert "PII" in detail or "personally" in detail.lower() + + MOCK_STORAGE.validate_file.side_effect = None + MOCK_STORAGE.validate_file.return_value = ["STUDENT"] + + +# --- Unit tests for validation helpers --- + + +def _make_inst(legacy_id: Any = None) -> Any: + """Minimal institution-like object for _infer_allowed_schemas_from_filename.""" + + class Inst: + legacy_id: Any = None + + inst = Inst() + inst.legacy_id = legacy_id + return inst + + +def test_infer_allowed_schemas_course_only() -> None: + """Filename with 'course' infers [COURSE].""" + inst = _make_inst(legacy_id=None) + assert _infer_allowed_schemas_from_filename("course_export.csv", inst) == ["COURSE"] + + +def test_infer_allowed_schemas_student_only() -> None: + """Filename with 'student' infers [STUDENT].""" + inst = _make_inst(legacy_id=None) + assert _infer_allowed_schemas_from_filename("student_data.csv", inst) == ["STUDENT"] + + +def test_infer_allowed_schemas_semester_only() -> None: + """Filename with 'semester' infers [SEMESTER].""" + inst = _make_inst(legacy_id=None) + assert _infer_allowed_schemas_from_filename("semester.csv", inst) == ["SEMESTER"] + + +def test_infer_allowed_schemas_cohort_infers_student() -> None: + """Filename with 'cohort' (no course) infers [STUDENT].""" + inst = _make_inst(legacy_id=None) + assert _infer_allowed_schemas_from_filename("cohort_2024.csv", inst) == ["STUDENT"] + + +def test_infer_allowed_schemas_multiple_keywords() -> None: + """Filename with student and course infers both, sorted.""" + inst = _make_inst(legacy_id=None) + result = _infer_allowed_schemas_from_filename("student_course_combined.csv", inst) + assert sorted(result) == ["COURSE", "STUDENT"] + + +def test_infer_allowed_schemas_legacy_arbitrary_returns_unknown() -> None: + """Legacy institution with non-descriptive filename gets [UNKNOWN].""" + inst = _make_inst(legacy_id="legacy_1") + assert _infer_allowed_schemas_from_filename("report_2024.csv", inst) == ["UNKNOWN"] + + +def test_infer_allowed_schemas_non_legacy_arbitrary_raises_422() -> None: + """Non-legacy institution with non-descriptive filename raises 422.""" + inst = _make_inst(legacy_id=None) + with pytest.raises(HTTPException) as exc_info: + _infer_allowed_schemas_from_filename("random.csv", inst) + assert exc_info.value.status_code == 422 + assert "Could not infer model(s)" in exc_info.value.detail + assert "random" in exc_info.value.detail + + +def test_validate_edvise_non_descriptive_filename_returns_422( + edvise_client: TestClient, +) -> None: + """Edvise institution with non-descriptive filename (no student/course/semester) returns 422.""" + MOCK_STORAGE.validate_file.return_value = ["STUDENT"] + + response = edvise_client.post( + "/institutions/" + + uuid_to_str(EDVISE_INST_UUID) + + "/input/validate-upload/report_2024.csv", + ) + + assert response.status_code == 422 + assert "Could not infer model(s)" in response.json()["detail"] + assert "report" in response.json()["detail"] + + +def test_validate_upload_second_call_succeeds_and_idempotent( + legacy_client: TestClient, +) -> None: + """Validating the same file twice returns 200 both times; no duplicate insert failure.""" + MOCK_STORAGE.validate_file.return_value = ["UNKNOWN"] + + response1 = legacy_client.post( + "/institutions/" + + uuid_to_str(LEGACY_INST_UUID) + + "/input/validate-upload/duplicate_test.csv", + ) + assert response1.status_code == 200 + assert response1.json()["name"] == "duplicate_test.csv" + assert response1.json()["file_types"] == ["UNKNOWN"] + + response2 = legacy_client.post( + "/institutions/" + + uuid_to_str(LEGACY_INST_UUID) + + "/input/validate-upload/duplicate_test.csv", + ) + assert response2.status_code == 200 + # Second call hits existing-file path; response payload is unchanged (ValidationResult omits status) + assert response2.json()["name"] == response1.json()["name"] + assert response2.json()["file_types"] == response1.json()["file_types"] + assert response2.json()["inst_id"] == response1.json()["inst_id"] + + +def test_validation_helper_edvise_schema_not_found( + edvise_client: TestClient, edvise_session: sqlalchemy.orm.Session +) -> None: + """Test error when edvise_id is set but no active Edvise Schema (ES) exists.""" + # Deactivate the Edvise Schema (ES) + edvise_schema = edvise_session.execute( + select(SchemaRegistryTable).where( + SchemaRegistryTable.is_edvise.is_(True), + SchemaRegistryTable.is_active.is_(True), + ) + ).scalar_one_or_none() + if edvise_schema: + edvise_schema.is_active = False + edvise_session.commit() + + # Clear cache to force reload + from .data import STATE + + STATE._edvise_cache = (0.0, None) + + MOCK_STORAGE.validate_file.return_value = ["STUDENT"] + + response = edvise_client.post( + "/institutions/" + + uuid_to_str(EDVISE_INST_UUID) + + "/input/validate-upload/test_student_file.csv", + ) + + assert response.status_code == 500 + assert "Edvise Schema (ES) not found" in response.json()["detail"] + assert "edvise_id" in response.json()["detail"] + + +def test_validation_helper_pdp_and_edvise_mutual_exclusivity( + edvise_client: TestClient, edvise_session: sqlalchemy.orm.Session +) -> None: + """Test that validation_helper rejects institutions with both pdp_id and edvise_id.""" + # Corrupt the institution data to have both pdp_id and edvise_id + corrupted_inst = edvise_session.execute( + select(InstTable).where(InstTable.id == EDVISE_INST_UUID) + ).scalar_one_or_none() + corrupted_inst.pdp_id = "pdp999" # type: ignore + edvise_session.commit() + + # Clear cache + from .data import STATE + + STATE._edvise_cache = (0.0, None) + + MOCK_STORAGE.validate_file.return_value = ["STUDENT"] + + response = edvise_client.post( + "/institutions/" + + uuid_to_str(EDVISE_INST_UUID) + + "/input/validate-upload/test_student_file.csv", + ) + + assert response.status_code == 500 + assert ( + "cannot have more than one of pdp_id, edvise_id, or legacy_id set" + in response.json()["detail"] + ) + + # Restore for other tests + corrupted_inst.pdp_id = None # type: ignore + edvise_session.commit() + + +def test_validation_helper_rejects_institution_without_school_type( + edvise_client: TestClient, edvise_session: sqlalchemy.orm.Session +) -> None: + """Upload validation requires pdp_id, edvise_id, or legacy_id on the institution.""" + inst = edvise_session.execute( + select(InstTable).where(InstTable.id == EDVISE_INST_UUID) + ).scalar_one() + saved = (inst.edvise_id, inst.pdp_id, inst.legacy_id) + inst.edvise_id = None # type: ignore + inst.pdp_id = None # type: ignore + inst.legacy_id = None # type: ignore + edvise_session.commit() + + from .data import STATE + + STATE._edvise_cache = (0.0, None) + + response = edvise_client.post( + "/institutions/" + + uuid_to_str(EDVISE_INST_UUID) + + "/input/validate-upload/test_student_file.csv", + ) + assert response.status_code == 500 + assert "no pdp_id, edvise_id, or legacy_id" in response.json()["detail"] + + inst.edvise_id, inst.pdp_id, inst.legacy_id = saved + edvise_session.commit() + + +def test_edvise_schema_cache( + edvise_client: TestClient, edvise_session: sqlalchemy.orm.Session +) -> None: + """Test that Edvise Schema (ES) is cached and reused.""" + from .data import STATE + + # Clear cache + STATE._edvise_cache = (0.0, None) + + MOCK_STORAGE.validate_file.return_value = ["STUDENT"] + + # First call: Should load from DB and set cache + response1 = edvise_client.post( + "/institutions/" + + uuid_to_str(EDVISE_INST_UUID) + + "/input/validate-upload/test_student1.csv", + ) + assert response1.status_code == 200 + + # Verify cache was set + cache_exp, cache_doc = STATE._edvise_cache + assert cache_doc is not None + assert cache_exp > time.monotonic() + + # Second call: Should use cached value (same expiration time) + response2 = edvise_client.post( + "/institutions/" + + uuid_to_str(EDVISE_INST_2_UUID) # Different institution, same schema + + "/input/validate-upload/test_student2.csv", + ) + assert response2.status_code == 200 + + # Verify cache expiration time is the same (cache was reused) + cache_exp2, cache_doc2 = STATE._edvise_cache + assert cache_doc2 is not None + assert cache_exp2 == cache_exp # Same expiration means cache was reused + + # Both institutions should use the same cached Edvise Schema (ES) + assert STATE._edvise_cache[1] is not None + assert STATE._edvise_cache[1] is cache_doc + + +def test_validate_file_edvise_schema_validation_errors( + edvise_client: TestClient, +) -> None: + """Test that validation errors are returned correctly for Edvise Schema (ES).""" + from ..validation import HardValidationError + + # Mock validation to raise an error + def mock_validate_file(*args, **kwargs): + raise HardValidationError( + missing_required=["student_id"], + extra_columns=[], + schema_errors=None, + failure_cases=None, + ) + + MOCK_STORAGE.validate_file.side_effect = mock_validate_file + + response = edvise_client.post( + "/institutions/" + + uuid_to_str(EDVISE_INST_UUID) + + "/input/validate-upload/invalid_student_file.csv", + ) + + assert response.status_code == 400 + # Check for user-friendly error message (Phase 4: Error Message Improvements) + detail = response.json()["detail"] + assert "Missing required columns" in detail or "student_id" in detail + # The message should be user-friendly, not technical "VALIDATION_FAILED" + assert "VALIDATION_FAILED" not in detail or "missing_required=" not in detail + + # Reset mock + MOCK_STORAGE.validate_file.side_effect = None + MOCK_STORAGE.validate_file.return_value = ["STUDENT"] + + +def test_edvise_schema_takes_precedence_over_custom( + edvise_client: TestClient, edvise_session: sqlalchemy.orm.Session +) -> None: + """Test that Edvise Schema (ES) is used instead of custom when edvise_id is set.""" + # Add a custom extension for this institution with unique version_label + custom_schema = SchemaRegistryTable( + doc_type=DocType.extension, + inst_id=EDVISE_INST_UUID, + is_pdp=False, + is_edvise=False, + version_label="1.0.1", # Use different version to avoid unique constraint + json_doc={"version": "1.0.1", "custom": {"data_models": {}}}, + is_active=True, + created_at=DATETIME_TESTING, + ) + edvise_session.add(custom_schema) + edvise_session.commit() + + # Clear cache + from .data import STATE + + STATE._edvise_cache = (0.0, None) + + # Capture schema, institution_id, and institution_identifier passed to validate_file + captured_schema = None + captured_institution_id = None + captured_institution_identifier = None + + def capture_schema(*args, **kwargs): + # fmt: off + nonlocal captured_schema, captured_institution_id, captured_institution_identifier + # fmt: on + # validate_file(bucket, file_name, allowed_schemas, base_schema, inst_schema, institution_id=..., institution_identifier=...) + if len(args) >= 5: + captured_schema = args[4] + elif "inst_schema" in kwargs: + captured_schema = kwargs["inst_schema"] + captured_institution_id = kwargs.get("institution_id") + captured_institution_identifier = kwargs.get("institution_identifier") + return ["STUDENT"] + + MOCK_STORAGE.validate_file.side_effect = capture_schema + + response = edvise_client.post( + "/institutions/" + + uuid_to_str(EDVISE_INST_UUID) + + "/input/validate-upload/test_student_file.csv", + ) + + # Should succeed using Edvise Schema (ES), not custom + assert response.status_code == 200 + + # Verify Edvise Schema (ES) was passed to validation (not custom) + assert captured_schema is not None + # Edvise Schema (ES) should have "edvise" or "institutions" structure + assert isinstance(captured_schema, dict) + assert ( + "edvise" in str(captured_schema).lower() + or captured_schema.get("institutions") is not None + ) + # Custom schema should NOT be in the captured schema + assert ( + "custom" not in str(captured_schema).lower() + or captured_schema.get("custom") is None + ) + # Verify correct institution_id so merge_model_columns uses institutions["edvise"] + assert captured_institution_id == "edvise" + # Router must pass institution_identifier (institution UUID) for Edvise normalization + assert captured_institution_identifier == uuid_to_str(EDVISE_INST_UUID) + + # Reset mock + MOCK_STORAGE.validate_file.side_effect = None + MOCK_STORAGE.validate_file.return_value = ["STUDENT"] + + +def test_validate_sftp_with_edvise_schema(edvise_client: TestClient) -> None: + """Test SFTP file validation uses Edvise Schema (ES) when edvise_id is set.""" + MOCK_STORAGE.validate_file.return_value = ["COURSE"] + + response = edvise_client.post( + "/institutions/" + + uuid_to_str(EDVISE_INST_UUID) + + "/input/validate-sftp/edvise_course_file.csv", + ) + + assert response.status_code == 200 + assert response.json()["name"] == "edvise_course_file.csv" + assert response.json()["file_types"] == ["COURSE"] + assert response.json()["inst_id"] == uuid_to_str(EDVISE_INST_UUID) + assert response.json()["source"] == "PDP_SFTP" + + # Verify that validate_file was called + assert MOCK_STORAGE.validate_file.called + + +def test_validate_edvise_unauthorized( + edvise_session: sqlalchemy.orm.Session, monkeypatch: Any +) -> None: + """Test validation endpoint with unauthorized access.""" + monkeypatch.setenv("SST_SKIP_EXT_GEN", "1") + + # Create a test client with a MODEL_OWNER user who only has access to EDVISE_INST_UUID + # This user should NOT have access to EDVISE_INST_2_UUID + def get_session_override(): + return edvise_session + + def get_current_active_user_override(): + # User belongs to EDVISE_INST_UUID, not DATAKINDER, so access is restricted + from ..utilities import AccessType, BaseUser + + return BaseUser( + uuid_to_str(USER_UUID), + uuid_to_str(EDVISE_INST_UUID), # User belongs to this institution + AccessType.MODEL_OWNER, # Not DATAKINDER, so access is restricted + "abc@example.com", + ) + + def storage_control_override(): + return MOCK_STORAGE + + app.include_router(router) + app.dependency_overrides[get_session] = get_session_override + app.dependency_overrides[get_current_active_user] = get_current_active_user_override + app.dependency_overrides[StorageControl] = storage_control_override + + client = TestClient(app) + try: + # Try to access EDVISE_INST_2_UUID which exists but user doesn't have access to + response = client.post( + "/institutions/" + + uuid_to_str( + EDVISE_INST_2_UUID + ) # Institution exists but user is unauthorized + + "/input/validate-upload/test_student.csv", + ) + assert response.status_code == 401 + assert "Not authorized" in response.json()["detail"] + finally: + app.dependency_overrides.clear() + + +def test_validate_edvise_invalid_filename(edvise_client: TestClient) -> None: + """Test validation rejects file names with '/'.""" + MOCK_STORAGE.validate_file.return_value = ["STUDENT"] + + response = edvise_client.post( + "/institutions/" + + uuid_to_str(EDVISE_INST_UUID) + + "/input/validate-upload/invalid/file.csv", + ) + assert response.status_code == 422 + assert "can't contain '/'" in response.json()["detail"] + + +def test_validate_edvise_course_file(edvise_client: TestClient) -> None: + """Test COURSE file validation with Edvise Schema (ES).""" + MOCK_STORAGE.validate_file.return_value = ["COURSE"] + + response = edvise_client.post( + "/institutions/" + + uuid_to_str(EDVISE_INST_UUID) + + "/input/validate-upload/edvise_course.csv", + ) + + assert response.status_code == 200 + assert response.json()["file_types"] == ["COURSE"] + assert response.json()["name"] == "edvise_course.csv" + assert response.json()["inst_id"] == uuid_to_str(EDVISE_INST_UUID) + + +def test_edvise_cache_expiration( + edvise_client: TestClient, edvise_session: sqlalchemy.orm.Session +) -> None: + """Test that expired cache reloads from database.""" + from .data import STATE + + # Set cache with expired TTL + old_exp = time.monotonic() - 1 + STATE._edvise_cache = (old_exp, {"old": "schema"}) + + MOCK_STORAGE.validate_file.return_value = ["STUDENT"] + + # Should reload from DB (cache expired) and update cache + response = edvise_client.post( + "/institutions/" + + uuid_to_str(EDVISE_INST_UUID) + + "/input/validate-upload/test_student.csv", + ) + assert response.status_code == 200 + + # Verify cache was updated with new expiration + cache_exp, cache_doc = STATE._edvise_cache + assert cache_doc is not None + assert cache_exp > old_exp # New expiration time means cache was reloaded + + +def test_edvise_cache_none_reloads(edvise_client: TestClient) -> None: + """Test that None in expired cache doesn't prevent reload.""" + from .data import STATE + + # Set cache with None but expired TTL + STATE._edvise_cache = (time.monotonic() - 1, None) + + MOCK_STORAGE.validate_file.return_value = ["STUDENT"] + + # Should reload from DB (not use None) + response = edvise_client.post( + "/institutions/" + + uuid_to_str(EDVISE_INST_UUID) + + "/input/validate-upload/test_student.csv", + ) + # Should succeed (schema exists in DB) + assert response.status_code == 200 + # Cache should now have the schema + assert STATE._edvise_cache[1] is not None + + +def test_edvise_cache_shared_across_institutions(edvise_client: TestClient) -> None: + """Test that all Edvise Schema (ES) institutions share the same cached schema.""" + from .data import STATE + + STATE._edvise_cache = (0.0, None) + + MOCK_STORAGE.validate_file.return_value = ["STUDENT"] + + # First institution + response1 = edvise_client.post( + "/institutions/" + + uuid_to_str(EDVISE_INST_UUID) + + "/input/validate-upload/test_student1.csv", + ) + assert response1.status_code == 200 + + # Get cached schema + cache_exp, cache_doc = STATE._edvise_cache + assert cache_doc is not None + + # Second institution should use same cache + response2 = edvise_client.post( + "/institutions/" + + uuid_to_str(EDVISE_INST_2_UUID) + + "/input/validate-upload/test_student2.csv", + ) + assert response2.status_code == 200 + + # Cache should be unchanged (same object reference) + assert STATE._edvise_cache[1] is cache_doc + + +def test_validate_edvise_inst_not_found(edvise_client: TestClient) -> None: + """Test validation with non-existent institution.""" + fake_uuid = uuid.UUID("00000000-0000-0000-0000-000000000000") + MOCK_STORAGE.validate_file.return_value = ["STUDENT"] + + response = edvise_client.post( + "/institutions/" + + uuid_to_str(fake_uuid) + + "/input/validate-upload/test_student.csv", + ) + # Should fail - either 401 (unauthorized) or 404 (not found) + assert response.status_code in [401, 404] diff --git a/src/webapp/routers/front_end_tables.py b/src/webapp/routers/front_end_tables.py new file mode 100644 index 00000000..94522eae --- /dev/null +++ b/src/webapp/routers/front_end_tables.py @@ -0,0 +1,506 @@ +"""API functions related to data.""" + +from databricks.sdk import WorkspaceClient +from typing import Annotated, Any, cast, IO, Optional +from fastapi import APIRouter, Depends, HTTPException, status, Query +from fastapi.responses import FileResponse +from sqlalchemy.orm import Session +from sqlalchemy.future import select +import logging +from ..config import databricks_vars, env_vars, gcs_vars +import tempfile +import pathlib + +from ..utilities import ( + has_access_to_inst_or_err, + BaseUser, + str_to_uuid, + get_current_active_user, + databricksify_inst_name, +) + +from ..database import ( + get_session, + local_session, + InstTable, + JobTable, +) + +from ..databricks import DatabricksControl + +# Set the logging +logging.basicConfig(format="%(asctime)s [%(levelname)s]: %(message)s") +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +router = APIRouter( + prefix="/institutions", + tags=["front_end_tables"], +) + +LOGGER = logging.getLogger(__name__) + + +## FE Inference Tables + + +# Get SHAP Values for Inference +@router.get("/{inst_id}/inference/top-features/{job_run_id}") +def get_inference_top_features( + inst_id: str, + job_run_id: str, + current_user: Annotated[BaseUser, Depends(get_current_active_user)], + sql_session: Annotated[Session, Depends(get_session)], +) -> Any: + """Returns top n features table for a specific institution.""" + # raise error at this level instead bc otherwise it's getting wrapped as a 200 + has_access_to_inst_or_err(inst_id, current_user) + local_session.set(sql_session) + query_result = ( + local_session.get() + .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) + .all() + ) + if not query_result or len(query_result) == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Institution not found.", + ) + if len(query_result) > 1: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Institution duplicates found.", + ) + + try: + dbc = DatabricksControl() + rows = dbc.fetch_table_data( + catalog_name=env_vars["CATALOG_NAME"], # type: ignore + inst_name=f"{query_result[0][0].name}", + table_name=f"inference_{job_run_id}_features_with_most_impact", + warehouse_id=env_vars["SQL_WAREHOUSE_ID"], # type: ignore + ) + + return rows + except ValueError as ve: + # Return a 400 error with the specific message from ValueError + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) + + +# Get Box plot values +@router.get("/{inst_id}/inference/features-boxplot-stat/{job_run_id}") +def get_inference_feature_boxstats( + inst_id: str, + job_run_id: str, + current_user: Annotated[BaseUser, Depends(get_current_active_user)], + sql_session: Annotated[Session, Depends(get_session)], + feature_name: Optional[str] = Query( + None, description="If provided, filter by this feature name" + ), +) -> Any: + """Returns box-plot stats for an institution/run. If `feature_name` is supplied, + only rows for that feature are returned.""" + # raise error at this level instead bc otherwise it's getting wrapped as a 200 + has_access_to_inst_or_err(inst_id, current_user) + local_session.set(sql_session) + query_result = ( + local_session.get() + .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) + .all() + ) + if not query_result or len(query_result) == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Institution not found.", + ) + if len(query_result) > 1: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Institution duplicates found.", + ) + + try: + dbc = DatabricksControl() + rows = dbc.fetch_table_data( + catalog_name=env_vars["CATALOG_NAME"], # type: ignore + inst_name=f"{query_result[0][0].name}", + table_name=f"inference_{job_run_id}_box_plot_table", + warehouse_id=env_vars["SQL_WAREHOUSE_ID"], # type: ignore + ) + if not feature_name: + return rows + + # Helper: extract feature_name from various shapes (top-level or JSON column) + def row_feature_name(row: dict[str, Any]) -> Optional[str]: + # common case: it's a top-level column + if "feature_name" in row and row["feature_name"] is not None: + return str(row["feature_name"]) + # fallback: search any dict-valued column for a 'feature_name' key + for v in row.values(): + if ( + isinstance(v, dict) + and "feature_name" in v + and v["feature_name"] is not None + ): + return str(v["feature_name"]) + return None + + filtered = [r for r in rows if row_feature_name(r) == feature_name] + + if not filtered: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Feature '{feature_name}' not found for run_id '{job_run_id}'.", + ) + + return filtered + + except ValueError as ve: + # Return a 400 error with the specific message from ValueError + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) + + +# Get SHAP Values for Inference +@router.get("/{inst_id}/inference/support-overview/{job_run_id}") +def get_inference_support_overview( + inst_id: str, + job_run_id: str, + current_user: Annotated[BaseUser, Depends(get_current_active_user)], + sql_session: Annotated[Session, Depends(get_session)], +) -> Any: + """Returns support score distribution table for a specific institution.""" + # raise error at this level instead bc otherwise it's getting wrapped as a 200 + has_access_to_inst_or_err(inst_id, current_user) + local_session.set(sql_session) + query_result = ( + local_session.get() + .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) + .all() + ) + if not query_result or len(query_result) == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Institution not found.", + ) + if len(query_result) > 1: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Institution duplicates found.", + ) + + try: + dbc = DatabricksControl() + rows = dbc.fetch_table_data( + catalog_name=env_vars["CATALOG_NAME"], # type: ignore + inst_name=f"{query_result[0][0].name}", + table_name=f"inference_{job_run_id}_support_overview", + warehouse_id=env_vars["SQL_WAREHOUSE_ID"], # type: ignore + ) + + return rows + except ValueError as ve: + # Return a 400 error with the specific message from ValueError + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) + + +@router.get("/{inst_id}/inference/feature_importance/{job_run_id}") +def get_inference_feature_importance( + inst_id: str, + job_run_id: str, + current_user: Annotated[BaseUser, Depends(get_current_active_user)], + sql_session: Annotated[Session, Depends(get_session)], +) -> Any: + """Returns feature importance table for a specific institution.""" + # raise error at this level instead bc otherwise it's getting wrapped as a 200 + has_access_to_inst_or_err(inst_id, current_user) + local_session.set(sql_session) + query_result = ( + local_session.get() + .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) + .all() + ) + if not query_result or len(query_result) == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Institution not found.", + ) + if len(query_result) > 1: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Institution duplicates found.", + ) + + try: + dbc = DatabricksControl() + rows = dbc.fetch_table_data( + catalog_name=env_vars["CATALOG_NAME"], # type: ignore + inst_name=f"{query_result[0][0].name}", + table_name=f"inference_{job_run_id}_shap_feature_importance", + warehouse_id=env_vars["SQL_WAREHOUSE_ID"], # type: ignore + ) + + return rows + except ValueError as ve: + # Return a 400 error with the specific message from ValueError + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) + + +## FE Training Tables + + +@router.get("/{inst_id}/training/feature_importance/{model_run_id}") +def get_training_feature_importance( + inst_id: str, + model_run_id: str, + current_user: Annotated[BaseUser, Depends(get_current_active_user)], + sql_session: Annotated[Session, Depends(get_session)], +) -> Any: + """Returns training feature importance table for a specific institution.""" + # raise error at this level instead bc otherwise it's getting wrapped as a 200 + has_access_to_inst_or_err(inst_id, current_user) + local_session.set(sql_session) + query_result = ( + local_session.get() + .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) + .all() + ) + if not query_result or len(query_result) == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Institution not found.", + ) + if len(query_result) > 1: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Institution duplicates found.", + ) + + try: + dbc = DatabricksControl() + rows = dbc.fetch_table_data( + catalog_name=env_vars["CATALOG_NAME"], # type: ignore + inst_name=f"{query_result[0][0].name}", + table_name=f"training_{model_run_id}_shap_feature_importance", + warehouse_id=env_vars["SQL_WAREHOUSE_ID"], # type: ignore + ) + + return rows + except ValueError as ve: + # Return a 400 error with the specific message from ValueError + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) + + +@router.get("/{inst_id}/training/confusion_matrix/{model_run_id}") +def get_training_confusion_matrix( + inst_id: str, + model_run_id: str, + current_user: Annotated[BaseUser, Depends(get_current_active_user)], + sql_session: Annotated[Session, Depends(get_session)], +) -> Any: + """Returns training confusion matrix table for a specific instituion.""" + # raise error at this level instead bc otherwise it's getting wrapped as a 200 + has_access_to_inst_or_err(inst_id, current_user) + local_session.set(sql_session) + query_result = ( + local_session.get() + .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) + .all() + ) + if not query_result or len(query_result) == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Institution not found.", + ) + if len(query_result) > 1: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Institution duplicates found.", + ) + + try: + dbc = DatabricksControl() + rows = dbc.fetch_table_data( + catalog_name=env_vars["CATALOG_NAME"], # type: ignore + inst_name=f"{query_result[0][0].name}", + table_name=f"training_{model_run_id}_confusion_matrix", + warehouse_id=env_vars["SQL_WAREHOUSE_ID"], # type: ignore + ) + + return rows + except ValueError as ve: + # Return a 400 error with the specific message from ValueError + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) + + +@router.get("/{inst_id}/training/roc_curve/{model_run_id}") +def get_training_roc_curve( + inst_id: str, + model_run_id: str, + current_user: Annotated[BaseUser, Depends(get_current_active_user)], + sql_session: Annotated[Session, Depends(get_session)], +) -> Any: + """Returns training roc curve table for a specific institution.""" + # raise error at this level instead bc otherwise it's getting wrapped as a 200 + has_access_to_inst_or_err(inst_id, current_user) + local_session.set(sql_session) + query_result = ( + local_session.get() + .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) + .all() + ) + if not query_result or len(query_result) == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Institution not found.", + ) + if len(query_result) > 1: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Institution duplicates found.", + ) + + try: + dbc = DatabricksControl() + rows = dbc.fetch_table_data( + catalog_name=env_vars["CATALOG_NAME"], # type: ignore + inst_name=f"{query_result[0][0].name}", + table_name=f"training_{model_run_id}_roc_curve", + warehouse_id=env_vars["SQL_WAREHOUSE_ID"], # type: ignore + ) + + return rows + except ValueError as ve: + # Return a 400 error with the specific message from ValueError + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) + + +@router.get("/{inst_id}/training/support-overview/{model_run_id}") +def get_training_support_overview( + inst_id: str, + model_run_id: str, + current_user: Annotated[BaseUser, Depends(get_current_active_user)], + sql_session: Annotated[Session, Depends(get_session)], +) -> Any: + """Returns training support overview table for a specific institution.""" + # raise error at this level instead bc otherwise it's getting wrapped as a 200 + has_access_to_inst_or_err(inst_id, current_user) + local_session.set(sql_session) + query_result = ( + local_session.get() + .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) + .all() + ) + if not query_result or len(query_result) == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Institution not found.", + ) + if len(query_result) > 1: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Institution duplicates found.", + ) + + try: + dbc = DatabricksControl() + rows = dbc.fetch_table_data( + catalog_name=env_vars["CATALOG_NAME"], # type: ignore + inst_name=f"{query_result[0][0].name}", + table_name=f"training_{model_run_id}_support_overview", + warehouse_id=env_vars["SQL_WAREHOUSE_ID"], # type: ignore + ) + + return rows + except ValueError as ve: + # Return a 400 error with the specific message from ValueError + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) + + +@router.get("/{inst_id}/training/model-cards/{model_run_id}") +def get_model_cards( + inst_id: str, + model_run_id: str, + current_user: Annotated[BaseUser, Depends(get_current_active_user)], + sql_session: Annotated[Session, Depends(get_session)], +) -> FileResponse: + has_access_to_inst_or_err(inst_id, current_user) + local_session.set(sql_session) + session = local_session.get() + query_result = session.execute( + select(InstTable).where(InstTable.id == str_to_uuid(inst_id)) + ).all() + + if not query_result or len(query_result) == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Institution not found.", + ) + if len(query_result) > 1: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Institution duplicates found.", + ) + + job_result = session.scalars( + select(JobTable) + .where(JobTable.model_run_id == model_run_id) + .order_by( + JobTable.triggered_at.desc() + ) # keep if multiple jobs can share a model_run_id + ).first() + + if job_result is None or not job_result.model_run_id: + raise HTTPException( + status_code=404, detail="No model run found for this model." + ) + + run_id = job_result.model_run_id + model_name = job_result.model.name + + try: + w = WorkspaceClient( + host=databricks_vars["DATABRICKS_HOST_URL"], + google_service_account=gcs_vars["GCP_SERVICE_ACCOUNT_EMAIL"], + ) + + LOGGER.info("Successfully created Databricks WorkspaceClient.") + except Exception as e: + LOGGER.exception( + "Failed to create Databricks WorkspaceClient with host: %s and service account: %s", + databricks_vars["DATABRICKS_HOST_URL"], + gcs_vars["GCP_SERVICE_ACCOUNT_EMAIL"], + ) + raise ValueError( + f"get_model_cards(): Workspace client initialization failed: {e}" + ) + + try: + env = str(env_vars["ENV"]).strip().upper() + SCHEMAS = {"DEV": "dev_sst_02", "STAGING": "staging_sst_01"} + if env not in SCHEMAS: + raise ValueError( + f"Unsupported ENV {env_vars.get('ENV')!r}; expected DEV or STAGING" + ) + env_schema = SCHEMAS[env] + + volume_path = f"/Volumes/{env_schema}/{databricksify_inst_name(query_result[0][0].name)}_gold/gold_volume/model_cards/{run_id}/model-card-{model_name}.pdf" + LOGGER.info(f"Attempting to download from {volume_path}") + response = w.files.download(volume_path) + stream = cast(IO[bytes], response.contents) + pdf_bytes = stream.read() + + LOGGER.info("Download successful, received %d bytes", len(pdf_bytes)) + except Exception as e: + LOGGER.exception(f"Failed to fetch model card: {e}") + raise HTTPException(500, detail=f"Failed to fetch model card: {e}") + + # Stream back as FileResponse + tmp = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) + tmp.write(pdf_bytes) + tmp.flush() + + return FileResponse( + tmp.name, + filename=pathlib.Path(tmp.name).name, + media_type="application/pdf", + ) diff --git a/src/webapp/routers/institutions.py b/src/webapp/routers/institutions.py index 35f50f39..9e9e87e6 100644 --- a/src/webapp/routers/institutions.py +++ b/src/webapp/routers/institutions.py @@ -2,15 +2,16 @@ import re -from typing import Annotated, Any, Dict +from typing import Annotated, Any, Dict, Optional, Tuple, cast from fastapi import HTTPException, status, APIRouter, Depends from pydantic import BaseModel from sqlalchemy.orm import Session from sqlalchemy.future import select -from sqlalchemy import and_, delete +from sqlalchemy import and_, delete, func from ..utilities import ( has_access_to_inst_or_err, + has_at_most_one_school_type, BaseUser, AccessType, get_external_bucket_name_from_uuid, @@ -19,6 +20,8 @@ get_current_active_user, SchemaType, PDP_SCHEMA_GROUP, + EDVISE_SCHEMA_GROUP, + LEGACY_SCHEMA_GROUP, UsState, get_external_bucket_name, ) @@ -36,6 +39,17 @@ tags=["institutions"], ) +# PATCH/POST: every institution must resolve to exactly one school type (IDs on the row). +_EXACTLY_ONE_SCHOOL_TYPE_DETAIL = ( + "Institution must be exactly one of PDP (set pdp_id), " + "Edvise Schema (ES) (set edvise_id or is_edvise), or Legacy " + "(set legacy_id or is_legacy)." +) +_MUTUALLY_EXCLUSIVE_SCHOOL_TYPE_DETAIL = ( + "An institution cannot be more than one of PDP, Edvise Schema (ES), or Legacy. " + "Please choose one schema type." +) + class InstitutionCreationRequest(BaseModel): """Institution data creation request. @@ -51,8 +65,15 @@ class InstitutionCreationRequest(BaseModel): allowed_emails: Dict[str, AccessType] | None = None # The following is a shortcut to specifying the allowed_schemas list and will mean # that the allowed_schemas will be augmented with the PDP_SCHEMA_GROUP. + # Note: is_pdp is kept for backward compatibility but is ignored. PDP status is derived from pdp_id presence. is_pdp: bool | None = None pdp_id: str | None = None + # Note: is_edvise is kept for backward compatibility. When True and edvise_id is omitted, edvise_id is auto-assigned. + is_edvise: bool | None = None + edvise_id: str | None = None + # Legacy schools: upload data in any format. When True and legacy_id is omitted, legacy_id is auto-assigned. + is_legacy: bool | None = None + legacy_id: str | None = None retention_days: int | None = None @@ -66,6 +87,8 @@ class Institution(BaseModel): # If zero, it follows DK defaults (deletion after completion). retention_days: int | None = None # In Days pdp_id: str | None = None + edvise_id: str | None = None + legacy_id: str | None = None @router.get("/institutions", response_model=list[Institution]) @@ -93,22 +116,284 @@ def read_all_inst( "state": elem[0].state, "retention_days": elem[0].retention_days, "pdp_id": None if elem[0].pdp_id is None else elem[0].pdp_id, + "edvise_id": None if elem[0].edvise_id is None else elem[0].edvise_id, + "legacy_id": None if elem[0].legacy_id is None else elem[0].legacy_id, } ) return res -@router.post("/institutions", response_model=Institution) -def create_institution( +def _request_has_more_than_one_school_type( req: InstitutionCreationRequest, - current_user: Annotated[BaseUser, Depends(get_current_active_user)], - sql_session: Annotated[Session, Depends(get_session)], - storage_control: Annotated[StorageControl, Depends(StorageControl)], - databricks_control: Annotated[DatabricksControl, Depends(DatabricksControl)], -) -> Any: - """Create a new institution. + pdp_id: Optional[str], + edvise_id: Optional[str], + legacy_id: Optional[str], +) -> bool: + """Return True if the request indicates more than one of PDP, Edvise Schema (ES), or Legacy.""" + pdp_set = bool(pdp_id) + edvise_set = bool(req.is_edvise) or bool(edvise_id) + legacy_set = bool(req.is_legacy) or bool(legacy_id) + return (pdp_set + edvise_set + legacy_set) > 1 + + +def _compute_edvise_legacy_ids_for_create( + sess: Session, + req: InstitutionCreationRequest, + edvise_id: Optional[str], + legacy_id: Optional[str], +) -> Tuple[Optional[str], Optional[str]]: + """Auto-assign edvise_id or legacy_id when type is set but no id provided. Returns (edvise_id, legacy_id).""" + if req.is_edvise and not edvise_id: + count = ( + sess.execute( + select(func.count()) + .select_from(InstTable) + .where(InstTable.edvise_id.isnot(None)) + ).scalar() + or 0 + ) + edvise_id = f"edvise_{count + 1}" + if req.is_legacy and not legacy_id: + count = ( + sess.execute( + select(func.count()) + .select_from(InstTable) + .where(InstTable.legacy_id.isnot(None)) + ).scalar() + or 0 + ) + legacy_id = f"legacy_{count + 1}" + return (edvise_id, legacy_id) - Only available to Datakinders. + +def _raise_if_existing_row_invalid_for_duplicate_post(existing: InstTable) -> None: + """Reject idempotent POST when the stored row violates school-type invariants.""" + ep, ee, el = existing.pdp_id, existing.edvise_id, existing.legacy_id + if not has_at_most_one_school_type(ep, ee, el): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=_MUTUALLY_EXCLUSIVE_SCHOOL_TYPE_DETAIL, + ) + if sum(bool(x) for x in (ep, ee, el)) != 1: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + "An institution with this name and state already exists but does not " + "have a valid school type configured. " + + _EXACTLY_ONE_SCHOOL_TYPE_DETAIL + ), + ) + + +def _raise_if_institution_name_patch_disallowed( + update_data: dict, existing_name: str +) -> None: + if "name" in update_data and update_data["name"] != existing_name: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Institution names cannot be changed.", + ) + + +def _normalize_patch_school_id_strings(update_data: dict) -> None: + for key in ("pdp_id", "edvise_id", "legacy_id"): + if key in update_data: + update_data[key] = (update_data[key] or "").strip() or None + + +def _resolve_merged_school_type_triple_for_patch( + existing_inst: InstTable, + update_data: dict, + sess: Session, +) -> Tuple[Optional[str], Optional[str], Optional[str], bool]: + """Merge PATCH school-type fields, auto-assign ids, enforce exactly one type.""" + old_type_triple = ( + existing_inst.pdp_id, + existing_inst.edvise_id, + existing_inst.legacy_id, + ) + _raise_if_institution_name_patch_disallowed(update_data, existing_inst.name) + _normalize_patch_school_id_strings(update_data) + final_pdp_id = ( + update_data["pdp_id"] if "pdp_id" in update_data else existing_inst.pdp_id + ) + final_edvise_id = ( + update_data["edvise_id"] + if "edvise_id" in update_data + else existing_inst.edvise_id + ) + final_legacy_id = ( + update_data["legacy_id"] + if "legacy_id" in update_data + else existing_inst.legacy_id + ) + if _patch_indicates_more_than_one_school_type( + update_data, final_pdp_id, final_edvise_id, final_legacy_id + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=_MUTUALLY_EXCLUSIVE_SCHOOL_TYPE_DETAIL, + ) + final_edvise_id, final_legacy_id = _compute_edvise_legacy_ids_for_patch( + sess, update_data, final_edvise_id, final_legacy_id + ) + if not has_at_most_one_school_type(final_pdp_id, final_edvise_id, final_legacy_id): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=_MUTUALLY_EXCLUSIVE_SCHOOL_TYPE_DETAIL, + ) + if sum(bool(x) for x in (final_pdp_id, final_edvise_id, final_legacy_id)) != 1: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=_EXACTLY_ONE_SCHOOL_TYPE_DETAIL, + ) + school_type_changed = old_type_triple != ( + final_pdp_id, + final_edvise_id, + final_legacy_id, + ) + return final_pdp_id, final_edvise_id, final_legacy_id, school_type_changed + + +def _require_single_institution_row_by_uuid(sess: Session, inst_id: str) -> InstTable: + """Load exactly one InstTable row by UUID or raise HTTP 400.""" + query_result = sess.execute( + select(InstTable).where(InstTable.id == str_to_uuid(inst_id)) + ).all() + if not query_result or len(query_result) != 1: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Unexpected number of institutions found with this id. Expected 1 got " + + str(len(query_result)), + ) + return cast(InstTable, query_result[0][0]) + + +def _persist_institution_patch_row_fields( + existing_inst: InstTable, + update_data: dict, + final_pdp_id: Optional[str], + final_edvise_id: Optional[str], + final_legacy_id: Optional[str], +) -> None: + """Apply non-schema PATCH fields and the resolved school-type triple to the ORM row.""" + if "state" in update_data: + existing_inst.state = update_data["state"] + if "allowed_emails" in update_data: + existing_inst.allowed_emails = update_data["allowed_emails"] + if "retention_days" in update_data: + existing_inst.retention_days = update_data["retention_days"] + existing_inst.pdp_id = final_pdp_id + existing_inst.edvise_id = final_edvise_id + existing_inst.legacy_id = final_legacy_id + + +def _apply_institution_schema_updates_from_patch( + existing_inst: InstTable, + update_data: dict, + school_type_changed: bool, + final_pdp_id: Optional[str], + final_edvise_id: Optional[str], + final_legacy_id: Optional[str], +) -> None: + if school_type_changed: + extra_allowed = ( + update_data["allowed_schemas"] if "allowed_schemas" in update_data else None + ) + existing_inst.schemas = _build_requested_schemas( + extra_allowed, + final_pdp_id, + final_edvise_id, + final_legacy_id, + ) + elif "allowed_schemas" in update_data: + existing_inst.schemas = update_data["allowed_schemas"] + + +def _patch_indicates_more_than_one_school_type( + update_data: dict, + pdp_id: Optional[str], + edvise_id: Optional[str], + legacy_id: Optional[str], +) -> bool: + """True if merged IDs plus explicit is_edvise/is_legacy flags imply more than one school type.""" + pdp_set = bool(pdp_id) + edvise_flag = update_data["is_edvise"] if "is_edvise" in update_data else False + legacy_flag = update_data["is_legacy"] if "is_legacy" in update_data else False + edvise_set = bool(edvise_id) or bool(edvise_flag) + legacy_set = bool(legacy_id) or bool(legacy_flag) + return (pdp_set + edvise_set + legacy_set) > 1 + + +def _compute_edvise_legacy_ids_for_patch( + sess: Session, + update_data: dict, + edvise_id: Optional[str], + legacy_id: Optional[str], +) -> Tuple[Optional[str], Optional[str]]: + """Auto-assign edvise_id/legacy_id when PATCH sets is_edvise/is_legacy True and id is still empty.""" + if update_data.get("is_edvise") and not edvise_id: + count = ( + sess.execute( + select(func.count()) + .select_from(InstTable) + .where(InstTable.edvise_id.isnot(None)) + ).scalar() + or 0 + ) + edvise_id = f"edvise_{count + 1}" + if update_data.get("is_legacy") and not legacy_id: + count = ( + sess.execute( + select(func.count()) + .select_from(InstTable) + .where(InstTable.legacy_id.isnot(None)) + ).scalar() + or 0 + ) + legacy_id = f"legacy_{count + 1}" + return (edvise_id, legacy_id) + + +def _build_requested_schemas( + allowed_schemas: Optional[list], + pdp_id: Optional[str], + edvise_id: Optional[str], + legacy_id: Optional[str], +) -> list: + """Merge optional explicit allowed_schemas with the schema group for the school type. + + Callers must ensure exactly one of pdp_id, edvise_id, or legacy_id is set; + the merged groups are always non-empty. + """ + requested_schemas = list(allowed_schemas) if allowed_schemas else [] + if pdp_id: + requested_schemas += list(PDP_SCHEMA_GROUP) + if edvise_id: + requested_schemas += list(EDVISE_SCHEMA_GROUP) + if legacy_id: + requested_schemas += list(LEGACY_SCHEMA_GROUP) + return list(set(requested_schemas)) + + +def _build_requested_schemas_for_create( + req: InstitutionCreationRequest, + pdp_id: Optional[str], + edvise_id: Optional[str], + legacy_id: Optional[str], +) -> list: + """Build the requested_schemas list from req and school-type IDs (same rules as PATCH).""" + return _build_requested_schemas(req.allowed_schemas, pdp_id, edvise_id, legacy_id) + + +def _validate_and_prepare_create_institution( + req: InstitutionCreationRequest, + current_user: BaseUser, + sql_session: Session, +) -> Tuple[Session, Optional[str], Optional[str], Optional[str]]: + """ + Validate request and compute normalized IDs. Returns (sess, pdp_id, edvise_id, legacy_id). + Raises HTTPException on validation failure. """ if not current_user.is_datakinder(): raise HTTPException( @@ -120,95 +405,156 @@ def create_institution( status_code=status.HTTP_400_BAD_REQUEST, detail="Please set the institution name.", ) - if (req.is_pdp and not req.pdp_id) or (req.pdp_id and not req.is_pdp): + if not re.match(r"^[A-Za-z0-9&_ -]*$", req.name): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Please set the PDP's Institution ID for PDP schools and check PDP as a schema type.", + detail="Only alphanumeric characters, -, _, &, and a space are allowed in institution names.", ) + pdp_id = (req.pdp_id or "").strip() or None + edvise_id = (req.edvise_id or "").strip() or None + legacy_id = (req.legacy_id or "").strip() or None - pattern = "^[A-Za-z0-9&_ -]*$" - if not re.match(pattern, req.name): + if _request_has_more_than_one_school_type(req, pdp_id, edvise_id, legacy_id): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Only alphanumeric characters, -, _, &, and a space are allowed in institution names.", + detail=_MUTUALLY_EXCLUSIVE_SCHOOL_TYPE_DETAIL, ) - local_session.set(sql_session) - query_result = ( - local_session.get() - .execute( - select(InstTable).where( - and_(InstTable.name == req.name, InstTable.state == req.state) - ) + sess = local_session.get() + edvise_id, legacy_id = _compute_edvise_legacy_ids_for_create( + sess, req, edvise_id, legacy_id + ) + if not has_at_most_one_school_type(pdp_id, edvise_id, legacy_id): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=_MUTUALLY_EXCLUSIVE_SCHOOL_TYPE_DETAIL, ) - .all() + school_type_count = sum(bool(x) for x in (pdp_id, edvise_id, legacy_id)) + if school_type_count != 1: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=_EXACTLY_ONE_SCHOOL_TYPE_DETAIL, + ) + return (sess, pdp_id, edvise_id, legacy_id) + + +def _institution_row_to_response(row: Any) -> Dict[str, Any]: + """Build the Institution response dict from an InstTable row.""" + return { + "inst_id": uuid_to_str(row.id), + "name": row.name, + "state": row.state, + "pdp_id": row.pdp_id, + "edvise_id": row.edvise_id, + "legacy_id": row.legacy_id, + "retention_days": row.retention_days, + } + + +def _create_institution_record_and_infrastructure( + sess: Session, + req: InstitutionCreationRequest, + pdp_id: Optional[str], + edvise_id: Optional[str], + legacy_id: Optional[str], + requested_schemas: list, + current_user: BaseUser, + storage_control: StorageControl, + databricks_control: DatabricksControl, +) -> Any: + """ + Add institution record, commit, create bucket and Databricks setup. Returns the created InstTable row. + Raises HTTPException on failure. + """ + sess.add( + InstTable( + name=req.name, + retention_days=req.retention_days, + pdp_id=pdp_id, + edvise_id=edvise_id, + legacy_id=legacy_id, + schemas=requested_schemas, + allowed_emails=req.allowed_emails, + state=req.state, + created_by=str_to_uuid(current_user.user_id), + ) + ) + sess.commit() + query_result = sess.execute( + select(InstTable).where(InstTable.name == req.name) + ).all() + if not query_result: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Database write of the institution creation failed.", + ) + if len(query_result) > 1: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Database write of the institution created duplicate entries.", + ) + inst_row = query_result[0][0] + bucket_name = get_external_bucket_name_from_uuid(inst_row.id) + try: + storage_control.create_bucket(bucket_name) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Storage bucket creation failed:" + str(e), + ) from e + try: + databricks_control.setup_new_inst(inst_row.name) + except Exception as e: + # DatabricksControl may raise various exceptions; catch and re-raise with context. + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Databricks setup failed:" + str(e), + ) from e + return inst_row + + +@router.post("/institutions", response_model=Institution) +def create_institution( + req: InstitutionCreationRequest, + current_user: Annotated[BaseUser, Depends(get_current_active_user)], + sql_session: Annotated[Session, Depends(get_session)], + storage_control: Annotated[StorageControl, Depends(StorageControl)], + databricks_control: Annotated[DatabricksControl, Depends(DatabricksControl)], +) -> Any: + """Create a new institution. Only available to Datakinders.""" + sess, pdp_id, edvise_id, legacy_id = _validate_and_prepare_create_institution( + req, current_user, sql_session ) + query_result = sess.execute( + select(InstTable).where( + and_(InstTable.name == req.name, InstTable.state == req.state) + ) + ).all() if len(query_result) == 0: - # If the institution does not exist create it and create a storage bucket for it. - requested_schemas = [] - if req.allowed_schemas: - requested_schemas = req.allowed_schemas - if req.is_pdp: - requested_schemas += PDP_SCHEMA_GROUP - # if no schema is set and PDP is not set, we default to custom. - if not requested_schemas: - requested_schemas = {SchemaType.UNKNOWN} - local_session.get().add( - InstTable( - name=req.name, - retention_days=req.retention_days, - pdp_id=req.pdp_id, - # Sets aren't json serializable, so turn them into lists first - schemas=list(set(requested_schemas)), - allowed_emails=req.allowed_emails, - state=req.state, - created_by=str_to_uuid(current_user.user_id), - ) - ) - local_session.get().commit() - query_result = ( - local_session.get() - .execute(select(InstTable).where(InstTable.name == req.name)) - .all() - ) - if not query_result: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Database write of the institution creation failed.", - ) - if len(query_result) > 1: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Database write of the institution created duplicate entries.", - ) - # Create a storage bucket for it. During creation, we have to include the /. - bucket_name = get_external_bucket_name_from_uuid(query_result[0][0].id) - try: - storage_control.create_bucket(bucket_name) - except ValueError as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Storage bucket creation failed:" + str(e), - ) from e - try: - databricks_control.setup_new_inst(query_result[0][0].name) - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Databricks setup failed:" + str(e), - ) from e + requested_schemas = _build_requested_schemas_for_create( + req, pdp_id, edvise_id, legacy_id + ) + row = _create_institution_record_and_infrastructure( + sess, + req, + pdp_id, + edvise_id, + legacy_id, + requested_schemas, + current_user, + storage_control, + databricks_control, + ) + else: + existing = query_result[0][0] + _raise_if_existing_row_invalid_for_duplicate_post(existing) + row = existing if len(query_result) > 1: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Institution duplicates found.", ) - return { - "inst_id": uuid_to_str(query_result[0][0].id), - "name": query_result[0][0].name, - "state": query_result[0][0].state, - "pdp_id": query_result[0][0].pdp_id, - "retention_days": query_result[0][0].retention_days, - } + return _institution_row_to_response(row) @router.patch("/institutions/{inst_id}", response_model=Institution) @@ -218,79 +564,48 @@ def update_inst( current_user: Annotated[BaseUser, Depends(get_current_active_user)], sql_session: Annotated[Session, Depends(get_session)], ) -> Any: - """Modifies an existing institution. Only some fields are allowed to be modified.""" + """Modifies an existing institution. + + The row must keep exactly one of pdp_id, edvise_id, or legacy_id (same as POST). + ``is_edvise`` / ``is_legacy`` in the body trigger the same auto-id assignment as + POST when the corresponding id is still empty after merging with the existing row. + + ``schemas`` is recomputed (like POST) only when the school-type triple actually + changes; ``allowed_schemas`` alone still replaces ``schemas`` when no type change. + """ if not current_user.is_datakinder(): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authorized to modify an institution.", ) - update_data = request.model_dump(exclude_unset=True) local_session.set(sql_session) - # Check that the batch exists. - query_result = ( - local_session.get() - .execute( - select(InstTable).where( - InstTable.id == str_to_uuid(inst_id), - ) - ) - .all() + sess = local_session.get() + existing_inst = _require_single_institution_row_by_uuid(sess, inst_id) + ( + final_pdp_id, + final_edvise_id, + final_legacy_id, + school_type_changed, + ) = _resolve_merged_school_type_triple_for_patch(existing_inst, update_data, sess) + _persist_institution_patch_row_fields( + existing_inst, + update_data, + final_pdp_id, + final_edvise_id, + final_legacy_id, ) - if not query_result or len(query_result) != 1: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Unexpected number of institutions found with this id. Expected 1 got " - + str(len(query_result)), - ) - existing_inst = query_result[0][0] - if "name" in update_data: - if update_data["name"] != existing_inst.name: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Institution names cannot be changed.", - ) - if ( - "is_pdp" in update_data - and update_data["is_pdp"] - and "pdp_id" not in update_data - and not existing_inst.pdp_id - ): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="If is_pdp is set, pdp_id must also be set.", - ) - - if "state" in update_data: - existing_inst.state = update_data["state"] - if "allowed_schemas" in update_data: - existing_inst.allowed_schemas = update_data["allowed_schemas"] - if "allowed_emails" in update_data: - existing_inst.allowed_emails = update_data["allowed_emails"] - if "is_pdp" in update_data: - existing_inst.is_pdp = update_data["is_pdp"] - if "pdp_id" in update_data: - existing_inst.pdp_id = update_data["pdp_id"] - if "retention_days" in update_data: - existing_inst.retention_days = update_data["retention_days"] - - local_session.get().commit() - res = ( - local_session.get() - .execute( - select(InstTable).where( - InstTable.id == str_to_uuid(inst_id), - ) - ) - .all() + _apply_institution_schema_updates_from_patch( + existing_inst, + update_data, + school_type_changed, + final_pdp_id, + final_edvise_id, + final_legacy_id, ) - return { - "inst_id": uuid_to_str(res[0][0].id), - "name": res[0][0].name, - "state": res[0][0].state, - "pdp_id": res[0][0].pdp_id, - "retention_days": res[0][0].retention_days, - } + sess.commit() + refreshed = _require_single_institution_row_by_uuid(sess, inst_id) + return _institution_row_to_response(refreshed) @router.delete("/institutions/{inst_id}", response_model=None) @@ -357,11 +672,17 @@ def read_inst_name( """Returns overview data on a specific institution. The root-level API view. Only visible to users of that institution or Datakinder access types. + + Note: Name matching is case-insensitive. The function will match institution names + regardless of the case of the input parameter. If multiple institutions with the same + name (case-insensitive) exist, this will raise an error. """ local_session.set(sql_session) query_result = ( local_session.get() - .execute(select(InstTable).where(InstTable.name == inst_name)) + .execute( + select(InstTable).where(func.lower(InstTable.name) == func.lower(inst_name)) + ) .all() ) @@ -382,6 +703,8 @@ def read_inst_name( "retention_days": query_result[0][0].retention_days, "state": query_result[0][0].state, "pdp_id": query_result[0][0].pdp_id, + "edvise_id": query_result[0][0].edvise_id, + "legacy_id": query_result[0][0].legacy_id, } @@ -416,6 +739,8 @@ def read_inst_pdp_id( "retention_days": query_result[0][0].retention_days, "state": query_result[0][0].state, "pdp_id": query_result[0][0].pdp_id, + "edvise_id": query_result[0][0].edvise_id, + "legacy_id": query_result[0][0].legacy_id, } @@ -452,4 +777,6 @@ def read_inst_id( "retention_days": query_result[0][0].retention_days, "state": query_result[0][0].state, "pdp_id": query_result[0][0].pdp_id, + "edvise_id": query_result[0][0].edvise_id, + "legacy_id": query_result[0][0].legacy_id, } diff --git a/src/webapp/routers/institutions_test.py b/src/webapp/routers/institutions_test.py index f2dba5f1..d0280629 100644 --- a/src/webapp/routers/institutions_test.py +++ b/src/webapp/routers/institutions_test.py @@ -3,6 +3,7 @@ import uuid import os from datetime import datetime +from typing import Generator from unittest import mock import pytest import sqlalchemy @@ -27,6 +28,7 @@ DATETIME_TESTING = datetime.today() UUID_1 = uuid.uuid4() UUID_2 = uuid.uuid4() +UUID_3 = uuid.uuid4() # For Edvise Schema (ES) test institution USER_UUID = uuid.UUID("5301a352-c03d-4a39-beec-16c5668c4700") USER_VALID_INST_UUID = uuid.UUID("1d7c75c3-3eda-4294-9c66-75ea8af97b55") INVALID_UUID = uuid.UUID("27316b89-5e04-474a-9ea4-97beaf72c9af") @@ -40,8 +42,6 @@ def session_fixture(): """Unit test database setup.""" engine = sqlalchemy.create_engine( "sqlite://", - echo=True, - echo_pool="debug", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) @@ -55,12 +55,16 @@ def session_fixture(): name="school_1", state="GA", pdp_id="456", + edvise_id=None, created_at=DATETIME_TESTING, updated_at=DATETIME_TESTING, ), InstTable( id=UUID_2, name="school_2", + pdp_id=None, + edvise_id=None, + legacy_id=None, created_at=DATETIME_TESTING, updated_at=DATETIME_TESTING, ), @@ -68,10 +72,20 @@ def session_fixture(): id=USER_VALID_INST_UUID, name="valid_school", pdp_id="12345", + edvise_id=None, state="NY", created_at=DATETIME_TESTING, updated_at=DATETIME_TESTING, ), + InstTable( + id=UUID_3, + name="edvise_test_school", + state="CA", + pdp_id=None, + edvise_id="edvise456", + created_at=DATETIME_TESTING, + updated_at=DATETIME_TESTING, + ), ] ) session.commit() @@ -81,7 +95,9 @@ def session_fixture(): @pytest.fixture(name="client") -def client_fixture(session: sqlalchemy.orm.Session): +def client_fixture( + session: sqlalchemy.orm.Session, +) -> Generator[TestClient, None, None]: """Unit test mocks setup for a non-DATAKINDER type.""" def get_session_override(): @@ -108,7 +124,9 @@ def databricks_control_override(): @pytest.fixture(name="datakinder_client") -def datakinder_client_fixture(session: sqlalchemy.orm.Session): +def datakinder_client_fixture( + session: sqlalchemy.orm.Session, +) -> Generator[TestClient, None, None]: """Unit test mocks setup for a DATAKINDER type.""" def get_session_override(): @@ -134,7 +152,7 @@ def databricks_control_override(): app.dependency_overrides.clear() -def test_read_all_inst(client: TestClient): +def test_read_all_inst(client: TestClient) -> None: """Test GET /institutions.""" # Unauthorized. @@ -146,16 +164,32 @@ def test_read_all_inst(client: TestClient): ) -def test_read_all_inst_datakinder(datakinder_client: TestClient): +def test_read_all_inst_datakinder(datakinder_client: TestClient) -> None: """Test GET /institutions using DATAKINDER type.""" # Authorized. response = datakinder_client.get("/institutions") assert response.status_code == 200 + data = response.json() + # Verify all institutions have edvise_id, pdp_id, and legacy_id fields + for inst in data: + assert "edvise_id" in inst + assert "pdp_id" in inst + assert "legacy_id" in inst + # Verify specific expected values + assert len(data) == 4 # UUID_1, UUID_2, UUID_3, USER_VALID_INST_UUID + school_1 = next(i for i in data if i["name"] == "school_1") + assert school_1["pdp_id"] == "456" + assert school_1["edvise_id"] is None + edvise_school = next(i for i in data if i["name"] == "edvise_test_school") + assert edvise_school["edvise_id"] == "edvise456" + assert edvise_school["pdp_id"] is None assert response.json() == [ { "inst_id": uuid_to_str(UUID_1), "name": "school_1", "pdp_id": "456", + "edvise_id": None, + "legacy_id": None, "retention_days": None, "state": "GA", }, @@ -163,6 +197,8 @@ def test_read_all_inst_datakinder(datakinder_client: TestClient): "inst_id": uuid_to_str(UUID_2), "name": "school_2", "pdp_id": None, + "edvise_id": None, + "legacy_id": None, "retention_days": None, "state": None, }, @@ -170,13 +206,24 @@ def test_read_all_inst_datakinder(datakinder_client: TestClient): "inst_id": uuid_to_str(USER_VALID_INST_UUID), "name": "valid_school", "pdp_id": "12345", + "edvise_id": None, + "legacy_id": None, "retention_days": None, "state": "NY", }, + { + "inst_id": uuid_to_str(UUID_3), + "name": "edvise_test_school", + "pdp_id": None, + "edvise_id": "edvise456", + "legacy_id": None, + "retention_days": None, + "state": "CA", + }, ] -def test_read_inst_by_name(client: TestClient): +def test_read_inst_by_name(client: TestClient) -> None: """Test GET /institutions/name/. For various user access types.""" # Unauthorized. response = client.get("/institutions/name/school_1") @@ -193,7 +240,47 @@ def test_read_inst_by_name(client: TestClient): assert response.json() == INSTITUTION_OBJ -def test_read_inst_by_pdp_id(client: TestClient): +def test_read_inst_by_name_case_insensitive(client: TestClient) -> None: + """Test GET /institutions/name/ with case-insensitive matching.""" + # Test with different case variations - should all match + test_cases = [ + "valid_school", # Original case + "Valid_School", # Title case + "VALID_SCHOOL", # All uppercase + "vAlId_ScHoOl", # Mixed case + ] + + for name_variant in test_cases: + response = client.get(f"/institutions/name/{name_variant}") + assert response.status_code == 200, f"Failed for variant: {name_variant}" + data = response.json() + assert data == INSTITUTION_OBJ, f"Response mismatch for variant: {name_variant}" + + +def test_read_inst_by_name_case_insensitive_lowercase( + datakinder_client: TestClient, +) -> None: + """Test GET /institutions/name/ with lowercase input when DB has mixed case.""" + # Test that lowercase input matches mixed case in database + # Using datakinder_client since regular client doesn't have access to school_1 + response = datakinder_client.get("/institutions/name/school_1") + assert response.status_code == 200 + # Verify it matches the institution with name "school_1" (lowercase in DB) + assert response.json()["name"] == "school_1" + + +def test_read_inst_by_name_case_insensitive_uppercase( + datakinder_client: TestClient, +) -> None: + """Test GET /institutions/name/ with uppercase input.""" + # Test that uppercase input matches lowercase in database + # Using datakinder_client since regular client doesn't have access to school_1 + response = datakinder_client.get("/institutions/name/SCHOOL_1") + assert response.status_code == 200 + assert response.json()["name"] == "school_1" + + +def test_read_inst_by_pdp_id(client: TestClient) -> None: """Test GET /institutions/pdp-id/. For various user access types.""" # Unauthorized. response = client.get("/institutions/pdp-id/456") @@ -210,7 +297,7 @@ def test_read_inst_by_pdp_id(client: TestClient): assert response.json() == INSTITUTION_OBJ -def test_read_inst(client: TestClient): +def test_read_inst(client: TestClient) -> None: """Test GET /institutions/. For various user access types.""" # Unauthorized. response = client.get("/institutions/" + uuid_to_str(UUID_1)) @@ -227,7 +314,7 @@ def test_read_inst(client: TestClient): assert response.json() == INSTITUTION_OBJ -def test_create_inst_unauth(client): +def test_create_inst_unauth(client: TestClient) -> None: """Test POST /institutions. For various user access types.""" os.environ["ENV"] = "DEV" # Unauthorized. @@ -236,7 +323,7 @@ def test_create_inst_unauth(client): assert response.text == '{"detail":"Not authorized to create an institution."}' -def test_create_inst(datakinder_client): +def test_create_inst(datakinder_client: TestClient) -> None: """Test POST /institutions. For various user access types.""" MOCK_STORAGE.create_bucket.return_value = None MOCK_STORAGE.create_folders.return_value = None @@ -256,7 +343,8 @@ def test_create_inst(datakinder_client): assert response.json()["name"] == "testing school" response = datakinder_client.post( - "/institutions", json={"name": "Testing A & M - Main Campus _ hello"} + "/institutions", + json={"name": "Testing A & M - Main Campus _ hello", "is_legacy": True}, ) assert response.status_code == 200 @@ -264,13 +352,156 @@ def test_create_inst(datakinder_client): "/institutions", json={"name": "Testing (invalid)"} ) assert response.status_code == 400 - assert ( - response.text - == '{"detail":"Only alphanumeric characters, -, _, &, and a space are allowed in institution names."}' + assert response.text == ( + '{"detail":"Only alphanumeric characters, -, _, &, ' + 'and a space are allowed in institution names."}' + ) + + +def test_create_inst_rejects_no_school_type(datakinder_client: TestClient) -> None: + """POST /institutions requires exactly one of PDP, Edvise Schema (ES), or Legacy.""" + os.environ["ENV"] = "DEV" + response = datakinder_client.post( + "/institutions", + json={"name": "no_type_school"}, + ) + assert response.status_code == 400 + assert "exactly one" in response.json()["detail"] + + +def test_create_inst_rejects_duplicate_when_existing_row_has_no_school_type( + datakinder_client: TestClient, +) -> None: + """POST must not return 200 for (name, state) match if the stored row is typeless.""" + os.environ["ENV"] = "DEV" + # UUID_2 fixture: name school_2, state None, all school-type ids null + response = datakinder_client.post( + "/institutions", + json={"name": "school_2", "is_legacy": True}, + ) + assert response.status_code == 400 + detail = response.json()["detail"] + assert "already exists" in detail + assert "exactly one" in detail.lower() + + +def test_create_inst_duplicate_name_state_ok_when_existing_row_is_valid( + datakinder_client: TestClient, +) -> None: + """Idempotent POST returns existing row when it already has exactly one school type.""" + os.environ["ENV"] = "DEV" + response = datakinder_client.post( + "/institutions", + json={ + "name": "school_1", + "state": "GA", + "pdp_id": "456", + "is_pdp": True, + }, + ) + assert response.status_code == 200 + data = response.json() + assert data["name"] == "school_1" + assert data["pdp_id"] == "456" + assert data["edvise_id"] is None + + +def test_create_inst_rejects_is_pdp_without_pdp_id( + datakinder_client: TestClient, +) -> None: + """is_pdp alone does not set a school type; pdp_id is required for PDP (POST parity).""" + os.environ["ENV"] = "DEV" + response = datakinder_client.post( + "/institutions", + json={"name": "pdp_flag_only", "state": "WA", "is_pdp": True}, + ) + assert response.status_code == 400 + assert "exactly one" in response.json()["detail"].lower() + + +def test_create_inst_rejects_duplicate_when_existing_row_has_conflicting_ids( + datakinder_client: TestClient, + session: sqlalchemy.orm.Session, +) -> None: + """POST (name, state) match must 400 if stored row violates mutual exclusivity.""" + inst = session.get(InstTable, UUID_1) + assert inst is not None + saved = (inst.pdp_id, inst.edvise_id, inst.legacy_id) + try: + inst.edvise_id = "corrupt_edvise" + session.commit() + response = datakinder_client.post( + "/institutions", + json={ + "name": "school_1", + "state": "GA", + "pdp_id": "456", + "is_pdp": True, + }, + ) + assert response.status_code == 400 + assert "more than one" in response.json()["detail"].lower() + finally: + inst.pdp_id, inst.edvise_id, inst.legacy_id = saved + session.commit() + + +def test_update_inst_patch_is_edvise_on_pdp_institution_returns_400( + datakinder_client: TestClient, +) -> None: + """Cannot set is_edvise intent while row still has pdp_id (must clear in same PATCH).""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_1), + json={"is_edvise": True}, + ) + assert response.status_code == 400 + assert "more than one" in response.json()["detail"].lower() + + +def test_update_inst_patch_both_is_edvise_and_is_legacy_returns_400( + datakinder_client: TestClient, +) -> None: + """PATCH cannot indicate both Edvise and Legacy in one request.""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_2), + json={"is_edvise": True, "is_legacy": True}, + ) + assert response.status_code == 400 + assert "more than one" in response.json()["detail"].lower() + + +def test_update_inst_allowed_schemas_only_updates_schemas( + datakinder_client: TestClient, + session: sqlalchemy.orm.Session, +) -> None: + """allowed_schemas without changing type triple replaces schemas (no recompute merge).""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + inst = session.get(InstTable, UUID_1) + assert inst is not None + inst.schemas = ["STUDENT", "COURSE"] + session.commit() + + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_1), + json={"allowed_schemas": ["UNKNOWN"]}, ) + assert response.status_code == 200 + session.refresh(inst) + assert inst.schemas == ["UNKNOWN"] -def test_edit_inst(datakinder_client): +def test_edit_inst(datakinder_client: TestClient) -> None: """Test PATCH /institutions/. For various user access types.""" MOCK_STORAGE.create_bucket.return_value = None MOCK_STORAGE.create_folders.return_value = None @@ -291,9 +522,10 @@ def test_edit_inst(datakinder_client): assert response.json()["name"] == "school_1" assert response.json()["state"] == "NY" assert response.json()["pdp_id"] == "123" + assert "edvise_id" in response.json() -def test_delete_inst(datakinder_client): +def test_delete_inst(datakinder_client: TestClient) -> None: """Test DELETE /institutions/. For various user access types.""" MOCK_STORAGE.delete_bucket.return_value = None MOCK_DATABRICKS.delete_inst.return_value = None @@ -308,3 +540,689 @@ def test_delete_inst(datakinder_client): response2 = datakinder_client.get("/institutions/" + uuid_to_str(UUID_1)) assert response2.status_code == 404 + + +# ============================================================================ +# CREATE INSTITUTION TESTS - Edvise Schema (ES) functionality +# ============================================================================ + + +def test_create_inst_with_edvise_success(datakinder_client: TestClient) -> None: + """Test POST /institutions with Edvise Schema (ES) (edvise_id) - happy path.""" + os.environ["ENV"] = "DEV" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + request_data = { + "name": "new_edvise_school", + "state": "TX", + "edvise_id": "edvise789", + "is_edvise": True, # Should be ignored but accepted + } + + response = datakinder_client.post("/institutions", json=request_data) + assert response.status_code == 200 + data = response.json() + assert data["name"] == "new_edvise_school" + assert data["state"] == "TX" + assert data["edvise_id"] == "edvise789" + assert data["pdp_id"] is None + assert "inst_id" in data + + +def test_create_inst_with_edvise_id_only(datakinder_client: TestClient) -> None: + """Test POST /institutions with edvise_id but no is_edvise flag.""" + os.environ["ENV"] = "DEV" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + request_data = { + "name": "edvise_only_test", + "edvise_id": "edvise999", + # Note: is_edvise not provided - should still work + } + + response = datakinder_client.post("/institutions", json=request_data) + assert response.status_code == 200 + data = response.json() + assert data["edvise_id"] == "edvise999" + assert data["pdp_id"] is None + + +def test_create_inst_mutual_exclusivity_error(datakinder_client: TestClient) -> None: + """Test POST /institutions with both PDP and Edvise Schema (ES) - should fail.""" + os.environ["ENV"] = "DEV" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + request_data = { + "name": "conflict_school", + "pdp_id": "pdp123", + "edvise_id": "edvise456", + } + + response = datakinder_client.post("/institutions", json=request_data) + assert response.status_code == 400 + assert "Please choose one schema type" in response.json()["detail"] + + +def test_create_inst_empty_string_normalization(datakinder_client: TestClient) -> None: + """Test POST /institutions - empty strings normalized to None.""" + os.environ["ENV"] = "DEV" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + # Empty strings normalize to None; institution stays Legacy-only + request_data = { + "name": "normalization_test", + "pdp_id": "", # Empty string + "edvise_id": " ", # Whitespace only + "is_legacy": True, + } + + response = datakinder_client.post("/institutions", json=request_data) + assert response.status_code == 200 + data = response.json() + assert data["pdp_id"] is None + assert data["edvise_id"] is None + assert data["legacy_id"] is not None + + +def test_create_inst_whitespace_stripping(datakinder_client: TestClient) -> None: + """Test POST /institutions - whitespace is stripped from IDs.""" + os.environ["ENV"] = "DEV" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + request_data = { + "name": "whitespace_test", + "edvise_id": " edvise123 ", # Has whitespace + } + + response = datakinder_client.post("/institutions", json=request_data) + assert response.status_code == 200 + data = response.json() + assert data["edvise_id"] == "edvise123" # Whitespace stripped + + +def test_create_inst_backward_compatibility_is_pdp_ignored( + datakinder_client: TestClient, +) -> None: + """Test POST /institutions - is_pdp flag is accepted but ignored when pdp_id is set.""" + os.environ["ENV"] = "DEV" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + request_data = { + "name": "backward_compat_test", + "is_pdp": True, + "pdp_id": "pdp_backward", + } + + response = datakinder_client.post("/institutions", json=request_data) + assert response.status_code == 200 + data = response.json() + assert data["pdp_id"] == "pdp_backward" + + +def test_create_inst_auto_assign_edvise_id( + datakinder_client: TestClient, +) -> None: + """Test POST /institutions - is_edvise=True with no edvise_id auto-assigns edvise_id.""" + os.environ["ENV"] = "DEV" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + request_data = { + "name": "auto_edvise_test", + "is_edvise": True, + "edvise_id": None, + } + + response = datakinder_client.post("/institutions", json=request_data) + assert response.status_code == 200 + data = response.json() + assert data["edvise_id"] is not None and data["edvise_id"].startswith("edvise_") + assert data["pdp_id"] is None + assert data["legacy_id"] is None + + +def test_create_inst_auto_assign_legacy_id( + datakinder_client: TestClient, +) -> None: + """Test POST /institutions - is_legacy=True with no legacy_id auto-assigns legacy_id.""" + os.environ["ENV"] = "DEV" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + request_data = { + "name": "auto_legacy_test", + "is_legacy": True, + "legacy_id": None, + } + + response = datakinder_client.post("/institutions", json=request_data) + assert response.status_code == 200 + data = response.json() + assert data["legacy_id"] == "legacy_1" + assert data["pdp_id"] is None + assert data["edvise_id"] is None + + +def test_create_inst_reject_both_edvise_and_legacy( + datakinder_client: TestClient, +) -> None: + """Test POST /institutions - is_edvise and is_legacy both True returns 400.""" + request_data = { + "name": "both_types_test", + "is_edvise": True, + "is_legacy": True, + } + response = datakinder_client.post("/institutions", json=request_data) + assert response.status_code == 400 + assert "cannot be more than one" in response.json()["detail"] + + +def test_create_inst_with_legacy_id_explicit(datakinder_client: TestClient) -> None: + """Test POST /institutions with explicit legacy_id (no auto-assign).""" + os.environ["ENV"] = "DEV" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + request_data = { + "name": "explicit_legacy_school", + "state": "TX", + "legacy_id": "custom_legacy_123", + } + response = datakinder_client.post("/institutions", json=request_data) + assert response.status_code == 200 + data = response.json() + assert data["legacy_id"] == "custom_legacy_123" + assert data["pdp_id"] is None + assert data["edvise_id"] is None + + +def test_create_inst_storage_bucket_fails(datakinder_client: TestClient) -> None: + """Test POST /institutions returns 500 when storage bucket creation raises.""" + os.environ["ENV"] = "DEV" + MOCK_STORAGE.create_bucket.side_effect = ValueError("Bucket already exists") + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + try: + response = datakinder_client.post( + "/institutions", + json={"name": "school_bucket_fail", "state": "NY", "is_legacy": True}, + ) + assert response.status_code == 500 + assert "Storage bucket creation failed" in response.json()["detail"] + finally: + MOCK_STORAGE.create_bucket.side_effect = None + + +def test_create_inst_databricks_setup_fails(datakinder_client: TestClient) -> None: + """Test POST /institutions returns 500 when Databricks setup raises.""" + os.environ["ENV"] = "DEV" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.side_effect = Exception( + "Databricks connection failed" + ) + + try: + response = datakinder_client.post( + "/institutions", + json={"name": "school_dbc_fail", "state": "CA", "is_legacy": True}, + ) + assert response.status_code == 500 + assert "Databricks setup failed" in response.json()["detail"] + finally: + MOCK_DATABRICKS.setup_new_inst.side_effect = None + + +# ============================================================================ +# UPDATE INSTITUTION TESTS - Edvise Schema (ES) functionality +# ============================================================================ + + +def test_update_inst_add_edvise_id(datakinder_client: TestClient) -> None: + """Test PATCH /institutions - add edvise_id to existing institution.""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + # UUID_2 starts with no school type; first PATCH assigns Edvise (ES) only. + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_2), + json={"edvise_id": "new_edvise_id"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["edvise_id"] == "new_edvise_id" + assert data["pdp_id"] is None + + +def test_update_inst_add_legacy_id(datakinder_client: TestClient) -> None: + """Test PATCH /institutions - add legacy_id when institution had no type yet.""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_2), + json={"legacy_id": "legacy_abc"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["legacy_id"] == "legacy_abc" + assert data["pdp_id"] is None + assert data["edvise_id"] is None + + +def test_update_inst_switch_pdp_to_edvise(datakinder_client: TestClient) -> None: + """Test PATCH /institutions - switch from PDP to Edvise Schema (ES).""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + # First clear PDP, then add Edvise Schema (ES) + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_1), + json={"pdp_id": None, "edvise_id": "switched_to_edvise"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["pdp_id"] is None + assert data["edvise_id"] == "switched_to_edvise" + + +def test_update_inst_switch_edvise_to_pdp(datakinder_client: TestClient) -> None: + """Test PATCH /institutions - switch from Edvise Schema (ES) to PDP.""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + # Switch UUID_3 (Edvise Schema (ES)) to PDP + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_3), + json={"edvise_id": None, "pdp_id": "switched_to_pdp"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["pdp_id"] == "switched_to_pdp" + assert data["edvise_id"] is None + + +def test_update_inst_mutual_exclusivity_error(datakinder_client: TestClient) -> None: + """Test PATCH /institutions - cannot set both PDP and Edvise Schema (ES).""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + # UUID_2 has no school type yet; still cannot set two types at once. + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_2), + json={"pdp_id": "pdp123", "edvise_id": "edvise456"}, + ) + assert response.status_code == 400 + assert "Please choose one schema type" in response.json()["detail"] + + +def test_update_inst_clear_edvise_id_requires_other_type( + datakinder_client: TestClient, +) -> None: + """PATCH cannot leave zero school types; clearing edvise must set another type.""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_2), + json={"edvise_id": "temp_edvise"}, + ) + assert response.status_code == 200 + + # Clearing the only school type is rejected + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_2), + json={"edvise_id": None}, + ) + assert response.status_code == 400 + assert "exactly one" in response.json()["detail"] + + # Atomic switch: clear edvise and set legacy in one request + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_2), + json={"edvise_id": None, "legacy_id": "after_switch"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["edvise_id"] is None + assert data["legacy_id"] == "after_switch" + + +def test_update_inst_empty_string_normalization(datakinder_client: TestClient) -> None: + """Test PATCH /institutions - empty strings normalized to None (final state still one type).""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + # UUID_3 is Edvise-only; empty pdp_id is normalized to None without dropping the type + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_3), + json={"pdp_id": ""}, + ) + assert response.status_code == 200 + data = response.json() + assert data["pdp_id"] is None + assert data["edvise_id"] == "edvise456" + + +def test_update_inst_whitespace_stripping(datakinder_client: TestClient) -> None: + """Test PATCH /institutions - whitespace is stripped from IDs.""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_2), + json={"edvise_id": " trimmed_id "}, # Has whitespace + ) + assert response.status_code == 200 + data = response.json() + assert data["edvise_id"] == "trimmed_id" # Whitespace stripped + + +# ============================================================================ +# GET ENDPOINT TESTS - edvise_id in Responses +# ============================================================================ + + +def test_read_inst_by_id_includes_edvise_id(client: TestClient) -> None: + """Test GET /institutions/{inst_id} - response includes edvise_id.""" + # Authorized access + response = client.get("/institutions/" + uuid_to_str(USER_VALID_INST_UUID)) + assert response.status_code == 200 + data = response.json() + assert "edvise_id" in data + assert data["edvise_id"] is None # This institution doesn't have Edvise Schema (ES) + + +def test_read_inst_by_name_includes_edvise_id(client: TestClient) -> None: + """Test GET /institutions/name/{name} - response includes edvise_id.""" + # Authorized access + response = client.get("/institutions/name/valid_school") + assert response.status_code == 200 + data = response.json() + assert "edvise_id" in data + + +def test_read_inst_by_pdp_id_includes_edvise_id(client: TestClient) -> None: + """Test GET /institutions/pdp-id/{pdp_id} - response includes edvise_id.""" + # Authorized access + response = client.get("/institutions/pdp-id/12345") + assert response.status_code == 200 + data = response.json() + assert "edvise_id" in data + assert data["edvise_id"] is None + + +# ============================================================================ +# EDGE CASES AND ERROR HANDLING +# ============================================================================ + + +def test_create_inst_none_values(datakinder_client: TestClient) -> None: + """Test POST /institutions - explicit None values handled correctly with Legacy type.""" + os.environ["ENV"] = "DEV" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + request_data = { + "name": "none_values_test", + "pdp_id": None, + "edvise_id": None, + "is_legacy": True, + } + + response = datakinder_client.post("/institutions", json=request_data) + assert response.status_code == 200 + data = response.json() + assert data["pdp_id"] is None + assert data["edvise_id"] is None + assert data["legacy_id"] is not None + + +def test_update_inst_partial_update_preserves_existing( + datakinder_client: TestClient, +) -> None: + """Test PATCH /institutions - partial update preserves existing edvise_id.""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + # First set edvise_id + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_2), + json={"edvise_id": "preserved_edvise"}, + ) + assert response.status_code == 200 + + # Update only state, edvise_id should be preserved + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_2), + json={"state": "FL"}, # Only update state + ) + assert response.status_code == 200 + data = response.json() + assert data["state"] == "FL" + assert data["edvise_id"] == "preserved_edvise" # Preserved + + +def test_update_inst_final_state_validation(datakinder_client: TestClient) -> None: + """Test PATCH /institutions - validates final state, not just update data.""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + # Institution already has pdp_id, try to add edvise_id + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_1), # Has pdp_id="456" + json={"edvise_id": "edvise999"}, # Try to add Edvise Schema (ES) + ) + assert response.status_code == 400 + assert "Please choose one schema type" in response.json()["detail"] + + +def test_update_inst_rejects_patch_when_final_state_has_no_school_type( + datakinder_client: TestClient, +) -> None: + """Institution rows must always have exactly one type; state-only PATCH cannot fix typeless rows.""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_2), + json={"state": "TX"}, + ) + assert response.status_code == 400 + assert "exactly one" in response.json()["detail"] + + +def test_update_inst_is_legacy_auto_assigns_id(datakinder_client: TestClient) -> None: + """PATCH is_legacy True assigns legacy_id when row had no type (same as POST).""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_2), + json={"is_legacy": True}, + ) + assert response.status_code == 200 + data = response.json() + assert data["legacy_id"] is not None + assert data["legacy_id"].startswith("legacy_") + assert data["pdp_id"] is None + assert data["edvise_id"] is None + + +def test_update_inst_is_edvise_auto_assigns_id(datakinder_client: TestClient) -> None: + """PATCH is_edvise True assigns edvise_id when row had no type (same as POST).""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_2), + json={"is_edvise": True}, + ) + assert response.status_code == 200 + data = response.json() + assert data["edvise_id"] is not None + assert data["edvise_id"].startswith("edvise_") + assert data["pdp_id"] is None + assert data["legacy_id"] is None + + +def test_update_inst_same_pdp_id_preserves_schemas( + datakinder_client: TestClient, + session: sqlalchemy.orm.Session, +) -> None: + """PATCH that does not change the school-type triple must not reset schemas.""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + inst = session.get(InstTable, UUID_1) + assert inst is not None + inst.schemas = ["MANUAL_SCHEMA_MARKER"] + session.commit() + + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_1), + json={"pdp_id": "456"}, + ) + assert response.status_code == 200 + session.refresh(inst) + assert inst.schemas == ["MANUAL_SCHEMA_MARKER"] + + +def test_update_inst_changed_pdp_id_recomputes_schemas( + datakinder_client: TestClient, + session: sqlalchemy.orm.Session, +) -> None: + """When pdp_id value changes, schemas are recomputed for the new type triple.""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + inst = session.get(InstTable, UUID_1) + assert inst is not None + inst.schemas = ["MANUAL_SCHEMA_MARKER"] + session.commit() + + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_1), + json={"pdp_id": "789"}, + ) + assert response.status_code == 200 + session.refresh(inst) + assert "MANUAL_SCHEMA_MARKER" not in inst.schemas + assert len(inst.schemas) > 0 + + +# ============================================================================ +# AUTHORIZATION TESTS +# ============================================================================ + + +def test_create_inst_edvise_unauthorized(client: TestClient) -> None: + """Test POST /institutions with Edvise Schema (ES) - unauthorized user.""" + os.environ["ENV"] = "DEV" + request_data = { + "name": "unauthorized_test", + "edvise_id": "edvise123", + } + + response = client.post("/institutions", json=request_data) + assert response.status_code == 401 + assert "Not authorized to create" in response.json()["detail"] + + +def test_update_inst_edvise_unauthorized(client: TestClient) -> None: + """Test PATCH /institutions with Edvise Schema (ES) - unauthorized user.""" + # Try to update institution user doesn't have access to + response = client.patch( + "/institutions/" + uuid_to_str(UUID_1), + json={"edvise_id": "edvise123"}, + ) + assert response.status_code == 401 + assert "Not authorized" in response.json()["detail"] + + +# ============================================================================ +# TENANT ISOLATION TESTS +# ============================================================================ + + +def test_read_inst_edvise_tenant_isolation(client: TestClient) -> None: + """Test GET /institutions/{inst_id} - cannot access other institution's Edvise Schema (ES) data.""" + # Try to access institution user doesn't belong to + response = client.get("/institutions/" + uuid_to_str(UUID_2)) + assert response.status_code == 401 + assert "Not authorized" in response.json()["detail"] + + +# ============================================================================ +# BACKWARD COMPATIBILITY TESTS +# ============================================================================ + + +def test_create_inst_old_format_still_works(datakinder_client: TestClient) -> None: + """Test POST /institutions - old request format with is_pdp still works.""" + os.environ["ENV"] = "DEV" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + # Old format: is_pdp flag (should be ignored, pdp_id used instead) + request_data = { + "name": "old_format_test", + "is_pdp": True, + "pdp_id": "pdp_old_format", + } + + response = datakinder_client.post("/institutions", json=request_data) + assert response.status_code == 200 + data = response.json() + # Should use pdp_id, not is_pdp flag + assert data["pdp_id"] == "pdp_old_format" + + +def test_update_inst_old_format_still_works(datakinder_client: TestClient) -> None: + """Test PATCH /institutions - old request format with is_pdp still works.""" + MOCK_STORAGE.create_bucket.return_value = None + MOCK_STORAGE.create_folders.return_value = None + MOCK_DATABRICKS.setup_new_inst.return_value = None + + # Old format: is_pdp flag (should be ignored) + response = datakinder_client.patch( + "/institutions/" + uuid_to_str(UUID_2), + json={"is_pdp": True, "pdp_id": "pdp_update"}, # is_pdp ignored + ) + assert response.status_code == 200 + data = response.json() + # Should use pdp_id value + assert data["pdp_id"] == "pdp_update" diff --git a/src/webapp/routers/models.py b/src/webapp/routers/models.py index cb7949f6..02be74ae 100644 --- a/src/webapp/routers/models.py +++ b/src/webapp/routers/models.py @@ -1,11 +1,11 @@ """API functions related to models.""" from datetime import datetime -from typing import Annotated, Any +from typing import Annotated, Any, cast import jsonpickle from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel -from sqlalchemy import and_ +from sqlalchemy import and_, update, or_ from sqlalchemy.orm import Session from sqlalchemy.future import select from ..databricks import DatabricksControl, DatabricksInferenceRunRequest @@ -33,6 +33,7 @@ import traceback import logging from ..gcsdbutils import update_db_from_bucket +from ..config import env_vars from ..gcsutil import StorageControl @@ -60,7 +61,7 @@ def check_file_types_valid_schema_configs( """Check that a list of files are valid for a given schema configuration.""" for config in valid_schema_configs: found = True - map_file_to_schema_config_obj = {} + map_file_to_schema_config_obj: dict = {} for idx, s in enumerate(file_types): for c in config: if c.schema_type in s: @@ -134,7 +135,8 @@ class InferenceRunRequest(BaseModel): """Parameters for an inference run.""" batch_name: str - # This MUST be set for uses of the PDP inference pipeline. + # Note: is_pdp is kept for backward compatibility but is ignored. + # PDP status is derived from the institution's pdp_id field. is_pdp: bool = False @@ -302,6 +304,49 @@ def read_inst_model( } +@router.delete("/{inst_id}/models/{model_name}") +def delete_model( + inst_id: str, + model_name: str, + current_user: Annotated[BaseUser, Depends(get_current_active_user)], + sql_session: Annotated[Session, Depends(get_session)], +) -> Any: + transformed_model_name = str(decode_url_piece(model_name)).strip() + has_access_to_inst_or_err(inst_id, current_user) + + local_session.set(sql_session) + sess = local_session.get() + + model_list = sess.execute( + select(ModelTable).where( + ModelTable.name == transformed_model_name, + ModelTable.inst_id == str_to_uuid(inst_id), + ) + ).scalar_one_or_none() + if model_list is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Model not found." + ) + + # 2) Optionally Delete models from databricks itself + # TODO: Add databricks deletion functionality + + try: + sess.delete(model_list) + sess.commit() + except Exception as e: + sess.rollback() + raise HTTPException( + status_code=500, detail=f"DB batch delete failed after file cleanup: {e}" + ) + + return { + "inst_id": inst_id, + "model_name": transformed_model_name, + "status": "Model deleted", + } + + @router.get("/{inst_id}/models/{model_name}/runs", response_model=list[RunInfo]) def read_inst_model_outputs( inst_id: str, @@ -356,6 +401,8 @@ def read_inst_model_outputs( "inst_id": uuid_to_str(query_result[0][0].inst_id), "m_name": query_result[0][0].name, "run_id": elem.id, + "model_run_id": elem.model_run_id, + "model_version": elem.model_version, "created_by": uuid_to_str(elem.created_by), "triggered_at": elem.triggered_at, "batch_name": elem.batch_name, @@ -458,11 +505,6 @@ def trigger_inference_run( """ model_name = decode_url_piece(model_name) has_access_to_inst_or_err(inst_id, current_user) - if not req.is_pdp: - raise HTTPException( - status_code=status.HTTP_501_NOT_IMPLEMENTED, - detail="Currently, only PDP inference is supported.", - ) local_session.set(sql_session) inst_result = ( local_session.get() @@ -481,6 +523,13 @@ def trigger_inference_run( detail="Unexpected number of institutions found: Expected 1, got " + str(len(inst_result)), ) + inst = inst_result[0][0] + # Check PDP status from institution's pdp_id (ignore req.is_pdp for backward compat) + if not inst.pdp_id: + raise HTTPException( + status_code=status.HTTP_501_NOT_IMPLEMENTED, + detail="Currently, only PDP inference is supported.", + ) query_result = ( local_session.get() .execute( @@ -546,7 +595,7 @@ def trigger_inference_run( model_name=model_name, gcp_external_bucket_name=get_external_bucket_name(inst_id), # The institution email to which pipeline success/failure notifications will get sent. - email=current_user.email, + email=cast(str, current_user.email), ) try: res = databricks_control.run_pdp_inference(db_req) @@ -558,6 +607,11 @@ def trigger_inference_run( detail=f"Databricks run_pdp_inference error. Error = {str(e)}", ) from e triggered_timestamp = datetime.now() + latest_model_version = databricks_control.fetch_model_version( + catalog_name=str(env_vars["CATALOG_NAME"]), + inst_name=inst_result[0][0].name, + model_name=model_name, + ) job = JobTable( id=res.job_run_id, triggered_at=triggered_timestamp, @@ -565,6 +619,8 @@ def trigger_inference_run( batch_name=req.batch_name, model_id=query_result[0][0].id, output_valid=False, + model_version=latest_model_version.version, + model_run_id=latest_model_version.run_id, ) local_session.get().add(job) return { @@ -575,4 +631,130 @@ def trigger_inference_run( "triggered_at": triggered_timestamp, "batch_name": req.batch_name, "output_valid": False, + "model_version": latest_model_version.version, + "model_run_id": latest_model_version.run_id, + } + + +@router.get("/{inst_id}/models/{model_name}/get-model-versions") +def get_model_versions( + inst_id: str, + model_name: str, + current_user: Annotated[BaseUser, Depends(get_current_active_user)], + sql_session: Annotated[Session, Depends(get_session)], + databricks_control: Annotated[DatabricksControl, Depends(DatabricksControl)], +) -> Any: + transformed_model_name = str(decode_url_piece(model_name)).strip() + has_access_to_inst_or_err(inst_id, current_user) + + local_session.set(sql_session) + query_result = ( + local_session.get() + .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) + .all() + ) + if not query_result or len(query_result) == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Institution not found.", + ) + if len(query_result) > 1: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Institution duplicates found.", + ) + + print(f"Initial model name = {model_name}") + print(f"Converted model name {transformed_model_name}") + + latest_model_version = databricks_control.fetch_model_version( + catalog_name=str(env_vars["CATALOG_NAME"]), + inst_name=f"{query_result[0][0].name}", + model_name=transformed_model_name, + ) + + return latest_model_version + + +@router.post("/{inst_id}/models/{model_name}/backfill-model-runs") +def backfill_model_runs( + inst_id: str, + model_name: str, + current_user: Annotated[BaseUser, Depends(get_current_active_user)], + sql_session: Annotated[Session, Depends(get_session)], + databricks_control: Annotated[DatabricksControl, Depends(DatabricksControl)], +) -> Any: + """Backfills missing model run metadata and returns the latest model version info. + + Temporary endpoint to populate model_run_id and model_version on existing jobs for this model. + Use only when backfilling historical job runs, not for regular operation. + """ + model_name = str(decode_url_piece(model_name)).strip() + has_access_to_inst_or_err(inst_id, current_user) + + # Load institution + local_session.set(sql_session) + inst_row = ( + local_session.get() + .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) + .all() + ) + + model_id = ( + local_session.get() + .execute( + select(ModelTable).where( + and_( + ModelTable.inst_id == str_to_uuid(inst_id), + ModelTable.name == model_name, + ) + ) + ) + .all() + ) + + if not inst_row or len(inst_row) == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Institution not found.", + ) + if len(inst_row) > 1: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Institution duplicates found.", + ) + + latest_mv = databricks_control.fetch_model_version( + catalog_name=str(env_vars["CATALOG_NAME"]), + inst_name=f"{inst_row[0][0].name}", + model_name=model_name, + ) + + mv_version = str(latest_mv.version) + mv_run_id = str(latest_mv.run_id) + + # UPDATE existing jobs for this model (only those missing values) + stmt = ( + update(JobTable) + .where(JobTable.model_id == model_id[0][0].id) + .where( + or_( + JobTable.model_run_id.is_(None), + JobTable.model_run_id == "", + JobTable.model_version.is_(None), + JobTable.model_version == "", + ) + ) + .values(model_run_id=mv_run_id, model_version=mv_version) + ) + result = local_session.get().execute(stmt) + updated_count = result.rowcount or 0 # type: ignore + local_session.get().commit() + + return { + "inst_id": str(inst_id), + "model_id": str(model_id[0][0].id), + "model_name": model_name, + "latest_model_version": {"version": mv_version, "run_id": mv_run_id}, + "updated_count": updated_count, } diff --git a/src/webapp/routers/models_test.py b/src/webapp/routers/models_test.py index 8643f98b..cfa57671 100644 --- a/src/webapp/routers/models_test.py +++ b/src/webapp/routers/models_test.py @@ -2,6 +2,7 @@ import uuid from unittest import mock +from typing import Any import pytest import jsonpickle from fastapi.testclient import TestClient @@ -13,7 +14,6 @@ USER_UUID, UUID_INVALID, DATETIME_TESTING, - MODEL_OBJ, SAMPLE_UUID, ) from ..main import app @@ -50,32 +50,32 @@ # TODO plumb through schema configs -def same_model_orderless(a_elem: ModelInfo, b_elem: ModelInfo): +def same_model_orderless(a_elem: ModelInfo, b_elem: ModelInfo) -> bool: """Check ModelInfo equality without order.""" if ( - a_elem["inst_id"] != b_elem["inst_id"] - or a_elem["name"] != b_elem["name"] - or a_elem["m_id"] != b_elem["m_id"] - or a_elem["valid"] != b_elem["valid"] - or a_elem["deleted"] != b_elem["deleted"] + a_elem.inst_id != b_elem.inst_id + or a_elem.name != b_elem.name + or a_elem.m_id != b_elem.m_id + or a_elem.valid != b_elem.valid + or a_elem.deleted != b_elem.deleted ): return False return True -def same_run_info_orderless(a_elem: RunInfo, b_elem: RunInfo): +def same_run_info_orderless(a_elem: RunInfo, b_elem: RunInfo) -> bool: """Check RunInfo equality without order.""" if ( - a_elem["inst_id"] != b_elem["inst_id"] - or a_elem["m_name"] != b_elem["m_name"] - or a_elem["run_id"] != b_elem["run_id"] - or a_elem["created_by"] != b_elem["created_by"] - or a_elem["triggered_at"] != b_elem["triggered_at"] - or a_elem["output_filename"] != b_elem["output_filename"] - or a_elem["output_valid"] != b_elem["output_valid"] - or a_elem["err_msg"] != b_elem["err_msg"] - or a_elem["batch_name"] != b_elem["batch_name"] - or a_elem["completed"] != b_elem["completed"] + a_elem.inst_id != b_elem.inst_id + or a_elem.m_name != b_elem.m_name + or a_elem.run_id != b_elem.run_id + or a_elem.created_by != b_elem.created_by + or a_elem.triggered_at != b_elem.triggered_at + or a_elem.output_filename != b_elem.output_filename + or a_elem.output_valid != b_elem.output_valid + or a_elem.err_msg != b_elem.err_msg + or a_elem.batch_name != b_elem.batch_name + or a_elem.completed != b_elem.completed ): return False return True @@ -86,8 +86,6 @@ def session_fixture(): """Unit test database setup.""" engine = sqlalchemy.create_engine( "sqlite://", - echo=True, - echo_pool="debug", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) @@ -161,6 +159,7 @@ def session_fixture(): completed=True, output_filename="file_output_one", created_by=created_by_UUID, + model_run_id="T2UFD", ) try: with sqlalchemy.orm.Session(engine) as session: @@ -169,6 +168,8 @@ def session_fixture(): InstTable( id=USER_VALID_INST_UUID, name="school_1", + pdp_id="12345", + edvise_id=None, created_at=DATETIME_TESTING, updated_at=DATETIME_TESTING, ), @@ -198,7 +199,7 @@ def session_fixture(): @pytest.fixture(name="client") -def client_fixture(session: sqlalchemy.orm.Session): +def client_fixture(session: sqlalchemy.orm.Session) -> Any: """Unit test mocks setup.""" def get_session_override(): @@ -224,26 +225,25 @@ def databricks_control_override(): app.dependency_overrides.clear() -def test_read_inst_models(client: TestClient): +def test_read_inst_models(client: TestClient) -> None: """Test GET /institutions/345/models.""" response = client.get( "/institutions/" + uuid_to_str(USER_VALID_INST_UUID) + "/models" ) assert response.status_code == 200 assert same_model_orderless( - response.json()[0], - { - "created_by": "", - "deleted": None, - "inst_id": "1d7c75c33eda42949c6675ea8af97b55", - "m_id": "e4862c62829440d8ab4c9c298f02f619", - "name": "sample_model_for_school_1", - "valid": True, - }, + ModelInfo(**response.json()[0]), + ModelInfo( + m_id="e4862c62829440d8ab4c9c298f02f619", + name="sample_model_for_school_1", + inst_id="1d7c75c33eda42949c6675ea8af97b55", + deleted=None, + valid=True, + ), ) -def test_read_inst_model(client: TestClient): +def test_read_inst_model(client: TestClient) -> None: """Test GET /institutions/345/models/10. For various user access types.""" # Unauthorized cases. response_unauth = client.get( @@ -264,10 +264,18 @@ def test_read_inst_model(client: TestClient): + "/models/sample_model_for_school_1" ) assert response.status_code == 200 - assert same_model_orderless(response.json(), MODEL_OBJ) + response_model = ModelInfo(**response.json()) + expected_model = ModelInfo( + deleted=None, + inst_id="1d7c75c33eda42949c6675ea8af97b55", + m_id="e4862c62829440d8ab4c9c298f02f619", + name="sample_model_for_school_1", + valid=True, + ) + assert same_model_orderless(response_model, expected_model) -def test_read_inst_model_outputs(client: TestClient): +def test_read_inst_model_outputs(client: TestClient) -> None: """Test GET /institutions/345/models/10/output.""" MOCK_STORAGE.list_blobs_in_folder.return_value = [] # Authorized. @@ -277,24 +285,23 @@ def test_read_inst_model_outputs(client: TestClient): + "/models/sample_model_for_school_1/runs" ) assert response.status_code == 200 - assert same_run_info_orderless( - response.json()[0], - { - "batch_name": "batch_foo", - "completed": True, - "created_by": "0ad8b77c49fb459a84b18d2c05722c4a", - "err_msg": None, - "inst_id": "1d7c75c33eda42949c6675ea8af97b55", - "m_name": "sample_model_for_school_1", - "output_filename": "file_output_one", - "output_valid": False, - "run_id": 123, - "triggered_at": "2024-12-24T20:22:20.132022", - }, + response_model = RunInfo(**response.json()[0]) + expected_model = RunInfo( + batch_name="batch_foo", + created_by="0ad8b77c49fb459a84b18d2c05722c4a", + err_msg=None, + inst_id="1d7c75c33eda42949c6675ea8af97b55", + m_name="sample_model_for_school_1", + output_filename="file_output_one", + output_valid=False, + run_id=123, + triggered_at=response_model.triggered_at, # copy from response + completed=response_model.completed, ) + assert same_run_info_orderless(response_model, expected_model) -def test_read_inst_model_output(client: TestClient): +def test_read_inst_model_output(client: TestClient) -> None: """Test GET /institutions/345/models/10/output/1.""" # Authorized. response = client.get( @@ -304,24 +311,23 @@ def test_read_inst_model_output(client: TestClient): + str(RUN_ID) ) assert response.status_code == 200 - assert same_run_info_orderless( - response.json(), - { - "batch_name": "batch_foo", - "completed": True, - "created_by": "0ad8b77c49fb459a84b18d2c05722c4a", - "err_msg": None, - "inst_id": "1d7c75c33eda42949c6675ea8af97b55", - "m_name": "sample_model_for_school_1", - "output_filename": "file_output_one", - "output_valid": False, - "run_id": 123, - "triggered_at": "2024-12-24T20:22:20.132022", - }, + response_model = RunInfo(**response.json()) + expected_model = RunInfo( + batch_name="batch_foo", + completed=True, + created_by="0ad8b77c49fb459a84b18d2c05722c4a", + err_msg=None, + inst_id="1d7c75c33eda42949c6675ea8af97b55", + m_name="sample_model_for_school_1", + output_filename="file_output_one", + output_valid=False, + run_id=123, + triggered_at=response_model.triggered_at, # copy from response ) + assert same_run_info_orderless(response_model, expected_model) -def test_create_model(client: TestClient): +def test_create_model(client: TestClient) -> None: """Depending on timeline, fellows may not get to this.""" schema_config_1 = { "schema_type": SchemaType.COURSE, @@ -342,7 +348,7 @@ def test_create_model(client: TestClient): assert response.status_code == 200 -def test_trigger_inference_run(client: TestClient): +def test_trigger_inference_run(client: TestClient) -> None: """Depending on timeline, fellows may not get to this.""" MOCK_DATABRICKS.run_pdp_inference.return_value = DatabricksInferenceRunResponse( job_run_id=123 diff --git a/src/webapp/routers/users_test.py b/src/webapp/routers/users_test.py index b8c3e999..dba515ef 100644 --- a/src/webapp/routers/users_test.py +++ b/src/webapp/routers/users_test.py @@ -2,6 +2,7 @@ import uuid from datetime import datetime +from typing import Generator from fastapi.testclient import TestClient import pytest import sqlalchemy @@ -26,8 +27,6 @@ def session_fixture(): """Unit test database setup.""" engine = sqlalchemy.create_engine( "sqlite://", - echo=True, - echo_pool="debug", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) @@ -79,7 +78,9 @@ def session_fixture(): @pytest.fixture(name="client") -def client_fixture(session: sqlalchemy.orm.Session): +def client_fixture( + session: sqlalchemy.orm.Session, +) -> Generator[TestClient, None, None]: """Unit test mocks setup for non-Datakinder.""" def get_session_override(): @@ -98,7 +99,9 @@ def get_current_active_user_override(): @pytest.fixture(name="datakinder_client") -def datakinder_client_fixture(session: sqlalchemy.orm.Session): +def datakinder_client_fixture( + session: sqlalchemy.orm.Session, +) -> Generator[TestClient, None, None]: """Unit test mocks setup for datakinder.""" def get_session_override(): @@ -116,7 +119,7 @@ def get_current_active_user_override(): app.dependency_overrides.clear() -def test_read_inst_users(client: TestClient): +def test_read_inst_users(client: TestClient) -> None: """Test GET /institutions//users.""" response = client.get( "/institutions/" + uuid_to_str(USER_VALID_INST_UUID) + "/users" @@ -140,7 +143,7 @@ def test_read_inst_users(client: TestClient): ] -def test_read_inst_user(client: TestClient): +def test_read_inst_user(client: TestClient) -> None: """Test GET /institutions//users/. For various user access types.""" # Authorized. response = client.get( @@ -159,7 +162,7 @@ def test_read_inst_user(client: TestClient): } -def test_read_inst_allowed_emails(datakinder_client: TestClient): +def test_read_inst_allowed_emails(datakinder_client: TestClient) -> None: """Test GET /institutions//allowable-emails.""" # Authorized. response = datakinder_client.get( diff --git a/src/webapp/test_helper.py b/src/webapp/test_helper.py index a65e427d..f9c4d570 100644 --- a/src/webapp/test_helper.py +++ b/src/webapp/test_helper.py @@ -65,12 +65,12 @@ "retention_days": 1, "pdp_id": "12345", "is_pdp": True, - "allowed_schemas": ["UNKNOWN"], "allowed_emails": {"foo@foobar.edu": "VIEWER"}, } INSTITUTION_REQ_BAREBONES = { "name": "testing school", + "is_legacy": True, } EMPTY_INSTITUTION_OBJ = { @@ -78,6 +78,8 @@ "name": "", "state": "", "pdp_id": None, + "edvise_id": None, + "legacy_id": None, "retention_days": 0, } @@ -86,6 +88,8 @@ "name": "valid_school", "state": "NY", "pdp_id": "12345", + "edvise_id": None, + "legacy_id": None, "retention_days": None, } diff --git a/src/webapp/utilities.py b/src/webapp/utilities.py index 460d4e1d..76dcff74 100644 --- a/src/webapp/utilities.py +++ b/src/webapp/utilities.py @@ -2,7 +2,7 @@ import uuid import re -from typing import Annotated, Final, Any +from typing import Annotated, Final, Any, Optional, Tuple, Union from urllib.parse import unquote from strenum import StrEnum # needed for python pre 3.11 import jwt @@ -147,6 +147,37 @@ class SchemaType(StrEnum): SchemaType.COURSE, } +EDVISE_SCHEMA_GROUP: Final = { + SchemaType.STUDENT, + SchemaType.COURSE, +} + +LEGACY_SCHEMA_GROUP: Final = { + SchemaType.UNKNOWN, +} + + +def has_at_most_one_school_type( + pdp_id: str | None, + edvise_id: str | None, + legacy_id: str | None, +) -> bool: + """ + Return True if at most one of pdp_id, edvise_id, or legacy_id is set. + + Used to enforce mutual exclusivity: at most one of PDP, Edvise Schema (ES), + or Legacy may be set (create requires exactly one). + + Args: + pdp_id: PDP institution identifier, or None. + edvise_id: Edvise Schema (ES) institution identifier, or None. + legacy_id: Legacy institution identifier, or None. + + Returns: + True if zero or one of the three IDs is set; False if two or more are set. + """ + return sum(bool(x) for x in (pdp_id, edvise_id, legacy_id)) <= 1 + class BaseUser(BaseModel): """BaseUser represents an access type. The frontend will include more detailed User info.""" @@ -163,7 +194,9 @@ class BaseUser(BaseModel): disabled: bool | None = None # Constructor - def __init__(self, usr: str | None, inst: str, access: str, email: str) -> None: + def __init__( + self, usr: str | None, inst: str | None, access: str | None, email: str | None + ) -> None: super().__init__(user_id=usr, institution=inst, access_type=access, email=email) def is_datakinder(self) -> Any: @@ -182,7 +215,7 @@ def is_viewer(self) -> Any: """Whether a given user is a viewer.""" return self.access_type and self.access_type == AccessType.VIEWER - def has_access_to_inst(self, inst: str) -> Any: + def has_access_to_inst(self, inst: str | None) -> Any: """Whether a given user has access to a given institution.""" return self.access_type and ( self.access_type == AccessType.DATAKINDER or self.institution == inst @@ -215,28 +248,28 @@ def has_stronger_permissions_than(self, other_access_type: AccessType) -> bool: return False -def get_user(sess: Session, username: str) -> BaseUser: +def get_user(sess: Session, username: str) -> Optional[BaseUser]: """Get user from a given username.""" if username == "api_key_initial": return BaseUser( - usr=env_vars["INITIAL_API_KEY_ID"], + usr=str(env_vars["INITIAL_API_KEY_ID"]), inst=None, access="DATAKINDER", email="api_key_initial", ) if username.startswith("api_key_"): api_key_uuid = username.removeprefix("api_key_") - query_result = sess.execute( + apikey_query_result = sess.execute( select(ApiKeyTable).where( ApiKeyTable.id == str_to_uuid(api_key_uuid), ) ).all() - if len(query_result) == 0 or len(query_result) > 1: + if len(apikey_query_result) == 0 or len(apikey_query_result) > 1: return None return BaseUser( - usr=uuid_to_str(query_result[0][0].id), - inst=uuid_to_str(query_result[0][0].inst_id), - access=query_result[0][0].access_type, + usr=uuid_to_str(apikey_query_result[0][0].id), + inst=uuid_to_str(apikey_query_result[0][0].inst_id), + access=apikey_query_result[0][0].access_type, email=username, ) query_result = sess.execute( @@ -254,13 +287,15 @@ def get_user(sess: Session, username: str) -> BaseUser: ) -def authenticate_api_key(api_key_enduser_tuple: str, sess: Session) -> BaseUser: +def authenticate_api_key( + api_key_enduser_tuple: Tuple[str, Optional[str], Optional[str]], sess: Session +) -> Union[BaseUser, bool]: """Authenticate an API key.""" (key, inst, enduser) = api_key_enduser_tuple # Check if it's the initial API key. This doesn't have enduser or inst. if key == env_vars["INITIAL_API_KEY"]: return BaseUser( - usr=env_vars["INITIAL_API_KEY_ID"], + usr=str(env_vars["INITIAL_API_KEY_ID"]), inst=None, access="DATAKINDER", email="api_key_initial", @@ -291,7 +326,7 @@ def authenticate_api_key(api_key_enduser_tuple: str, sess: Session) -> BaseUser: user_query = select(AccountTable).where( and_( AccountTable.email == enduser, - AccountTable.inst_id == uuid_to_str(inst), + AccountTable.inst_id == inst, ) ) user_result = sess.execute(user_query).all() @@ -330,7 +365,9 @@ async def get_current_user( if not token_from_key: raise credentials_exception payload = jwt.decode( - token_from_key, env_vars["SECRET_KEY"], algorithms=env_vars["ALGORITHM"] + token_from_key, + str(env_vars["SECRET_KEY"]), + algorithms=env_vars["ALGORITHM"], ) usrname = payload.get("sub") if usrname is None: @@ -345,14 +382,14 @@ async def get_current_user( async def get_current_active_user( current_user: Annotated[BaseUser, Depends(get_current_user)], -): +) -> BaseUser: """Get the active user..""" if current_user.disabled: raise HTTPException(status_code=400, detail="Inactive user") return current_user -def has_access_to_inst_or_err(inst: str, user: BaseUser): +def has_access_to_inst_or_err(inst: str, user: BaseUser) -> None: """Raise error if a given user does not have access to a given institution.""" if not user.has_access_to_inst(inst): raise HTTPException( @@ -361,7 +398,7 @@ def has_access_to_inst_or_err(inst: str, user: BaseUser): ) -def has_full_data_access_or_err(user: BaseUser, resource_type: str): +def has_full_data_access_or_err(user: BaseUser, resource_type: str) -> None: """Raise error if a given user does not have data access to a given institution.""" if not user.has_full_data_access(): raise HTTPException( @@ -370,7 +407,7 @@ def has_full_data_access_or_err(user: BaseUser, resource_type: str): ) -def model_owner_and_higher_or_err(user: BaseUser, resource_type: str): +def model_owner_and_higher_or_err(user: BaseUser, resource_type: str) -> None: """Raise error if a given user does not have model ownership or higher.""" if not user.access_type or user.access_type not in ( AccessType.MODEL_OWNER, @@ -382,29 +419,33 @@ def model_owner_and_higher_or_err(user: BaseUser, resource_type: str): ) -def prepend_env_prefix(name: str) -> str: +def prepend_env_prefix(name: str) -> Any: """Prepend the env prefix. At this point the value should not be empty as we checked on app startup.""" - return env_vars["ENV"].lower() + "_" + name + env = str(env_vars["ENV"]).lower() + # Use dev_ prefix for LOCAL environment + if env == "local": + env = "dev" + return env + "_" + name -def uuid_to_str(uuid_val: uuid.UUID) -> str: +def uuid_to_str(uuid_val: uuid.UUID) -> Any: """Convert UUID obj to string.""" if uuid_val is None: return "" return uuid_val.hex -def str_to_uuid(hex_str: str) -> uuid.UUID: +def str_to_uuid(hex_str: Optional[str]) -> uuid.UUID: """Convert str to UUID obj (database needs UUID obj).""" return uuid.UUID(hex_str) -def get_external_bucket_name_from_uuid(inst_id: uuid.UUID) -> str: +def get_external_bucket_name_from_uuid(inst_id: uuid.UUID) -> Any: """Get the GCP bucket name which has the env prepended taking in the UUID obj.""" return prepend_env_prefix(uuid_to_str(inst_id)) -def get_external_bucket_name(inst_id: str) -> str: +def get_external_bucket_name(inst_id: str) -> Any: """Get the GCP bucket name which has the env prepended taking in the uuid as str.""" return prepend_env_prefix(inst_id) diff --git a/src/webapp/utilities_test.py b/src/webapp/utilities_test.py index e8046674..29617c9d 100644 --- a/src/webapp/utilities_test.py +++ b/src/webapp/utilities_test.py @@ -6,6 +6,7 @@ from .utilities import ( has_access_to_inst_or_err, has_full_data_access_or_err, + has_at_most_one_school_type, uuid_to_str, databricksify_inst_name, ) @@ -27,6 +28,18 @@ def test_base_user_class_functions(): assert not VIEWER.has_full_data_access() +def test_has_at_most_one_school_type() -> None: + """Test mutual exclusivity helper: at most one of pdp_id, edvise_id, legacy_id may be set.""" + assert has_at_most_one_school_type(None, None, None) is True + assert has_at_most_one_school_type("pdp1", None, None) is True + assert has_at_most_one_school_type(None, "edvise1", None) is True + assert has_at_most_one_school_type(None, None, "legacy1") is True + assert has_at_most_one_school_type("pdp1", "edvise1", None) is False + assert has_at_most_one_school_type("pdp1", None, "legacy1") is False + assert has_at_most_one_school_type(None, "edvise1", "legacy1") is False + assert has_at_most_one_school_type("pdp1", "edvise1", "legacy1") is False + + def test_has_access_to_inst_or_err(): """Testing valid check for access to institution.""" with pytest.raises(HTTPException) as err: @@ -48,25 +61,31 @@ def test_databricksify_inst_name(): Testing databricksifying institution name """ assert ( - databricksify_inst_name("Motlow State Community College") == "motlow_state_cc" + databricksify_inst_name("The University of Mildly Impressive Achievements") + == "the_uni_of_mildly_impressive_achievements" + ) + assert ( + databricksify_inst_name("Dandelion Technical & Tractor College") + == "dandelion_technical_tractor_col" + ) + assert ( + databricksify_inst_name("Fernwood & Finch Academy") == "fernwood_finch_academy" ) assert ( - databricksify_inst_name("Metro State University Denver") - == "metro_state_uni_denver" + databricksify_inst_name("The Center for Applied Napping") + == "the_center_for_applied_napping" ) - assert databricksify_inst_name("Kentucky State University") == "kentucky_state_uni" - assert databricksify_inst_name("Central Arizona College") == "central_arizona_col" assert ( - databricksify_inst_name("Harrisburg University of Science and Technology") - == "harrisburg_uni_st" + databricksify_inst_name("Harrisville University of Science and Technology") + == "harrisville_uni_st" ) assert ( - databricksify_inst_name("Southeast Kentucky community technical college") - == "southeast_kentucky_ctc" + databricksify_inst_name("University of Questionable Decisions") + == "uni_of_questionable_decisions" ) assert ( - databricksify_inst_name("Northwest State Community College") - == "northwest_state_cc" + databricksify_inst_name("Badger Hollow University of Science & Scones") + == "badger_hollow_uni_of_science_scones" ) with pytest.raises(ValueError) as err: diff --git a/src/webapp/validation.py b/src/webapp/validation.py index 3f359aaf..d3f8a67f 100644 --- a/src/webapp/validation.py +++ b/src/webapp/validation.py @@ -1,28 +1,120 @@ -"""File validation functions for various schemas. (Record by record validation happens in the -pipelines, this is for general file validation.) +"""File validation functions for various schemas. +Record-by-record validation happens in the pipelines; this module performs +general file validation with performance-focused improvements. + +Key speed-ups (without losing accuracy): +- Header-only pass to discover/resolve columns before full load +- Selective, typed CSV read via `usecols` and dtype mapping +- Exact-name Pandera schemas (avoid regex column matching) +- Fuzzy matching only for unresolved headers; use rapidfuzz if available +- Precompiled regexes and set-based membership checks inside Pandera checks """ -from typing import Any +from __future__ import annotations +import io +import os import json import re -from typing import Union, List, Dict, Optional import logging +import tempfile +from contextlib import contextmanager +from functools import lru_cache +from typing import ( + Any, + BinaryIO, + Callable, + Dict, + Generator, + List, + Optional, + Tuple, + Union, + cast, +) import pandas as pd from pandera import Column, Check, DataFrameSchema -from pandera.errors import SchemaErrors -from thefuzz import fuzz +from pandera.errors import SchemaError, SchemaErrors + +from edvise.dataio.read import read_raw_pdp_cohort_data, read_raw_pdp_course_data +from edvise.utils.data_cleaning import handling_duplicates + +from . import validation_pdp_edvise as pdp_edvise + +# Type for PDP converter functions (DataFrame -> DataFrame); used for cohort/course. +PDPConverterFunc = Optional[Callable[[pd.DataFrame], pd.DataFrame]] + + +def _default_pdp_course_duplicate_converter(df: pd.DataFrame) -> pd.DataFrame: + """ + PDP course duplicate cleanup for read_raw_pdp_course_data. + + Passes the schema selector as the second *positional* argument so this works + with current edvise (``schema_type``) and older builds that used the same slot + for ``school_type``. Do not pass bare ``handling_duplicates`` as a converter: + read_raw_pdp_course_data calls ``converter_func(df)`` with a single argument. + """ + return handling_duplicates(df, "pdp") + + +# --------------------------------------------------------------------------- # +# Logging +# --------------------------------------------------------------------------- # + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- # +# Public entry points +# --------------------------------------------------------------------------- # def validate_file_reader( - filename: str, + filename: Union[str, os.PathLike[str], BinaryIO, io.TextIOWrapper, io.StringIO], allowed_schema: list[str], base_schema: dict, inst_schema: Optional[Dict[Any, Any]] = None, + institution_id: str = "pdp", + institution_identifier: Optional[str] = None, + pdp_cohort_converter_func: PDPConverterFunc = None, + pdp_course_converter_func: PDPConverterFunc = None, ) -> dict[str, Any]: - """Validates given a filename.""" - return validate_dataset(filename, base_schema, inst_schema, allowed_schema) + """ + Validate a CSV from a path or file-like handle against schema selection. + + Thin wrapper around :func:`validate_dataset` with the same arguments + reordered for call sites that pass ``allowed_schema`` first. + + Args: + filename: Path or file-like object for the CSV. + allowed_schema: List of model names to validate against. + base_schema: Base schema dict (e.g. base.data_models). + inst_schema: Optional extension schema with institutions.* blocks. + institution_id: Key into inst_schema["institutions"]: "edvise", "pdp", + or "legacy" (any-format). Default "pdp". + institution_identifier: Optional institution identifier (e.g. UUID) for display/context. + pdp_cohort_converter_func: Optional cohort row transform before Pandera; default + None. Batch PDP jobs may still apply school-specific cohort converters via ``dataio``. + pdp_course_converter_func: Optional course converter; default duplicate handling only. + + Returns: + Dict with validation_status, schemas, missing_optional, unknown_extra_columns, + and on success normalized_df (DataFrame, or None if nothing was validated). + + Raises: + HardValidationError: When required columns are missing, schema validation fails, + or encoding cannot be resolved (decode failures use failure_cases, not UnicodeError). + """ + return validate_dataset( + filename, + base_schema, + inst_schema, + allowed_schema, + institution_id, + institution_identifier, + pdp_cohort_converter_func=pdp_cohort_converter_func, + pdp_course_converter_func=pdp_course_converter_func, + ) class HardValidationError(Exception): @@ -32,11 +124,17 @@ def __init__( extra_columns: Optional[List[str]] = None, schema_errors: Any = None, failure_cases: Any = None, + raw_to_canon: Optional[Dict[str, str]] = None, + canon_to_raw: Optional[Dict[str, str]] = None, + merged_specs: Optional[Dict[str, dict]] = None, ): self.missing_required = missing_required or [] self.extra_columns = extra_columns or [] self.schema_errors = schema_errors self.failure_cases = failure_cases + self.raw_to_canon = raw_to_canon or {} + self.canon_to_raw = canon_to_raw or {} + self.merged_specs = merged_specs or {} parts = [] if self.missing_required: parts.append(f"Missing required columns: {self.missing_required}") @@ -47,12 +145,18 @@ def __init__( super().__init__("; ".join(parts)) +# --------------------------------------------------------------------------- # +# Utilities +# --------------------------------------------------------------------------- # + + +@lru_cache(maxsize=4096) def normalize_col(name: str) -> str: - name = name.strip().lower() # Lowercase and trim whitespace - name = re.sub(r"[^a-z0-9_]", "_", name) # Replace non-alphanum with underscore - name = re.sub(r"_+", "_", name) # Collapse multiple underscores - name = name.strip("_") # Remove leading/trailing underscores - return name + """Normalize a column name: trim, lowercase, non-alnum->'_', collapse '_'s.""" + name = name.strip().lower() + name = re.sub(r"[^a-z0-9_]", "_", name) + name = re.sub(r"_+", "_", name) + return name.strip("_") def load_json(path: str) -> Any: @@ -60,57 +164,7 @@ def load_json(path: str) -> Any: with open(path, "r") as f: return json.load(f) except Exception as e: - raise FileNotFoundError(f"Failed to load JSON schema at {path}: {e}") - - -def rename_columns_to_match_schema( - df: pd.DataFrame, - canon_to_aliases: Dict[str, List[str]], - threshold: int = 90, -) -> pd.DataFrame: - """ - Rename incoming columns using fuzzy match against schema-defined column names and aliases. - - Args: - df: Incoming dataframe - canon_to_aliases: Mapping from canonical column names to list of aliases (including the canonical name itself) - threshold: Fuzzy match score threshold to rename - - Returns: - A new DataFrame with renamed columns - """ - from collections import defaultdict - - new_column_names = {} - log_info = defaultdict(list) - - schema_names = [] - for canon, aliases in canon_to_aliases.items(): - for name in aliases: - schema_names.append((name, canon)) # (alias_or_name, canonical_name) - - for incoming_col in df.columns: - best_score = 0 - best_match = None - best_canon = None - - for schema_col, canon in schema_names: - score = fuzz.ratio(incoming_col.lower(), schema_col.lower()) - if score > best_score: - best_score = score - best_match = schema_col - best_canon = canon - - if best_score >= threshold and incoming_col != best_canon: - new_column_names[incoming_col] = best_canon - log_info[incoming_col].append( - f"Renamed '{incoming_col}' -> '{best_canon}' (matched on '{best_match}', score={best_score})" - ) - - for k, v in log_info.items(): - logging.info(" | ".join(v)) - - return df.rename(columns=new_column_names) + raise FileNotFoundError(f"Failed to load JSON schema at {path}: {e}") from e def merge_model_columns( @@ -119,10 +173,12 @@ def merge_model_columns( institution: str, model: str, ) -> Dict[str, dict]: + """ + Merge base model columns with institution-specific extension, if present. + """ base_models = base_schema.get("base", {}).get("data_models", {}) if model not in base_models: - if logging: - logging.error(f"Model '{model}' not found in base schema") + logger.error("Model '%s' not found in base schema", model) raise KeyError(f"Model '{model}' not in base schema") merged = dict(base_models[model].get("columns", {})) if extension_schema: @@ -133,148 +189,1004 @@ def merge_model_columns( return merged -def build_schema(specs: Dict[str, dict]) -> DataFrameSchema: - columns = {} - for canon, spec in specs.items(): - names = [canon] + spec.get("aliases", []) - pattern = r"^(?:" + "|".join(map(re.escape, names)) + r")$" +def get_extension_model_columns_only( + extension_schema: Any, + institution: str, + model: str, +) -> Dict[str, dict]: + """ + Return only the extension columns for the given institution and model (no base). + Used for PDP and Edvise Schema (ES) so we do not pull in base columns that don't match the repo schema. + """ + if not extension_schema: + return {} + inst_block = extension_schema.get("institutions", {}).get(institution, {}) + ext_models = inst_block.get("data_models", {}) + if model not in ext_models: + return {} + return dict(ext_models[model].get("columns", {})) + + +# --------------------------------------------------------------------------- # +# Encoding sniffing (mypy-friendly) +# --------------------------------------------------------------------------- # + +Src = Union[str, os.PathLike[str], BinaryIO, io.TextIOWrapper, io.StringIO] + + +def _read_sample(buf: BinaryIO, n: int) -> bytes: + pos = buf.tell() if buf.seekable() else None + chunk = buf.read(n) + if pos is not None: + buf.seek(pos) + return chunk + + +def sniff_encoding(src: Src, sample_bytes: int = 1_048_576) -> str: + """ + Best-guess encoding via BOM detection + utf-8 trial. + Works with a filesystem path, a binary stream, or a TextIOWrapper. + Restores stream position if seekable. Raises if latin-1 would be used (by default). + """ + # --- read a small binary sample --- + if isinstance(src, (str, os.PathLike)): + with open(src, "rb") as f: + chunk: bytes = f.read(sample_bytes) + elif isinstance(src, io.TextIOWrapper): + # Text wrapper => use underlying binary buffer, cast to BinaryIO for mypy + chunk = _read_sample(cast(BinaryIO, src.buffer), sample_bytes) + else: + # Already a binary stream + chunk = _read_sample(cast(BinaryIO, src), sample_bytes) + + # --- BOMs first --- + if chunk.startswith(b"\xef\xbb\xbf"): + return "utf-8-sig" + if chunk.startswith(b"\xff\xfe\x00\x00"): + return "utf-32le" + if chunk.startswith(b"\x00\x00\xfe\xff"): + return "utf-32be" + if chunk.startswith(b"\xff\xfe"): + return "utf-16le" + if chunk.startswith(b"\xfe\xff"): + return "utf-16be" + + # --- utf-8 strict on sample --- + try: + chunk.decode("utf-8") + return "utf-8" + except UnicodeDecodeError: + raise UnicodeError( + "file is not UTF-8/UTF-16/UTF-32; please re-export as UTF-8." + ) + + +def _reset_to_start_if_possible(src: Src) -> None: + """Best-effort reset to the beginning for file-like objects.""" + try: + if hasattr(src, "seek") and callable(getattr(src, "seek")): + src.seek(0) # type: ignore[attr-defined] + except Exception: + pass + + +# --------------------------------------------------------------------------- # +# Fast header pass & mapping +# --------------------------------------------------------------------------- # + + +def _spec_alias_lookup( + merged_specs: Dict[str, dict], +) -> Tuple[Dict[str, str], Dict[str, List[str]]]: + """ + Build: + - alias2canon: normalized alias -> canonical + - canon_to_aliases_norm: canonical -> list of normalized aliases (incl. canonical) + """ + alias2canon: Dict[str, str] = {} + canon_to_aliases_norm: Dict[str, List[str]] = {} + for canon, spec in merged_specs.items(): + aliases = [canon] + spec.get("aliases", []) + normed = [normalize_col(a) for a in aliases] + canon_to_aliases_norm[canon] = normed + for a in normed: + alias2canon[a] = canon + return alias2canon, canon_to_aliases_norm + + +def _fuzzy_map_unresolved( + unresolved: List[Tuple[str, str]], # [(raw_header, normalized_header)] + choices: List[str], # normalized aliases + alias2canon: Dict[str, str], + threshold: int = 90, +) -> Dict[str, str]: # raw_header -> canonical + """ + Fuzzy-match only the unresolved headers, using RapidFuzz if available, otherwise thefuzz. + """ + mapping: Dict[str, str] = {} + try: + from rapidfuzz import process, fuzz as rf_fuzz # type: ignore + + for raw, norm in unresolved: + hit = process.extractOne( + norm, choices, scorer=rf_fuzz.ratio, score_cutoff=threshold + ) + if hit: + best_alias, score, _ = hit + mapping[raw] = alias2canon[best_alias] # type: ignore[index] + except Exception: + # fallback to thefuzz if rapidfuzz is unavailable + try: + from thefuzz import fuzz as tf_fuzz # type: ignore + except Exception: + # If neither library is available, do not fuzz-map anything. + return mapping + for raw, norm in unresolved: + best_score = 0 + best_alias = None + for alias in choices: + s = tf_fuzz.ratio(norm, alias) + if s > best_score: + best_score, best_alias = s, alias + if best_alias and best_score >= threshold: + mapping[raw] = alias2canon[best_alias] + return mapping + + +def _header_missing_and_extra( + merged_specs: Dict[str, dict], + raw_to_canon: Dict[str, str], + unresolved: List[Tuple[str, str]], + known_aliases: set, +) -> Tuple[List[str], List[str], List[str]]: + """Compute missing_required, missing_optional, unknown_extra from header mapping.""" + incoming_canons = set(raw_to_canon.values()) + missing_required = [ + c + for c, spec in merged_specs.items() + if spec.get("required", False) and c not in incoming_canons + ] + missing_optional = [ + c + for c, spec in merged_specs.items() + if not spec.get("required", False) and c not in incoming_canons + ] + unknown_extra = sorted( + {norm for (_, norm) in unresolved if norm not in known_aliases} + ) + return missing_required, missing_optional, unknown_extra + + +def _header_pass( + filename: Src, + encoding: str, + merged_specs: Dict[str, dict], + fuzzy_threshold: int = 90, +) -> Tuple[List[str], Dict[str, str], List[str], List[str], List[str]]: + """ + Read only the header. Return: + - raw_cols: list of column names as in file + - raw_to_canon: mapping raw header -> canonical (after exact+fuzzy) + - missing_required: list of canonical columns missing + - missing_optional: list of optional canonical columns missing + - unknown_extra: normalized headers that don't map to any alias + """ + header_df = pd.read_csv(filename, encoding=encoding, nrows=0) + raw_cols = list(header_df.columns) + + alias2canon, canon_to_aliases_norm = _spec_alias_lookup(merged_specs) + known_aliases = set(alias2canon.keys()) + + raw_to_canon: Dict[str, str] = {} + unresolved: List[Tuple[str, str]] = [] + for raw in raw_cols: + norm = normalize_col(raw) + if norm in alias2canon: + raw_to_canon[raw] = alias2canon[norm] + else: + unresolved.append((raw, norm)) + + if unresolved: + choices = list(known_aliases) + fuzzy_map = _fuzzy_map_unresolved( + unresolved, choices, alias2canon, threshold=fuzzy_threshold + ) + raw_to_canon.update(fuzzy_map) + + missing_required, missing_optional, unknown_extra = _header_missing_and_extra( + merged_specs, raw_to_canon, unresolved, known_aliases + ) + return raw_cols, raw_to_canon, missing_required, missing_optional, unknown_extra + + +def _pandas_dtype_and_parse_dates( + merged_specs: Dict[str, dict], +) -> Tuple[Dict[str, Any], List[str]]: + """ + Conservative mapping from spec dtype -> pandas read_csv dtype/parse_dates. + Keeps behavior stable while avoiding heavy inference. + """ + dtype_map: Dict[str, Any] = {} + parse_dates: List[str] = [] + + for canon, spec in merged_specs.items(): + dt = str(spec.get("dtype")) + if dt in {"string", "str", "object"}: + dtype_map[canon] = "string" + elif dt in {"int", "int64", "Int64"}: + dtype_map[canon] = "Int64" # nullable integers are safer for dirty data + elif dt in {"float", "float64"}: + dtype_map[canon] = "float64" + elif "datetime" in dt or "date" in dt: + parse_dates.append(canon) + elif dt in {"bool", "boolean"}: + dtype_map[canon] = "boolean" + elif dt == "category": + dtype_map[canon] = "category" + else: + # leave to pandas inference + pass + + return dtype_map, parse_dates + + +def _build_exact_schema( + specs: Dict[str, dict], only_canons: List[str] +) -> DataFrameSchema: + """ + Build a Pandera schema with exact column names (no regex). + This avoids regex matching overhead during validation. + """ + cols: Dict[str, Column] = {} + for canon in only_canons: + spec = specs[canon] checks = [] for chk in spec.get("checks", []): + args = list(chk.get("args", [])) + # precompile regex patterns once + if ( + chk["type"] in {"str_matches", "matches"} + and args + and isinstance(args[0], str) + ): + args[0] = re.compile(args[0]) + # set-based membership for faster 'isin' + if chk["type"] in {"isin", "is_in"} and args and isinstance(args[0], list): + args[0] = set(args[0]) + factory = getattr(Check, chk["type"]) - checks.append(factory(*chk.get("args", []), **chk.get("kwargs", {}))) + checks.append(factory(*args, **chk.get("kwargs", {}))) - columns[pattern] = Column( - name=pattern, - regex=True, + cols[canon] = Column( + name=canon, + regex=False, dtype=spec["dtype"], nullable=spec["nullable"], - required=spec.get("required", False), + required=True, # present-by-construction checks=checks or None, coerce=spec.get("coerce", False), ) - return DataFrameSchema(columns, strict=False) + return DataFrameSchema(cols, strict=False) -def validate_dataset( - filename: str, - base_schema: dict, - ext_schema: Optional[Dict[Any, Any]] = None, - models: Union[str, List[str], None] = None, - institution_id: str = "pdp", +# --------------------------------------------------------------------------- # +# Main validation helpers +# --------------------------------------------------------------------------- # + + +def _header_pass_and_build_canon_mappings( + filename: Src, + enc: str, + merged_specs: Dict[str, dict], +) -> Tuple[Dict[str, str], Dict[str, str], List[str], List[str], List[str], List[str]]: + """Run header pass; if missing required columns raise; else return mappings and present_canons.""" + _, raw_to_canon, missing_required, missing_optional, unknown_extra = _header_pass( + filename, enc, merged_specs, fuzzy_threshold=90 + ) + if missing_required: + logger.error("Missing required columns: %s", missing_required) + canon_to_raw_for_missing: Dict[str, str] = {} + for canon in missing_required: + for raw, mapped_canon in raw_to_canon.items(): + if mapped_canon == canon: + canon_to_raw_for_missing[canon] = raw + break + if canon not in canon_to_raw_for_missing: + canon_to_raw_for_missing[canon] = canon + raise HardValidationError( + missing_required=missing_required, + raw_to_canon=raw_to_canon, + canon_to_raw=canon_to_raw_for_missing, + merged_specs=merged_specs, + ) + _reset_to_start_if_possible(filename) + canon_to_raw: Dict[str, str] = {} + for raw, canon in raw_to_canon.items(): + if canon not in canon_to_raw or normalize_col(raw) == canon: + canon_to_raw[canon] = raw + present_canons = sorted(canon_to_raw.keys()) + return ( + raw_to_canon, + canon_to_raw, + missing_required, + missing_optional, + unknown_extra, + present_canons, + ) + + +def _get_csv_read_kwargs( + filename: Src, + enc: str, + canon_to_raw: Dict[str, str], + merged_specs: Dict[str, dict], +) -> Tuple[Dict[str, Any], str, List[str]]: + """Build read_csv kwargs and return (read_kwargs, engine, parse_dates_canons).""" + canon_dtype_map, parse_dates_canons = _pandas_dtype_and_parse_dates(merged_specs) + raw_dtype_map = { + canon_to_raw[c]: dt for c, dt in canon_dtype_map.items() if c in canon_to_raw + } + parse_dates_raw = [canon_to_raw[c] for c in parse_dates_canons if c in canon_to_raw] + engine = "c" + try: + import pyarrow # noqa: F401 + + engine = "pyarrow" + except ImportError: + pass + read_kwargs: Dict[str, Any] = dict( + encoding=enc, + usecols=list(canon_to_raw.values()), + dtype=raw_dtype_map or None, + engine=engine, + ) + if engine == "c" and isinstance(filename, (str, os.PathLike)): + read_kwargs["memory_map"] = True + if parse_dates_raw: + read_kwargs["parse_dates"] = parse_dates_raw + return read_kwargs, engine, parse_dates_canons + + +def _read_dataframe_with_specs( + filename: Src, + enc: str, + canon_to_raw: Dict[str, str], + merged_specs: Dict[str, dict], +) -> pd.DataFrame: + """Read CSV with spec-based dtypes/parse_dates; return DataFrame with canonical column names.""" + _reset_to_start_if_possible(filename) + read_kwargs, engine, parse_dates_canons = _get_csv_read_kwargs( + filename, enc, canon_to_raw, merged_specs + ) + try: + df = pd.read_csv( + filename, **{k: v for k, v in read_kwargs.items() if v is not None} + ) + except Exception as read_ex: + logger.exception("CSV read failed: %s", read_ex) + raise HardValidationError( + schema_errors="The file could not be read. Please check that it is a valid CSV file.", + raw_to_canon={raw: canon for canon, raw in canon_to_raw.items()}, + canon_to_raw=canon_to_raw, + merged_specs=merged_specs, + ) from read_ex + if engine == "pyarrow" and parse_dates_canons: + for canon in parse_dates_canons: + raw = str(canon_to_raw.get(canon)) + if raw and raw in df.columns: + df[raw] = pd.to_datetime(df[raw], errors="coerce") + df.rename( + columns={ + raw: canon for canon, raw in canon_to_raw.items() if raw in df.columns + }, + inplace=True, + ) + return df + + +def _try_pdp_repo_validation_and_return( + df: pd.DataFrame, + model_list: List[str], + canon_to_raw: Dict[str, str], + raw_to_canon: Dict[str, str], + missing_optional: List[str], + unknown_extra: List[str], + merged_specs: Dict[str, dict], + institution_id: str, +) -> Optional[Dict[str, Any]]: + """If PDP single-model, run repo schema and return result dict; otherwise return None.""" + schema_class = pdp_edvise.get_edvise_schema_for_upload(institution_id, model_list) + if schema_class is None: + return None + validation_df, display_canon_to_raw = ( + pdp_edvise.rename_pdp_dataframe_to_repo_schema(df, canon_to_raw, model_list) + ) + pdp_edvise.validate_dataframe_with_edvise_schema( + validation_df, + schema_class, + raw_to_canon, + display_canon_to_raw, + merged_specs, + ) + if missing_optional or unknown_extra: + return { + "validation_status": "passed_with_soft_errors", + "schemas": model_list, + "missing_optional": missing_optional, + "optional_validation_failures": [], + "failure_cases": [], + "unknown_extra_columns": unknown_extra, + "normalized_df": validation_df, + } + return { + "validation_status": "passed", + "schemas": model_list, + "missing_optional": [], + "unknown_extra_columns": [], + "normalized_df": validation_df, + } + + +def _validate_optional_columns_json( + df: pd.DataFrame, + merged_specs: Dict[str, dict], + present_canons: List[str], +) -> Tuple[List[str], List[dict]]: + """Validate optional columns with JSON schema; return (opt_failures, failure_cases_records).""" + optional_canons = [ + c for c in present_canons if not merged_specs[c].get("required", False) + ] + opt_failures: List[str] = [] + failure_cases_records: List[dict] = [] + if optional_canons: + opt_schema = _build_exact_schema(merged_specs, optional_canons) + try: + opt_schema.validate(df[optional_canons], lazy=True) + except SchemaErrors as err: + opt_failures = sorted(set(err.failure_cases["column"])) + failure_cases_records = err.failure_cases.to_dict(orient="records") + return opt_failures, failure_cases_records + + +def _validate_with_json_schemas_return( + df: pd.DataFrame, + model_list: List[str], + merged_specs: Dict[str, dict], + present_canons: List[str], + canon_to_raw: Dict[str, str], + raw_to_canon: Dict[str, str], + missing_optional: List[str], + unknown_extra: List[str], ) -> Dict[str, Any]: - df = pd.read_csv(filename) - df = df.rename(columns={c: normalize_col(c) for c in df.columns}) - incoming = set(df.columns) + """Run JSON-based Pandera validation and return result dict (passed or passed_with_soft_errors).""" + required_canons = [ + c for c in present_canons if merged_specs[c].get("required", False) + ] + if required_canons: + req_schema = _build_exact_schema(merged_specs, required_canons) + try: + req_schema.validate(df[required_canons], lazy=False) + except SchemaErrors as err: + logger.error("Required column validation failed.") + raise HardValidationError( + schema_errors=err.schema_errors, + failure_cases=err.failure_cases.to_dict(orient="records"), + raw_to_canon=raw_to_canon, + canon_to_raw=canon_to_raw, + merged_specs=merged_specs, + ) + opt_failures, failure_cases_records = _validate_optional_columns_json( + df, merged_specs, present_canons + ) + logger.info("missing_optional = %s", missing_optional) + if opt_failures or missing_optional or unknown_extra: + return { + "validation_status": "passed_with_soft_errors", + "schemas": model_list, + "missing_optional": missing_optional, + "optional_validation_failures": opt_failures, + "failure_cases": failure_cases_records, + "unknown_extra_columns": unknown_extra, + "normalized_df": df, + } + return { + "validation_status": "passed", + "schemas": model_list, + "missing_optional": [], + "unknown_extra_columns": [], + "normalized_df": df, + } + - # 2) merge requested models +def _run_validation_flow( + df: pd.DataFrame, + model_list: List[str], + merged_specs: Dict[str, dict], + present_canons: List[str], + canon_to_raw: Dict[str, str], + raw_to_canon: Dict[str, str], + missing_optional: List[str], + unknown_extra: List[str], + institution_id: str, +) -> Dict[str, Any]: + """Run PDP path if applicable; otherwise JSON validation. Returns result dict.""" + pdp_result = _try_pdp_repo_validation_and_return( + df, + model_list, + canon_to_raw, + raw_to_canon, + missing_optional, + unknown_extra, + merged_specs, + institution_id, + ) + if pdp_result is not None: + return pdp_result + return _validate_with_json_schemas_return( + df, + model_list, + merged_specs, + present_canons, + canon_to_raw, + raw_to_canon, + missing_optional, + unknown_extra, + ) + + +def _compute_model_list_and_merged_specs( + base_schema: dict, + ext_schema: Optional[Dict[Any, Any]], + institution_id: str, + models: Union[str, List[str], None], +) -> Tuple[List[str], Dict[str, dict]]: + """Compute model_list and merged_specs from models and schema.""" if models is None: model_list = [] elif isinstance(models, str): model_list = [models] else: - model_list = list(models) # <- ensures it's not a set - + model_list = list(models) merged_specs: Dict[str, dict] = {} for m in model_list: specs = merge_model_columns(base_schema, ext_schema, institution_id, m.lower()) merged_specs.update(specs) + return model_list, merged_specs - canon_to_aliases = { - canon: [normalize_col(alias) for alias in [canon] + spec.get("aliases", [])] - for canon, spec in merged_specs.items() - } - df = rename_columns_to_match_schema(df, canon_to_aliases) - df.columns = [ - normalize_col(c) for c in df.columns - ] # Final normalization after renaming - incoming = set(df.columns) +# --------------------------------------------------------------------------- # +# PDP single-model path: edvise read + Pandera validate. Cohort converter defaults +# to None so validated row sets can differ from batch jobs that use dataio converters. +# --------------------------------------------------------------------------- # - # 3) build canon → set(normalized names) - canon_to_norms: Dict[str, set] = { - canon: {normalize_col(alias) for alias in [canon] + spec.get("aliases", [])} - for canon, spec in merged_specs.items() - } +# Datetime formats to try for PDP course (same order as pdp_data_audit) +PDP_COURSE_DTTM_FORMATS = ("ISO8601", "%Y%m%d.0", "%Y%m%d") - pattern_to_canon = { - r"^(?:" - + "|".join(map(re.escape, [canon] + spec.get("aliases", []))) - + r")$": canon - for canon, spec in merged_specs.items() - } - # 4) find extra / missing - all_norms = set().union(*canon_to_norms.values()) if canon_to_norms else set() - extra_columns = sorted(incoming - all_norms) +def _validate_pdp_converter_callables( + pdp_cohort_converter_func: PDPConverterFunc, + pdp_course_converter_func: PDPConverterFunc, +) -> None: + """Raise HardValidationError if a provided converter is not callable (so API returns 400).""" + if pdp_cohort_converter_func is not None and not callable( + pdp_cohort_converter_func + ): + raise HardValidationError( + schema_errors="pdp_cohort_converter_func must be callable (DataFrame -> DataFrame)", + failure_cases=[], + ) + if pdp_course_converter_func is not None and not callable( + pdp_course_converter_func + ): + raise HardValidationError( + schema_errors="pdp_course_converter_func must be callable (DataFrame -> DataFrame)", + failure_cases=[], + ) - missing_required = [ - canon - for canon, norms in canon_to_norms.items() - if merged_specs[canon].get("required", False) and norms.isdisjoint(incoming) - ] - missing_optional = [ - canon - for canon, norms in canon_to_norms.items() - if not merged_specs[canon].get("required", False) and norms.isdisjoint(incoming) - ] +def _convert_pdp_schema_errors_to_hard( + e: Union[SchemaErrors, SchemaError], model_set: set[str] +) -> None: + """Log and re-raise Pandera schema errors as HardValidationError (no return).""" + logger.error( + "PDP edvise schema validation failed: model_set=%s, error=%s", + model_set, + e, + exc_info=True, + ) + hard = pdp_edvise._convert_schema_errors_to_hard_validation_error( + e, raw_to_canon={}, canon_to_raw={}, merged_specs={} + ) + raise hard from e - # Hard-fail on missing required or any extra columns - if missing_required or extra_columns: - if logging: - logging.error( - f"Missing required or extra columns detected, missing_required = {missing_required}, extra_columns = {extra_columns}" - ) - raise HardValidationError( - missing_required=missing_required, extra_columns=extra_columns + +def _read_pdp_validated_dataframe( + path: str, + model_set: set[str], + cohort_converter: PDPConverterFunc, + course_converter_func: PDPConverterFunc, +) -> pd.DataFrame: + """Read and validate PDP cohort or course data; return validated DataFrame or raise.""" + if model_set == {"STUDENT"}: + return read_raw_pdp_cohort_data( + file_path=path, + schema=pdp_edvise.get_edvise_schema_for_models(["STUDENT"]), + converter_func=cohort_converter, + spark_session=None, ) + if model_set == {"COURSE"}: + return _read_pdp_course_edvise( + path, course_converter_func=course_converter_func + ) + raise HardValidationError( + schema_errors=f"PDP single-model expected; got models={list(model_set)}", + failure_cases=[], + ) + + +@contextmanager +def _path_for_edvise_read(filename: Src, enc: str) -> Generator[str, None, None]: + """ + Yield a file path that edvise read_raw_pdp_* can use. - # 5) build Pandera schema & validate (hard-fail on any error) - schema = build_schema(merged_specs) + If filename is a path, yield it. If file-like, read content, write to a temp + file (utf-8), yield that path; the temp file is always removed on exit. + + Args: + filename: Path or file-like to read from. + enc: Encoding used to decode file-like content before writing utf-8 temp. + + Yields: + Path to a CSV file (original or temp). + + Raises: + HardValidationError: If file-like read fails (with failure_cases=[str(e)]). + """ + if isinstance(filename, (str, os.PathLike)): + yield str(filename) + return + try: + raw = filename.read() + except Exception as e: + # Intentionally broad: any read failure becomes HardValidationError for API. + logger.error("Could not read file for validation: %s", e, exc_info=True) + raise HardValidationError( + schema_errors="Could not read file for validation.", + failure_cases=[str(e)], + ) from e + if isinstance(raw, bytes): + raw = raw.decode(enc) + fd, path = tempfile.mkstemp(suffix=".csv") try: - schema.validate(df, lazy=True) - except SchemaErrors as err: - # TODO: Log validation failure for DS to review - failed_normals = set(err.failure_cases["column"]) - failed_canons = {pattern_to_canon.get(p, p) for p in failed_normals} - - # split into required vs optional failures - req_failures = [ - c for c in failed_canons if merged_specs.get(c, {}).get("required", False) - ] - opt_failures = [ - c - for c in failed_canons - if not merged_specs.get(c, {}).get("required", False) - ] - - if req_failures: - if logging: - logging.error( - f"Schema validation failed on required columns, schema_errors = {err.schema_errors}, failure_cases = {err.failure_cases.to_dict(orient='records')}" + os.write(fd, raw.encode("utf-8")) + except Exception: + try: + os.unlink(path) + except OSError: + pass + raise + finally: + os.close(fd) + try: + yield path + finally: + try: + os.unlink(path) + except OSError: + pass + + +def _read_pdp_course_edvise( + path: str, + course_converter_func: PDPConverterFunc = None, +) -> pd.DataFrame: + """ + Read and validate a PDP course CSV using edvise helpers. + + Tries each value in ``PDP_COURSE_DTTM_FORMATS`` with each converter: optional + ``course_converter_func`` first, then :func:`_default_pdp_course_duplicate_converter`. + + Batch PDP jobs may also try school-specific converters from ``dataio``; this + path only runs converters passed in here, so results may differ from those jobs. + + Args: + path: Path to course CSV. + course_converter_func: Optional school-specific converter; if None, only the + default duplicate-handling converter is used. + + Returns: + Validated DataFrame from ``read_raw_pdp_course_data`` for the first successful + converter and datetime format. + + Raises: + HardValidationError: If every converter and format combination fails. + """ + default_converters = (_default_pdp_course_duplicate_converter,) + converters = ( + (course_converter_func,) if course_converter_func is not None else () + ) + default_converters + last_error: Optional[Exception] = None + for converter in converters: + for fmt in PDP_COURSE_DTTM_FORMATS: + try: + return read_raw_pdp_course_data( + file_path=path, + schema=pdp_edvise.get_edvise_schema_for_models(["COURSE"]), + dttm_format=fmt, + converter_func=converter, + spark_session=None, ) - raise HardValidationError( - schema_errors=err.schema_errors, - failure_cases=err.failure_cases.to_dict(orient="records"), + except ValueError as e: + last_error = e + except TypeError as e: + if "school_type" in str(e) or "schema_type" in str(e): + last_error = None + break + raise + error_message = ( + "Course data did not parse with any known datetime format." + if last_error is not None + else "Course validation failed (datetime format or schema)." + ) + validation_error = HardValidationError( + schema_errors=error_message, + failure_cases=[str(last_error)] if last_error else [], + ) + logger.error( + "PDP course validation failed: path=%s, last_error=%s", + path, + last_error, + ) + if last_error is not None: + raise validation_error from last_error + raise validation_error + + +def _validate_pdp_with_edvise_read( + filename: Src, + enc: str, + model_list: List[str], + institution_id: str, + pdp_cohort_converter_func: PDPConverterFunc = None, + pdp_course_converter_func: PDPConverterFunc = None, +) -> Dict[str, Any]: + """ + Validate a single-model PDP cohort or course file via edvise read and Pandera. + + Writes file-like inputs to a temp path, then calls ``read_raw_pdp_cohort_data`` + (STUDENT) or ``_read_pdp_course_edvise`` (COURSE). Cohort rows are only + transformed when ``pdp_cohort_converter_func`` is set; batch jobs may still + filter cohort rows via ``dataio``, so API output rows are not guaranteed to + match pipeline output for the same file. + + Args: + filename: Path or file-like CSV source. + enc: Encoding from :func:`sniff_encoding` (used when materializing file-like input). + model_list: Exactly one model, e.g. ``["STUDENT"]`` or ``["COURSE"]``. + institution_id: Schema namespace (e.g. ``"pdp"``); reserved for callers and logging. + pdp_cohort_converter_func: Optional ``DataFrame -> DataFrame`` step before cohort + schema validation; ``None`` means validate rows as read. + pdp_course_converter_func: Optional course converter before default duplicate handling. + + Returns: + Dict with validation_status, schemas, missing_optional, unknown_extra_columns, + and normalized_df on success. + + Raises: + HardValidationError: If converters are non-callable, read fails, or Pandera + validation fails (including converted SchemaErrors). + """ + _reset_to_start_if_possible(filename) + model_set = {str(m).strip().upper() for m in model_list if m} + + _validate_pdp_converter_callables( + pdp_cohort_converter_func, pdp_course_converter_func + ) + cohort_converter = pdp_cohort_converter_func + + with _path_for_edvise_read(filename, enc) as path: + try: + df = _read_pdp_validated_dataframe( + path, + model_set, + cohort_converter, + pdp_course_converter_func, ) - else: - if logging: - logging.info(f"missing_optional = {missing_optional}") - print("Optional column validation errors on: ", opt_failures) return { - "validation_status": "passed_with_soft_errors", + "validation_status": "passed", "schemas": model_list, - "missing_optional": missing_optional, - "optional_validation_failures": opt_failures, - "failure_cases": err.failure_cases.to_dict(orient="records"), + "missing_optional": [], + "unknown_extra_columns": [], + "normalized_df": df, } - if logging: - logging.info(f"missing_optional = {missing_optional}") - # 6) success (with possible soft misses) + except (SchemaErrors, SchemaError) as e: + _convert_pdp_schema_errors_to_hard(e, model_set) + except HardValidationError: + raise + except Exception as e: + logger.exception( + "PDP validation failed: model_set=%s, error=%s", model_set, e + ) + raise HardValidationError( + schema_errors=f"PDP validation failed (model_set={model_set!r}): {e}", + failure_cases=[str(e)], + ) from e + + return {} # Unreachable: every path above returns or raises + + +# --------------------------------------------------------------------------- # +# Main validation +# --------------------------------------------------------------------------- # + + +def _validate_legacy_any_format( + filename: Src, + enc: str, + models: Union[str, List[str], None], +) -> Dict[str, Any]: + """ + Legacy institutions: accept any CSV format (encoding check only, no schema). + + Reads the file as CSV with no column or type checks; returns the DataFrame + as-is as normalized_df so it can be written to validated/. + + Args: + filename: Path or file-like object for the CSV. + enc: Encoding already sniffed for the file. + models: Allowed schema names (e.g. ["STUDENT"]); used for response only. + + Returns: + Dict with validation_status, schemas, missing_optional, unknown_extra_columns, + and normalized_df (the DataFrame as read, or empty if read failed/empty). + + Raises: + HardValidationError: If the file cannot be read or parsed as CSV, or if + column names indicate PII (e.g. email, ssn, first_name); such files + are rejected before being written to raw/ or validated/. + """ + if models is None: + model_list: List[str] = ["UNKNOWN"] + elif isinstance(models, str): + model_list = [models] + else: + model_list = list(models) + if not model_list: + model_list = ["UNKNOWN"] + + with _path_for_edvise_read(filename, enc) as path: + read_enc = "utf-8" if not isinstance(filename, (str, os.PathLike)) else enc + try: + df = pd.read_csv(path, encoding=read_enc) + except ( + pd.errors.ParserError, + pd.errors.EmptyDataError, + UnicodeDecodeError, + OSError, + ) as e: + logger.exception("Legacy CSV read failed: %s", e) + raise HardValidationError( + schema_errors="Legacy upload: could not read CSV.", + failure_cases=[str(e)], + ) from e + if df is None or not isinstance(df, pd.DataFrame): + df = pd.DataFrame() + + # PII check: reject legacy uploads that contain columns indicating PII (before moving to raw/validated). + # Run whenever there are columns (including header-only CSVs: df.empty is True for 0 rows). + if len(df.columns) > 0: + # Lazy import to avoid circular dependency: validation_error_formatter imports from this module. + from .validation_error_formatter import _is_pii_column + + pii_columns = [str(c) for c in df.columns if _is_pii_column(str(c))] + if pii_columns: + logger.warning( + "Legacy upload rejected: PII columns detected: %s", pii_columns + ) + raise HardValidationError( + schema_errors=( + "Legacy upload: file contains columns that may contain personally identifiable information (PII). " + "Please remove or de-identify these columns before uploading." + ), + failure_cases=pii_columns, + ) + return { - "validation_status": ( - "passed_with_soft_errors" if missing_optional else "passed" - ), + "validation_status": "passed", "schemas": model_list, - "missing_optional": missing_optional, + "missing_optional": [], + "unknown_extra_columns": [], + "normalized_df": df, } + + +def validate_dataset( + filename: Src, + base_schema: dict, + ext_schema: Optional[Dict[Any, Any]] = None, + models: Union[str, List[str], None] = None, + institution_id: str = "pdp", + institution_identifier: Optional[str] = None, + pdp_cohort_converter_func: PDPConverterFunc = None, + pdp_course_converter_func: PDPConverterFunc = None, +) -> Dict[str, Any]: + """ + Validate a dataset against merged base and optional extension schemas. + + Detects encoding, merges institution column specs, then routes to legacy + any-format handling, PDP edvise read (single-model STUDENT/COURSE), or + JSON Pandera validation. ``institution_id == "legacy"`` skips column schema checks. + + Args: + filename: CSV path or file-like object. + base_schema: Base schema dict (e.g. base.data_models). + ext_schema: Optional extension schema with institutions.* blocks. + models: Model name(s) to validate; ``None`` follows merged_specs resolution. + institution_id: Institutions key, or ``"legacy"`` for encoding-only validation. + institution_identifier: Optional UUID string for caller context (e.g. Edvise). + pdp_cohort_converter_func: Optional cohort transform before Pandera; default ``None``. + Batch PDP jobs may still apply school-specific cohort converters via ``dataio``. + pdp_course_converter_func: Optional course converter before default duplicate handling. + + Returns: + Dict with validation_status, schemas, missing_optional, unknown_extra_columns, + and normalized_df (``None`` when merged_specs is empty). + + Raises: + HardValidationError: On decode failure, missing columns, schema errors, or + other validation failures (including Unicode decode issues from sniff_encoding). + """ + try: + enc = sniff_encoding(filename) + except UnicodeError as ex: + raise HardValidationError(schema_errors="decode_error", failure_cases=[str(ex)]) + _reset_to_start_if_possible(filename) + + if institution_id == "legacy": + return _validate_legacy_any_format(filename, enc, models) + + model_list, merged_specs = _compute_model_list_and_merged_specs( + base_schema, ext_schema, institution_id, models + ) + if not merged_specs: + return { + "validation_status": "passed", + "schemas": model_list, + "missing_optional": [], + "unknown_extra_columns": [], + "normalized_df": None, + } + + # Route PDP STUDENT/COURSE to edvise read path (cohort converter optional; see section above). + if pdp_edvise.get_edvise_schema_for_upload(institution_id, model_list) is not None: + return _validate_pdp_with_edvise_read( + filename, + enc, + model_list, + institution_id, + pdp_cohort_converter_func=pdp_cohort_converter_func, + pdp_course_converter_func=pdp_course_converter_func, + ) + + ( + raw_to_canon, + canon_to_raw, + missing_required, + missing_optional, + unknown_extra, + present_canons, + ) = _header_pass_and_build_canon_mappings(filename, enc, merged_specs) + + df = _read_dataframe_with_specs(filename, enc, canon_to_raw, merged_specs) + + return _run_validation_flow( + df, + model_list, + merged_specs, + present_canons, + canon_to_raw, + raw_to_canon, + missing_optional, + unknown_extra, + institution_id, + ) diff --git a/src/webapp/validation_error_formatter.py b/src/webapp/validation_error_formatter.py new file mode 100644 index 00000000..fab8d016 --- /dev/null +++ b/src/webapp/validation_error_formatter.py @@ -0,0 +1,1306 @@ +"""Formats validation errors into human-readable messages. + +This module converts technical validation errors (with canonical column names, +row indices, and check types) into user-friendly error messages that include: +- User-friendly column names (raw headers from the file) +- Row numbers (1-indexed for users) +- Clear explanations of what's wrong +- Guidance on how to fix issues +""" + +import logging +import math +from typing import Dict, List, Optional, Any, TYPE_CHECKING + +if TYPE_CHECKING: + from .validation import HardValidationError + +# Logger for formatter errors +_logger = logging.getLogger(__name__) + +# Optional import for normalize_col (used in _format_extra_columns) +# Imported at module level with try/except to avoid breaking environments without it +# This is safe because: +# 1. We handle ImportError/AttributeError gracefully +# 2. We fall back to function-level import if module-level fails +# 3. We don't cache stale functions (each call to +# _get_normalize_col_function checks availability) +try: + from .validation import normalize_col as _normalize_col_func + + HAS_NORMALIZE_COL = True +except (ImportError, AttributeError): + # Safe fallback - function will try to import again if needed + HAS_NORMALIZE_COL = False + _normalize_col_func = None # type: ignore + +# PII indicators - high-risk indicators that should always trigger masking +# These use substring matching because they're unambiguous +PII_HIGH_RISK_INDICATORS = { + "email", + "ssn", + "social_security", + "phone", + "telephone", + "date_of_birth", + "dob", + "birthdate", + "birth_date", + "passport", + "driver_license", + "license_number", + "credit_card", + "bank_account", + "account_number", + "ip_address", + "mac_address", +} + +# PII indicators - medium-risk indicators that require exact or token matching +# These are common words that appear in non-PII contexts (e.g., "course_name") +# Note: "name" alone is excluded to avoid false positives, but patterns like +# "*_name" (student_name, employee_name, etc.) are caught via token matching +PII_MEDIUM_RISK_INDICATORS = { + "first_name", + "last_name", + "middle_name", + "full_name", + "address", + # Note: student_id is excluded; it is a standard de-identified identifier for all institution types. + # Note: Patterns like "student_name", "employee_name", "guardian_name" will + # be caught because they contain "name" as a token, but we check + # false positives first +} + +# Common non-PII column name patterns that should NOT be flagged +# These are compound names where a PII indicator appears but isn't actually PII +PII_FALSE_POSITIVE_PATTERNS = { + "student_id", # Standard de-identified identifier for all institution types + "course_name", + "school_name", + "district_name", + "institution_name", + "class_name", + "section_name", + "program_name", + "department_name", + "file_name", + "column_name", + "table_name", + "field_name", +} + +# Limits for error message generation +MAX_VALUE_LENGTH = 200 # Maximum length for non-PII values in error messages +MAX_MESSAGE_LENGTH = 10000 # Maximum total error message length +MAX_ERROR_EXAMPLES = 10 # Maximum examples per column + +# Human-readable messages for PDP schema check names (edvise repo schemas). +# Keeps messaging consistent without changing the edvise package. +PDP_EDVISE_CHECK_MESSAGES: Dict[str, str] = { + "check_num_institutions": "All rows must have the same institution ID.", + "num_credits_attempted_ge_earned": "Credits attempted must be greater than or equal to credits earned for each course row.", + "unique": "Duplicate rows are not allowed; each row must be unique (e.g. unique student_id for cohort, or unique combination of student_id, term, and course for course files).", + "column_in_dataframe": "This column is required but is missing from the file.", +} + + +def _sanitize_string(value: str, max_length: int = MAX_VALUE_LENGTH) -> str: + """ + Sanitize string values to prevent injection and limit length. + + Args: + value: String to sanitize + max_length: Maximum length before truncation + + Returns: + Sanitized string + """ + if not isinstance(value, str): + value = str(value) + + # Truncate if too long + if len(value) > max_length: + value = value[:max_length] + "..." + + # Replace newlines and control characters to prevent formatting issues + value = value.replace("\n", " ").replace("\r", " ") + # Remove other control characters + value = "".join(char for char in value if ord(char) >= 32 or char in "\t") + + return value + + +def _format_missing_required(error: "HardValidationError") -> Optional[str]: + """Format missing required columns error message.""" + if not error.missing_required: + return None + + # Get mappings + canon_to_raw = _get_canon_to_raw_mapping(error) + merged_specs = getattr(error, "merged_specs", {}) or {} + + missing_display = [] + for canon in error.missing_required: + # Skip invalid entries + if not canon or not isinstance(canon, str): + continue + + # Get raw column name (user-friendly) + if isinstance(canon_to_raw, dict): + raw = canon_to_raw.get(canon, canon) + else: + raw = str(canon) + spec = merged_specs.get(canon, {}) if isinstance(merged_specs, dict) else {} + desc = spec.get("description", "") if isinstance(spec, dict) else "" + + # Sanitize column names and descriptions + raw = _sanitize_string(str(raw), max_length=100) + desc = _sanitize_string(str(desc), max_length=200) if desc else "" + + # Format: 'Column Name' (description if available) + if desc: + missing_display.append(f"'{raw}' ({desc})") + else: + missing_display.append(f"'{raw}'") + + # Handle case where all entries were invalid (missing_display is empty) + if not missing_display: + return ( + "Missing required columns detected. " + "Please check your file and ensure all required columns are present." + ) + + return ( + f"Missing required columns: {', '.join(missing_display)}. " + f"These columns must be present in your file." + ) + + +def _get_normalize_col_function() -> Optional[Any]: + """Get normalize_col function if available.""" + if HAS_NORMALIZE_COL and _normalize_col_func: + return _normalize_col_func + try: + from .validation import normalize_col + + return normalize_col + except (ImportError, AttributeError): + return None + + +def _find_raw_names_in_mapping( + norm_col: str, raw_to_canon: Dict[str, str] +) -> List[str]: + """Find all raw names that map to norm_col in raw_to_canon.""" + raw_names = [] + for raw, canon in raw_to_canon.items(): + if canon == norm_col: + raw_names.append(raw) + return raw_names + + +def _find_raw_names_via_normalize( + norm_col: str, raw_to_canon: Dict[str, str], normalize_col: Any +) -> List[str]: + """Find raw names by normalizing each raw name and comparing.""" + raw_names = [] + for raw in raw_to_canon.keys(): + if normalize_col(raw) == norm_col: + raw_names.append(raw) + return raw_names + + +def _find_raw_names_for_normalized( + norm_col: str, + raw_to_canon: Dict[str, str], + normalize_col: Optional[Any], +) -> List[str]: + """Find all raw names that normalize to the given normalized column name.""" + if not isinstance(raw_to_canon, dict): + return [] + + # Find all raw names that map to this normalized column in raw_to_canon + # This uses the existing mapping rather than re-normalizing (more accurate) + raw_names = _find_raw_names_in_mapping(norm_col, raw_to_canon) + + # If no matches found in raw_to_canon, try reverse lookup with normalize_col + if not raw_names and normalize_col: + raw_names = _find_raw_names_via_normalize(norm_col, raw_to_canon, normalize_col) + + return raw_names + + +def _choose_display_name_for_extra_column(norm_col: str, raw_names: List[str]) -> str: + """Choose display name for extra column using deterministic rules.""" + if not raw_names: + return str(norm_col) + + # Check for exact match first + exact_match = next((raw for raw in raw_names if raw == norm_col), None) + if exact_match: + return exact_match + + if len(raw_names) == 1: + return raw_names[0] + + # Multiple matches: show first one (deterministic) with note if > 1 + if len(raw_names) > 2: + return f"{raw_names[0]} (and {len(raw_names) - 1} similar)" + + # Exactly 2 matches: show both + return f"{raw_names[0]} or {raw_names[1]}" + + +def _format_extra_columns(error: "HardValidationError") -> Optional[str]: + """Format extra columns error message.""" + if not error.extra_columns: + return None + + raw_to_canon = getattr(error, "raw_to_canon", {}) or {} + normalize_col = _get_normalize_col_function() + extra_display = [] + + for norm_col in error.extra_columns: + if norm_col is None: + continue + + raw_names = _find_raw_names_for_normalized( + norm_col, raw_to_canon, normalize_col + ) + display_name = _choose_display_name_for_extra_column(norm_col, raw_names) + extra_display.append(f"'{_sanitize_string(display_name, max_length=100)}'") + + return ( + f"Unexpected columns found: {', '.join(extra_display)}. " + f"Please remove these columns or rename them to match the expected schema." + ) + + +def _try_to_dict_records(failure_cases: Any) -> Optional[List[dict]]: + """Try to convert failure_cases using to_dict(orient='records').""" + if not hasattr(failure_cases, "to_dict"): + return None + + try: + result = failure_cases.to_dict(orient="records") + if isinstance(result, list) and all(isinstance(item, dict) for item in result): + return result + except (AttributeError, ValueError, TypeError) as e: + _logger.debug("Failed to convert with to_dict(orient='records'): %s", e) + + return None + + +def _convert_dict_of_dicts_to_list(result: dict) -> Optional[List[dict]]: + """Convert {col: {row: val}} format to list of dicts.""" + if not isinstance(result, dict) or not result: + return None + + first_key = next(iter(result)) + if not isinstance(result[first_key], dict) or not result[first_key]: + return None + + # Get max row index from all columns (defensive: handle variable-length columns) + max_row_idx = max( + (max(inner_dict.keys()) if isinstance(inner_dict, dict) and inner_dict else -1) + for inner_dict in result.values() + if isinstance(inner_dict, dict) + ) + + if max_row_idx < 0: + return None + + rows = [] + for row_idx in range(max_row_idx + 1): + row_dict = { + col: ( + result[col].get(row_idx, None) + if isinstance(result[col], dict) + else None + ) + for col in result + } + rows.append(row_dict) + + return rows + + +def _try_to_dict_fallback(failure_cases: Any) -> Optional[List[dict]]: + """Try fallback conversion using to_dict() without orient.""" + if not hasattr(failure_cases, "to_dict"): + return None + + try: + result = failure_cases.to_dict() + # Try dict of dicts conversion + converted = _convert_dict_of_dicts_to_list(result) + if converted: + return converted + # If single dict, wrap in list + if isinstance(result, dict): + return [result] + except (AttributeError, TypeError, ValueError, KeyError) as fallback_err: + _logger.debug("Fallback to_dict() conversion also failed: %s", fallback_err) + + return None + + +def _normalize_failure_cases(failure_cases: Any) -> List[dict]: + """ + Normalize failure_cases to a list of dicts. + + Handles multiple formats: + - List of dicts (already normalized) + - pandas DataFrame or DataFrame-like objects (converts with to_dict("records")) + - Other iterables (converts to list, filters dicts) + + Uses behavior-based detection (try to_dict("records")) rather than type-shape checks + to handle various tabular types (pandas, polars, etc.). + """ + if failure_cases is None: + return [] + + # Try behavior-based detection: to_dict("records") + result = _try_to_dict_records(failure_cases) + if result is not None: + return result + + # Try fallback: to_dict() without orient + result = _try_to_dict_fallback(failure_cases) + if result is not None: + return result + + # Check for empty (safe for lists, dicts, etc.) + try: + if not failure_cases: + return [] + except (ValueError, TypeError): + # DataFrame/array-like objects can raise on truthiness check + pass + + # Handle list of dicts + if isinstance(failure_cases, list): + return [case for case in failure_cases if isinstance(case, dict)] + + # Try to convert other iterables to list + try: + converted = list(failure_cases) + return [case for case in converted if isinstance(case, dict)] + except (TypeError, ValueError): + return [] + + +def _check_and_handle_nan(row_idx: Any) -> Optional[Any]: + """Check if row_idx is NaN and return None if so.""" + try: + if isinstance(row_idx, float) and math.isnan(row_idx): + return None + except (TypeError, AttributeError): + pass + return row_idx + + +def _convert_numpy_integer(row_idx: Any) -> Any: + """Convert numpy integer types to Python int.""" + try: + import numpy as np + + if isinstance(row_idx, (np.integer, np.int64, np.int32)): + return int(row_idx) + except (ImportError, ValueError, OverflowError): + pass + return row_idx + + +def _normalize_integer_index(row_idx: int) -> Optional[int]: + """Normalize integer row index to 1-indexed.""" + try: + if row_idx >= 0: + return row_idx + 1 # Convert 0-indexed to 1-indexed + except (ValueError, OverflowError): + pass + return None + + +def _normalize_float_index(row_idx: float) -> Optional[Any]: + """Normalize float row index (only whole numbers).""" + try: + if row_idx.is_integer(): + idx_int = int(row_idx) + if idx_int >= 0: + return idx_int + 1 # Convert 0-indexed to 1-indexed + # Non-integer float - return sanitized string instead of misleading conversion + return _sanitize_string(str(row_idx), max_length=50) + except (ValueError, OverflowError, AttributeError): + return None + + +def _normalize_row_index(row_idx: Any) -> Optional[Any]: + """ + Normalize row index to a displayable format. + + Handles: + - int/float: Convert to 1-indexed (Pandera uses 0-indexed) + - numpy.integer: Convert to int + - NaN/None: Return None + - Other types: Return sanitized string representation + """ + if row_idx is None: + return None + + # Handle NaN + checked = _check_and_handle_nan(row_idx) + if checked is None: + return None + + # Handle numpy integer types + row_idx = _convert_numpy_integer(checked) + + # Handle int + if isinstance(row_idx, int): + return _normalize_integer_index(row_idx) + + # Handle float + if isinstance(row_idx, float): + return _normalize_float_index(row_idx) + + # For other types (e.g., string indices, MultiIndex), return sanitized string + return _sanitize_string(str(row_idx), max_length=50) + + +def _group_failure_cases_by_column(failure_cases: List[dict]) -> Dict[str, List[dict]]: + """ + Group failure cases by column name. + + Schema-level checks (without a column) are grouped under "_schema_level". + """ + by_column: Dict[str, List[dict]] = {} + for case in failure_cases: + if not isinstance(case, dict): + continue + + # Handle column name - use "_schema_level" for schema-level checks + canon_col = case.get("column") + if not canon_col or not isinstance(canon_col, str): + canon_col = "_schema_level" + else: + canon_col = str(canon_col) + + # Normalize row index + row_idx = case.get("index", -1) + row_num = _normalize_row_index(row_idx) + + check = case.get("check", "validation") + value = case.get("failure_case", "N/A") + + if canon_col not in by_column: + by_column[canon_col] = [] + by_column[canon_col].append( + { + "row": row_num, + "check": str(check) if check else "validation", + "value": value, + } + ) + return by_column + + +def _is_pii_column(column_name: str) -> bool: + """ + Check if a column name indicates PII using a tiered approach. + + Tier 1: High-risk indicators (substring match) - unambiguous + Tier 2: Medium-risk indicators (exact/token match) - avoid false positives + Tier 3: False positive patterns (explicit denylist) + Tier 4: Schema metadata (if available in future) + + Args: + column_name: Column name to check (can be canonical or raw) + + Returns: + True if column likely contains PII, False otherwise + """ + if not column_name or not isinstance(column_name, str): + return False + + col_lower = column_name.lower() + + # Tier 3: Check false positive patterns first (explicit denylist) + if col_lower in PII_FALSE_POSITIVE_PATTERNS: + return False + + # Tier 1: High-risk indicators - substring matching (unambiguous) + if any(indicator in col_lower for indicator in PII_HIGH_RISK_INDICATORS): + return True + + # Tier 2: Medium-risk indicators - exact match or token match (avoid false positives) + # Split on common separators: underscore, hyphen, space + tokens = set() + for sep in ["_", "-", " "]: + if sep in col_lower: + tokens.update(col_lower.split(sep)) + tokens.add(col_lower) # Also check full name + + # Check if any token exactly matches a medium-risk indicator + if any(token in PII_MEDIUM_RISK_INDICATORS for token in tokens): + return True + + # Also check for "*_name" patterns (student_name, employee_name, etc.) + # These are likely PII unless in the false positive denylist (already checked above) + # We check this after medium-risk indicators to catch patterns like "student_name" + if col_lower.endswith("_name") and col_lower != "name": + # Already checked false positives above, so if we get here, it's likely PII + return True + + return False + + +def _mask_pii_value(value: Any, max_visible_chars: int = 2) -> str: + """ + Mask PII values to prevent exposure in error messages. + + Args: + value: The value to mask + max_visible_chars: Maximum characters to show at start/end + + Returns: + Masked value string (e.g., "AB***XY" for "ABCDEFGHXY") + """ + if value is None: + return "N/A" + + value_str = str(value) + + # Handle empty strings - don't mask as "****" (would be misleading) + if not value_str or not value_str.strip(): + return "" + + # Limit length to prevent DoS + if len(value_str) > MAX_VALUE_LENGTH: + value_str = value_str[:MAX_VALUE_LENGTH] + + length = len(value_str) + + # For very short values (length <= 4), show 4 asterisks to avoid leaking length + # This prevents inference: "**" vs "***" vs "****" would reveal length + if length <= max_visible_chars * 2: + return "*" * 4 + + # Show first and last few characters with masking in between + start = value_str[:max_visible_chars] + end = value_str[-max_visible_chars:] if length > max_visible_chars * 2 else "" + masked = "*" * min(length - (max_visible_chars * 2), 6) + + return f"{start}{masked}{end}" + + +def _format_single_error_example(err: dict, is_pii: bool, spec: dict) -> Optional[str]: + """Format a single error example.""" + if not isinstance(err, dict): + return None + + row_num = err.get("row") + # Handle row number display - can be int (1-indexed) or string (for non-int indices) + if row_num is None: + row_msg = "Unknown row" + elif isinstance(row_num, (int, str)): + row_msg = f"Row {row_num}" + else: + # Fallback for unexpected types + row_msg = f"Row {_sanitize_string(str(row_num), max_length=20)}" + + # Mask PII values before displaying + raw_value = err.get("value", "N/A") + if is_pii: + display_value = _mask_pii_value(raw_value) + value_msg = f"found '{display_value}' (value masked for privacy)" + else: + # Truncate non-PII values + value_str = str(raw_value) + if len(value_str) > MAX_VALUE_LENGTH: + value_str = value_str[:MAX_VALUE_LENGTH] + "..." + value_msg = f"found '{_sanitize_string(value_str)}'" + + # Human-readable check descriptions + check_type = err.get("check", "validation") + check_msg = _format_check_error(check_type, spec, err.get("value")) + + return f"{row_msg}: {check_msg}. Current value: {value_msg}" + + +def _get_canon_to_raw_mapping(error: "HardValidationError") -> Dict[str, str]: + """ + Get canon_to_raw mapping, deriving from raw_to_canon if needed. + + Handles non-bijective mappings (multiple raw names map to same canonical): + - Uses first raw name seen for each canonical + - Falls back to canonical name if no mapping exists + """ + # Prefer canon_to_raw if available + canon_to_raw = getattr(error, "canon_to_raw", {}) or {} + if isinstance(canon_to_raw, dict) and canon_to_raw: + return canon_to_raw + + # Derive from raw_to_canon (inverse mapping) + raw_to_canon = getattr(error, "raw_to_canon", {}) or {} + if isinstance(raw_to_canon, dict) and raw_to_canon: + # Build inverse, using first raw name for each canonical (handles non-bijective) + derived: Dict[str, str] = {} + for raw, canon in raw_to_canon.items(): + if canon not in derived: # First occurrence wins + derived[canon] = raw + return derived + + return {} + + +def _format_schema_level_errors(errors: List[dict]) -> List[str]: + """Format schema-level validation errors (no column).""" + messages: List[str] = [] + spec: dict = {} # No column-specific spec for schema-level errors + is_pii = False # Schema-level errors don't contain PII values + + error_examples = [] + for err in errors[:MAX_ERROR_EXAMPLES]: + example = _format_single_error_example(err, is_pii, spec) + if example: + error_examples.append(example) + + if error_examples: + messages.append( + "File-level validation errors:\n" + + "\n".join(f" • {ex}" for ex in error_examples) + ) + + if len(errors) > MAX_ERROR_EXAMPLES: + messages.append( + f"File-level: {len(errors) - MAX_ERROR_EXAMPLES} additional errors found. " + f"Please review your file structure." + ) + + return messages + + +def _format_column_specific_errors( + canon_col: str, + errors: List[dict], + canon_to_raw: Dict[str, str], + merged_specs: Dict[str, dict], +) -> List[str]: + """Format validation errors for a specific column.""" + messages = [] + + # Validate and normalize column name + if not canon_col or not isinstance(canon_col, str): + canon_col = "unknown" + + # Get raw column name (user-friendly) + raw_col = ( + canon_to_raw.get(canon_col, canon_col) + if isinstance(canon_to_raw, dict) + else canon_col + ) + spec = merged_specs.get(canon_col, {}) if isinstance(merged_specs, dict) else {} + + # Sanitize column name + raw_col = _sanitize_string(str(raw_col), max_length=100) + + # Check if this column contains PII + is_pii = _is_pii_column(str(canon_col)) or _is_pii_column(raw_col) + + # Group errors and format (limit to MAX_ERROR_EXAMPLES) + error_examples = [] + for err in errors[:MAX_ERROR_EXAMPLES]: + example = _format_single_error_example(err, is_pii, spec) + if example: + error_examples.append(example) + + if error_examples: + messages.append( + f"Column '{raw_col}' has validation errors:\n" + + "\n".join(f" • {ex}" for ex in error_examples) + ) + + if len(errors) > MAX_ERROR_EXAMPLES: + messages.append( + f"Column '{raw_col}': {len(errors) - MAX_ERROR_EXAMPLES} additional errors found. " + f"Please review all rows for this column." + ) + + return messages + + +def _format_column_validation_errors( + canon_col: str, + errors: List[dict], + error: "HardValidationError", +) -> List[str]: + """Format validation errors for a single column or schema-level errors.""" + # Get mappings + canon_to_raw = _get_canon_to_raw_mapping(error) + merged_specs = getattr(error, "merged_specs", {}) or {} + + # Handle schema-level errors (no column) + if canon_col == "_schema_level": + return _format_schema_level_errors(errors) + + # Column-specific errors + return _format_column_specific_errors(canon_col, errors, canon_to_raw, merged_specs) + + +def _format_schema_validation_errors(error: "HardValidationError") -> List[str]: + """Format schema validation errors with row numbers.""" + messages: List[str] = [] + + failure_cases = _normalize_failure_cases(error.failure_cases) + if not failure_cases: + return messages + + by_column = _group_failure_cases_by_column(failure_cases) + + # Sort columns for deterministic output: schema-level first, then alphabetical + sorted_columns = sorted( + by_column.keys(), + key=lambda x: (x != "_schema_level", x), # _schema_level comes first + ) + + # Format errors by column + for canon_col in sorted_columns: + errors = by_column[canon_col] + column_messages = _format_column_validation_errors(canon_col, errors, error) + messages.extend(column_messages) + + return messages + + +def _add_message_if_fits( + messages: List[str], + current_length: int, + new_msg: Optional[str], + max_length: int = MAX_MESSAGE_LENGTH, +) -> int: + """ + Add message to list if it fits within size limit. Returns updated length. + + Uses <= for comparison to allow messages up to max_length. + Accounts for "\n\n" separator that will be added between messages. + """ + if not new_msg: + return current_length + + # Account for separator: "\n\n" (2 chars) if not first message + separator_len = 2 if messages else 0 + total_len = current_length + len(new_msg) + separator_len + + if total_len <= max_length: + messages.append(new_msg) + return total_len + + return current_length + + +def _format_decode_error( + error: "HardValidationError", current_length: int +) -> tuple[List[str], int]: + """Format decode error section. Returns (messages, updated_length).""" + messages: List[str] = [] + try: + if hasattr(error, "schema_errors") and error.schema_errors == "decode_error": + if hasattr(error, "failure_cases") and error.failure_cases: + decode_msg = ( + str(error.failure_cases[0]) + if isinstance(error.failure_cases, list) + else str(error.failure_cases) + ) + decode_msg = _sanitize_string(decode_msg, max_length=200) + decode_text = ( + f"File encoding error: {decode_msg}. " + f"Please ensure your file is saved as UTF-8 encoding." + ) + current_length = _add_message_if_fits( + messages, current_length, decode_text + ) + except (AttributeError, TypeError, ValueError, IndexError) as e: + _logger.debug("Error formatting decode error: %s", e, exc_info=True) + return messages, current_length + + +def _format_generic_schema_error( + error: "HardValidationError", current_length: int +) -> tuple[List[str], int]: + """Format generic schema error section. Returns (messages, updated_length).""" + messages: List[str] = [] + try: + if ( + hasattr(error, "schema_errors") + and error.schema_errors + and error.schema_errors != "decode_error" + ): + schema_error_text = _sanitize_string( + str(error.schema_errors), max_length=500 + ) + schema_text = ( + f"Schema validation error: {schema_error_text}. " + f"Please check your file format and column definitions." + ) + current_length = _add_message_if_fits(messages, current_length, schema_text) + except (AttributeError, TypeError, ValueError) as e: + _logger.debug("Error formatting generic schema error: %s", e, exc_info=True) + return messages, current_length + + +def _format_all_error_sections(error: "HardValidationError") -> List[str]: + """Format all error sections and return messages with length tracking.""" + messages: List[str] = [] + current_length = 0 + + # Missing required columns + try: + missing_msg = _format_missing_required(error) + current_length = _add_message_if_fits(messages, current_length, missing_msg) + except Exception as e: + _logger.debug("Error formatting missing required columns: %s", e, exc_info=True) + + # Extra columns + try: + extra_msg = _format_extra_columns(error) + current_length = _add_message_if_fits(messages, current_length, extra_msg) + except Exception as e: + _logger.debug("Error formatting extra columns: %s", e, exc_info=True) + + # Schema validation errors (with row numbers) + try: + schema_msgs = _format_schema_validation_errors(error) + for msg in schema_msgs: + separator_len = 2 if messages else 0 + if current_length + len(msg) + separator_len <= MAX_MESSAGE_LENGTH: + messages.append(msg) + current_length += len(msg) + separator_len + else: + # Add truncation notice + truncation_msg = "Additional validation errors were truncated due to message size limits." + current_length = _add_message_if_fits( + messages, current_length, truncation_msg + ) + break + except Exception as e: + _logger.debug("Error formatting schema validation errors: %s", e, exc_info=True) + + # Decode errors + try: + decode_msgs, current_length = _format_decode_error(error, current_length) + messages.extend(decode_msgs) + except Exception as e: + _logger.debug("Error formatting decode errors: %s", e, exc_info=True) + + # Generic schema errors + try: + schema_msgs, current_length = _format_generic_schema_error( + error, current_length + ) + messages.extend(schema_msgs) + except Exception as e: + _logger.debug("Error formatting generic schema errors: %s", e, exc_info=True) + + return messages + + +def _get_fallback_message(error: "HardValidationError") -> str: + """Get fallback error message if formatting fails.""" + try: + fallback = str(error) + # If str(error) is empty or just whitespace, use default message + if not fallback or not fallback.strip(): + return "Validation error occurred. Please check your file format and try again." + return _sanitize_string(fallback, max_length=MAX_MESSAGE_LENGTH) + except (AttributeError, TypeError, ValueError) as e: + _logger.debug("Error getting fallback message: %s", e, exc_info=True) + return "Validation error occurred. Please check your file format and try again." + + +def format_validation_error( + error: "HardValidationError", +) -> str: + """ + Convert technical validation errors to human-readable messages. + + Args: + error: HardValidationError instance with validation failure details + + Returns: + Formatted string with user-friendly error messages + + Raises: + ValueError: If error is None or invalid + """ + # Input validation + if error is None: + raise ValueError("error cannot be None") + + if not hasattr(error, "missing_required"): + # Invalid error object - return safe fallback + return "Validation error occurred. Please check your file format and try again." + + messages = _format_all_error_sections(error) + + if not messages: + return _get_fallback_message(error) + + result = "\n\n".join(messages) + # Final safety check - truncate if somehow exceeded + if len(result) > MAX_MESSAGE_LENGTH: + result = ( + result[: MAX_MESSAGE_LENGTH - 50] + + "\n\n[Error message truncated due to size limits.]" + ) + + return result + + +def _format_str_length_error(check_spec: Optional[dict]) -> str: + """Format str_length check error message.""" + kwargs = ( + check_spec.get("kwargs", {}) + if check_spec and isinstance(check_spec, dict) + else {} + ) + min_val = kwargs.get("min_value") if isinstance(kwargs, dict) else None + max_val = kwargs.get("max_value") if isinstance(kwargs, dict) else None + + if min_val is not None and max_val is not None: + return f"Value must be between {min_val} and {max_val} characters long" + elif min_val is not None: + return f"Value must be at least {min_val} characters long" + elif max_val is not None: + return f"Value must be at most {max_val} characters long" + return "Value length validation failed" + + +def _categorize_values_for_sorting( + allowed_list: List[Any], +) -> tuple[List[tuple[float, str, Any]], List[Any]]: + """Categorize values into numeric and non-numeric for sorting.""" + numeric_values = [] + non_numeric_values = [] + + for val in allowed_list: + try: + numeric_val = float(val) + # Handle NaN and inf - push to non-numeric bucket for predictable ordering + if math.isnan(numeric_val) or math.isinf(numeric_val): + non_numeric_values.append(val) + else: + # Use (numeric_value, str(original)) as sort key to break ties deterministically + numeric_values.append((numeric_val, str(val), val)) + except (ValueError, TypeError, OverflowError): + non_numeric_values.append(val) + + return numeric_values, non_numeric_values + + +def _sort_allowed_values(allowed_list: List[Any]) -> List[Any]: + """Sort allowed values deterministically (numeric first, then non-numeric).""" + try: + numeric_values, non_numeric_values = _categorize_values_for_sorting( + allowed_list + ) + # Sort numeric values by (numeric, string) tuple, non-numeric by string + numeric_sorted = [val for _, _, val in sorted(numeric_values)] + non_numeric_sorted = sorted(non_numeric_values, key=str) + return numeric_sorted + non_numeric_sorted + except (TypeError, ValueError, ImportError): + # If sorting fails (or math not available), use string sort + try: + return sorted(allowed_list, key=str) + except TypeError: + # If items aren't comparable, use original order + return allowed_list + + +def _format_isin_error(check_spec: Optional[dict]) -> str: + """Format isin/is_in check error message.""" + args = ( + check_spec.get("args", []) + if check_spec and isinstance(check_spec, dict) + else [] + ) + if not args or not isinstance(args[0], (list, set, tuple)): + return "Value must be one of the allowed values" + + allowed_list = list(args[0])[:10] # Limit to 10 values for readability + allowed = _sort_allowed_values(allowed_list) + + if len(allowed) <= 5: + return f"Value must be one of: {', '.join(map(str, allowed))}" + + return f"Value must be one of the allowed values (e.g., {', '.join(map(str, allowed[:5]))}, ...)" + + +def _format_matches_error(check_spec: Optional[dict]) -> str: + """Format matches/str_matches check error message.""" + args = ( + check_spec.get("args", []) + if check_spec and isinstance(check_spec, dict) + else [] + ) + if args: + pattern = str(args[0]) + # Try to provide helpful description for common patterns + if "\\d{4}-\\d{2}" in pattern: + return "Value must match the format YYYY-YY (e.g., 2025-26)" + elif "\\d{4}" in pattern: + return "Value must be a 4-digit year" + else: + return "Value must match the required format pattern" + return "Value must match the required format" + + +def _format_ge_error(check_spec: Optional[dict]) -> str: + """Format ge (greater than or equal) check error message.""" + kwargs = ( + check_spec.get("kwargs", {}) + if check_spec and isinstance(check_spec, dict) + else {} + ) + if isinstance(kwargs, dict): + min_val = kwargs.get("ge", kwargs.get("min_value")) + if min_val is not None: + return f"Value must be greater than or equal to {min_val}" + return "Value must be greater than or equal to the minimum" + + +def _format_le_error(check_spec: Optional[dict]) -> str: + """Format le (less than or equal) check error message.""" + kwargs = ( + check_spec.get("kwargs", {}) + if check_spec and isinstance(check_spec, dict) + else {} + ) + if isinstance(kwargs, dict): + max_val = kwargs.get("le", kwargs.get("max_value")) + if max_val is not None: + return f"Value must be less than or equal to {max_val}" + return "Value must be less than or equal to the maximum" + + +def _format_gt_error(check_spec: Optional[dict]) -> str: + """Format gt (strictly greater than) check error message.""" + kwargs = ( + check_spec.get("kwargs", {}) + if check_spec and isinstance(check_spec, dict) + else {} + ) + if isinstance(kwargs, dict): + min_val = kwargs.get("gt", kwargs.get("min_value")) + if min_val is not None: + return f"Value must be greater than {min_val}" + # Also check args for gt (some specs use args instead of kwargs) + args = ( + check_spec.get("args", []) + if check_spec and isinstance(check_spec, dict) + else [] + ) + if args and len(args) > 0: + min_val = args[0] + return f"Value must be greater than {min_val}" + return "Value must be greater than the minimum" + + +def _format_lt_error(check_spec: Optional[dict]) -> str: + """Format lt (strictly less than) check error message.""" + kwargs = ( + check_spec.get("kwargs", {}) + if check_spec and isinstance(check_spec, dict) + else {} + ) + if isinstance(kwargs, dict): + max_val = kwargs.get("lt", kwargs.get("max_value")) + if max_val is not None: + return f"Value must be less than {max_val}" + # Also check args for lt (some specs use args instead of kwargs) + args = ( + check_spec.get("args", []) + if check_spec and isinstance(check_spec, dict) + else [] + ) + if args and len(args) > 0: + max_val = args[0] + return f"Value must be less than {max_val}" + return "Value must be less than the maximum" + + +def _extract_base_check_type(check_type: str) -> str: + """ + Extract the base check type from parameterized check types. + + Pandera provides check types with arguments like: + - "isin(['A', 'B', 'C'])" -> "isin" + - "str_length(3, None)" -> "str_length" + - "greater_than(0)" -> "greater_than" + - "Check.isin(['A'])" -> "isin" (namespaced, extracts final token) + - "pandera.Check.str_length(3, None)" -> "str_length" (multi-level namespace) + - "str_matches(re.compile('...'))" -> "str_matches" (complex repr) + + Handles edge cases: + - Empty/None/non-string: returns safe empty string + - Already base: "isin" -> "isin" + - Namespaced: extracts final token after last dot + - Spaces: "isin (['A'])" -> "isin" (after strip) + + Args: + check_type: The check type string (may be parameterized) + + Returns: + The base check type name (without parameters or namespace), stripped of whitespace + """ + # Handle non-string types safely - return empty string to avoid noisy output + if not isinstance(check_type, str): + return "" + + # Handle empty string + if not check_type: + return "" + + # Extract base type by taking everything before the first '(' + # This safely handles: + # - Parameterized: "isin(['A', 'B'])" -> "isin" + # - Complex repr: "str_matches(re.compile('...'))" -> "str_matches" + # - Spaces: "isin (['A'])" -> "isin" (after strip) + if "(" in check_type: + base = check_type.split("(")[0].strip() + else: + base = check_type.strip() + + # Extract final token after last dot to handle namespaced types + # This ensures "Check.isin(['A'])" -> "isin" (matches spec with type="isin") + # and "pandera.Check.str_length(3, None)" -> "str_length" + if "." in base: + base = base.split(".")[-1].strip() + + return base + + +# Alias mapping for check type normalization +# Maps Pandera's verbose check names to the short names used in specs +# IMPORTANT: Preserves semantic correctness (strict vs non-strict comparisons) +_CHECK_TYPE_ALIASES = { + # Strict comparisons (> and <) + "greater_than": "gt", # Strict: > x + "gt": "gt", # Already canonical + "less_than": "lt", # Strict: < x + "lt": "lt", # Already canonical + # Non-strict comparisons (≥ and ≤) + "greater_than_or_equal_to": "ge", # Non-strict: ≥ x + "less_than_or_equal_to": "le", # Non-strict: ≤ x + # Other aliases + "is_in": "isin", # Handle both spellings (conceptually equivalent) +} + + +def _normalize_check_type_alias(check_type: str) -> str: + """ + Normalize check type aliases to match spec keys. + + Pandera may emit verbose check names (e.g., "greater_than(0)") while + specs use short names (e.g., "ge"). This function maps aliases to their + canonical forms. + + Args: + check_type: Base check type (already extracted from parameterized form) + + Returns: + Normalized check type that matches spec keys + """ + return _CHECK_TYPE_ALIASES.get(check_type, check_type) + + +def _find_check_spec(check_type: str, spec: dict) -> Optional[dict]: + """ + Find the check specification that matches the check type. + + Handles parameterized check types by extracting the base type. + Also handles aliases (e.g., "greater_than" → "gt", "greater_than_or_equal_to" → "ge"). + Prioritizes base type match first to avoid over-matching. + """ + if not isinstance(spec, dict): + return None + + # Extract base check type to handle parameterized checks + base_check_type = _extract_base_check_type(check_type) + # Normalize aliases to match spec keys (e.g., "greater_than" → "ge") + normalized_check_type = _normalize_check_type_alias(base_check_type) + + checks = spec.get("checks", []) if isinstance(spec.get("checks"), list) else [] + + for chk in checks: + if isinstance(chk, dict): + chk_type = chk.get("type") + # Try multiple matching strategies: + # 1. Normalized alias match (e.g., "greater_than" → "ge" matches spec "ge") + # 2. Base type match (for non-aliased checks) + # 3. Exact match (for backwards compatibility) + if ( + chk_type == normalized_check_type + or chk_type == base_check_type + or chk_type == check_type + ): + return chk + + return None + + +def _format_check_error(check_type: str, spec: dict, value: Any) -> str: + """ + Convert technical check names to human-readable descriptions. + + Handles parameterized check types from Pandera (e.g., "isin(['A', 'B', 'C'])"). + Also handles aliases (e.g., "greater_than" → "gt", "greater_than_or_equal_to" → "ge"). + Preserves semantic correctness: strict comparisons (> and <) vs non-strict (≥ and ≤). + Only formats specific check types if a matching spec is found. + """ + # Extract base check type for matching (Pandera provides parameterized types) + base_check_type = _extract_base_check_type(check_type) + # Normalize aliases to match spec keys (e.g., "greater_than" → "gt") + normalized_check_type = _normalize_check_type_alias(base_check_type) + check_spec = _find_check_spec(check_type, spec) + + # Only format specific check types if a matching spec was found + # This ensures we don't format "greater_than" as "gt" when spec has "ge" + if check_spec is not None: + # Format based on normalized check type (spec was found, so safe to format) + if normalized_check_type == "str_length": + return _format_str_length_error(check_spec) + + if normalized_check_type in {"isin", "is_in"}: + return _format_isin_error(check_spec) + + if normalized_check_type == "ge": + return _format_ge_error(check_spec) + + if normalized_check_type == "le": + return _format_le_error(check_spec) + + if normalized_check_type == "gt": + return _format_gt_error(check_spec) + + if normalized_check_type == "lt": + return _format_lt_error(check_spec) + + if base_check_type in {"matches", "str_matches"}: + return _format_matches_error(check_spec) + + # Format check types that don't require a spec match + if base_check_type in {"not_nullable", "not_null"}: + return "This field cannot be empty" + + if base_check_type == "nullable": + return "Value validation failed" + + # PDP/Edvise Schema (ES) check names (same validation as edvise repo) + if base_check_type in PDP_EDVISE_CHECK_MESSAGES: + return PDP_EDVISE_CHECK_MESSAGES[base_check_type] + + # Generic fallback - use original check_type for display (may include parameters) + # This handles cases where check type doesn't match any spec (e.g., "greater_than" with "ge" spec) + return f"Validation failed for {check_type} check" diff --git a/src/webapp/validation_error_formatter_integration_test.py b/src/webapp/validation_error_formatter_integration_test.py new file mode 100644 index 00000000..2a0e8b04 --- /dev/null +++ b/src/webapp/validation_error_formatter_integration_test.py @@ -0,0 +1,614 @@ +"""Integration tests for validation_error_formatter with real Pandera errors. + +These tests use actual Pandera schema validation to generate real SchemaErrors +objects, ensuring the formatter handles real-world failure_cases shapes correctly. +""" + +import pytest + +try: + import pandas as pd + import numpy as np + from pandera import DataFrameSchema, Column, Check + from pandera.errors import SchemaErrors, SchemaError + + HAS_PANDERA = True +except ImportError: + HAS_PANDERA = False + pd = None # type: ignore + np = None # type: ignore + SchemaErrors = None # type: ignore + SchemaError = None # type: ignore + +from .validation import HardValidationError +from .validation_error_formatter import ( + format_validation_error, + MAX_MESSAGE_LENGTH, + MAX_ERROR_EXAMPLES, +) + + +# ============================================================================ +# Integration Tests with Real Pandera Errors +# ============================================================================ + + +@pytest.mark.skipif(not HAS_PANDERA, reason="pandera not available") +def test_integration_pandera_missing_required_columns() -> None: + """Test formatter with real Pandera error for missing required columns.""" + # Note: We simulate missing columns by creating HardValidationError directly + # (as validation.py does) rather than using actual Pandera validation + error = HardValidationError( + missing_required=["student_id", "grade"], + raw_to_canon={"Student ID": "student_id", "Grade": "grade"}, + canon_to_raw={"student_id": "Student ID", "grade": "Grade"}, + merged_specs={ + "student_id": {"description": "Unique student identifier"}, + "grade": {"description": "Student grade"}, + }, + ) + + result = format_validation_error(error) + + # Assertions: Check specific content, not just "no exception" + assert len(result) > 0 + assert len(result) <= MAX_MESSAGE_LENGTH + # Section header + assert "Missing required columns" in result + # Column display names (raw headers) + assert "Student ID" in result # Raw header, not canonical + assert "Grade" in result # Raw header, not canonical + # Guidance text + assert "must be present" in result.lower() or "required" in result.lower() + + +@pytest.mark.skipif(not HAS_PANDERA, reason="pandera not available") +def test_integration_pandera_type_errors() -> None: + """Test formatter with real Pandera type validation errors.""" + schema = DataFrameSchema( + { + "age": Column(int, nullable=False), + "score": Column(float, nullable=False), + } + ) + + # Create DataFrame with wrong types + df = pd.DataFrame( + { + "age": ["not_a_number", "also_not_a_number"], + "score": ["invalid", "also_invalid"], + } + ) + + try: + schema.validate(df, lazy=True) # Use lazy=True to collect all errors + pytest.fail("Expected SchemaErrors to be raised") + except SchemaErrors as e: + # Convert to HardValidationError format (as validation.py does) + # Handle case where failure_cases might be a DataFrame + if hasattr(e.failure_cases, "to_dict"): + failure_cases = e.failure_cases.to_dict(orient="records") + else: + failure_cases = [] + + error = HardValidationError( + failure_cases=failure_cases, + raw_to_canon={"Age": "age", "Score": "score"}, + canon_to_raw={"age": "Age", "score": "Score"}, + merged_specs={ + "age": {"dtype": "int64", "nullable": False}, + "score": {"dtype": "float64", "nullable": False}, + }, + ) + + result = format_validation_error(error) + + # Assertions: Check specific content + assert len(result) > 0 + assert len(result) <= MAX_MESSAGE_LENGTH + if failure_cases: + # Column display names (raw headers) + assert "Age" in result # Raw header + assert "Score" in result # Raw header + # Section header + assert "Column" in result or "has validation errors" in result.lower() + # Row numbers or "Unknown row" + assert "Row" in result or "Unknown row" in result + # Example values should be present (may be masked if PII) + # Type errors show the actual type found + assert ( + "object" in result.lower() + or "int" in result.lower() + or "float" in result.lower() + ) + + +@pytest.mark.skipif(not HAS_PANDERA, reason="pandera not available") +def test_integration_pandera_isin_errors() -> None: + """Test formatter with real Pandera isin check errors.""" + schema = DataFrameSchema( + { + "grade": Column(str, checks=Check.isin(["A", "B", "C", "D", "F"])), + "status": Column(str, checks=Check.isin(["active", "inactive", "pending"])), + } + ) + + # Create DataFrame with invalid values + df = pd.DataFrame( + { + "grade": ["X", "Y", "Z"], + "status": ["invalid", "also_invalid", "third_invalid"], + } + ) + + try: + schema.validate(df, lazy=True) # Use lazy=True to collect all errors + pytest.fail("Expected SchemaErrors to be raised") + except SchemaErrors as e: + # Handle case where failure_cases might be a DataFrame + if hasattr(e.failure_cases, "to_dict"): + failure_cases = e.failure_cases.to_dict(orient="records") + else: + failure_cases = [] + + error = HardValidationError( + failure_cases=failure_cases, + raw_to_canon={"Grade": "grade", "Status": "status"}, + canon_to_raw={"grade": "Grade", "status": "Status"}, + merged_specs={ + "grade": { + "checks": [{"type": "isin", "args": [["A", "B", "C", "D", "F"]]}], + }, + "status": { + "checks": [ + {"type": "isin", "args": [["active", "inactive", "pending"]]} + ], + }, + }, + ) + + result = format_validation_error(error) + + # Assertions: Check specific content + assert len(result) > 0 + assert len(result) <= MAX_MESSAGE_LENGTH + # Column display names (raw headers) + assert "Grade" in result # Raw header + assert "Status" in result # Raw header + # Section header + assert "Column" in result or "has validation errors" in result.lower() + # Row numbers (1-indexed) + assert "Row 1" in result # First row error + assert "Row 2" in result # Second row error + # isin check mention + assert ( + "isin" in result.lower() + or "one of" in result.lower() + or "allowed values" in result.lower() + ) + # Example values should be shown (not masked - these are not PII) + assert "X" in result or "Y" in result or "Z" in result # Actual values shown + assert "invalid" in result.lower() # Actual values shown + + +@pytest.mark.skipif(not HAS_PANDERA, reason="pandera not available") +def test_integration_pandera_nullability_errors() -> None: + """Test formatter with real Pandera nullability errors.""" + schema = DataFrameSchema( + { + "student_id": Column(str, nullable=False), + "name": Column(str, nullable=False), + } + ) + + # Create DataFrame with null values + df = pd.DataFrame( + { + "student_id": ["STU001", None, "STU003"], + "name": ["Alice", "Bob", None], + } + ) + + try: + schema.validate(df, lazy=True) # Use lazy=True to collect all errors + pytest.fail("Expected SchemaErrors to be raised") + except SchemaErrors as e: + # Handle case where failure_cases might be a DataFrame + if hasattr(e.failure_cases, "to_dict"): + failure_cases = e.failure_cases.to_dict(orient="records") + else: + failure_cases = [] + + error = HardValidationError( + failure_cases=failure_cases, + raw_to_canon={"Student ID": "student_id", "Name": "name"}, + canon_to_raw={"student_id": "Student ID", "name": "Name"}, + merged_specs={ + "student_id": {"nullable": False}, + "name": {"nullable": False}, + }, + ) + + result = format_validation_error(error) + + # Assertions: Check specific content + assert len(result) > 0 + assert len(result) <= MAX_MESSAGE_LENGTH + # Column display names (raw headers) + assert "Student ID" in result # Raw header + assert "Name" in result # Raw header + # Section header + assert "Column" in result or "has validation errors" in result.lower() + # Row numbers (1-indexed) - null values should be at specific rows + assert "Row 2" in result # Second row has null student_id + assert "Row 3" in result # Third row has null name + # Nullability error message + assert ( + "cannot be empty" in result.lower() + or "null" in result.lower() + or "empty" in result.lower() + or "required" in result.lower() + ) + + +@pytest.mark.skipif(not HAS_PANDERA, reason="pandera not available") +def test_integration_pandera_schema_level_checks() -> None: + """Test formatter with real Pandera schema-level validation errors. + + This test ensures schema-level errors (without a column field) are properly + formatted using the _schema_level path. + """ + # Schema-level check: ensure all rows have at least one non-null value + schema = DataFrameSchema( + columns={ + "col1": Column(str, nullable=True), + "col2": Column(str, nullable=True), + }, + checks=Check(lambda df: len(df) > 0, name="non_empty_dataframe"), + ) + + # Create empty DataFrame + df = pd.DataFrame(columns=["col1", "col2"]) + + try: + schema.validate(df) + pytest.fail("Expected SchemaError or SchemaErrors to be raised") + except (SchemaErrors, SchemaError) as e: + # Schema-level checks may raise SchemaError (singular) or SchemaErrors (plural) + # Handle case where failure_cases might be a DataFrame or missing + failure_cases = [] + if hasattr(e, "failure_cases") and e.failure_cases is not None: + if hasattr(e.failure_cases, "to_dict"): + failure_cases = e.failure_cases.to_dict(orient="records") + + # If failure_cases don't have a column field, ensure they're treated as schema-level + # Manually create a schema-level failure case if needed + if not failure_cases or all( + case.get("column") for case in failure_cases if isinstance(case, dict) + ): + # Create a schema-level failure case (no column field) + failure_cases = [ + { + "check": "non_empty_dataframe", + "failure_case": "DataFrame is empty", + # No "column" field - this triggers schema-level formatting + } + ] + + error = HardValidationError( + failure_cases=failure_cases, + raw_to_canon={"Col1": "col1", "Col2": "col2"}, + canon_to_raw={"col1": "Col1", "col2": "Col2"}, + merged_specs={}, + ) + + result = format_validation_error(error) + + # Assertions: Check schema-level formatting path + assert len(result) > 0 + assert len(result) <= MAX_MESSAGE_LENGTH + # Schema-level section header (not "Column '...'") + assert "File-level" in result or "file-level" in result.lower() + # Should NOT have "Column" prefix for schema-level errors + assert "Column 'File-level" not in result + # Should have validation error content + assert "validation" in result.lower() or "error" in result.lower() + + +@pytest.mark.skipif(not HAS_PANDERA, reason="pandera not available") +def test_integration_pandera_row_numbering() -> None: + """Test that formatter correctly converts 0-indexed to 1-indexed row numbers.""" + schema = DataFrameSchema( + { + "value": Column(int, checks=Check.greater_than(0)), + } + ) + + # Create DataFrame with errors at specific rows + df = pd.DataFrame( + { + "value": [1, -5, 3, -10, 5], # Rows 1 and 3 (0-indexed) have errors + } + ) + + try: + schema.validate(df, lazy=True) # Use lazy=True to collect all errors + pytest.fail("Expected SchemaErrors to be raised") + except SchemaErrors as e: + # Handle case where failure_cases might be a DataFrame + if hasattr(e.failure_cases, "to_dict"): + failure_cases = e.failure_cases.to_dict(orient="records") + else: + failure_cases = [] + + error = HardValidationError( + failure_cases=failure_cases, + raw_to_canon={"Value": "value"}, + canon_to_raw={"value": "Value"}, + merged_specs={ + "value": {"checks": [{"type": "ge", "kwargs": {"ge": 0}}]}, + }, + ) + + result = format_validation_error(error) + + # Assertions: Check row numbering (0-indexed to 1-indexed conversion) + assert len(result) > 0 + assert len(result) <= MAX_MESSAGE_LENGTH + # Column display name + assert "Value" in result # Raw header + # Section header + assert "Column" in result or "has validation errors" in result.lower() + # Row numbers (1-indexed) - specific rows with errors + assert "Row 2" in result # 0-indexed row 1 -> 1-indexed row 2 (value=-5) + assert "Row 4" in result # 0-indexed row 3 -> 1-indexed row 4 (value=-10) + # Should NOT have "Row 0" or "Row 1" (0-indexed) + assert "Row 0" not in result + # Example values should be shown (negative numbers, not PII) + assert "-5" in result or "-10" in result or "negative" in result.lower() + + +@pytest.mark.skipif(not HAS_PANDERA, reason="pandera not available") +def test_integration_pandera_pii_masking() -> None: + """Test that formatter masks PII values in real Pandera errors. + + This test covers both "should mask" (student_name, email, ssn) and + "should not mask" (course_name) to prevent regressions both ways. + """ + schema = DataFrameSchema( + { + "student_name": Column(str, checks=Check.str_length(min_value=3)), + "course_name": Column(str, checks=Check.str_length(min_value=3)), + "email": Column(str, checks=Check.str_length(min_value=5)), + "ssn": Column(str, checks=Check.str_length(min_value=9)), + } + ) + + # Create DataFrame with both PII and non-PII values that fail validation + df = pd.DataFrame( + { + "student_name": ["AB"], # Too short, contains PII - SHOULD BE MASKED + "course_name": ["XY"], # Too short, NOT PII - SHOULD NOT BE MASKED + "email": ["ab@c"], # Too short, contains PII - SHOULD BE MASKED + "ssn": ["123"], # Too short, contains PII - SHOULD BE MASKED + } + ) + + try: + schema.validate(df, lazy=True) # Use lazy=True to collect all errors + pytest.fail("Expected SchemaErrors to be raised") + except SchemaErrors as e: + # Handle case where failure_cases might be a DataFrame + if hasattr(e.failure_cases, "to_dict"): + failure_cases = e.failure_cases.to_dict(orient="records") + else: + failure_cases = [] + + error = HardValidationError( + failure_cases=failure_cases, + raw_to_canon={ + "Student Name": "student_name", + "Course Name": "course_name", + "Email": "email", + "SSN": "ssn", + }, + canon_to_raw={ + "student_name": "Student Name", + "course_name": "Course Name", + "email": "Email", + "ssn": "SSN", + }, + merged_specs={ + "student_name": { + "checks": [{"type": "str_length", "kwargs": {"min_value": 3}}] + }, + "course_name": { + "checks": [{"type": "str_length", "kwargs": {"min_value": 3}}] + }, + "email": { + "checks": [{"type": "str_length", "kwargs": {"min_value": 5}}] + }, + "ssn": {"checks": [{"type": "str_length", "kwargs": {"min_value": 9}}]}, + }, + ) + + result = format_validation_error(error) + + # Assertions: Check PII masking (both positive and negative cases) + assert len(result) > 0 + assert len(result) <= MAX_MESSAGE_LENGTH + # Column display names + assert "Student Name" in result + assert "Course Name" in result + assert "Email" in result + assert "SSN" in result + # Section headers + assert "Column" in result or "has validation errors" in result.lower() + # PII values SHOULD BE MASKED (student_name, email, ssn) + assert "AB" not in result # student_name value masked + assert "ab@c" not in result # email value masked + assert "123" not in result # ssn value masked + # Non-PII values SHOULD NOT BE MASKED (course_name) + assert "XY" in result # course_name value shown (not PII) + # Masking indicators + assert "*" in result or "" in result or "masked" in result.lower() + + +@pytest.mark.skipif(not HAS_PANDERA, reason="pandera not available") +def test_integration_pandera_truncation_behavior() -> None: + """Test that formatter truncates messages when there are many errors. + + This test forces truncation deterministically by generating enough failures + to exceed MAX_ERROR_EXAMPLES and/or MAX_MESSAGE_LENGTH. + """ + schema = DataFrameSchema( + { + "value": Column(int, checks=Check.greater_than(0)), + } + ) + + # Create DataFrame with many errors (more than MAX_ERROR_EXAMPLES) + # Generate enough to guarantee truncation + num_errors = MAX_ERROR_EXAMPLES + 5 # Ensure we exceed the limit + df = pd.DataFrame( + { + "value": [-1] * num_errors, # Many rows with errors + } + ) + + try: + schema.validate(df, lazy=True) # Use lazy=True to collect all errors + pytest.fail("Expected SchemaErrors to be raised") + except SchemaErrors as e: + # Handle case where failure_cases might be a DataFrame + if hasattr(e.failure_cases, "to_dict"): + failure_cases = e.failure_cases.to_dict(orient="records") + else: + failure_cases = [] + + error = HardValidationError( + failure_cases=failure_cases, + raw_to_canon={"Value": "value"}, + canon_to_raw={"value": "Value"}, + merged_specs={ + "value": {"checks": [{"type": "ge", "kwargs": {"ge": 0}}]}, + }, + ) + + result = format_validation_error(error) + + # Assertions: Check truncation behavior deterministically + assert len(result) > 0 + assert len(result) <= MAX_MESSAGE_LENGTH + # Column display name + assert "Value" in result + # Section header + assert "Column" in result or "has validation errors" in result.lower() + # Truncation notice should appear (we have more than MAX_ERROR_EXAMPLES) + assert "additional errors" in result.lower() or "additional" in result.lower() + # Should show some examples (up to MAX_ERROR_EXAMPLES) + assert "Row" in result # At least some row numbers shown + # Should mention the count of additional errors + additional_count = num_errors - MAX_ERROR_EXAMPLES + assert str(additional_count) in result or "additional" in result.lower() + + +@pytest.mark.skipif(not HAS_PANDERA, reason="pandera not available") +def test_integration_pandera_mixed_error_types() -> None: + """Test formatter with real Pandera errors containing multiple error types.""" + schema = DataFrameSchema( + { + "student_id": Column( + str, nullable=False, checks=Check.str_length(min_value=3) + ), + "grade": Column(str, checks=Check.isin(["A", "B", "C", "D", "F"])), + "age": Column(int, nullable=False), + } + ) + + # Create DataFrame with multiple types of errors + df = pd.DataFrame( + { + "student_id": ["AB", None, "STU003"], # Too short, null, valid + "grade": ["X", "Y", "A"], # Invalid, invalid, valid + "age": [15, None, 20], # Valid, null, valid + } + ) + + try: + schema.validate(df, lazy=True) # Use lazy=True to collect all errors + pytest.fail("Expected SchemaErrors to be raised") + except SchemaErrors as e: + # Handle case where failure_cases might be a DataFrame + if hasattr(e.failure_cases, "to_dict"): + failure_cases = e.failure_cases.to_dict(orient="records") + else: + failure_cases = [] + + error = HardValidationError( + failure_cases=failure_cases, + raw_to_canon={ + "Student ID": "student_id", + "Grade": "grade", + "Age": "age", + }, + canon_to_raw={ + "student_id": "Student ID", + "grade": "Grade", + "age": "Age", + }, + merged_specs={ + "student_id": { + "nullable": False, + "checks": [{"type": "str_length", "kwargs": {"min_value": 3}}], + }, + "grade": { + "checks": [{"type": "isin", "args": [["A", "B", "C", "D", "F"]]}], + }, + "age": { + "nullable": False, + }, + }, + ) + + result = format_validation_error(error) + + # Assertions: Check multiple error types + assert len(result) > 0 + assert len(result) <= MAX_MESSAGE_LENGTH + # Column display names (raw headers) + assert "Student ID" in result # Raw header + assert "Grade" in result # Raw header + assert "Age" in result # Raw header + # Section headers + assert "Column" in result or "has validation errors" in result.lower() + # Row numbers (1-indexed) - specific rows with errors + assert ( + "Row 1" in result + ) # First row has multiple errors (student_id too short, grade invalid) + assert ( + "Row 2" in result + ) # Second row has multiple errors (student_id null, grade invalid, age null) + # Row 3 is valid, so it won't appear in errors + # Different error types should be mentioned + # - str_length error for student_id + assert ( + "length" in result.lower() + or "characters" in result.lower() + or "at least" in result.lower() + or "short" in result.lower() + or "minimum" in result.lower() + ) + # - isin error for grade + assert ( + "isin" in result.lower() + or "one of" in result.lower() + or "allowed" in result.lower() + ) + # - nullability error for age + assert ( + "cannot be empty" in result.lower() + or "null" in result.lower() + or "empty" in result.lower() + or "required" in result.lower() + ) diff --git a/src/webapp/validation_error_formatter_snapshot_test.py b/src/webapp/validation_error_formatter_snapshot_test.py new file mode 100644 index 00000000..7fe4b6b5 --- /dev/null +++ b/src/webapp/validation_error_formatter_snapshot_test.py @@ -0,0 +1,665 @@ +"""Golden snapshot tests for validation error formatter. + +These tests compare formatted output against expected "golden" files to catch +UX regressions. The expected outputs are stored in fixtures and should be +updated when intentional formatting changes are made. + +To update snapshots, set UPDATE_SNAPSHOTS=1 environment variable or use +pytest --update-snapshots flag. +""" + +import pytest +import os +from pathlib import Path +from unittest.mock import patch + +try: + import pandas as pd + from pandera.errors import SchemaErrors, SchemaError + + HAS_PANDERA = True +except ImportError: + HAS_PANDERA = False + pd = None # type: ignore + SchemaErrors = None # type: ignore + SchemaError = None # type: ignore + +from .validation import HardValidationError +from .validation_error_formatter import format_validation_error + + +# ============================================================================ +# Snapshot Update Control +# ============================================================================ + + +def _should_update_snapshots() -> bool: + """Check if snapshots should be updated (via env var or pytest flag).""" + # Check environment variable + if os.getenv("UPDATE_SNAPSHOTS") == "1": + return True + # Check pytest config option (if available) + try: + config = pytest.config # type: ignore + if hasattr(config, "option") and hasattr(config.option, "update_snapshots"): + return getattr(config.option, "update_snapshots", False) + except (AttributeError, RuntimeError): + pass + return False + + +# ============================================================================ +# Fixture Paths +# ============================================================================ + +# Directory for golden snapshot files +_SNAPSHOT_DIR = Path(__file__).parent / "fixtures" / "validation_error_snapshots" + + +def _get_snapshot_path(test_name: str) -> Path: + """Get path to snapshot file for a test.""" + _SNAPSHOT_DIR.mkdir(parents=True, exist_ok=True) + return _SNAPSHOT_DIR / f"{test_name}.txt" + + +def _read_snapshot(test_name: str) -> str: + """Read expected snapshot content.""" + snapshot_path = _get_snapshot_path(test_name) + if snapshot_path.exists(): + return snapshot_path.read_text(encoding="utf-8") + return "" + + +def _write_snapshot(test_name: str, content: str) -> None: + """Write snapshot content to file.""" + snapshot_path = _get_snapshot_path(test_name) + snapshot_path.write_text(content, encoding="utf-8") + + +def _normalize_snapshot(content: str) -> str: + """Normalize snapshot content for comparison. + + Handles: + - Platform newlines (\r\n → \n) + - Trailing whitespace on each line + - Trailing empty lines + - Does NOT collapse internal whitespace (to catch real UX regressions) + """ + # Normalize newlines: \r\n → \n + content = content.replace("\r\n", "\n").replace("\r", "\n") + + lines = content.splitlines() + # Remove trailing whitespace from each line (but preserve internal whitespace) + normalized = [line.rstrip() for line in lines] + + # Remove trailing empty lines + while normalized and not normalized[-1]: + normalized.pop() + + # Return with single trailing newline (or empty string if no content) + return "\n".join(normalized) + "\n" if normalized else "" + + +# ============================================================================ +# Snapshot Test Cases +# ============================================================================ + + +@pytest.mark.skipif(not HAS_PANDERA, reason="pandera not available") +def test_snapshot_missing_required_columns() -> None: + """Snapshot test: Missing required columns error.""" + error = HardValidationError( + missing_required=["student_id", "grade", "age"], + raw_to_canon={ + "Student ID": "student_id", + "Grade": "grade", + "Age": "age", + }, + canon_to_raw={ + "student_id": "Student ID", + "grade": "Grade", + "age": "Age", + }, + merged_specs={ + "student_id": {"description": "Unique student identifier"}, + "grade": {"description": "Student grade (A-F)"}, + "age": {"description": "Student age"}, + }, + ) + + result = format_validation_error(error) + normalized_result = _normalize_snapshot(result) + + snapshot_name = "missing_required_columns" + expected = _read_snapshot(snapshot_name) + + if _should_update_snapshots(): + # Update mode: write snapshot + _write_snapshot(snapshot_name, normalized_result) + pytest.skip(f"Updated snapshot: {snapshot_name}.txt") + + if not expected: + # Missing snapshot: fail with instructions + pytest.fail( + f"Snapshot file missing: {_get_snapshot_path(snapshot_name)}\n" + f"To create/update snapshots, run with UPDATE_SNAPSHOTS=1 environment variable:\n" + f" UPDATE_SNAPSHOTS=1 pytest {__file__}::{snapshot_name}\n" + f"\nCurrent output:\n{normalized_result}" + ) + + expected_normalized = _normalize_snapshot(expected) + assert normalized_result == expected_normalized, ( + f"Snapshot mismatch for {snapshot_name}.\n" + f"Expected:\n{expected_normalized}\n\n" + f"Got:\n{normalized_result}\n\n" + f"To update snapshot, run with UPDATE_SNAPSHOTS=1:\n" + f" UPDATE_SNAPSHOTS=1 pytest {__file__}::{snapshot_name}" + ) + + +@pytest.mark.skipif(not HAS_PANDERA, reason="pandera not available") +def test_snapshot_extra_columns_ambiguous() -> None: + """Snapshot test: Extra columns with ambiguous raw names.""" + error = HardValidationError( + extra_columns=["student_id"], + raw_to_canon={ + "Student ID": "student_id", + "StudentID": "student_id", # Multiple raw names normalize to same canonical + "STUDENT-ID": "student_id", + }, + canon_to_raw={ + "student_id": "Student ID", # First encountered + }, + merged_specs={}, + ) + + result = format_validation_error(error) + normalized_result = _normalize_snapshot(result) + + # Assert exact ambiguity rule phrase: with 3+ matches, should show "X (and N similar)" + assert "(and" in result and "similar" in result, ( + f"Expected ambiguity phrase '(and N similar)' for 3+ matches, " + f"but got: {result[:200]}" + ) + # Verify the exact format: "X (and N similar)" + assert ( + "Student ID (and 2 similar)" in result or "student_id (and 2 similar)" in result + ), f"Expected 'Student ID (and 2 similar)' format, but got: {result[:200]}" + + snapshot_name = "extra_columns_ambiguous" + expected = _read_snapshot(snapshot_name) + + if _should_update_snapshots(): + _write_snapshot(snapshot_name, normalized_result) + pytest.skip(f"Updated snapshot: {snapshot_name}.txt") + + if not expected: + pytest.fail( + f"Snapshot file missing: {_get_snapshot_path(snapshot_name)}\n" + f"To create/update snapshots, run with UPDATE_SNAPSHOTS=1 environment variable:\n" + f" UPDATE_SNAPSHOTS=1 pytest {__file__}::{snapshot_name}\n" + f"\nCurrent output:\n{normalized_result}" + ) + + expected_normalized = _normalize_snapshot(expected) + assert normalized_result == expected_normalized, ( + f"Snapshot mismatch for {snapshot_name}.\n" + f"Expected:\n{expected_normalized}\n\n" + f"Got:\n{normalized_result}\n\n" + f"To update snapshot, run with UPDATE_SNAPSHOTS=1:\n" + f" UPDATE_SNAPSHOTS=1 pytest {__file__}::{snapshot_name}" + ) + + +@pytest.mark.skipif(not HAS_PANDERA, reason="pandera not available") +def test_snapshot_mixed_types_multiple_columns() -> None: + """Snapshot test: Mixed error types across multiple columns.""" + error = HardValidationError( + failure_cases=[ + { + "column": "student_id", + "index": 0, + "check": "str_length(3, None)", + "failure_case": "AB", # Too short + }, + { + "column": "grade", + "index": 0, + "check": "isin(['A', 'B', 'C', 'D', 'F'])", + "failure_case": "X", # Invalid value + }, + { + "column": "age", + "index": 1, + "check": "nullable", + "failure_case": None, # Null value + }, + { + "column": "score", + "index": 2, + "check": "greater_than(0)", + "failure_case": -5, # Negative value + }, + ], + raw_to_canon={ + "Student ID": "student_id", + "Grade": "grade", + "Age": "age", + "Score": "score", + }, + canon_to_raw={ + "student_id": "Student ID", + "grade": "Grade", + "age": "Age", + "score": "Score", + }, + merged_specs={ + "student_id": { + "checks": [{"type": "str_length", "kwargs": {"min_value": 3}}], + }, + "grade": { + "checks": [{"type": "isin", "args": [["A", "B", "C", "D", "F"]]}], + }, + "age": { + "nullable": False, + }, + "score": { + "checks": [{"type": "ge", "kwargs": {"ge": 0}}], + }, + }, + ) + + result = format_validation_error(error) + normalized_result = _normalize_snapshot(result) + + snapshot_name = "mixed_types_multiple_columns" + expected = _read_snapshot(snapshot_name) + + if _should_update_snapshots(): + _write_snapshot(snapshot_name, normalized_result) + pytest.skip(f"Updated snapshot: {snapshot_name}.txt") + + if not expected: + pytest.fail( + f"Snapshot file missing: {_get_snapshot_path(snapshot_name)}\n" + f"To create/update snapshots, run with UPDATE_SNAPSHOTS=1 environment variable:\n" + f" UPDATE_SNAPSHOTS=1 pytest {__file__}::{snapshot_name}\n" + f"\nCurrent output:\n{normalized_result}" + ) + + expected_normalized = _normalize_snapshot(expected) + assert normalized_result == expected_normalized, ( + f"Snapshot mismatch for {snapshot_name}.\n" + f"Expected:\n{expected_normalized}\n\n" + f"Got:\n{normalized_result}\n\n" + f"To update snapshot, run with UPDATE_SNAPSHOTS=1:\n" + f" UPDATE_SNAPSHOTS=1 pytest {__file__}::{snapshot_name}" + ) + + +@pytest.mark.skipif(not HAS_PANDERA, reason="pandera not available") +def test_snapshot_multiple_rows_same_column() -> None: + """Snapshot test: Multiple rows with errors in the same column.""" + error = HardValidationError( + failure_cases=[ + { + "column": "grade", + "index": 0, + "check": "isin(['A', 'B', 'C', 'D', 'F'])", + "failure_case": "X", + }, + { + "column": "grade", + "index": 1, + "check": "isin(['A', 'B', 'C', 'D', 'F'])", + "failure_case": "Y", + }, + { + "column": "grade", + "index": 2, + "check": "isin(['A', 'B', 'C', 'D', 'F'])", + "failure_case": "Z", + }, + { + "column": "grade", + "index": 3, + "check": "isin(['A', 'B', 'C', 'D', 'F'])", + "failure_case": "W", + }, + ], + raw_to_canon={"Grade": "grade"}, + canon_to_raw={"grade": "Grade"}, + merged_specs={ + "grade": { + "checks": [{"type": "isin", "args": [["A", "B", "C", "D", "F"]]}], + }, + }, + ) + + result = format_validation_error(error) + normalized_result = _normalize_snapshot(result) + + snapshot_name = "multiple_rows_same_column" + expected = _read_snapshot(snapshot_name) + + if _should_update_snapshots(): + _write_snapshot(snapshot_name, normalized_result) + pytest.skip(f"Updated snapshot: {snapshot_name}.txt") + + if not expected: + pytest.fail( + f"Snapshot file missing: {_get_snapshot_path(snapshot_name)}\n" + f"To create/update snapshots, run with UPDATE_SNAPSHOTS=1 environment variable:\n" + f" UPDATE_SNAPSHOTS=1 pytest {__file__}::{snapshot_name}\n" + f"\nCurrent output:\n{normalized_result}" + ) + + expected_normalized = _normalize_snapshot(expected) + assert normalized_result == expected_normalized, ( + f"Snapshot mismatch for {snapshot_name}.\n" + f"Expected:\n{expected_normalized}\n\n" + f"Got:\n{normalized_result}\n\n" + f"To update snapshot, run with UPDATE_SNAPSHOTS=1:\n" + f" UPDATE_SNAPSHOTS=1 pytest {__file__}::{snapshot_name}" + ) + + +@pytest.mark.skipif(not HAS_PANDERA, reason="pandera not available") +def test_snapshot_schema_level_errors() -> None: + """Snapshot test: Schema-level errors (no column).""" + error = HardValidationError( + failure_cases=[ + { + # No "column" field - schema-level error + "check": "non_empty_dataframe", + "failure_case": "DataFrame is empty", + }, + { + # No "column" field - schema-level error + "check": "row_count", + "failure_case": "Row count mismatch", + }, + ], + raw_to_canon={}, + canon_to_raw={}, + merged_specs={}, + ) + + result = format_validation_error(error) + normalized_result = _normalize_snapshot(result) + + snapshot_name = "schema_level_errors" + expected = _read_snapshot(snapshot_name) + + if _should_update_snapshots(): + _write_snapshot(snapshot_name, normalized_result) + pytest.skip(f"Updated snapshot: {snapshot_name}.txt") + + if not expected: + pytest.fail( + f"Snapshot file missing: {_get_snapshot_path(snapshot_name)}\n" + f"To create/update snapshots, run with UPDATE_SNAPSHOTS=1 environment variable:\n" + f" UPDATE_SNAPSHOTS=1 pytest {__file__}::{snapshot_name}\n" + f"\nCurrent output:\n{normalized_result}" + ) + + expected_normalized = _normalize_snapshot(expected) + assert normalized_result == expected_normalized, ( + f"Snapshot mismatch for {snapshot_name}.\n" + f"Expected:\n{expected_normalized}\n\n" + f"Got:\n{normalized_result}\n\n" + f"To update snapshot, run with UPDATE_SNAPSHOTS=1:\n" + f" UPDATE_SNAPSHOTS=1 pytest {__file__}::{snapshot_name}" + ) + + +@pytest.mark.skipif(not HAS_PANDERA, reason="pandera not available") +def test_snapshot_truncation_many_errors() -> None: + """Snapshot test: Truncation behavior with many errors. + + Uses a fixed limit (10) explicitly set in the test to ensure maximum + stability. The snapshot represents the intended UX, not whatever the + global constant happens to be. + """ + # Use fixed limit explicitly set in test for maximum stability + # This snapshot represents UX with max 10 examples, regardless of global constant + # Generate enough errors to trigger truncation (10 + 5 = 15) + num_errors = 15 # Guaranteed to exceed the fixed limit of 10 + + failure_cases = [] + for i in range(num_errors): + failure_cases.append( + { + "column": "value", + "index": i, + "check": "greater_than(0)", + "failure_case": -1, + } + ) + + error = HardValidationError( + failure_cases=failure_cases, + raw_to_canon={"Value": "value"}, + canon_to_raw={"value": "Value"}, + merged_specs={ + "value": { + "checks": [{"type": "ge", "kwargs": {"ge": 0}}], + }, + }, + ) + + # Patch MAX_ERROR_EXAMPLES to 10 for this test to ensure snapshot stability + with patch("src.webapp.validation_error_formatter.MAX_ERROR_EXAMPLES", 10): + result = format_validation_error(error) + + normalized_result = _normalize_snapshot(result) + + snapshot_name = "truncation_many_errors" + expected = _read_snapshot(snapshot_name) + + if _should_update_snapshots(): + _write_snapshot(snapshot_name, normalized_result) + pytest.skip(f"Updated snapshot: {snapshot_name}.txt") + + if not expected: + pytest.fail( + f"Snapshot file missing: {_get_snapshot_path(snapshot_name)}\n" + f"To create/update snapshots, run with UPDATE_SNAPSHOTS=1 environment variable:\n" + f" UPDATE_SNAPSHOTS=1 pytest {__file__}::{snapshot_name}\n" + f"\nCurrent output:\n{normalized_result}" + ) + + expected_normalized = _normalize_snapshot(expected) + assert normalized_result == expected_normalized, ( + f"Snapshot mismatch for {snapshot_name}.\n" + f"Expected:\n{expected_normalized}\n\n" + f"Got:\n{normalized_result}\n\n" + f"To update snapshot, run with UPDATE_SNAPSHOTS=1:\n" + f" UPDATE_SNAPSHOTS=1 pytest {__file__}::{snapshot_name}" + ) + + +@pytest.mark.skipif(not HAS_PANDERA, reason="pandera not available") +def test_snapshot_pii_masking_mixed() -> None: + """Snapshot test: PII masking with mixed PII and non-PII columns. + + Uses distinctive canary values for PII to avoid false positives from + substring matches in other parts of the output. + """ + # Use distinctive canary values that won't appear as substrings elsewhere + pii_email = "canary_pii_email_123@example.com" + pii_name = "CANARY_PII_NAME_123" + pii_ssn = "CANARY_PII_SSN_123456789" + + error = HardValidationError( + failure_cases=[ + { + "column": "student_name", + "index": 0, + "check": "str_length(3, None)", + "failure_case": pii_name, # PII - should be masked + }, + { + "column": "course_name", + "index": 0, + "check": "str_length(3, None)", + "failure_case": "XY", # NOT PII - should be shown + }, + { + "column": "email", + "index": 1, + "check": "str_length(5, None)", + "failure_case": pii_email, # PII - should be masked + }, + { + "column": "ssn", + "index": 2, + "check": "str_length(9, None)", + "failure_case": pii_ssn, # PII - should be masked + }, + { + "column": "grade", + "index": 1, + "check": "isin(['A', 'B', 'C'])", + "failure_case": "X", # NOT PII - should be shown + }, + ], + raw_to_canon={ + "Student Name": "student_name", + "Course Name": "course_name", + "Email": "email", + "SSN": "ssn", + "Grade": "grade", + }, + canon_to_raw={ + "student_name": "Student Name", + "course_name": "Course Name", + "email": "Email", + "ssn": "SSN", + "grade": "Grade", + }, + merged_specs={ + "student_name": { + "checks": [{"type": "str_length", "kwargs": {"min_value": 3}}], + }, + "course_name": { + "checks": [{"type": "str_length", "kwargs": {"min_value": 3}}], + }, + "email": { + "checks": [{"type": "str_length", "kwargs": {"min_value": 5}}], + }, + "ssn": { + "checks": [{"type": "str_length", "kwargs": {"min_value": 9}}], + }, + "grade": { + "checks": [{"type": "isin", "args": [["A", "B", "C"]]}], + }, + }, + ) + + result = format_validation_error(error) + normalized_result = _normalize_snapshot(result) + + # Negative assertion: raw PII values must NOT appear in output + # Using distinctive canaries to avoid false positives from substring matches + assert pii_email not in result, f"PII leakage: email '{pii_email}' found in output" + assert pii_name not in result, f"PII leakage: name '{pii_name}' found in output" + assert pii_ssn not in result, f"PII leakage: SSN '{pii_ssn}' found in output" + # Non-PII values should be shown + assert "XY" in result, "Non-PII value 'XY' should be shown (not masked)" + assert "X" in result, "Non-PII value 'X' should be shown (not masked)" + + snapshot_name = "pii_masking_mixed" + expected = _read_snapshot(snapshot_name) + + if _should_update_snapshots(): + _write_snapshot(snapshot_name, normalized_result) + pytest.skip(f"Updated snapshot: {snapshot_name}.txt") + + if not expected: + pytest.fail( + f"Snapshot file missing: {_get_snapshot_path(snapshot_name)}\n" + f"To create/update snapshots, run with UPDATE_SNAPSHOTS=1 environment variable:\n" + f" UPDATE_SNAPSHOTS=1 pytest {__file__}::{snapshot_name}\n" + f"\nCurrent output:\n{normalized_result}" + ) + + expected_normalized = _normalize_snapshot(expected) + assert normalized_result == expected_normalized, ( + f"Snapshot mismatch for {snapshot_name}.\n" + f"Expected:\n{expected_normalized}\n\n" + f"Got:\n{normalized_result}\n\n" + f"To update snapshot, run with UPDATE_SNAPSHOTS=1:\n" + f" UPDATE_SNAPSHOTS=1 pytest {__file__}::{snapshot_name}" + ) + + +@pytest.mark.skipif(not HAS_PANDERA, reason="pandera not available") +def test_snapshot_complete_error_flow() -> None: + """Snapshot test: Complete error flow with all error types.""" + error = HardValidationError( + missing_required=["student_id"], + extra_columns=["extra_col"], + failure_cases=[ + { + "column": "grade", + "index": 0, + "check": "isin(['A', 'B', 'C'])", + "failure_case": "X", + }, + { + "column": "age", + "index": 1, + "check": "nullable", + "failure_case": None, + }, + ], + raw_to_canon={ + "Student ID": "student_id", + "Grade": "grade", + "Age": "age", + "Extra Col": "extra_col", + }, + canon_to_raw={ + "student_id": "Student ID", + "grade": "Grade", + "age": "Age", + "extra_col": "Extra Col", + }, + merged_specs={ + "student_id": {"description": "Student identifier"}, + "grade": { + "checks": [{"type": "isin", "args": [["A", "B", "C"]]}], + }, + "age": { + "nullable": False, + }, + }, + ) + + result = format_validation_error(error) + normalized_result = _normalize_snapshot(result) + + snapshot_name = "complete_error_flow" + expected = _read_snapshot(snapshot_name) + + if _should_update_snapshots(): + _write_snapshot(snapshot_name, normalized_result) + pytest.skip(f"Updated snapshot: {snapshot_name}.txt") + + if not expected: + pytest.fail( + f"Snapshot file missing: {_get_snapshot_path(snapshot_name)}\n" + f"To create/update snapshots, run with UPDATE_SNAPSHOTS=1 environment variable:\n" + f" UPDATE_SNAPSHOTS=1 pytest {__file__}::{snapshot_name}\n" + f"\nCurrent output:\n{normalized_result}" + ) + + expected_normalized = _normalize_snapshot(expected) + assert normalized_result == expected_normalized, ( + f"Snapshot mismatch for {snapshot_name}.\n" + f"Expected:\n{expected_normalized}\n\n" + f"Got:\n{normalized_result}\n\n" + f"To update snapshot, run with UPDATE_SNAPSHOTS=1:\n" + f" UPDATE_SNAPSHOTS=1 pytest {__file__}::{snapshot_name}" + ) diff --git a/src/webapp/validation_error_formatter_test.py b/src/webapp/validation_error_formatter_test.py new file mode 100644 index 00000000..61d5f326 --- /dev/null +++ b/src/webapp/validation_error_formatter_test.py @@ -0,0 +1,1641 @@ +"""Comprehensive tests for validation_error_formatter module.""" + +import pytest +from typing import Any, Dict, List, Optional +from unittest.mock import Mock + +try: + import pandas as pd + import numpy as np + + HAS_PANDAS = True +except ImportError: + HAS_PANDAS = False + pd = None # type: ignore + np = None # type: ignore + +from .validation import HardValidationError +from .validation_error_formatter import ( + format_validation_error, + _format_missing_required, + _format_extra_columns, + _normalize_failure_cases, + _group_failure_cases_by_column, + _is_pii_column, + _mask_pii_value, + _format_column_validation_errors, + _format_schema_validation_errors, + _format_check_error, + _extract_base_check_type, + _find_check_spec, + _normalize_check_type_alias, + _sanitize_string, + _get_canon_to_raw_mapping, + MAX_VALUE_LENGTH, + MAX_MESSAGE_LENGTH, + PII_HIGH_RISK_INDICATORS, +) + + +# ============================================================================ +# Test Fixtures +# ============================================================================ + + +@pytest.fixture +def sample_error() -> HardValidationError: + """Create a sample HardValidationError for testing.""" + return HardValidationError( + missing_required=["student_id", "grade"], + extra_columns=[], + schema_errors=None, + failure_cases=None, + raw_to_canon={"Student ID": "student_id", "Grade": "grade"}, + canon_to_raw={"student_id": "Student ID", "grade": "Grade"}, + merged_specs={ + "student_id": { + "description": "A unique identifier for each student", + "checks": [{"type": "str_length", "kwargs": {"min_value": 1}}], + }, + "grade": { + "description": "Student grade", + "checks": [], + }, + }, + ) + + +@pytest.fixture +def error_with_failure_cases() -> HardValidationError: + """Create HardValidationError with failure cases.""" + return HardValidationError( + missing_required=[], + extra_columns=[], + schema_errors=None, + failure_cases=[ + { + "column": "student_id", + "index": 0, + "check": "str_length", + "failure_case": "AB", # Too short + }, + { + "column": "grade", + "index": 1, + "check": "isin", + "failure_case": "X", + }, + ], + raw_to_canon={"Student ID": "student_id", "Grade": "grade"}, + canon_to_raw={"student_id": "Student ID", "grade": "Grade"}, + merged_specs={ + "student_id": { + "description": "A unique identifier", + "checks": [{"type": "str_length", "kwargs": {"min_value": 3}}], + }, + "grade": { + "description": "Student grade", + "checks": [{"type": "isin", "args": [["A", "B", "C", "D", "F"]]}], + }, + }, + ) + + +@pytest.fixture +def error_with_pii() -> HardValidationError: + """Create HardValidationError with PII in failure cases.""" + return HardValidationError( + missing_required=[], + extra_columns=[], + schema_errors=None, + failure_cases=[ + { + "column": "student_id", + "index": 0, + "check": "str_length", + "failure_case": "STU-12345-ABCDEF", + }, + { + "column": "email", + "index": 1, + "check": "matches", + "failure_case": "john.doe@example.com", + }, + ], + raw_to_canon={"Student ID": "student_id", "Email": "email"}, + canon_to_raw={"student_id": "Student ID", "email": "Email"}, + merged_specs={ + "student_id": { + "description": "Student identifier", + "checks": [{"type": "str_length", "kwargs": {"min_value": 10}}], + }, + "email": { + "description": "Email address", + "checks": [{"type": "matches", "args": [r"^[^@]+@[^@]+\.[^@]+$"]}], + }, + }, + ) + + +# ============================================================================ +# Tests for _sanitize_string +# ============================================================================ + + +def test_sanitize_string_normal() -> None: + """Test sanitize_string with normal string.""" + result = _sanitize_string("normal_string") + assert result == "normal_string" + + +def test_sanitize_string_with_newlines() -> None: + """Test sanitize_string removes newlines.""" + result = _sanitize_string("line1\nline2\rline3") + assert result == "line1 line2 line3" + assert "\n" not in result + assert "\r" not in result + + +def test_sanitize_string_truncates_long() -> None: + """Test sanitize_string truncates very long strings.""" + long_string = "a" * 500 + result = _sanitize_string(long_string) + assert len(result) <= MAX_VALUE_LENGTH + 3 # +3 for "..." + assert result.endswith("...") + + +def test_sanitize_string_removes_control_chars() -> None: + """Test sanitize_string removes control characters.""" + result = _sanitize_string("text\x00\x01\x02text") + assert "\x00" not in result + assert "\x01" not in result + assert "\x02" not in result + + +def test_sanitize_string_with_custom_length() -> None: + """Test sanitize_string with custom max_length.""" + result = _sanitize_string("a" * 100, max_length=50) + assert len(result) <= 53 # 50 + "..." + + +def test_sanitize_string_non_string_input() -> None: + """Test sanitize_string converts non-string to string.""" + result = _sanitize_string(12345) # type: ignore[arg-type] + assert result == "12345" + + +# ============================================================================ +# Tests for _is_pii_column +# ============================================================================ + + +def test_is_pii_column_student_id() -> None: + """student_id is a standard de-identified identifier; not treated as PII.""" + assert _is_pii_column("student_id") is False + assert _is_pii_column("Student_ID") is False + assert _is_pii_column("STUDENT_ID") is False + + +def test_is_pii_column_email() -> None: + """Test PII detection for email.""" + assert _is_pii_column("email") is True + assert _is_pii_column("email_address") is True + assert _is_pii_column("user_email") is True + + +def test_is_pii_column_name() -> None: + """Test PII detection for name fields.""" + assert _is_pii_column("first_name") is True + assert _is_pii_column("last_name") is True + assert _is_pii_column("full_name") is True + # Note: "name" alone is not flagged to avoid false positives (e.g., "course_name") + # Only specific variants like "first_name", "last_name" are flagged + + +def test_is_pii_column_ssn() -> None: + """Test PII detection for SSN.""" + assert _is_pii_column("ssn") is True + assert _is_pii_column("social_security") is True + assert _is_pii_column("social_security_number") is True + + +def test_is_pii_column_non_pii() -> None: + """Test PII detection returns False for non-PII columns.""" + assert _is_pii_column("grade") is False + assert _is_pii_column("student_id") is False # standard de-identified identifier + assert _is_pii_column("course_name") is False + assert _is_pii_column("credits") is False + assert _is_pii_column("term") is False + + +def test_is_pii_column_high_risk_indicators() -> None: + """Test high-risk PII indicators are detected (substring matching).""" + for indicator in PII_HIGH_RISK_INDICATORS: + assert _is_pii_column(indicator) is True + assert _is_pii_column(f"prefix_{indicator}") is True + assert _is_pii_column(f"{indicator}_suffix") is True + + +def test_is_pii_column_medium_risk_token_matching() -> None: + """Test medium-risk PII indicators use token matching (reduces false positives).""" + # These should match (token match) + assert _is_pii_column("first_name") is True + assert _is_pii_column("last_name") is True + assert _is_pii_column("full_name") is True + assert _is_pii_column("home_address") is True + + # These should NOT match (false positive prevention) + assert _is_pii_column("course_name") is False + assert _is_pii_column("district_name") is False + assert _is_pii_column("school_name") is False + assert _is_pii_column("column_name") is False + assert _is_pii_column("file_name") is False + + +# ============================================================================ +# Tests for _mask_pii_value +# ============================================================================ + + +def test_mask_pii_value_long() -> None: + """Test masking long PII values.""" + result = _mask_pii_value("ABCDEFGHXY") + assert result.startswith("AB") + assert result.endswith("XY") + assert "*" in result + assert "ABCDEFGHXY" not in result + + +def test_mask_pii_value_short() -> None: + """Test masking short PII values.""" + result = _mask_pii_value("AB") + assert result == "****" + assert "AB" not in result + + +def test_mask_pii_value_very_short() -> None: + """Test masking very short PII values.""" + result = _mask_pii_value("A") + assert result == "****" + + +def test_mask_pii_value_none() -> None: + """Test masking None value.""" + result = _mask_pii_value(None) + assert result == "N/A" + + +def test_mask_pii_value_email() -> None: + """Test masking email address.""" + result = _mask_pii_value("john.doe@example.com") + assert result.startswith("jo") + assert result.endswith("om") + assert "@example.com" not in result + assert "john.doe" not in result + + +def test_mask_pii_value_truncates_very_long() -> None: + """Test masking truncates very long values.""" + very_long = "A" * 1000 + result = _mask_pii_value(very_long) + # Should be truncated before masking + assert len(result) < 1000 + + +def test_mask_pii_value_non_string() -> None: + """Test masking non-string values.""" + result = _mask_pii_value(12345) + assert "*" in result or result == "N/A" + + +# ============================================================================ +# Tests for _normalize_failure_cases +# ============================================================================ + + +def test_normalize_failure_cases_list() -> None: + """Test normalizing list of dicts.""" + cases = [{"column": "test", "index": 0}] + result = _normalize_failure_cases(cases) + assert result == cases + + +def test_normalize_failure_cases_empty_list() -> None: + """Test normalizing empty list.""" + result = _normalize_failure_cases([]) + assert result == [] + + +def test_normalize_failure_cases_none() -> None: + """Test normalizing None.""" + result = _normalize_failure_cases(None) + assert result == [] + + +def test_normalize_failure_cases_filters_non_dicts() -> None: + """Test normalizing filters out non-dict items.""" + cases = [{"column": "test"}, "not_a_dict", 123, {"column": "test2"}] + result = _normalize_failure_cases(cases) + assert len(result) == 2 + assert all(isinstance(item, dict) for item in result) + + +def test_normalize_failure_cases_converts_iterable() -> None: + """Test normalizing converts iterable to list.""" + cases = ({"column": "test"}, {"column": "test2"}) + result = _normalize_failure_cases(cases) + assert isinstance(result, list) + assert len(result) == 2 + + +def test_normalize_failure_cases_invalid_type() -> None: + """Test normalizing handles invalid types.""" + result = _normalize_failure_cases("not_iterable") + assert result == [] + + +@pytest.mark.skipif(not HAS_PANDAS, reason="pandas not available") +def test_normalize_failure_cases_dataframe() -> None: + """Test normalizing pandas DataFrame (critical fix for Pandera integration).""" + df = pd.DataFrame( + [ + {"column": "col1", "index": 0, "check": "test", "failure_case": "val1"}, + {"column": "col2", "index": 1, "check": "test", "failure_case": "val2"}, + ] + ) + result = _normalize_failure_cases(df) + assert isinstance(result, list) + assert len(result) == 2 + assert all(isinstance(item, dict) for item in result) + assert result[0]["column"] == "col1" + assert result[1]["column"] == "col2" + + +@pytest.mark.skipif(not HAS_PANDAS, reason="pandas not available") +def test_normalize_failure_cases_dataframe_empty() -> None: + """Test normalizing empty DataFrame.""" + df = pd.DataFrame() + result = _normalize_failure_cases(df) + assert result == [] + + +# ============================================================================ +# Tests for _group_failure_cases_by_column +# ============================================================================ + + +def test_group_failure_cases_by_column() -> None: + """Test grouping failure cases by column.""" + cases = [ + {"column": "col1", "index": 0, "check": "str_length", "failure_case": "val1"}, + {"column": "col1", "index": 1, "check": "str_length", "failure_case": "val2"}, + {"column": "col2", "index": 0, "check": "isin", "failure_case": "val3"}, + ] + result = _group_failure_cases_by_column(cases) + assert "col1" in result + assert "col2" in result + assert len(result["col1"]) == 2 + assert len(result["col2"]) == 1 + assert result["col1"][0]["row"] == 1 # 0-indexed to 1-indexed + assert result["col1"][1]["row"] == 2 + + +def test_group_failure_cases_by_column_negative_index() -> None: + """Test grouping handles negative index.""" + cases = [ + {"column": "col1", "index": -1, "check": "test", "failure_case": "val"}, + ] + result = _group_failure_cases_by_column(cases) + assert result["col1"][0]["row"] is None + + +def test_group_failure_cases_by_column_missing_fields() -> None: + """Test grouping handles missing fields.""" + cases: List[Dict[str, Any]] = [ + {"column": "col1"}, # Missing other fields + {"column": "col2", "index": 0}, + ] + result = _group_failure_cases_by_column(cases) + assert "col1" in result + assert "col2" in result + + +def test_group_failure_cases_by_column_schema_level() -> None: + """Test grouping schema-level errors (no column).""" + cases: List[Dict[str, Any]] = [ + {"index": 0, "check": "test", "failure_case": "val1"}, # No column + {"column": None, "index": 1, "check": "test", "failure_case": "val2"}, + {"column": "", "index": 2, "check": "test", "failure_case": "val3"}, + ] + result = _group_failure_cases_by_column(cases) + assert "_schema_level" in result + assert len(result["_schema_level"]) == 3 + + +def test_group_failure_cases_by_column_nan_index() -> None: + """Test grouping handles NaN row indices.""" + cases = [ + { + "column": "col1", + "index": float("nan"), + "check": "test", + "failure_case": "val", + }, + ] + result = _group_failure_cases_by_column(cases) + assert "col1" in result + assert result["col1"][0]["row"] is None + + +@pytest.mark.skipif(not HAS_PANDAS, reason="numpy not available") +def test_group_failure_cases_by_column_numpy_index() -> None: + """Test grouping handles numpy integer types.""" + cases = [ + { + "column": "col1", + "index": np.int64(0), + "check": "test", + "failure_case": "val", + }, + { + "column": "col2", + "index": np.int32(1), + "check": "test", + "failure_case": "val", + }, + ] + result = _group_failure_cases_by_column(cases) + assert result["col1"][0]["row"] == 1 # 0-indexed to 1-indexed + assert result["col2"][0]["row"] == 2 + + +def test_group_failure_cases_by_column_string_index() -> None: + """Test grouping handles string indices (non-int row labels).""" + cases = [ + {"column": "col1", "index": "row_1", "check": "test", "failure_case": "val"}, + ] + result = _group_failure_cases_by_column(cases) + assert "col1" in result + # Should return sanitized string, not None + assert result["col1"][0]["row"] is not None + assert "row_1" in str(result["col1"][0]["row"]) + + +def test_normalize_row_index() -> None: + """Test row index normalization.""" + from .validation_error_formatter import _normalize_row_index + + # Normal int + assert _normalize_row_index(0) == 1 # 0-indexed to 1-indexed + assert _normalize_row_index(5) == 6 + + # Negative + assert _normalize_row_index(-1) is None + + # NaN + assert _normalize_row_index(float("nan")) is None + + # None + assert _normalize_row_index(None) is None + + # String index + result = _normalize_row_index("row_1") + assert result is not None + assert isinstance(result, str) + + # Float + assert _normalize_row_index(0.0) == 1 + # Non-integer float returns sanitized string (not misleading conversion) + result_float = _normalize_row_index(5.7) + assert isinstance(result_float, str) + assert "5.7" in result_float + + +@pytest.mark.skipif(not HAS_PANDAS, reason="numpy not available") +def test_normalize_row_index_numpy() -> None: + """Test row index normalization with numpy types.""" + from .validation_error_formatter import _normalize_row_index + + assert _normalize_row_index(np.int64(0)) == 1 + assert _normalize_row_index(np.int32(5)) == 6 + assert _normalize_row_index(np.nan) is None + + +# ============================================================================ +# Tests for _format_missing_required +# ============================================================================ + + +def test_format_missing_required(sample_error: HardValidationError) -> None: + """Test formatting missing required columns.""" + result = _format_missing_required(sample_error) + assert result is not None + assert "Missing required columns" in result + assert "Student ID" in result + assert "Grade" in result + assert "A unique identifier for each student" in result + + +def test_format_missing_required_empty() -> None: + """Test formatting with no missing columns.""" + error = HardValidationError(missing_required=[]) + result = _format_missing_required(error) + assert result is None + + +def test_format_missing_required_no_mappings() -> None: + """Test formatting with missing mappings.""" + error = HardValidationError( + missing_required=["student_id"], + canon_to_raw=None, # type: ignore + merged_specs=None, # type: ignore + ) + result = _format_missing_required(error) + assert result is not None + assert "student_id" in result + + +def test_format_missing_required_no_description() -> None: + """Test formatting without column descriptions.""" + error = HardValidationError( + missing_required=["student_id"], + canon_to_raw={"student_id": "Student ID"}, + merged_specs={"student_id": {}}, # No description + ) + result = _format_missing_required(error) + assert result is not None + assert "Student ID" in result + assert "(" not in result or "description" not in result.lower() + + +def test_format_missing_required_non_bijective_mapping() -> None: + """Test formatting with non-bijective mapping (multiple raw → same canonical).""" + # Only raw_to_canon provided, should derive canon_to_raw (first occurrence wins) + error = HardValidationError( + missing_required=["student_id"], + raw_to_canon={ + "Student ID": "student_id", + "StudentID": "student_id", + }, # Two raw → one canon + canon_to_raw=None, # Not provided, should derive + merged_specs={"student_id": {}}, + ) + result = _format_missing_required(error) + assert result is not None + # Should use first raw name seen + assert "Student ID" in result or "student_id" in result + + +def test_get_canon_to_raw_mapping() -> None: + """Test canon_to_raw mapping helper.""" + # Prefer canon_to_raw if available + error1 = HardValidationError( + missing_required=[], + canon_to_raw={"student_id": "Student ID"}, + raw_to_canon={"Student ID": "student_id", "StudentID": "student_id"}, + ) + mapping1 = _get_canon_to_raw_mapping(error1) + assert mapping1["student_id"] == "Student ID" + + # Derive from raw_to_canon if canon_to_raw not available + error2 = HardValidationError( + missing_required=[], + canon_to_raw=None, # type: ignore + raw_to_canon={"Student ID": "student_id", "StudentID": "student_id"}, + ) + mapping2 = _get_canon_to_raw_mapping(error2) + # First occurrence should win + assert mapping2["student_id"] == "Student ID" + + +# ============================================================================ +# Tests for _format_extra_columns +# ============================================================================ + + +def test_format_extra_columns() -> None: + """Test formatting extra columns.""" + error = HardValidationError(extra_columns=["unknown_col1", "unknown_col2"]) + result = _format_extra_columns(error) + assert result is not None + assert "Unexpected columns found" in result + assert "unknown_col1" in result + assert "unknown_col2" in result + + +def test_format_extra_columns_empty() -> None: + """Test formatting with no extra columns.""" + error = HardValidationError(extra_columns=[]) + result = _format_extra_columns(error) + assert result is None + + +def test_format_extra_columns_sanitizes() -> None: + """Test formatting sanitizes column names.""" + error = HardValidationError(extra_columns=["col\nwith\nnewlines"]) + result = _format_extra_columns(error) + assert result is not None + assert "\n" not in result + + +# ============================================================================ +# Tests for _format_check_error +# ============================================================================ + + +def test_format_check_error_str_length() -> None: + """Test formatting str_length check error.""" + spec = {"checks": [{"type": "str_length", "kwargs": {"min_value": 3}}]} + result = _format_check_error("str_length", spec, "AB") + assert "at least 3 characters" in result + + +def test_format_check_error_str_length_range() -> None: + """Test formatting str_length with min and max.""" + spec = { + "checks": [{"type": "str_length", "kwargs": {"min_value": 3, "max_value": 10}}] + } + result = _format_check_error("str_length", spec, "AB") + assert "between 3 and 10" in result + + +def test_format_check_error_isin() -> None: + """Test formatting isin check error.""" + spec = {"checks": [{"type": "isin", "args": [["A", "B", "C"]]}]} + result = _format_check_error("isin", spec, "X") + assert "one of" in result.lower() + assert "A" in result or "B" in result or "C" in result + + +def test_format_check_error_isin_many_values() -> None: + """Test formatting isin with many values.""" + spec = {"checks": [{"type": "isin", "args": [["A", "B", "C", "D", "E", "F", "G"]]}]} + result = _format_check_error("isin", spec, "X") + assert "one of the allowed values" in result.lower() + + +def test_format_check_error_matches() -> None: + """Test formatting matches check error.""" + spec = {"checks": [{"type": "matches", "args": [r"^\d{4}-\d{2}$"]}]} + result = _format_check_error("matches", spec, "invalid") + assert "YYYY-YY" in result or "format" in result.lower() + + +def test_format_check_error_ge() -> None: + """Test formatting ge check error.""" + spec = {"checks": [{"type": "ge", "kwargs": {"ge": 0}}]} + result = _format_check_error("ge", spec, -1) + assert "greater than or equal to 0" in result + + +def test_format_check_error_le() -> None: + """Test formatting le check error.""" + spec = {"checks": [{"type": "le", "kwargs": {"le": 100}}]} + result = _format_check_error("le", spec, 101) + assert "less than or equal to 100" in result + + +def test_format_check_error_gt() -> None: + """Test formatting gt (strictly greater than) check error.""" + spec = {"checks": [{"type": "gt", "args": [0]}]} + result = _format_check_error("gt", spec, -1) + assert "greater than 0" in result + assert "greater than or equal" not in result # Should be strict + + +def test_format_check_error_lt() -> None: + """Test formatting lt (strictly less than) check error.""" + spec = {"checks": [{"type": "lt", "args": [100]}]} + result = _format_check_error("lt", spec, 101) + assert "less than 100" in result + assert "less than or equal" not in result # Should be strict + + +def test_format_check_error_not_nullable() -> None: + """Test formatting not_nullable check error.""" + spec: Dict[str, Any] = {"checks": []} + result = _format_check_error("not_nullable", spec, None) + assert "cannot be empty" in result.lower() + + +def test_format_check_error_pdp_check_num_institutions() -> None: + """PDP/Edvise Schema (ES) check names get human-readable messages.""" + spec: Dict[str, Any] = {} + result = _format_check_error("check_num_institutions", spec, None) + assert "institution" in result.lower() + assert "same" in result.lower() + assert "check_num_institutions" not in result + + +def test_format_check_error_unknown() -> None: + """Test formatting unknown check type.""" + spec: Dict[str, Any] = {"checks": []} + result = _format_check_error("unknown_check", spec, "value") + assert "unknown_check" in result.lower() + + +def test_format_check_error_parameterized_isin() -> None: + """Test formatting parameterized isin check error (Pandera format).""" + spec = {"checks": [{"type": "isin", "args": [["A", "B", "C", "D", "F"]]}]} + # Pandera provides check types with arguments like "isin(['A', 'B', 'C', 'D', 'F'])" + result = _format_check_error("isin(['A', 'B', 'C', 'D', 'F'])", spec, "X") + assert "one of" in result.lower() + assert "A" in result or "B" in result or "C" in result + + +def test_format_check_error_parameterized_str_length() -> None: + """Test formatting parameterized str_length check error (Pandera format).""" + spec = {"checks": [{"type": "str_length", "kwargs": {"min_value": 3}}]} + # Pandera provides check types with arguments like "str_length(3, None)" + result = _format_check_error("str_length(3, None)", spec, "AB") + assert "at least 3 characters" in result + + +def test_format_check_error_parameterized_gt() -> None: + """Test formatting parameterized gt (strict greater than) check error with alias handling.""" + spec = {"checks": [{"type": "gt", "args": [0]}]} + # Pandera provides check types with arguments like "greater_than(0)" + # Alias handling should map "greater_than" → "gt" to match the spec + result = _format_check_error("greater_than(0)", spec, -1) + assert "greater than 0" in result + assert "greater than or equal" not in result # Should be strict, not non-strict + + +def test_format_check_error_parameterized_ge() -> None: + """Test formatting parameterized ge (greater than or equal) check error with alias handling.""" + spec = {"checks": [{"type": "ge", "kwargs": {"ge": 0}}]} + # Pandera provides check types with arguments like "greater_than_or_equal_to(0)" + # Alias handling should map "greater_than_or_equal_to" → "ge" to match the spec + result = _format_check_error("greater_than_or_equal_to(0)", spec, -1) + assert "greater than or equal to 0" in result + + +# ============================================================================ +# Tests for _extract_base_check_type (edge cases) +# ============================================================================ + + +def test_extract_base_check_type_already_base() -> None: + """Test extraction when check type is already base (no parameters).""" + assert _extract_base_check_type("isin") == "isin" + assert _extract_base_check_type("str_length") == "str_length" + assert _extract_base_check_type(" isin ") == "isin" # Strip whitespace + + +def test_extract_base_check_type_parameterized() -> None: + """Test extraction from parameterized check types.""" + assert _extract_base_check_type("isin(['A', 'B', 'C'])") == "isin" + assert _extract_base_check_type("str_length(3, None)") == "str_length" + assert _extract_base_check_type("greater_than(0)") == "greater_than" + + +def test_extract_base_check_type_namespaced() -> None: + """Test extraction from namespaced check types (extracts final token).""" + # Should extract final token after last dot to match specs with base type + assert _extract_base_check_type("Check.isin(['A'])") == "isin" + assert _extract_base_check_type("pandera.Check.isin(['A'])") == "isin" + assert _extract_base_check_type("pandera.Check.str_length(3, None)") == "str_length" + + +def test_extract_base_check_type_with_spaces() -> None: + """Test extraction handles spaces around parentheses.""" + assert _extract_base_check_type("isin (['A', 'B'])") == "isin" + assert _extract_base_check_type("str_length (3, None)") == "str_length" + + +def test_extract_base_check_type_complex_repr() -> None: + """Test extraction from complex repr strings.""" + assert _extract_base_check_type("str_matches(re.compile('...'))") == "str_matches" + assert _extract_base_check_type("isin(pd.Series(['A', 'B']))") == "isin" + + +def test_extract_base_check_type_edge_cases() -> None: + """Test extraction handles edge cases safely.""" + # Empty string + assert _extract_base_check_type("") == "" + + # None and non-string types return empty string (safe default, avoids noisy output) + assert _extract_base_check_type(None) == "" # type: ignore + assert _extract_base_check_type(123) == "" # type: ignore + assert _extract_base_check_type([]) == "" # type: ignore + assert _extract_base_check_type({"type": "isin"}) == "" # type: ignore + + # String with no parentheses + assert _extract_base_check_type("simple_check") == "simple_check" + + # String with only opening parenthesis (malformed but safe) + assert _extract_base_check_type("isin(") == "isin" + + # String with nested parentheses + assert _extract_base_check_type("isin(['A', 'B', ('C', 'D')])") == "isin" + + +def test_extract_base_check_type_no_over_match() -> None: + """Test that extraction doesn't over-match (e.g., isinstance vs isin).""" + # These should extract correctly without confusing similar names + assert _extract_base_check_type("isin(['A'])") == "isin" + assert _extract_base_check_type("isinstance(['A'])") == "isinstance" + assert _extract_base_check_type("is_in(['A'])") == "is_in" + # Verify they're different + assert _extract_base_check_type("isin(['A'])") != _extract_base_check_type( + "isinstance(['A'])" + ) + + +def test_find_check_spec_prioritizes_base_match() -> None: + """Test that _find_check_spec prioritizes base type match.""" + # Spec with base type "isin" + spec = {"checks": [{"type": "isin", "args": [["A", "B", "C"]]}]} + + # Parameterized check type should match via base type + result = _find_check_spec("isin(['A', 'B', 'C', 'D', 'F'])", spec) + assert result is not None + assert result["type"] == "isin" + assert result["args"] == [["A", "B", "C"]] + + # Base type should also match + result2 = _find_check_spec("isin", spec) + assert result2 is not None + assert result2["type"] == "isin" + + +def test_find_check_spec_handles_aliases() -> None: + """Test that _find_check_spec handles alias normalization with correct semantics.""" + # Test strict comparison: "greater_than" → "gt" + spec_gt = {"checks": [{"type": "gt", "args": [0]}]} + result = _find_check_spec("greater_than(0)", spec_gt) + assert result is not None + assert result["type"] == "gt" + + # Test non-strict comparison: "greater_than_or_equal_to" → "ge" + spec_ge = {"checks": [{"type": "ge", "kwargs": {"ge": 0}}]} + result = _find_check_spec("greater_than_or_equal_to(0)", spec_ge) + assert result is not None + assert result["type"] == "ge" + + # Test strict less than: "less_than" → "lt" + spec_lt = {"checks": [{"type": "lt", "args": [100]}]} + result = _find_check_spec("less_than(100)", spec_lt) + assert result is not None + assert result["type"] == "lt" + + # Test alias normalization directly (verify semantic correctness) + assert _normalize_check_type_alias("greater_than") == "gt" # Strict + assert _normalize_check_type_alias("gt") == "gt" # Already canonical + assert _normalize_check_type_alias("greater_than_or_equal_to") == "ge" # Non-strict + assert _normalize_check_type_alias("less_than") == "lt" # Strict + assert _normalize_check_type_alias("lt") == "lt" # Already canonical + assert _normalize_check_type_alias("less_than_or_equal_to") == "le" # Non-strict + assert _normalize_check_type_alias("isin") == "isin" # No alias needed + + +# ============================================================================ +# Tests for _format_column_validation_errors +# ============================================================================ + + +def test_format_column_validation_errors( + error_with_failure_cases: HardValidationError, +) -> None: + """Test formatting column validation errors.""" + errors = [ + {"row": 1, "check": "str_length", "value": "AB"}, + {"row": 2, "check": "isin", "value": "X"}, + ] + result = _format_column_validation_errors("grade", errors, error_with_failure_cases) + assert len(result) > 0 + assert "Grade" in result[0] + assert "Row 1" in result[0] or "Row 2" in result[0] + + +def test_format_column_validation_errors_pii_masking( + error_with_pii: HardValidationError, +) -> None: + """Test PII masking in column validation errors (email is PII and masked; student_id is not).""" + errors = [ + {"row": 2, "check": "matches", "value": "john.doe@example.com"}, + ] + result = _format_column_validation_errors("email", errors, error_with_pii) + assert len(result) > 0 + assert "john.doe@example.com" not in result[0] + assert "masked for privacy" in result[0] + assert "*" in result[0] + + +def test_format_column_validation_errors_limit_examples() -> None: + """Test limiting error examples.""" + error = HardValidationError( + missing_required=[], + canon_to_raw={"col": "Column"}, + merged_specs={"col": {}}, + ) + # Create 15 errors + errors = [{"row": i, "check": "test", "value": f"val{i}"} for i in range(15)] + result = _format_column_validation_errors("col", errors, error) + # Should mention additional errors + assert any("additional errors" in msg.lower() for msg in result) + + +def test_format_column_validation_errors_no_mappings() -> None: + """Test formatting with missing mappings.""" + error = HardValidationError( + missing_required=[], + canon_to_raw=None, # type: ignore + merged_specs=None, # type: ignore + ) + errors = [{"row": 1, "check": "test", "value": "val"}] + result = _format_column_validation_errors("col", errors, error) + assert len(result) > 0 + + +def test_format_column_validation_errors_schema_level() -> None: + """Test formatting schema-level errors (no column).""" + error = HardValidationError( + missing_required=[], + canon_to_raw={}, + merged_specs={}, + ) + errors = [ + {"row": 1, "check": "dataframe_check", "value": "error1"}, + {"row": 2, "check": "multi_column_check", "value": "error2"}, + ] + result = _format_column_validation_errors("_schema_level", errors, error) + assert len(result) > 0 + assert "File-level validation errors" in result[0] + assert "Column" not in result[0] # Should not say "Column '...'" + + +# ============================================================================ +# Tests for _format_schema_validation_errors +# ============================================================================ + + +def test_format_schema_validation_errors( + error_with_failure_cases: HardValidationError, +) -> None: + """Test formatting schema validation errors.""" + result = _format_schema_validation_errors(error_with_failure_cases) + assert len(result) > 0 + assert any("Student ID" in msg or "Grade" in msg for msg in result) + + +def test_format_schema_validation_errors_empty() -> None: + """Test formatting with no failure cases.""" + error = HardValidationError(failure_cases=[]) + result = _format_schema_validation_errors(error) + assert result == [] + + +def test_format_schema_validation_errors_invalid_cases() -> None: + """Test formatting with invalid failure cases.""" + error = HardValidationError(failure_cases="not_a_list") + result = _format_schema_validation_errors(error) + assert isinstance(result, list) + + +def test_format_schema_validation_errors_deterministic_ordering() -> None: + """Test that schema validation errors are sorted deterministically.""" + error = HardValidationError( + failure_cases=[ + {"column": "zebra", "index": 0, "check": "test", "failure_case": "val"}, + {"column": "alpha", "index": 1, "check": "test", "failure_case": "val"}, + {"index": 2, "check": "test", "failure_case": "val"}, # Schema-level + {"column": "beta", "index": 3, "check": "test", "failure_case": "val"}, + ], + canon_to_raw={"alpha": "Alpha", "beta": "Beta", "zebra": "Zebra"}, + merged_specs={"alpha": {}, "beta": {}, "zebra": {}}, + ) + result1 = _format_schema_validation_errors(error) + result2 = _format_schema_validation_errors(error) + + # Results should be identical (deterministic) + assert result1 == result2 + + # Schema-level should come first, then alphabetical + result_text = "\n".join(result1) + schema_level_pos = result_text.find("File-level") + alpha_pos = result_text.find("Alpha") + beta_pos = result_text.find("Beta") + zebra_pos = result_text.find("Zebra") + + # Schema-level should be first + if schema_level_pos >= 0: + assert schema_level_pos < alpha_pos or alpha_pos < 0 + # Then alphabetical + if alpha_pos >= 0 and beta_pos >= 0: + assert alpha_pos < beta_pos + if beta_pos >= 0 and zebra_pos >= 0: + assert beta_pos < zebra_pos + + +# ============================================================================ +# Tests for format_validation_error (Main Function) +# ============================================================================ + + +def test_format_validation_error_missing_required( + sample_error: HardValidationError, +) -> None: + """Test formatting error with missing required columns.""" + result = format_validation_error(sample_error) + assert "Missing required columns" in result + assert "Student ID" in result + + +def test_format_validation_error_extra_columns() -> None: + """Test formatting error with extra columns.""" + error = HardValidationError(extra_columns=["unknown1", "unknown2"]) + result = format_validation_error(error) + assert "Unexpected columns found" in result + assert "unknown1" in result + + +def test_format_validation_error_failure_cases( + error_with_failure_cases: HardValidationError, +) -> None: + """Test formatting error with failure cases.""" + result = format_validation_error(error_with_failure_cases) + assert "validation errors" in result.lower() or "Row" in result + + +def test_format_validation_error_pii_masking( + error_with_pii: HardValidationError, +) -> None: + """Test PII masking: email is masked; student_id is not PII so its value is shown.""" + result = format_validation_error(error_with_pii) + assert "john.doe@example.com" not in result + assert "masked for privacy" in result + # student_id is a standard de-identified identifier, so its value is not masked + assert "STU-12345-ABCDEF" in result + + +def test_format_validation_error_decode_error() -> None: + """Test formatting decode error.""" + error = HardValidationError( + schema_errors="decode_error", + failure_cases=["UnicodeDecodeError: invalid encoding"], + ) + result = format_validation_error(error) + assert "File encoding error" in result + assert "UTF-8" in result + + +def test_format_validation_error_generic_schema_error() -> None: + """Test formatting generic schema error.""" + error = HardValidationError(schema_errors="Invalid schema format") + result = format_validation_error(error) + assert "Schema validation error" in result + + +def test_format_validation_error_none() -> None: + """Test formatting with None error raises ValueError.""" + with pytest.raises(ValueError, match="cannot be None"): + format_validation_error(None) # type: ignore + + +def test_format_validation_error_invalid_object() -> None: + """Test formatting with invalid error object.""" + # Mock without missing_required attribute and without schema_errors + invalid_error = Mock(spec=[]) # Empty spec means no attributes + result = format_validation_error(invalid_error) # type: ignore + assert "Validation error occurred" in result + + +def test_format_validation_error_empty() -> None: + """Test formatting error with all empty attributes.""" + error = HardValidationError() + result = format_validation_error(error) + # Should return fallback message + assert len(result) > 0 + + +def test_format_validation_error_message_size_limit() -> None: + """Test message size limit enforcement.""" + # Create error with many failure cases + failure_cases = [ + { + "column": f"col_{i}", + "index": i, + "check": "test", + "failure_case": "x" * 100, # Long values + } + for i in range(100) + ] + error = HardValidationError( + failure_cases=failure_cases, + canon_to_raw={f"col_{i}": f"Column {i}" for i in range(100)}, + merged_specs={f"col_{i}": {} for i in range(100)}, + ) + result = format_validation_error(error) + # Message should be within limits (may be truncated if it exceeds) + assert len(result) <= MAX_MESSAGE_LENGTH + 100 # Allow some buffer + # If message is very long, it should either be truncated or contain a notice + # (The current implementation truncates at the section level, so very long messages + # may not show all errors but won't necessarily have a truncation notice unless + # the final result exceeds MAX_MESSAGE_LENGTH) + if len(result) >= MAX_MESSAGE_LENGTH: + assert "truncated" in result.lower() or "size limits" in result.lower() + + +def test_format_validation_error_all_types() -> None: + """Test formatting error with all error types.""" + error = HardValidationError( + missing_required=["col1"], + extra_columns=["col2"], + schema_errors="test_error", + failure_cases=[ + {"column": "col3", "index": 0, "check": "test", "failure_case": "val"} + ], + canon_to_raw={"col1": "Column 1", "col3": "Column 3"}, + merged_specs={"col1": {}, "col3": {}}, + ) + result = format_validation_error(error) + assert "Missing required columns" in result + assert "Unexpected columns" in result + assert "Schema validation error" in result + assert "validation errors" in result.lower() or "Row" in result + + +def test_format_validation_error_handles_exceptions() -> None: + """Test that exceptions in formatting are handled gracefully.""" + # Create error that might cause issues + error = HardValidationError( + missing_required=["col"], + canon_to_raw={"col": object()}, # type: ignore[dict-item] # Invalid type that might cause issues + merged_specs={"col": object()}, # type: ignore[dict-item] # Invalid type + ) + # Should not raise, should return something + result = format_validation_error(error) + assert isinstance(result, str) + assert len(result) > 0 + + +def test_format_validation_error_very_long_values() -> None: + """Test handling of very long values.""" + error = HardValidationError( + failure_cases=[ + { + "column": "col", + "index": 0, + "check": "test", + "failure_case": "x" * 1000, # Very long value + } + ], + canon_to_raw={"col": "Column"}, + merged_specs={"col": {}}, + ) + result = format_validation_error(error) + # Should be truncated + assert len(result) < MAX_MESSAGE_LENGTH + + +def test_format_validation_error_special_characters() -> None: + """Test handling of special characters in values.""" + error = HardValidationError( + failure_cases=[ + { + "column": "col", + "index": 0, + "check": "test", + "failure_case": "value\nwith\nnewlines", + } + ], + canon_to_raw={"col": "Column"}, + merged_specs={"col": {}}, + ) + result = format_validation_error(error) + # Should sanitize newlines + assert "\n" not in result or result.count("\n") < 3 + + +def test_format_validation_error_pii_false_positive_prevention() -> None: + """Test that PII detection doesn't flag false positives like 'course_name'.""" + error = HardValidationError( + failure_cases=[ + { + "column": "course_name", + "index": 0, + "check": "test", + "failure_case": "Math 101", + }, + { + "column": "district_name", + "index": 1, + "check": "test", + "failure_case": "District A", + }, + ], + canon_to_raw={"course_name": "Course Name", "district_name": "District Name"}, + merged_specs={"course_name": {}, "district_name": {}}, + ) + result = format_validation_error(error) + # Values should NOT be masked (not PII) + assert "Math 101" in result + assert "District A" in result + assert "masked for privacy" not in result + + +def test_format_validation_error_pii_true_positive() -> None: + """Test that PII detection correctly flags true positives.""" + error = HardValidationError( + failure_cases=[ + { + "column": "first_name", + "index": 0, + "check": "test", + "failure_case": "John", + }, + { + "column": "email_address", + "index": 1, + "check": "test", + "failure_case": "john@example.com", + }, + ], + canon_to_raw={"first_name": "First Name", "email_address": "Email Address"}, + merged_specs={"first_name": {}, "email_address": {}}, + ) + result = format_validation_error(error) + # Values should be masked (PII) + assert "John" not in result + assert "john@example.com" not in result + assert "masked for privacy" in result + + +@pytest.mark.skipif(not HAS_PANDAS, reason="pandas not available") +def test_format_validation_error_dataframe_failure_cases() -> None: + """Test formatting with DataFrame failure_cases (critical fix).""" + df = pd.DataFrame( + [ + {"column": "grade", "index": 0, "check": "isin", "failure_case": "X"}, + {"column": "grade", "index": 1, "check": "isin", "failure_case": "Y"}, + ] + ) + error = HardValidationError( + failure_cases=df, # DataFrame, not list + canon_to_raw={"grade": "Grade"}, + merged_specs={ + "grade": {"checks": [{"type": "isin", "args": [["A", "B", "C"]]}]} + }, + ) + result = format_validation_error(error) + # Should format errors (not drop them) + assert "Grade" in result + assert "validation errors" in result.lower() or "Row" in result + + +# ============================================================================ +# Integration Tests +# ============================================================================ + + +def _create_complete_error_failure_cases() -> List[dict]: + """Create failure cases for complete error fixture.""" + return [ + { + "column": "student_id", + "index": 0, + "check": "str_length", + "failure_case": "AB", # Too short + }, + { + "column": "email", + "index": 1, + "check": "matches", + "failure_case": "invalid-email", # PII + }, + { + "column": "grade", + "index": 2, + "check": "isin", + "failure_case": "X", + }, + ] + + +def _create_complete_error_mappings() -> tuple[Dict[str, str], Dict[str, str]]: + """Create column mappings for complete error fixture.""" + raw_to_canon = { + "Student ID": "student_id", + "Course Name": "course_name", + "Email": "email", + "Grade": "grade", + } + canon_to_raw = { + "student_id": "Student ID", + "course_name": "Course Name", + "email": "Email", + "grade": "Grade", + } + return raw_to_canon, canon_to_raw + + +def _create_complete_error_specs() -> Dict[str, dict]: + """Create merged specs for complete error fixture.""" + return { + "student_id": { + "description": "Unique student identifier", + "checks": [{"type": "str_length", "kwargs": {"min_value": 3}}], + }, + "course_name": {"description": "Name of the course"}, + "email": { + "description": "Student email", + "checks": [{"type": "matches", "args": [r"^[^@]+@[^@]+\.[^@]+$"]}], + }, + "grade": { + "description": "Course grade", + "checks": [{"type": "isin", "args": [["A", "B", "C", "D", "F"]]}], + }, + } + + +@pytest.fixture +def complete_error() -> HardValidationError: + """Create a complete error with all error types for integration testing.""" + raw_to_canon, canon_to_raw = _create_complete_error_mappings() + return HardValidationError( + missing_required=["student_id", "course_name"], + extra_columns=["unknown_field"], + failure_cases=_create_complete_error_failure_cases(), + raw_to_canon=raw_to_canon, + canon_to_raw=canon_to_raw, + merged_specs=_create_complete_error_specs(), + ) + + +def test_integration_all_error_types_present( + complete_error: HardValidationError, +) -> None: + """Test that all error types are present in formatted output.""" + result = format_validation_error(complete_error) + assert "Missing required columns" in result + assert "Unexpected columns found" in result + assert "validation errors" in result.lower() + + +def test_integration_pii_masking_and_non_pii_display( + complete_error: HardValidationError, +) -> None: + """Test PII masking and non-PII value display in integration.""" + result = format_validation_error(complete_error) + # Check PII is masked + assert "invalid-email" not in result + assert "masked for privacy" in result + # Check non-PII values are shown + assert "X" in result # Grade is not PII + + +def test_integration_user_friendly_column_names( + complete_error: HardValidationError, +) -> None: + """Test that user-friendly column names are used.""" + result = format_validation_error(complete_error) + assert "Student ID" in result + assert "Course Name" in result + assert "Email" in result + + +def test_integration_row_numbering_and_message_size( + complete_error: HardValidationError, +) -> None: + """Test row numbering and message size limits.""" + result = format_validation_error(complete_error) + # Check row numbers are 1-indexed + assert "Row 1" in result or "Row 2" in result or "Row 3" in result + # Check message is reasonable size + assert len(result) < MAX_MESSAGE_LENGTH + + +# ============================================================================ +# Tests for Recently Added Fixes +# ============================================================================ + + +def test_format_extra_columns_reverse_lookup_single() -> None: + """Test extra columns reverse lookup with single raw name.""" + error = HardValidationError( + extra_columns=["student_id"], + raw_to_canon={"Student ID": "student_id"}, # Maps to different canonical + ) + result = _format_extra_columns(error) + assert result is not None + # Should find raw name via reverse lookup + assert "Student ID" in result + + +def test_format_extra_columns_reverse_lookup_multiple() -> None: + """Test extra columns reverse lookup with multiple raw names (ambiguity handling).""" + error = HardValidationError( + extra_columns=["student_id"], + raw_to_canon={ + "Student ID": "student_id", # First occurrence + "StudentID": "student_id", # Second occurrence (same normalized) + "STUDENT-ID": "student_id", # Third occurrence + }, + ) + result = _format_extra_columns(error) + assert result is not None + # Should use first encountered raw name (deterministic) + assert "Student ID" in result + + +def test_format_extra_columns_reverse_lookup_exact_match() -> None: + """Test extra columns reverse lookup prefers exact match.""" + error = HardValidationError( + extra_columns=["student_id"], + raw_to_canon={ + "Student ID": "student_id", # First occurrence + "student_id": "student_id", # Exact match (should be preferred) + }, + ) + result = _format_extra_columns(error) + assert result is not None + # Should prefer exact match + assert "student_id" in result + + +def test_format_extra_columns_reverse_lookup_two_matches() -> None: + """Test extra columns reverse lookup with exactly 2 matches.""" + error = HardValidationError( + extra_columns=["student_id"], + raw_to_canon={ + "Student ID": "student_id", + "StudentID": "student_id", + }, + ) + result = _format_extra_columns(error) + assert result is not None + # Should show both with "or" + assert "Student ID" in result + assert "StudentID" in result + assert " or " in result + + +def test_format_extra_columns_reverse_lookup_three_plus_matches() -> None: + """Test extra columns reverse lookup with 3+ matches.""" + error = HardValidationError( + extra_columns=["student_id"], + raw_to_canon={ + "Student ID": "student_id", + "StudentID": "student_id", + "STUDENT-ID": "student_id", + "student_id_col": "student_id", + }, + ) + result = _format_extra_columns(error) + assert result is not None + # Should show first with count + assert "Student ID" in result + assert "and" in result.lower() + assert "similar" in result.lower() + + +def test_normalize_failure_cases_dataframe_like_no_iterrows() -> None: + """Test normalize_failure_cases with DataFrame-like object without iterrows (behavior-based).""" + + # Mock object with to_dict but no iterrows (behavior-based detection) + class MockDataFrame: + def to_dict(self, orient: Optional[str] = None) -> Any: + if orient == "records": + return [{"column": "col1", "index": 0}] + return {"col1": {0: "val"}} + + mock_df = MockDataFrame() + result = _normalize_failure_cases(mock_df) + assert isinstance(result, list) + assert len(result) == 1 + assert result[0]["column"] == "col1" + + +def test_normalize_failure_cases_dataframe_fallback_to_dict() -> None: + """Test normalize_failure_cases fallback when to_dict(orient='records') fails.""" + + # Mock object where orient='records' fails but to_dict() works + class MockDataFrame: + def to_dict(self, orient: Optional[str] = None) -> Any: + if orient == "records": + raise ValueError("orient='records' not supported") + # Return dict of dicts format + return {"col1": {0: "val1"}, "col2": {0: "val2"}} + + mock_df = MockDataFrame() + result = _normalize_failure_cases(mock_df) + # Should handle fallback gracefully + assert isinstance(result, list) + + +def test_normalize_row_index_non_integer_float() -> None: + """Test normalize_row_index with non-integer float (should return sanitized string).""" + from .validation_error_formatter import _normalize_row_index + + result = _normalize_row_index(5.7) + # Should return sanitized string, not misleading conversion + assert isinstance(result, str) + assert "5.7" in result or "5" in result + + +def test_normalize_row_index_whole_number_float() -> None: + """Test normalize_row_index with whole number float (should convert correctly).""" + from .validation_error_formatter import _normalize_row_index + + result = _normalize_row_index(5.0) + # Should convert to int and add 1 (0-indexed to 1-indexed) + assert result == 6 + + result = _normalize_row_index(0.0) + assert result == 1 + + +def test_format_isin_error_numeric_tie_breaking() -> None: + """Test isin error formatting with numeric tie-breaking (01, 1, 1.0).""" + from .validation_error_formatter import _format_isin_error + + check_spec = {"type": "isin", "args": [["01", "1", "1.0", "2", "10"]]} + result1 = _format_isin_error(check_spec) + result2 = _format_isin_error(check_spec) + + # Should be deterministic (same result each time) + assert result1 == result2 + + # Should show values in sorted order + assert "01" in result1 + assert "1" in result1 + assert "1.0" in result1 + # Order should be deterministic (numeric sort with string tie-breaker) + idx_01 = result1.find("01") + idx_1 = result1.find("1") + idx_10 = result1.find("10") + idx_2 = result1.find("2") + # Should be in order: 01, 1, 1.0, 2, 10 (or similar deterministic order) + assert idx_01 < idx_10 + assert idx_1 < idx_10 + assert idx_2 < idx_10 + + +def test_format_isin_error_nan_inf_handling() -> None: + """Test isin error formatting with NaN/inf values (should go to non-numeric bucket).""" + from .validation_error_formatter import _format_isin_error + + check_spec = {"type": "isin", "args": [["2", "NaN", "10", "inf", "1"]]} + result = _format_isin_error(check_spec) + + # Should handle NaN/inf gracefully + assert "NaN" in result or "inf" in result + # Numeric values should be sorted + assert "1" in result + assert "2" in result + assert "10" in result + + +def test_format_isin_error_set_input() -> None: + """Test isin error formatting with set input (unstable ordering → stable output).""" + from .validation_error_formatter import _format_isin_error + + # Set has unstable ordering + check_spec = {"type": "isin", "args": [{"A", "B", "C", "D"}]} + result1 = _format_isin_error(check_spec) + result2 = _format_isin_error(check_spec) + + # Should be deterministic (same result each time) + assert result1 == result2 + + +def test_format_isin_error_mixed_numeric_non_numeric() -> None: + """Test isin error formatting with mixed numeric and non-numeric values.""" + from .validation_error_formatter import _format_isin_error + + check_spec = {"type": "isin", "args": [["2", "A", "10", "1", "B"]]} + result = _format_isin_error(check_spec) + + # Numeric should come first, sorted: 1, 2, 10 + # Then non-numeric, sorted: A, B + idx_1 = result.find("1") + idx_2 = result.find("2") + idx_10 = result.find("10") + idx_a = result.find("A") + idx_b = result.find("B") + + # Numeric before non-numeric + assert idx_1 < idx_a + assert idx_2 < idx_a + assert idx_10 < idx_a + # Numeric sorted + assert idx_1 < idx_2 < idx_10 + # Non-numeric sorted + assert idx_a < idx_b + + +def test_format_missing_required_empty_display() -> None: + """Test format_missing_required with all invalid entries (empty display).""" + error = HardValidationError( + missing_required=[None, "", 123, object()], # type: ignore[list-item] # All invalid + canon_to_raw={}, + merged_specs={}, + ) + result = _format_missing_required(error) + assert result is not None + # Should return generic message, not empty list + assert "Missing required columns detected" in result + assert ( + "These columns must be present" in result or "check your file" in result.lower() + ) diff --git a/src/webapp/validation_extension.py b/src/webapp/validation_extension.py deleted file mode 100644 index b0b97d34..00000000 --- a/src/webapp/validation_extension.py +++ /dev/null @@ -1,155 +0,0 @@ -"""Validation extension.""" - -from typing import Union, List, Dict, Optional, Any -import pandas as pd -import tempfile -import os -import json - -from .validation import validate_dataset, normalize_col, HardValidationError - - -def load_json(path: str) -> Any: - """Load JSON from a file, returning {} on failure.""" - try: - with open(path, "r") as f: - return json.load(f) - except (FileNotFoundError, json.JSONDecodeError): - return {} - - -def infer_column_schema(series: pd.Series, cate_threshold: int = 10) -> dict: - """ - Infer a minimal Pandera-style schema for a pandas Series. - Categorical-like columns are marked as 'category' but NOT constrained to a fixed set of values. - """ - - non_null = series.dropna() - has_nulls = bool(series.isna().any()) - uniques = non_null.unique().tolist() - - checks: List[Dict[str, Any]] = [] - - # candidate for categorical, but relaxed (no fixed categories) - if 1 < len(uniques) <= cate_threshold: - return { - "dtype": "category", - "coerce": True, - "nullable": True, - "required": True, - "aliases": [], - "checks": [], # no 'isin' constraint - } - - # numeric / datetime / bool / string fallback - dt = series.dtype - if pd.api.types.is_integer_dtype(dt): - dtype = "Int64" if has_nulls else "int64" - checks = [] - elif pd.api.types.is_float_dtype(dt): - dtype = "float64" - checks = [] - elif pd.api.types.is_bool_dtype(dt): - dtype = "boolean" - checks = [] - elif pd.api.types.is_datetime64_any_dtype(dt): - dtype = "datetime64[ns]" - checks = [] - else: - dtype = "string" - checks = [{"type": "str_length", "kwargs": {"min_value": 1}}] - - return { - "dtype": dtype, - "coerce": True, - "nullable": True, - "required": True, - "aliases": [], - "checks": checks, - } - - -def generate_extension_schema( - df: Union[pd.DataFrame, str], - models: Union[str, List[str]], - institution_id: str, - base_schema: Dict, # <- reference only, not mutated - existing_extension: Optional[Dict] = None, # <- merged into/returned -) -> Dict: - """ - - Use validate_dataset(...) with base_schema (+ existing_extension if provided) - to detect columns not represented there. - - Infer specs for those "extra" columns and add only those to the extension. - - Return the extension dict (no writes, base_schema untouched). - """ - # Load/normalize DF (keep a path for validate_dataset which expects a filename) - orig_path: Optional[str] = None - if isinstance(df, str): - orig_path = df - df = pd.read_csv(df) - df = df.rename(columns=lambda c: normalize_col(c)) - - # Ensure validate_dataset gets a path - tmp_path = None - data_path = orig_path - if data_path is None: - tmp = tempfile.NamedTemporaryFile(suffix=".csv", delete=False) - df.to_csv(tmp.name, index=False) - tmp_path = tmp.name - data_path = tmp_path - - # Validate to discover extras (not in base or existing extension) - extras: List[str] = [] - try: - validate_dataset( - filename=data_path, - base_schema=base_schema, - ext_schema=existing_extension, # columns already in extension won't be "extra" - models=models, - institution_id=institution_id, - ) - except HardValidationError as e: - extras = e.extra_columns or [] - finally: - if tmp_path: - try: - os.unlink(tmp_path) - except OSError: - pass - - # Nothing new to add - if not extras: - return existing_extension or { - "version": base_schema.get("version", "1.0.0"), - "institutions": {institution_id: {"data_models": {}}}, - } - - # Keep only extras actually present in the (normalized) DF - extras = [c for c in extras if c in df.columns] - - # Infer minimal specs for each extra column - inferred = {col: infer_column_schema(df[col]) for col in extras} - - # Start from provided extension (or a fresh skeleton); base_schema is NOT modified - extension = existing_extension or { - "version": base_schema.get("version", "1.0.0"), - "institutions": {institution_id: {"data_models": {}}}, - } - - inst_block = ( - extension.setdefault("institutions", {}) - .setdefault(institution_id, {}) - .setdefault("data_models", {}) - ) - - model_list = [models] if isinstance(models, str) else list(models or []) - - # Merge ONLY the inferred extras into the extension - for model in model_list: - cols_block = inst_block.setdefault(model, {}).setdefault("columns", {}) - # don’t overwrite anything that might already exist in extension - for col, spec in inferred.items(): - if col not in cols_block: - cols_block[col] = spec - - return extension diff --git a/src/webapp/validation_pdp_edvise.py b/src/webapp/validation_pdp_edvise.py new file mode 100644 index 00000000..fe0cdf6f --- /dev/null +++ b/src/webapp/validation_pdp_edvise.py @@ -0,0 +1,283 @@ +"""PDP Pandera schemas re-exported from edvise for upload validation. + +Imports ``RawPDPCohortDataSchema`` and ``RawPDPCourseDataSchema`` so PDP uploads use +the same column and type rules as edvise pipeline audits. Cohort row transforms run +in ``validation.py`` (optional converter) and can differ from batch ``dataio`` hooks; +this module only supplies schema classes and helpers. + +Non-PDP Edvise institutions use JSON-based validation elsewhere (different columns). + +Requires the ``edvise`` package (see pyproject.toml). +""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast + +if TYPE_CHECKING: + from .validation import HardValidationError + +import pandas as pd +from pandera.errors import SchemaError, SchemaErrors + +from edvise.data_audit.schemas.raw_cohort import RawPDPCohortDataSchema +from edvise.data_audit.schemas.raw_course import RawPDPCourseDataSchema + +logger = logging.getLogger(__name__) + + +def _get_hard_validation_error_class() -> type: + """Import HardValidationError lazily to avoid circular import with validation.""" + from .validation import HardValidationError + + return HardValidationError + + +# Institution namespaces that use edvise repo schemas (RawPDPCohortDataSchema / RawPDPCourseDataSchema). +# Only PDP uses repo validation; Edvise has a different shape and uses JSON-based validation only. +PDP_EDVISE_NAMESPACES = frozenset({"pdp"}) + + +def rename_pdp_dataframe_to_repo_schema( + df: pd.DataFrame, + canon_to_raw: Dict[str, str], + model_list: Optional[List[str]] = None, +) -> tuple[pd.DataFrame, Dict[str, str]]: + """ + Ensure PDP DataFrame column names and shape match edvise repo schemas. + + Uploads are expected to have all required columns (e.g. per-year credit columns + for cohort). Extension + base merge use repo-shaped canonicals. + - Cohort only: if program_of_study_year_1 is missing, copy from program_of_study_term_1. + + Returns: + (df, display_canon_to_raw): DataFrame (repo-shaped) and repo column name -> raw header for errors. + """ + out = df.copy() + models = {str(m).strip().upper() for m in (model_list or []) if m} + is_cohort = "STUDENT" in models + + display_canon_to_raw = dict(canon_to_raw) + + if is_cohort: + if ( + "program_of_study_term_1" in out.columns + and "program_of_study_year_1" not in out.columns + ): + out["program_of_study_year_1"] = out["program_of_study_term_1"].copy() + display_canon_to_raw["program_of_study_year_1"] = display_canon_to_raw.get( + "program_of_study_term_1", "program_of_study_year_1" + ) + + return out, display_canon_to_raw + + +def is_edvise_schema_available() -> bool: + """Return True; edvise is required for PDP validation and is always available when this module loads.""" + return True + + +def get_edvise_schema_for_upload( + institution_id: str, + model_list: Optional[List[str]] = None, +) -> Optional[type]: + """ + Return the edvise repo schema class for this upload, or None. + + Use this as the single check: when not None, run that schema and skip JSON + Pandera. Only PDP uses repo validation (edvise package required); Edvise + institution has a different shape and uses JSON validation only. + + Args: + institution_id: Schema namespace (e.g. "pdp", "edvise", or "legacy"). Only "pdp" uses repo schema. + model_list: Inferred model names from filename (e.g. ["STUDENT"], ["COURSE"]). May be None. + + Returns: + RawPDPCohortDataSchema for PDP+STUDENT, RawPDPCourseDataSchema for PDP+COURSE, + or None (non-PDP or multi-model; use JSON-based validation). + """ + if not institution_id or not isinstance(institution_id, str): + return None + if institution_id not in PDP_EDVISE_NAMESPACES: + return None + if model_list is not None and not isinstance(model_list, list): + return None + model_set = {str(m).strip().upper() for m in (model_list or []) if m} + if model_set == {"STUDENT"}: + return cast(Optional[type], RawPDPCohortDataSchema) + if model_set == {"COURSE"}: + return cast(Optional[type], RawPDPCourseDataSchema) + return None + + +def should_use_edvise_schema( + institution_id: str, + model_list: List[str], +) -> bool: + """True when upload should use edvise schema (same condition as get_edvise_schema_for_upload).""" + return get_edvise_schema_for_upload(institution_id, model_list) is not None + + +def get_edvise_schema_for_models(model_list: List[str]) -> Optional[type]: + """Return edvise schema for single-model list (pdp namespace). For tests/callers that don't have institution_id.""" + return get_edvise_schema_for_upload("pdp", model_list) + + +def _normalize_failure_cases_for_formatter(failure_cases: Any) -> List[Dict[str, Any]]: + """ + Convert Pandera failure_cases to a list of dicts with keys the formatter expects. + + Formatter expects each record to have: column, index, check, failure_case. + Pandera may use different column names (e.g. schema_context, check_number); + we keep only the keys needed for formatting. + """ + if failure_cases is None: + return [] + records: List[Dict[str, Any]] = [] + if hasattr(failure_cases, "to_dict"): + try: + raw_records = failure_cases.to_dict(orient="records") + except (TypeError, ValueError): + return [] + if not isinstance(raw_records, list): + return [] + for row in raw_records: + if not isinstance(row, dict): + continue + # Pandera uses 'failure_case' (singular); some versions may differ. + normalized = { + "column": row.get("column"), + "index": row.get("index", -1), + "check": row.get("check", "validation"), + "failure_case": row.get( + "failure_case", row.get("failure_cases", "N/A") + ), + } + records.append(normalized) + return records + if isinstance(failure_cases, list): + for item in failure_cases: + if isinstance(item, dict): + normalized = { + "column": item.get("column"), + "index": item.get("index", -1), + "check": item.get("check", "validation"), + "failure_case": item.get( + "failure_case", item.get("failure_cases", "N/A") + ), + } + records.append(normalized) + return records + + +def _extract_missing_required_from_pandera_error(err: Any) -> List[str]: + """ + Derive missing required column names from a Pandera SchemaErrors exception. + + When the edvise schema requires columns not present in the DataFrame, + Pandera may report them in failure_cases with a check that indicates + missing column (e.g. "column_in_dataframe"). Only rows whose check + suggests a missing-column failure are included; we do not treat + value-check failures (e.g. wrong category) as missing columns. + """ + missing: List[str] = [] + if not hasattr(err, "failure_cases") or err.failure_cases is None: + return missing + try: + df = err.failure_cases + if hasattr(df, "columns") and "column" in df.columns: + for _, row in df.iterrows(): + col = row.get("column") + check = str(row.get("check", "")) + if col and isinstance(col, str) and col not in missing: + if "column" in check.lower() or "missing" in check.lower(): + missing.append(col) + except (AttributeError, TypeError, ValueError) as e: + logger.debug("Could not extract missing_required from Pandera error: %s", e) + return missing + + +def _convert_schema_errors_to_hard_validation_error( + err: Any, + raw_to_canon: Dict[str, str], + canon_to_raw: Dict[str, str], + merged_specs: Dict[str, dict], +) -> "HardValidationError": + """ + Convert a Pandera SchemaErrors (or single SchemaError) to HardValidationError. + + Normalizes failure_cases to the shape the validation_error_formatter expects + and derives missing_required when the failure is due to missing columns. + + Returns: + HardValidationError with normalized failure_cases, optional missing_required, + and schema_errors, for the formatter to produce human-readable messages. + """ + failure_cases = getattr(err, "failure_cases", None) + normalized_failure_cases = _normalize_failure_cases_for_formatter(failure_cases) + missing_required = _extract_missing_required_from_pandera_error(err) + schema_errors = getattr(err, "schema_errors", None) + if schema_errors is None: + schema_errors = str(err) if err else None + logger.error( + "PDP/Edvise Schema (ES) validation failed: missing_required=%s, failure_cases_count=%s", + missing_required, + len(normalized_failure_cases), + ) + HardValidationErrorClass = _get_hard_validation_error_class() + return cast( + "HardValidationError", + HardValidationErrorClass( + missing_required=missing_required if missing_required else None, + extra_columns=None, + schema_errors=schema_errors, + failure_cases=normalized_failure_cases, + raw_to_canon=raw_to_canon, + canon_to_raw=canon_to_raw, + merged_specs=merged_specs, + ), + ) + + +def validate_dataframe_with_edvise_schema( + df: pd.DataFrame, + schema_class: type, + raw_to_canon: Dict[str, str], + canon_to_raw: Dict[str, str], + merged_specs: Dict[str, dict], +) -> None: + """ + Validate a DataFrame with the given edvise schema (cohort or course). + + Uses the same schemas as the edvise repo so rules are identical everywhere. + Raises HardValidationError with normalized failure_cases and optional + missing_required when validation fails. + + Args: + df: DataFrame with canonical column names (from header pass + read). + schema_class: RawPDPCohortDataSchema or RawPDPCourseDataSchema. + raw_to_canon: Mapping from raw file headers to canonical names. + canon_to_raw: Mapping from canonical names to raw file headers. + merged_specs: Merged JSON spec for formatter context. + + Raises: + HardValidationError: When schema validation fails (missing columns or row-level checks). + """ + HardValidationError = _get_hard_validation_error_class() + if df is None or df.empty: + raise HardValidationError( + schema_errors="PDP/Edvise Schema (ES) validation failed: empty or missing DataFrame", + raw_to_canon=raw_to_canon, + canon_to_raw=canon_to_raw, + merged_specs=merged_specs, + ) + try: + # Lazy=True so all failures are collected in one SchemaErrors. + schema_class.validate(df, lazy=True) # type: ignore[attr-defined] + except (SchemaErrors, SchemaError) as e: + # Pandera raises SchemaErrors for lazy validation; single failure may raise SchemaError. + hard = _convert_schema_errors_to_hard_validation_error( + e, raw_to_canon, canon_to_raw, merged_specs + ) + raise hard from e diff --git a/src/webapp/validation_pdp_edvise_test.py b/src/webapp/validation_pdp_edvise_test.py new file mode 100644 index 00000000..d560d690 --- /dev/null +++ b/src/webapp/validation_pdp_edvise_test.py @@ -0,0 +1,322 @@ +"""Unit tests for PDP/Edvise Schema (ES) validation (same validation as edvise repo).""" + +import pandas as pd +from typing import Any, Dict, cast +from unittest.mock import MagicMock + +import pytest + +from src.webapp.validation import HardValidationError +from src.webapp.validation_pdp_edvise import ( + PDP_EDVISE_NAMESPACES, + _extract_missing_required_from_pandera_error, + _normalize_failure_cases_for_formatter, + get_edvise_schema_for_models, + get_edvise_schema_for_upload, + is_edvise_schema_available, + rename_pdp_dataframe_to_repo_schema, + should_use_edvise_schema, + validate_dataframe_with_edvise_schema, +) + + +def test_should_use_edvise_schema_returns_false_for_empty_institution_id() -> None: + """Empty or invalid institution_id should not use edvise schema.""" + assert should_use_edvise_schema("", ["STUDENT"]) is False + assert should_use_edvise_schema(" ", ["COURSE"]) is False + + +def test_should_use_edvise_schema_returns_false_for_custom_namespace() -> None: + """Custom institution UUID should not use edvise schema.""" + assert ( + should_use_edvise_schema("a1b2c3d4-e5f6-7890-abcd-ef1234567890", ["STUDENT"]) + is False + ) + + +def test_should_use_edvise_schema_returns_false_for_multi_model() -> None: + """Multiple models (STUDENT and COURSE) should not use edvise schema.""" + assert should_use_edvise_schema("pdp", ["STUDENT", "COURSE"]) is False + assert should_use_edvise_schema("edvise", ["COURSE", "STUDENT"]) is False + + +def test_should_use_edvise_schema_returns_false_for_other_models() -> None: + """SEMESTER or other model alone should not use edvise schema.""" + assert should_use_edvise_schema("pdp", ["SEMESTER"]) is False + assert should_use_edvise_schema("pdp", []) is False + + +def test_should_use_edvise_schema_behavior_for_pdp_single_model() -> None: + """For pdp with single STUDENT or COURSE, edvise schema is always used (edvise required).""" + assert should_use_edvise_schema("pdp", ["STUDENT"]) is True + assert should_use_edvise_schema("pdp", ["COURSE"]) is True + assert is_edvise_schema_available() is True + + +def test_should_use_edvise_schema_edvise_namespace_uses_json_validation() -> None: + """edvise namespace does not use repo schema; uses JSON-based validation (different shape).""" + assert should_use_edvise_schema("edvise", ["STUDENT"]) is False + assert should_use_edvise_schema("edvise", ["COURSE"]) is False + + +def test_should_use_edvise_schema_normalizes_model_names_to_uppercase() -> None: + """Lowercase or mixed-case model names are normalized so single STUDENT/COURSE still match.""" + assert should_use_edvise_schema("pdp", ["student"]) is True + assert should_use_edvise_schema("pdp", ["course"]) is True + assert should_use_edvise_schema("pdp", ["Student"]) is True + + +def test_get_edvise_schema_for_models_returns_none_for_multi_model() -> None: + """Multiple models should return None.""" + assert get_edvise_schema_for_models(["STUDENT", "COURSE"]) is None + + +def test_get_edvise_schema_for_models_returns_none_for_empty() -> None: + """Empty model list should return None.""" + assert get_edvise_schema_for_models([]) is None + + +def test_get_edvise_schema_for_models_returns_none_for_other_model() -> None: + """SEMESTER alone should return None.""" + assert get_edvise_schema_for_models(["SEMESTER"]) is None + + +def test_get_edvise_schema_for_models_returns_class_when_available() -> None: + """STUDENT returns cohort schema, COURSE returns course schema (edvise required).""" + cohort_schema = get_edvise_schema_for_models(["STUDENT"]) + course_schema = get_edvise_schema_for_models(["COURSE"]) + assert cohort_schema is not None + assert course_schema is not None + assert cohort_schema.__name__ == "RawPDPCohortDataSchema" + assert course_schema.__name__ == "RawPDPCourseDataSchema" + + +def test_get_edvise_schema_for_models_normalizes_lowercase_model_names() -> None: + """Lowercase model names are normalized so get_edvise_schema_for_models still returns schema.""" + cohort = get_edvise_schema_for_models(["student"]) + course = get_edvise_schema_for_models(["course"]) + assert cohort is not None and cohort.__name__ == "RawPDPCohortDataSchema" + assert course is not None and course.__name__ == "RawPDPCourseDataSchema" + + +def test_normalize_failure_cases_for_formatter_returns_empty_for_none() -> None: + """None input should return empty list.""" + assert _normalize_failure_cases_for_formatter(None) == [] + + +def test_normalize_failure_cases_for_formatter_keeps_expected_keys() -> None: + """Output records should have column, index, check, failure_case.""" + mock_df = MagicMock() + mock_df.to_dict.return_value = [ + {"column": "cohort_term", "index": 0, "check": "isin", "failure_case": "Fall"}, + {"column": "gpa", "index": 2, "check": "ge", "failure_case": 5.0}, + ] + result = _normalize_failure_cases_for_formatter(mock_df) + assert len(result) == 2 + for record in result: + assert "column" in record + assert "index" in record + assert "check" in record + assert "failure_case" in record + assert result[0]["column"] == "cohort_term" + assert result[0]["failure_case"] == "Fall" + assert result[1]["index"] == 2 + + +def test_normalize_failure_cases_for_formatter_handles_failure_cases_key() -> None: + """Some Pandera versions may use failure_cases (plural); we normalize to failure_case.""" + mock_df = MagicMock() + mock_df.to_dict.return_value = [ + {"column": "x", "index": 0, "check": "gt", "failure_cases": 10}, + ] + result = _normalize_failure_cases_for_formatter(mock_df) + assert len(result) == 1 + assert result[0]["failure_case"] == 10 + + +def test_extract_missing_required_returns_empty_for_none_failure_cases() -> None: + """When failure_cases is None, return empty list.""" + err = MagicMock() + err.failure_cases = None + assert _extract_missing_required_from_pandera_error(err) == [] + + +def test_extract_missing_required_does_not_treat_value_checks_as_missing() -> None: + """Value-check failures (e.g. isin) must not be reported as missing_required.""" + err = MagicMock() + err.failure_cases = pd.DataFrame( + [ + { + "column": "cohort_term", + "check": "isin", + "index": 0, + "failure_case": "Fall", + }, + ] + ) + assert _extract_missing_required_from_pandera_error(err) == [] + + +def test_extract_missing_required_includes_only_missing_column_checks() -> None: + """Only rows with check indicating missing column are returned.""" + err = MagicMock() + err.failure_cases = pd.DataFrame( + [ + {"column": "cohort_term", "check": "isin", "index": 0}, + {"column": "other_col", "check": "column_in_dataframe", "index": -1}, + ] + ) + result = _extract_missing_required_from_pandera_error(err) + assert result == ["other_col"] + + +def test_get_edvise_schema_for_upload_single_entry_point() -> None: + """get_edvise_schema_for_upload is the single check: None = use JSON path, else run repo schema (PDP only).""" + assert get_edvise_schema_for_upload("", ["STUDENT"]) is None + assert get_edvise_schema_for_upload("pdp", ["STUDENT", "COURSE"]) is None + assert get_edvise_schema_for_upload("edvise", ["COURSE"]) is None + assert get_edvise_schema_for_upload("edvise", ["STUDENT"]) is None + assert get_edvise_schema_for_upload("pdp", ["STUDENT"]) is not None + assert get_edvise_schema_for_upload("pdp", ["COURSE"]) is not None + assert get_edvise_schema_for_upload("other-uuid", ["STUDENT"]) is None + + +def test_get_edvise_schema_for_upload_rejects_non_list_model_list() -> None: + """When model_list is not a list (e.g. wrong type), return None to fall back to JSON validation.""" + assert ( + get_edvise_schema_for_upload("pdp", None) is None + ) # None is allowed, treated as [] + # Intentionally pass wrong types to assert runtime rejection: + assert get_edvise_schema_for_upload("pdp", cast(Any, "STUDENT")) is None + assert get_edvise_schema_for_upload("pdp", cast(Any, {"STUDENT"})) is None + + +def test_pdp_edvise_namespaces_pdp_only_uses_repo_schema() -> None: + """Only PDP uses edvise repo schema; Edvise has a different shape and uses JSON validation.""" + assert "pdp" in PDP_EDVISE_NAMESPACES + assert "edvise" not in PDP_EDVISE_NAMESPACES + assert len(PDP_EDVISE_NAMESPACES) == 1 + + +# --------------------------------------------------------------------------- # +# rename_pdp_dataframe_to_repo_schema +# --------------------------------------------------------------------------- # + + +def test_rename_pdp_dataframe_to_repo_schema_program_of_study_year_1_fallback() -> None: + """Cohort: when program_of_study_year_1 is missing, copy from program_of_study_term_1.""" + df = pd.DataFrame( + { + "program_of_study_term_1": ["230101", "261504"], + "cohort_term": ["FALL", "SPRING"], + } + ) + canon_to_raw = { + "program_of_study_term_1": "Program of Study Term 1", + "cohort_term": "Cohort Term", + } + out_df, display_canon_to_raw = rename_pdp_dataframe_to_repo_schema( + df, canon_to_raw, model_list=["STUDENT"] + ) + assert "program_of_study_year_1" in out_df.columns + assert list(out_df["program_of_study_year_1"]) == ["230101", "261504"] + assert ( + display_canon_to_raw.get("program_of_study_year_1") == "Program of Study Term 1" + ) + + +def test_rename_pdp_dataframe_to_repo_schema_cohort_has_both_unchanged() -> None: + """Cohort with both program_of_study columns leaves year_1 unchanged.""" + df = pd.DataFrame( + { + "program_of_study_term_1": ["230101"], + "program_of_study_year_1": ["261504"], + } + ) + canon_to_raw = { + "program_of_study_term_1": "Term 1", + "program_of_study_year_1": "Year 1", + } + out_df, display_canon_to_raw = rename_pdp_dataframe_to_repo_schema( + df, canon_to_raw, model_list=["STUDENT"] + ) + assert list(out_df["program_of_study_year_1"]) == ["261504"] + assert display_canon_to_raw["program_of_study_year_1"] == "Year 1" + + +def test_rename_pdp_dataframe_to_repo_schema_course_unchanged() -> None: + """COURSE model: no fallback; DataFrame and display mapping unchanged.""" + df = pd.DataFrame({"course_id": ["C1"], "credits": [3]}) + canon_to_raw = {"course_id": "Course ID", "credits": "Credits"} + out_df, display_canon_to_raw = rename_pdp_dataframe_to_repo_schema( + df, canon_to_raw, model_list=["COURSE"] + ) + assert list(out_df.columns) == ["course_id", "credits"] + assert "program_of_study_year_1" not in out_df.columns + assert display_canon_to_raw == canon_to_raw + + +# --------------------------------------------------------------------------- # +# validate_dataframe_with_edvise_schema +# --------------------------------------------------------------------------- # + + +def test_validate_dataframe_with_edvise_schema_empty_raises() -> None: + """Empty or missing DataFrame raises HardValidationError with expected message.""" + empty_df = pd.DataFrame() + canon_to_raw: Dict[str, str] = {} + raw_to_canon: Dict[str, str] = {} + merged_specs: Dict[str, Any] = {} + with pytest.raises(HardValidationError, match="empty or missing DataFrame"): + validate_dataframe_with_edvise_schema( + empty_df, + type("MockSchema", (), {}), + raw_to_canon, + canon_to_raw, + merged_specs, + ) + + +def test_validate_dataframe_with_edvise_schema_invalid_raises_hard_validation_error() -> ( + None +): + """Pandera schema failure is converted to HardValidationError with failure_cases or missing_required.""" + from edvise.data_audit.schemas.raw_cohort import RawPDPCohortDataSchema + + # Minimal columns; schema will fail (missing required columns and/or value checks). + df = pd.DataFrame( + { + "institution_id": [123], + "cohort": ["2016-17"], + "student_guid": ["abc"], + "cohort_term": ["INVALID_TERM"], + } + ) + raw_to_canon = { + "iid": "institution_id", + "c": "cohort", + "g": "student_guid", + "ct": "cohort_term", + } + canon_to_raw = { + "institution_id": "iid", + "cohort": "c", + "student_guid": "g", + "cohort_term": "ct", + } + merged_specs: Dict[str, Any] = {} + + with pytest.raises(HardValidationError) as exc_info: + validate_dataframe_with_edvise_schema( + df, + RawPDPCohortDataSchema, + raw_to_canon, + canon_to_raw, + merged_specs, + ) + err = exc_info.value + assert ( + getattr(err, "failure_cases", None) is not None + or getattr(err, "schema_errors", None) + or getattr(err, "missing_required", None) is not None + ) diff --git a/src/webapp/validation_pdp_read_path_test.py b/src/webapp/validation_pdp_read_path_test.py new file mode 100644 index 00000000..a7dab60b --- /dev/null +++ b/src/webapp/validation_pdp_read_path_test.py @@ -0,0 +1,406 @@ +""" +Tests for the PDP branch of validation (edvise ``read_raw_pdp_*`` integration). + +Covers routing from ``validate_file_reader``, cohort/course converter wiring, and errors. +""" + +import io +from pathlib import Path +from typing import Any, cast +from unittest.mock import patch + +import pandas as pd +import pytest +from pandera.errors import SchemaErrors + + +from src.webapp.validation import ( + HardValidationError, + _path_for_edvise_read, + _read_pdp_course_edvise, + _validate_pdp_with_edvise_read, + validate_file_reader, +) + + +# --------------------------------------------------------------------------- # +# PDP path routing (validate_file_reader calls _validate_pdp_with_edvise_read) +# --------------------------------------------------------------------------- # + + +def test_validate_file_reader_pdp_student_calls_edvise_read_path( + tmp_path: Path, +) -> None: + """When institution_id is pdp and allowed_schema is [STUDENT], PDP edvise-read path is used.""" + csv_path = tmp_path / "cohort.csv" + pd.DataFrame({"x": [1]}).to_csv(csv_path, index=False) + + with ( + patch( + "src.webapp.validation._compute_model_list_and_merged_specs", + return_value=( + ["STUDENT"], + {"student_id": {"dtype": "string", "required": True}}, + ), + ), + patch( + "src.webapp.validation.pdp_edvise.get_edvise_schema_for_upload", + return_value=object(), # non-None so PDP path is taken + ), + patch( + "src.webapp.validation._validate_pdp_with_edvise_read", + return_value={ + "validation_status": "passed", + "schemas": ["STUDENT"], + "missing_optional": [], + "unknown_extra_columns": [], + "normalized_df": pd.DataFrame({"student_id": ["s1"]}), + }, + ) as mock_pdp, + ): + result = validate_file_reader( + str(csv_path), + ["STUDENT"], + base_schema={"base": {"data_models": {}}}, + inst_schema={"institutions": {"pdp": {"data_models": {}}}}, + institution_id="pdp", + ) + assert result["validation_status"] == "passed" + assert result["schemas"] == ["STUDENT"] + assert result["normalized_df"] is not None + assert list(result["normalized_df"]["student_id"]) == ["s1"] + mock_pdp.assert_called_once() + # _validate_pdp_with_edvise_read(filename, enc, model_list, institution_id) – positional + call_args = mock_pdp.call_args[0] + assert call_args[2] == ["STUDENT"] + assert call_args[3] == "pdp" + + +def test_validate_file_reader_pdp_course_calls_edvise_read_path(tmp_path: Path) -> None: + """When institution_id is pdp and allowed_schema is [COURSE], PDP edvise-read path is used.""" + csv_path = tmp_path / "course.csv" + pd.DataFrame({"y": [1]}).to_csv(csv_path, index=False) + + with ( + patch( + "src.webapp.validation._compute_model_list_and_merged_specs", + return_value=( + ["COURSE"], + {"course_id": {"dtype": "string", "required": True}}, + ), + ), + patch( + "src.webapp.validation.pdp_edvise.get_edvise_schema_for_upload", + return_value=object(), + ), + patch( + "src.webapp.validation._validate_pdp_with_edvise_read", + return_value={ + "validation_status": "passed", + "schemas": ["COURSE"], + "missing_optional": [], + "unknown_extra_columns": [], + "normalized_df": pd.DataFrame({"course_id": ["c1"]}), + }, + ) as mock_pdp, + ): + result = validate_file_reader( + str(csv_path), + ["COURSE"], + base_schema={"base": {"data_models": {}}}, + inst_schema={"institutions": {"pdp": {"data_models": {}}}}, + institution_id="pdp", + ) + assert result["validation_status"] == "passed" + assert result["schemas"] == ["COURSE"] + mock_pdp.assert_called_once() + assert mock_pdp.call_args[0][2] == ["COURSE"] + + +# --------------------------------------------------------------------------- # +# _path_for_edvise_read +# --------------------------------------------------------------------------- # + + +def test_path_for_edvise_read_with_path_yields_same_path(tmp_path: Path) -> None: + """When filename is a path, context manager yields that path (no temp file).""" + path = tmp_path / "data.csv" + path.write_text("a,b\n1,2") + with _path_for_edvise_read(str(path), "utf-8") as resolved: + assert resolved == str(path) + assert Path(resolved).exists() + + +def test_path_for_edvise_read_with_file_like_yields_temp_path_and_cleans_up() -> None: + """When filename is file-like, yields path to temp file; temp file is removed on exit.""" + content = "col1,col2\n1,2" + stream = io.StringIO(content) + with _path_for_edvise_read(stream, "utf-8") as resolved: + assert Path(resolved).exists() + assert Path(resolved).read_text() == content + temp_path = Path(resolved) + assert not temp_path.exists() + + +def test_path_for_edvise_read_file_like_read_failure_raises_hard_validation_error() -> ( + None +): + """When file-like read() raises, HardValidationError is raised with context.""" + + # Use a real file-like that is not str/PathLike so we hit the read() path + class BrokenReader(io.BytesIO): + def read(self, *args: object, **kwargs: object) -> bytes: + raise OSError("read failed") + + broken = BrokenReader(b"x") + with pytest.raises(HardValidationError, match="Could not read file") as exc_info: + with _path_for_edvise_read(broken, "utf-8") as _: + pass + assert exc_info.value.schema_errors is not None + assert "read failed" in str(exc_info.value.failure_cases) or "read failed" in str( + exc_info.value.schema_errors + ) + + +# --------------------------------------------------------------------------- # +# _validate_pdp_with_edvise_read +# --------------------------------------------------------------------------- # + + +def test_validate_pdp_with_edvise_read_student_success_returns_normalized_df( + tmp_path: Path, +) -> None: + """When STUDENT and read_raw_pdp_cohort_data returns df, result contains normalized_df.""" + csv_path = tmp_path / "cohort.csv" + csv_path.write_text("student_id,cohort\ns1,2016") + expected_df = pd.DataFrame({"student_id": ["s1"], "cohort": ["2016"]}) + + with patch( + "src.webapp.validation.read_raw_pdp_cohort_data", + return_value=expected_df, + ): + result = _validate_pdp_with_edvise_read( + str(csv_path), + enc="utf-8", + model_list=["STUDENT"], + institution_id="pdp", + ) + assert result["validation_status"] == "passed" + assert result["schemas"] == ["STUDENT"] + assert result["normalized_df"] is not None + pd.testing.assert_frame_equal(result["normalized_df"], expected_df) + + +def test_validate_pdp_with_edvise_read_schema_errors_converted_to_hard_validation_error( + tmp_path: Path, +) -> None: + """When edvise schema validation raises SchemaErrors, HardValidationError is raised.""" + from edvise.data_audit.schemas.raw_cohort import RawPDPCohortDataSchema + + csv_path = tmp_path / "cohort.csv" + csv_path.write_text("a,b\n1,2") + + # Obtain a real SchemaErrors by validating a dataframe that fails schema + bad_df = pd.DataFrame( + {"institution_id": [1], "cohort": ["x"], "student_guid": ["y"]} + ) + schema_err_to_raise: SchemaErrors | None = None + try: + RawPDPCohortDataSchema.validate(bad_df, lazy=True) # type: ignore[attr-defined] + except SchemaErrors as real_err: + schema_err_to_raise = real_err + else: + pytest.skip( + "RawPDPCohortDataSchema did not raise SchemaErrors for minimal bad df" + ) + assert schema_err_to_raise is not None + + with patch( + "src.webapp.validation.read_raw_pdp_cohort_data", + side_effect=schema_err_to_raise, + ): + with pytest.raises(HardValidationError) as exc_info: + _validate_pdp_with_edvise_read( + str(csv_path), + enc="utf-8", + model_list=["STUDENT"], + institution_id="pdp", + ) + err = exc_info.value + assert err.schema_errors is not None or err.failure_cases is not None + + +def test_validate_pdp_with_edvise_read_invalid_model_set_raises_hard_validation_error( + tmp_path: Path, +) -> None: + """When model_set is not STUDENT or COURSE, HardValidationError is raised.""" + csv_path = tmp_path / "x.csv" + csv_path.write_text("x\n1") + + with patch( + "src.webapp.validation.read_raw_pdp_cohort_data", return_value=pd.DataFrame() + ): + with pytest.raises( + HardValidationError, match="PDP single-model expected" + ) as exc_info: + _validate_pdp_with_edvise_read( + str(csv_path), + enc="utf-8", + model_list=["UNKNOWN"], + institution_id="pdp", + ) + assert "models=" in str(exc_info.value.schema_errors) + + +def test_validate_pdp_with_edvise_read_accepts_file_like() -> None: + """File-like input is read and passed to edvise read (temp file created and removed).""" + content = "student_id,cohort\ns1,2016" + stream = io.StringIO(content) + expected_df = pd.DataFrame({"student_id": ["s1"], "cohort": ["2016"]}) + + with patch( + "src.webapp.validation.read_raw_pdp_cohort_data", + return_value=expected_df, + ) as mock_read: + result = _validate_pdp_with_edvise_read( + stream, + enc="utf-8", + model_list=["STUDENT"], + institution_id="pdp", + ) + assert result["validation_status"] == "passed" + assert result["normalized_df"] is not None + pd.testing.assert_frame_equal(result["normalized_df"], expected_df) + mock_read.assert_called_once() + # Edvise read was given a path (temp file when file-like); keyword is file_path + assert "file_path" in mock_read.call_args[1] + assert isinstance(mock_read.call_args[1]["file_path"], str) + # Cohort validation uses no converter unless pdp_cohort_converter_func is passed + assert mock_read.call_args[1]["converter_func"] is None + + +def test_validate_pdp_with_edvise_read_student_uses_custom_cohort_converter_when_provided( + tmp_path: Path, +) -> None: + """When pdp_cohort_converter_func is provided, it is passed to read_raw_pdp_cohort_data.""" + csv_path = tmp_path / "cohort.csv" + csv_path.write_text("student_id,cohort\ns1,2016") + expected_df = pd.DataFrame({"student_id": ["s1"], "cohort": ["2016"]}) + custom_converter = lambda df: df # noqa: E731 + + with patch( + "src.webapp.validation.read_raw_pdp_cohort_data", + return_value=expected_df, + ) as mock_read: + _validate_pdp_with_edvise_read( + str(csv_path), + enc="utf-8", + model_list=["STUDENT"], + institution_id="pdp", + pdp_cohort_converter_func=custom_converter, + ) + mock_read.assert_called_once() + assert mock_read.call_args[1]["converter_func"] is custom_converter + + +def test_validate_pdp_with_edvise_read_non_callable_cohort_converter_raises_hard_validation_error( + tmp_path: Path, +) -> None: + """When pdp_cohort_converter_func is not callable, HardValidationError is raised (API returns 400).""" + csv_path = tmp_path / "cohort.csv" + csv_path.write_text("student_id,cohort\ns1,2016") + + with pytest.raises(HardValidationError, match="callable"): + _validate_pdp_with_edvise_read( + str(csv_path), + enc="utf-8", + model_list=["STUDENT"], + institution_id="pdp", + pdp_cohort_converter_func=cast(Any, "not a function"), + ) + + +def test_validate_pdp_with_edvise_read_non_callable_course_converter_raises_hard_validation_error( + tmp_path: Path, +) -> None: + """When pdp_course_converter_func is not callable, HardValidationError is raised (API returns 400).""" + csv_path = tmp_path / "course.csv" + csv_path.write_text("student_id,academic_year\ns1,2020") + + with pytest.raises(HardValidationError, match="callable"): + _validate_pdp_with_edvise_read( + str(csv_path), + enc="utf-8", + model_list=["COURSE"], + institution_id="pdp", + pdp_course_converter_func=cast(Any, 123), + ) + + +# --------------------------------------------------------------------------- # +# _read_pdp_course_edvise +# --------------------------------------------------------------------------- # + + +def test_read_pdp_course_edvise_success_returns_dataframe() -> None: + """When read_raw_pdp_course_data returns a df, _read_pdp_course_edvise returns it.""" + expected = pd.DataFrame({"course_id": ["c1"], "credits": [3]}) + with patch( + "src.webapp.validation.read_raw_pdp_course_data", + return_value=expected, + ): + result = _read_pdp_course_edvise("/nonexistent/path.csv") + pd.testing.assert_frame_equal(result, expected) + + +def test_read_pdp_course_edvise_all_attempts_fail_raises_hard_validation_error() -> ( + None +): + """When all converter/format attempts raise ValueError, HardValidationError is raised.""" + with patch( + "src.webapp.validation.read_raw_pdp_course_data", + side_effect=ValueError("bad datetime"), + ): + with pytest.raises(HardValidationError, match="datetime format") as exc_info: + _read_pdp_course_edvise("/nonexistent/path.csv") + assert ( + "datetime" in str(exc_info.value.schema_errors).lower() + or "format" in str(exc_info.value.schema_errors).lower() + ) + + +def test_read_pdp_course_edvise_falls_back_after_custom_converter_fails() -> None: + """When custom converter fails all datetime formats, default PDP converter is used.""" + expected = pd.DataFrame({"course_id": ["c1"]}) + with patch( + "src.webapp.validation.read_raw_pdp_course_data", + side_effect=[ + ValueError("bad datetime"), + ValueError("bad datetime"), + ValueError("bad datetime"), + expected, + ], + ) as mock_read: + result = _read_pdp_course_edvise( + "/path.csv", + course_converter_func=lambda df: df, # noqa: ARG005 + ) + pd.testing.assert_frame_equal(result, expected) + assert mock_read.call_count == 4 + + +def test_read_pdp_course_edvise_custom_converter_tried_first() -> None: + """When course_converter_func is provided, it is tried before default converters.""" + expected = pd.DataFrame({"course_id": ["c1"]}) + custom_converter = lambda df: df # noqa: E731 + with patch( + "src.webapp.validation.read_raw_pdp_course_data", + return_value=expected, + ) as mock_read: + result = _read_pdp_course_edvise( + "/path.csv", course_converter_func=custom_converter + ) + pd.testing.assert_frame_equal(result, expected) + # Custom converter should have been used (first call succeeds) + assert mock_read.call_count == 1 + assert mock_read.call_args[1]["converter_func"] is custom_converter diff --git a/src/webapp/validation_schemas/edvise_schema_extension.json b/src/webapp/validation_schemas/edvise_schema_extension.json new file mode 100644 index 00000000..b9e8310f --- /dev/null +++ b/src/webapp/validation_schemas/edvise_schema_extension.json @@ -0,0 +1,660 @@ +{ + "version": "1.0.0", + "institutions": { + "edvise": { + "data_models": { + "student": { + "required": true, + "columns": { + "student_id": { + "dtype": "string", + "coerce": true, + "nullable": false, + "required": true, + "aliases": ["student_id", "guid", "student_guid", "study_id"], + "checks": [ + { + "type": "str_length", + "args": [], + "kwargs": {"min_value": 1} + } + ], + "description": "A unique identifier for each student. Use a de-identified ID (not your internal student ID) that you can map back to your records. Each student should have a unique ID." + }, + "cohort_year": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["cohort", "FirstNonDualCreditAcademicYear"], + "checks": [ + { + "type": "matches", + "args": [ + "^\\d{4}-\\d{2}$" + ], + "kwargs": {} + } + ], + "description": "Academic year when the student first enrolled, in YYYY-YY format (e.g., 2025-26). Optional - will be calculated from course data if not provided." + }, + "cohort_term": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["semester", "term", "FirstNonDualCreditTerm"], + "checks": [ + { + "type": "matches", + "args": [ + "(?i)^(\\d{4})?\\s?(Fall|Winter|Spring|Summer|FA|WI|SP|SU|SM)\\s?(\\d{4})?$" + ], + "kwargs": {} + } + ], + "description": "Term when the student first enrolled (e.g., Fall, Spring, FA, SP). Can include year (e.g., Fall2024, 2024FA). Optional - will be calculated from course data if not provided." + }, + "first_enrollment_date": { + "dtype": "datetime64[ns]", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [], + "description": "Date when the student first enrolled in a course." + }, + "student_age": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["age_range", "age"], + "checks": [ + { + "type": "matches", + "args": [ + "(?i)^((1[3-9]|[2-9][0-9]|100)|20\\s+and\\s+younger|older\\s+than\\s+24|>20\\s*-\\s*24)$" + ], + "kwargs": {} + } + ], + "description": "Student's age. Can be a number (13-100) or a category like '20 and younger' or 'Older than 24'." + }, + "enrollment_type": { + "dtype": "category", + "coerce": true, + "nullable": false, + "required": true, + "aliases": ["enrollment_status", "student_type", "time_status", "AdmissionBasis"], + "checks": [ + { + "type": "matches", + "args": [ + "(?i).*(first[-\\s]?time|freshman|transfer|re[-\\s]?admit|readmit).*" + ], + "kwargs": {} + } + ], + "description": "Student's enrollment type. Must contain: First-Time/Freshman, Transfer, or Re-admit/Readmit (e.g., 'First-time student', 'Transfer student')." + }, + "race": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [], + "description": "Student's race." + }, + "ethnicity": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [], + "description": "Student's ethnicity." + }, + "gender": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [], + "description": "Student's gender. Must include at least Male/M and Female/F categories. Maximum of 5 different values allowed." + }, + "first_gen": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["first_generation", "firstgen", "first_gen_student", "first_generation_college_student", "first_gen_flag", "firstgenflag"], + "checks": [], + "description": "Whether the student is the first in their family to attend college. Must include at least one 'Yes' or 'Y' value. Maximum of 3 different values allowed." + }, + "pell_status_first_year": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [ + { + "type": "isin", + "args": [ + [ + "Y", + "Yes", + "N", + "No" + ] + ], + "kwargs": {} + } + ], + "description": "Whether the student received a Pell Grant in their first year. Use Yes/Y or No/N." + }, + "credential_type_sought_year_1": { + "dtype": "category", + "coerce": true, + "nullable": false, + "required": true, + "aliases": ["program_at_first_enrollment", "ProgramAtFirstEnrollment", "DeclaredDegree"], + "checks": [ + { + "type": "matches", + "args": [ + "(?i).*(bachelor|ba|bs|associate|aa|as|aas|certificate|certification).*" + ], + "kwargs": {} + } + ], + "description": "Degree program the student was enrolled in when they first started (e.g., 'Bachelor's Degree', 'Associate Degree', 'Certificate Program'). Must contain: Bachelor/BA/BS, Associate/AA/AS/AAS, or Certificate." + }, + "program_of_study_term_1": { + "dtype": "category", + "coerce": true, + "nullable": false, + "required": true, + "aliases": ["major_at_first_enrollment", "MajorAtFirstEnrollment", "major"], + "checks": [], + "description": "The student's major when they first enrolled (e.g., Biology, Mathematics, History)." + }, + "incarcerated_status": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [], + "description": "Whether the student is currently or was previously incarcerated." + }, + "military_status": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [], + "description": "Whether the student is a veteran or on active military duty." + }, + "employment_status": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [], + "description": "Whether the student is employed." + }, + "disability_status": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [], + "description": "Whether the student identifies as having a disability." + }, + "first_bachelors_grad_date": { + "dtype": "datetime64[ns]", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [], + "description": "Date the student graduated with a bachelor's degree. Leave blank if they did not graduate or did not pursue this degree." + }, + "first_associates_grad_date": { + "dtype": "datetime64[ns]", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [], + "description": "Date the student graduated with an associate's degree. Leave blank if they did not graduate or did not pursue this degree." + }, + "degree_grad": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["program_at_graduation", "AwardedDegree"], + "checks": [ + { + "type": "matches", + "args": [ + "(?i).*(bachelor|ba|bs|associate|aa|as|aas|certificate|certification).*" + ], + "kwargs": {} + } + ], + "description": "Degree program the student completed at graduation (e.g., 'Bachelor's Degree', 'Associate Degree', 'Certificate'). May differ from their initial program. Must contain: Bachelor/BA/BS, Associate/AA/AS/AAS, or Certificate." + }, + "major_grad": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["major_at_graduation"], + "checks": [], + "description": "The student's major at graduation. May differ from their initial major if they changed programs." + }, + "certificate1_date": { + "dtype": "datetime64[ns]", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["first_certificate_received_date"], + "checks": [], + "description": "Date the student received their first certificate. Leave blank if they did not receive a certificate." + }, + "certificate2_date": { + "dtype": "datetime64[ns]", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["second_certificate_received_date"], + "checks": [], + "description": "Date the student received their second certificate. Leave blank if they did not receive a second certificate." + }, + "certificate3_date": { + "dtype": "datetime64[ns]", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["third_certificate_received_date"], + "checks": [], + "description": "Date the student received their third certificate. Leave blank if they did not receive a third certificate." + }, + "credits_earned_ap": { + "dtype": "float64", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["credits_earned_through_ap"], + "checks": [], + "description": "Number of credits earned through Advanced Placement (AP) courses." + }, + "credits_earned_dual_enrollment": { + "dtype": "float64", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["credits_earned_through_dual_enrollment"], + "checks": [], + "description": "Number of credits earned through dual enrollment while in high school." + } + } + }, + "course": { + "required": true, + "columns": { + "student_id": { + "dtype": "string", + "coerce": true, + "nullable": false, + "required": true, + "aliases": ["student_id", "guid", "student_guid", "study_id"], + "checks": [ + { + "type": "str_length", + "args": [], + "kwargs": {"min_value": 1} + } + ], + "description": "A unique identifier for each student. Use a de-identified ID (not your internal student ID) that you can map back to your records. Each row should be unique by student ID, term, and course." + }, + "academic_year": { + "dtype": "category", + "coerce": true, + "nullable": false, + "required": true, + "aliases": [], + "checks": [ + { + "type": "matches", + "args": [ + "^\\d{4}-\\d{2}$" + ], + "kwargs": {} + } + ], + "description": "Academic year when the student took the course, in YYYY-YY format (e.g., 2025-26)." + }, + "academic_term": { + "dtype": "category", + "coerce": true, + "nullable": false, + "required": true, + "aliases": ["semester", "term"], + "checks": [ + { + "type": "matches", + "args": [ + "(?i)^(\\d{4})?\\s?(Fall|Winter|Spring|Summer|FA|WI|SP|SU|SM)\\s?(\\d{4})?$" + ], + "kwargs": {} + } + ], + "description": "Term when the student took the course (e.g., Fall, Spring, FA, SP). Can include year (e.g., Fall2024, 2024FA)." + }, + "course_prefix": { + "dtype": "category", + "coerce": true, + "nullable": false, + "required": true, + "aliases": [], + "checks": [], + "description": "Course prefix from your catalog (e.g., ENGL, MATH, HIST). Each student should have one row per term and course." + }, + "course_number": { + "dtype": "float64", + "coerce": true, + "nullable": false, + "required": true, + "aliases": [], + "checks": [], + "description": "Course number from your catalog (e.g., 101, 201). Each student should have one row per term and course." + }, + "course_name": { + "dtype": "category", + "coerce": true, + "nullable": false, + "required": true, + "aliases": [], + "checks": [], + "description": "Full course title (e.g., 'Introduction to Psychology', 'Calculus I')." + }, + "department": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [], + "description": "Department offering the course (e.g., 'College of Arts & Sciences', 'Department of Mathematics')." + }, + "course_classification": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [], + "description": "How the course was delivered (e.g., Lecture, Lab, Seminar)." + }, + "course_type": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["CreditType"], + "checks": [], + "description": "Type of course (e.g., Undergraduate, Graduate, GED, Adult Education)." + }, + "course_begin_date": { + "dtype": "datetime64[ns]", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [], + "description": "Course start date." + }, + "course_end_date": { + "dtype": "datetime64[ns]", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [], + "description": "Course end date." + }, + "grade": { + "dtype": "category", + "coerce": true, + "nullable": false, + "required": true, + "aliases": [], + "checks": [ + { + "type": "isin", + "args": [ + [ + "A+", + "A", + "A-", + "B+", + "B", + "B-", + "C+", + "C", + "C-", + "D+", + "D", + "D-", + "F", + "P", + "PASS", + "S", + "SAT", + "U", + "UNSAT", + "W", + "WD", + "I", + "IP", + "AU", + "NG", + "NR", + "M", + "O", + "0", + "1", + "2", + "3", + "4" + ] + ], + "kwargs": {} + } + ], + "description": "Student's grade for the course. Accepts letter grades (A-F), numeric grades (0-4), or special codes (P, W, I, etc.)." + }, + "course_credits_attempted": { + "dtype": "float64", + "coerce": true, + "nullable": false, + "required": true, + "aliases": ["CreditHours", "number_of_credits_attempted", "course_credits"], + "checks": [], + "description": "Number of credits the course is worth." + }, + "course_credits_earned": { + "dtype": "float64", + "coerce": true, + "nullable": false, + "required": true, + "aliases": ["number_of_credits_earned", "course_credits_earned"], + "checks": [], + "description": "Number of credits the student earned in the course." + }, + "delivery_method": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["delivery_method", "delivery_type"], + "checks": [], + "description": "Course delivery method (e.g., Online, In-Person, Hybrid, Face-to-Face)." + }, + "core_course": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [], + "description": "Whether the course is part of general education or core requirements (e.g., Yes, No)." + }, + "pass_fail_flag": { + "dtype": "category", + "coerce": true, + "nullable": false, + "required": true, + "aliases": [], + "checks": [ + { + "type": "isin", + "args": [ + [ + "Fail", + "Pass", + "P", + "F" + ] + ], + "kwargs": {} + } + ], + "description": "Whether the student passed or failed the course. Use P/Pass or F/Fail." + }, + "prerequisite_course_flag": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [], + "description": "Whether this course is a prerequisite for other courses (e.g., Yes, No)." + }, + "course_instructor_employment_status": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["course_instructor_appointment_type"], + "checks": [], + "description": "Instructor's employment type (e.g., Adjunct, Tenured, Part-Time, Full-Time)." + }, + "gateway_or_development_flag": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["math_or_english_gateway"], + "checks": [], + "description": "Whether the course is part of a gateway, college readiness, or skills development program (e.g., Yes, No)." + }, + "course_section_size": { + "dtype": "float64", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["TotalClassSize"], + "checks": [], + "description": "Total number of students enrolled in this course section." + }, + "term_enrollment_intensity": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["FullOrPartTime"], + "checks": [ + { + "type": "matches", + "args": [ + "(?i).*(full[\\s-]?time|part[\\s-]?time).*" + ], + "kwargs": {} + } + ], + "description": "Whether the student was enrolled full-time or part-time this term. Must contain 'full-time' or 'part-time' (case-insensitive, with or without spaces/dashes)." + }, + "term_degree": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["DeclaredDegree"], + "checks": [ + { + "type": "matches", + "args": [ + "(?i).*(bachelor|ba|bs|associate|aa|as|aas|certificate|certification).*" + ], + "kwargs": {} + } + ], + "description": "Degree program the student is pursuing this term (e.g., 'Bachelor's Degree', 'Associate Degree', 'Certificate'). Must contain: Bachelor/BA/BS, Associate/AA/AS/AAS, or Certificate." + }, + "term_major": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["term_program_of_study"], + "checks": [], + "description": "The student's intended major for this term." + }, + "intent_to_transfer_flag": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [], + "description": "Whether the student has expressed intent to transfer this term (e.g., Yes, No)." + }, + "term_pell_recipient": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [], + "checks": [ + { + "type": "isin", + "args": [ + [ + "Y", + "Yes", + "N", + "No" + ] + ], + "kwargs": {} + } + ], + "description": "Whether the student received a Pell Grant this term. Use Yes/Y or No/N." + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/webapp/validation_schemas/pdp_schema_extension.json b/src/webapp/validation_schemas/pdp_schema_extension.json index 25971b88..ee277065 100644 --- a/src/webapp/validation_schemas/pdp_schema_extension.json +++ b/src/webapp/validation_schemas/pdp_schema_extension.json @@ -10,102 +10,78 @@ "coerce": true, "nullable": true, "required": true, - "aliases": ["semester"], + "aliases": [ + "semester" + ], "checks": [] }, - "enrollment_type": { - "dtype": "string", - "coerce": true, - "nullable": true, - "required": false, - "aliases": ["time_status"], - "checks": [ - {"type": "str_length", "args": [], "kwargs": {"min_value": 1}} - ] - }, - "number_of_credits_attempted": { - "dtype": "float64", + "first_gen": { + "dtype": "category", "coerce": true, "nullable": true, "required": false, - "aliases": ["credits_attempted", "ug_att", "number_of_credits_attempted_year_1", "number_of_credits_attempted_year_2", - "number_of_credits_attempted_year_3", "number_of_credits_attempted_year_4"], + "aliases": ["first_generation_student", "first_generation_college_student", "first_gen_flag", "firstgenflag"], "checks": [] }, - "number_of_credits_earned": { - "dtype": "float64", + "student_age": { + "dtype": "string", "coerce": true, "nullable": true, "required": false, - "aliases": ["credits_earned", "ug_earn", "number_of_credits_earned_year_1", "number_of_credits_earned_year_2", - "number_of_credits_earned_year_3", "number_of_credits_earned_year_4"], - "checks": [{"type": "ge", "args": [0.0]}] - }, - "attempteddevenglishy1": { - "dtype": "category", - "coerce": true, - "nullable": true, - "required": true, - "aliases": [], - "checks": [] - }, - "attempteddevmathy1": { - "dtype": "category", - "coerce": true, - "nullable": true, - "required": true, - "aliases": [], + "aliases": ["age", "age_(range)", "age_range", "age_group", "agegroup"], "checks": [] }, - "attemptedgatewayenglishyear1": { - "dtype": "category", - "coerce": true, - "nullable": true, - "required": true, - "aliases": [], - "checks": [] - }, - "attemptedgatewaymathyear1": { - "dtype": "category", + "incarcerated_status": { + "dtype": "string", "coerce": true, "nullable": true, - "required": true, - "aliases": [], + "required": false, + "aliases": ["incarceration_status", "incarceratedflag"], "checks": [] }, - "attendance_status_term_1": { + "pell_status_first_year": { "dtype": "category", "coerce": true, "nullable": true, - "required": true, - "aliases": [], + "required": false, + "aliases": ["awarded_pell_ever"], "checks": [] }, - "cohort": { - "dtype": "category", + "program_of_study_term_1": { + "dtype": "string", "coerce": true, "nullable": true, - "required": true, - "aliases": [], + "required": false, + "aliases": ["program_at_first_enrollment"], "checks": [] }, - "completeddevenglishy1": { + "program_of_study_year_1": { "dtype": "string", "coerce": true, "nullable": true, - "required": true, + "required": false, "aliases": [], "checks": [] }, - "completeddevmathy1": { + "enrollment_type": { "dtype": "string", "coerce": true, "nullable": true, - "required": true, - "aliases": [], - "checks": [] + "required": false, + "aliases": [ + "time_status" + ], + "checks": [ + { + "type": "str_length", + "args": [], + "kwargs": { + "min_value": 1 + } + } + ] }, - "completedgatewayenglishyear1": { + "attendance_status_term_1": { "dtype": "category", "coerce": true, "nullable": true, @@ -113,7 +89,7 @@ "aliases": [], "checks": [] }, - "completedgatewaymathyear1": { + "cohort": { "dtype": "category", "coerce": true, "nullable": true, @@ -249,22 +225,6 @@ "aliases": [], "checks": [] }, - "gatewayenglishgradey1": { - "dtype": "category", - "coerce": true, - "nullable": true, - "required": true, - "aliases": [], - "checks": [] - }, - "gatewaymathgradey1": { - "dtype": "category", - "coerce": true, - "nullable": true, - "required": true, - "aliases": [], - "checks": [] - }, "gpa_group_term_1": { "dtype": "string", "coerce": true, @@ -314,7 +274,7 @@ "checks": [] }, "special_program": { - "dtype": "float64", + "dtype": "string", "coerce": true, "nullable": true, "required": true, @@ -433,7 +393,6 @@ "aliases": [], "checks": [] }, - "time_to_credential": { "dtype": "float64", "coerce": true, @@ -537,11 +496,194 @@ "required": true, "aliases": [], "checks": [] + }, + "attempted_dev_english_y_1": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": true, + "aliases": ["attempted_dev_english_y_1", "attempteddevenglishy1"], + "checks": [] + }, + "attempted_dev_math_y_1": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": true, + "aliases": ["attempted_dev_math_y_1", "attempteddevmathy1"], + "checks": [] + }, + "attempted_gateway_english_year_1": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": true, + "aliases": ["attempted_gateway_english_year_1", "attemptedgatewayenglishyear1"], + "checks": [] + }, + "attempted_gateway_math_year_1": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": true, + "aliases": ["attempted_gateway_math_year_1", "attemptedgatewaymathyear1"], + "checks": [] + }, + "completed_dev_english_y_1": { + "dtype": "string", + "coerce": true, + "nullable": true, + "required": true, + "aliases": ["completed_dev_english_y_1", "completeddevenglishy1"], + "checks": [] + }, + "completed_dev_math_y_1": { + "dtype": "string", + "coerce": true, + "nullable": true, + "required": true, + "aliases": ["completed_dev_math_y_1", "completeddevmathy1"], + "checks": [] + }, + "completed_gateway_english_year_1": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": true, + "aliases": ["completed_gateway_english_year_1", "completedgatewayenglishyear1"], + "checks": [] + }, + "completed_gateway_math_year_1": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": true, + "aliases": ["completed_gateway_math_year_1", "completedgatewaymathyear1"], + "checks": [] + }, + "gateway_english_grade_y_1": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": true, + "aliases": ["gateway_english_grade_y_1", "gatewayenglishgradey1"], + "checks": [] + }, + "gateway_math_grade_y_1": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": true, + "aliases": ["gateway_math_grade_y_1", "gatewaymathgradey1"], + "checks": [] + }, + "number_of_credits_attempted_year_1": { + "dtype": "float64", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [ + "number_of_credits_attempted_year_1", + "number_of_credits_attempted", + "credits_attempted", + "ug_att" + ], + "checks": [] + }, + "number_of_credits_attempted_year_2": { + "dtype": "float64", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["number_of_credits_attempted_year_2"], + "checks": [] + }, + "number_of_credits_attempted_year_3": { + "dtype": "float64", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["number_of_credits_attempted_year_3"], + "checks": [] + }, + "number_of_credits_attempted_year_4": { + "dtype": "float64", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["number_of_credits_attempted_year_4"], + "checks": [] + }, + "number_of_credits_earned_year_1": { + "dtype": "float64", + "coerce": true, + "nullable": true, + "required": false, + "aliases": [ + "number_of_credits_earned_year_1", + "number_of_credits_earned", + "credits_earned", + "ug_earn" + ], + "checks": [ + { + "type": "ge", + "args": [0] + } + ] + }, + "number_of_credits_earned_year_2": { + "dtype": "float64", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["number_of_credits_earned_year_2"], + "checks": [{"type": "ge", "args": [0]}] + }, + "number_of_credits_earned_year_3": { + "dtype": "float64", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["number_of_credits_earned_year_3"], + "checks": [{"type": "ge", "args": [0]}] + }, + "number_of_credits_earned_year_4": { + "dtype": "float64", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["number_of_credits_earned_year_4"], + "checks": [{"type": "ge", "args": [0]}] } } }, "course": { "columns": { + "cohort_term": { + "dtype": "category", + "coerce": true, + "nullable": true, + "required": true, + "aliases": ["semester", "term_desc"], + "checks": [] + }, + "number_of_credits_attempted": { + "dtype": "float64", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["course_credits", "att_hrs"], + "checks": [] + }, + "delivery_method": { + "dtype": "string", + "coerce": true, + "nullable": true, + "required": false, + "aliases": ["course_delivery_method", "inst_method", "modality"], + "checks": [] + }, "academic_term": { "dtype": "category", "coerce": true, diff --git a/src/webapp/validation_test.py b/src/webapp/validation_test.py index 92bc1f48..5fc54cf3 100644 --- a/src/webapp/validation_test.py +++ b/src/webapp/validation_test.py @@ -30,6 +30,26 @@ MOCK_EXT_SCHEMA: dict = {"institutions": {"pdp": {"data_models": {}}}} +# Extension with "edvise" block only; test_model has required "baz_col" in edvise +MOCK_EXT_SCHEMA_EDVISE: dict = { + "institutions": { + "edvise": { + "data_models": { + "test_model": { + "columns": { + "baz_col": { + "dtype": "str", + "nullable": False, + "required": True, + "aliases": ["baz"], + }, + } + } + } + } + } +} + @pytest.fixture def tmp_csv_file(tmp_path: Path) -> str: @@ -57,6 +77,41 @@ def test_validate_file_reader_passes(tmp_csv_file): assert result["schemas"] == ["test_model"] +def test_validate_file_reader_return_normalized_df(tmp_csv_file): + """On success, result includes normalized_df (canonical columns).""" + with ( + patch("src.webapp.validation.load_json") as mock_load, + patch("os.path.exists", return_value=True), + ): + mock_load.side_effect = lambda path: ( + MOCK_BASE_SCHEMA if "base" in path else MOCK_EXT_SCHEMA + ) + result = validate_file_reader( + tmp_csv_file, + ["test_model"], + base_schema=MOCK_BASE_SCHEMA, + inst_schema=MOCK_EXT_SCHEMA, + ) + assert result["validation_status"] == "passed" + assert "normalized_df" in result + assert result["normalized_df"] is not None + assert list(result["normalized_df"].columns) == ["foo_col", "bar_col"] + + +def test_validate_file_reader_empty_schema_returns_normalized_df_none(tmp_csv_file): + """When allowed_schema is empty, short-circuit returns normalized_df: None.""" + result = validate_file_reader( + tmp_csv_file, + [], + base_schema=MOCK_BASE_SCHEMA, + inst_schema=MOCK_EXT_SCHEMA, + ) + assert result["validation_status"] == "passed" + assert result["schemas"] == [] + assert "normalized_df" in result + assert result["normalized_df"] is None + + def test_validate_file_reader_fails_missing_required(tmp_path): df = pd.DataFrame({"bar_col": ["x", "y"]}) # Missing "foo_col" file_path = tmp_path / "invalid.csv" @@ -69,11 +124,220 @@ def test_validate_file_reader_fails_missing_required(tmp_path): mock_load.side_effect = lambda path: ( MOCK_BASE_SCHEMA if "base" in path else MOCK_EXT_SCHEMA ) + # Use a custom institution_id so we merge base + extension (not extension-only). + # PDP/Edvise use extension-only and this test's extension has empty pdp data_models. with pytest.raises(HardValidationError) as exc_info: validate_file_reader( str(file_path), ["test_model"], base_schema=MOCK_BASE_SCHEMA, inst_schema=MOCK_EXT_SCHEMA, + institution_id="custom-inst-id", ) assert "Missing required columns" in str(exc_info.value) + + +def test_validate_file_reader_uses_institution_id_for_extension_block( + tmp_path: Path, + tmp_csv_file: str, +) -> None: + """Passing institution_id selects the correct extension block (e.g. edvise vs pdp).""" + # File has base columns only; extension has required "baz_col" only under institutions["edvise"] + df = pd.DataFrame({"foo_col": [1], "bar_col": ["a"]}) + file_path = tmp_path / "no_baz.csv" + df.to_csv(file_path, index=False) + + with ( + patch("src.webapp.validation.load_json") as mock_load, + patch("os.path.exists", return_value=True), + ): + mock_load.side_effect = lambda path: ( + MOCK_BASE_SCHEMA if "base" in path else MOCK_EXT_SCHEMA_EDVISE + ) + # institution_id="edvise" -> merge_model_columns uses institutions["edvise"] -> baz_col required -> missing + with pytest.raises(HardValidationError) as exc_info: + validate_file_reader( + str(file_path), + ["test_model"], + base_schema=MOCK_BASE_SCHEMA, + inst_schema=MOCK_EXT_SCHEMA_EDVISE, + institution_id="edvise", + ) + assert "baz_col" in str(exc_info.value) or "Missing required" in str( + exc_info.value + ) + # institution_id="pdp" -> institutions["pdp"] missing -> no extra columns merged -> only base required (foo_col present) + result = validate_file_reader( + str(file_path), + ["test_model"], + base_schema=MOCK_BASE_SCHEMA, + inst_schema=MOCK_EXT_SCHEMA_EDVISE, + institution_id="pdp", + ) + assert result["validation_status"] == "passed" + + # institution_identifier is accepted and does not affect non-Edvise path + mock_load.side_effect = lambda path: ( + MOCK_BASE_SCHEMA if "base" in path else MOCK_EXT_SCHEMA + ) + result_with_id = validate_file_reader( + tmp_csv_file, + ["test_model"], + base_schema=MOCK_BASE_SCHEMA, + inst_schema=MOCK_EXT_SCHEMA, + institution_identifier="optional-uuid-for-edvise-only", + ) + assert result_with_id["validation_status"] == "passed" + + +def test_validate_file_reader_legacy_accepts_any_format(tmp_path: Path) -> None: + """Legacy institutions: any CSV format is accepted (encoding + read only, no schema).""" + csv_path = tmp_path / "any_columns.csv" + csv_path.write_text("a,b,c\n1,2,hello\n3,4,world", encoding="utf-8") + result = validate_file_reader( + str(csv_path), + ["STUDENT"], + base_schema=MOCK_BASE_SCHEMA, + inst_schema=None, + institution_id="legacy", + ) + assert result["validation_status"] == "passed" + assert result["schemas"] == ["STUDENT"] + assert result["normalized_df"] is not None + df = result["normalized_df"] + assert list(df.columns) == ["a", "b", "c"] + assert len(df) == 2 + + +def test_validate_file_reader_legacy_accepts_student_id_column(tmp_path: Path) -> None: + """Legacy institutions: student_id is an allowed de-identified identifier column.""" + csv_path = tmp_path / "with_student_id.csv" + csv_path.write_text( + "student_id,grade,term\nabc123,A,Fall\nxyz789,B,Spring", encoding="utf-8" + ) + result = validate_file_reader( + str(csv_path), + ["STUDENT"], + base_schema=MOCK_BASE_SCHEMA, + inst_schema=None, + institution_id="legacy", + ) + assert result["validation_status"] == "passed" + assert list(result["normalized_df"].columns) == ["student_id", "grade", "term"] + + +def test_validate_file_reader_legacy_header_only_csv_passes(tmp_path: Path) -> None: + """Legacy institutions: CSV with only a header row (no data) passes; PII check runs on column names only.""" + csv_path = tmp_path / "header_only.csv" + csv_path.write_text("col_a,col_b,col_c\n", encoding="utf-8") + result = validate_file_reader( + str(csv_path), + ["STUDENT"], + base_schema=MOCK_BASE_SCHEMA, + inst_schema=None, + institution_id="legacy", + ) + assert result["validation_status"] == "passed" + df = result["normalized_df"] + assert list(df.columns) == ["col_a", "col_b", "col_c"] + assert len(df) == 0 + + +def test_validate_file_reader_legacy_rejects_header_only_pii_columns( + tmp_path: Path, +) -> None: + """Legacy institutions: header-only CSV with PII column names is rejected (no data rows).""" + csv_path = tmp_path / "header_only_pii.csv" + csv_path.write_text("email,ssn\n", encoding="utf-8") + with pytest.raises(HardValidationError) as exc_info: + validate_file_reader( + str(csv_path), + ["STUDENT"], + base_schema=MOCK_BASE_SCHEMA, + inst_schema=None, + institution_id="legacy", + ) + err = exc_info.value + assert "PII" in (err.schema_errors or "") + failure_cases = err.failure_cases or [] + assert "email" in failure_cases + assert "ssn" in failure_cases + + +def test_validate_file_reader_legacy_rejects_pii_columns(tmp_path: Path) -> None: + """Legacy institutions: files with column names indicating PII are rejected before raw/validated.""" + csv_path = tmp_path / "with_pii.csv" + csv_path.write_text( + "id,email,score\n1,user@example.com,85\n2,other@test.org,90", encoding="utf-8" + ) + with pytest.raises(HardValidationError) as exc_info: + validate_file_reader( + str(csv_path), + ["STUDENT"], + base_schema=MOCK_BASE_SCHEMA, + inst_schema=None, + institution_id="legacy", + ) + err = exc_info.value + assert "PII" in (err.schema_errors or "") + assert "email" in (err.failure_cases or []) + + +def test_validate_file_reader_legacy_rejects_multiple_pii_columns( + tmp_path: Path, +) -> None: + """Legacy institutions: all detected PII column names are listed in the error.""" + csv_path = tmp_path / "multi_pii.csv" + csv_path.write_text( + "first_name,last_name,grade\nAlice,Smith,A\nBob,Jones,B", + encoding="utf-8", + ) + with pytest.raises(HardValidationError) as exc_info: + validate_file_reader( + str(csv_path), + ["STUDENT"], + base_schema=MOCK_BASE_SCHEMA, + inst_schema=None, + institution_id="legacy", + ) + err = exc_info.value + assert "PII" in (err.schema_errors or "") + failure_cases = err.failure_cases or [] + assert "first_name" in failure_cases + assert "last_name" in failure_cases + + +def test_validate_file_reader_csv_read_failure_raises_hard_validation_error( + tmp_csv_file: str, +) -> None: + """When the CSV body cannot be read (e.g. malformed), HardValidationError is raised with a clear message.""" + with ( + patch("src.webapp.validation.load_json") as mock_load, + patch("os.path.exists", return_value=True), + patch("src.webapp.validation.pd.read_csv") as mock_read_csv, + ): + mock_load.side_effect = lambda path: ( + MOCK_BASE_SCHEMA if "base" in path else MOCK_EXT_SCHEMA + ) + # First call is header-only (nrows=0); second is full read. Fail the full read. + call_count = 0 + + def read_csv_side_effect(*args: object, **kwargs: object) -> pd.DataFrame: + nonlocal call_count + call_count += 1 + if kwargs.get("nrows") == 0: + return pd.DataFrame({"foo_col": [], "bar_col": []}) + raise ValueError("Bad CSV data") + + mock_read_csv.side_effect = read_csv_side_effect + + with pytest.raises(HardValidationError, match="valid CSV file") as exc_info: + validate_file_reader( + tmp_csv_file, + ["test_model"], + base_schema=MOCK_BASE_SCHEMA, + inst_schema=MOCK_EXT_SCHEMA, + ) + assert "could not be read" in str(exc_info.value).lower() or "valid CSV" in str( + exc_info.value + ) diff --git a/src/worker/Dockerfile b/src/worker/Dockerfile index bd2c6bb3..ae55383e 100644 --- a/src/worker/Dockerfile +++ b/src/worker/Dockerfile @@ -13,6 +13,11 @@ WORKDIR /app # Add project files ADD uv.lock pyproject.toml /app/ +# Install git and ca-certificates +RUN apt-get update \ + && apt-get install -y --no-install-recommends git ca-certificates \ + && rm -rf /var/lib/apt/lists/* + # Install dependencies RUN uv sync --frozen --no-install-project diff --git a/src/worker/README.md b/src/worker/README.md index 75c26efb..57fdb1d8 100644 --- a/src/worker/README.md +++ b/src/worker/README.md @@ -79,7 +79,7 @@ Enter into the root directory of the repo. You're now in your virtual env with all your dependencies added. -For all of the following, the steps above are pre-requisites and you should be in the root folder of `sst-app-api/`. +For all of the following, the steps above are pre-requisites and you should be in the root folder of `edvise-api/`. ### Spin up the app locally: diff --git a/src/worker/main_test.py b/src/worker/main_test.py index 2f07251d..f8306099 100644 --- a/src/worker/main_test.py +++ b/src/worker/main_test.py @@ -54,9 +54,15 @@ def sftp_files(client: TestClient) -> Any: assert response.json() == {"sftp_files": {}} +@patch("src.worker.main.split_csv_and_generate_signed_urls", return_value={}) +@patch("src.worker.utilities.get_token", return_value="fake_token") @patch("google.auth.default") def test_execute_pdp_pull( - mock_auth_default: Any, client: TestClient, monkeypatch: Any + mock_auth_default: Any, + mock_get_token: Any, + mock_split_csv: Any, + client: TestClient, + monkeypatch: Any, ) -> None: """Test POST /execute-pdp-pull with mocked authentication.""" # Set up dummy credentials with the correct universe_domain. @@ -68,9 +74,7 @@ def test_execute_pdp_pull( dummy_credentials.universe_domain = "googleapis.com" # Set the expected domain mock_auth_default.return_value = (dummy_credentials, "dummy-project") - MOCK_STORAGE.copy_from_sftp_to_gcs.side_effect = ( - lambda filename: f"processed_{filename}" - ) + MOCK_STORAGE.copy_from_sftp_to_gcs.side_effect = lambda *args, **kwargs: None MOCK_STORAGE.create_bucket_if_not_exists.return_value = None # Optionally, if there's a process_file or similar function, you can mock it too. diff --git a/terraform/modules/cloudbuild/main.tf b/terraform/modules/cloudbuild/main.tf index c317f62d..e23af55f 100644 --- a/terraform/modules/cloudbuild/main.tf +++ b/terraform/modules/cloudbuild/main.tf @@ -7,7 +7,7 @@ resource "google_project_service" "services" { resource "google_artifact_registry_repository" "student_success_tool" { location = var.region - repository_id = "sst-app-api" + repository_id = "edvise-api" format = "DOCKER" cleanup_policy_dry_run = false cleanup_policies { @@ -35,7 +35,7 @@ resource "google_artifact_registry_repository" "student_success_tool" { resource "google_artifact_registry_repository" "sst_app_ui" { location = var.region - repository_id = "sst-app-ui" + repository_id = "edvise-ui" format = "DOCKER" cleanup_policy_dry_run = false cleanup_policies { @@ -70,7 +70,7 @@ resource "google_cloudbuild_trigger" "python_apps" { for_each = var.environment == "dev" ? [1] : [] content { owner = "datakind" - name = "sst-app-api" + name = "edvise-api" push { branch = "develop" } @@ -81,7 +81,7 @@ resource "google_cloudbuild_trigger" "python_apps" { content { ref = "refs/heads/develop" repo_type = "GITHUB" - uri = "https://github.com/datakind/sst-app-api" + uri = "https://github.com/datakind/edvise-api" } } build { @@ -92,19 +92,19 @@ resource "google_cloudbuild_trigger" "python_apps" { "-f", "src/${each.key}/Dockerfile", "-t", - "${var.region}-docker.pkg.dev/${var.project}/sst-app-api/${each.key}:$COMMIT_SHA", + "${var.region}-docker.pkg.dev/${var.project}/edvise-api/${each.key}:$COMMIT_SHA", "-t", - "${var.region}-docker.pkg.dev/${var.project}/sst-app-api/${each.key}:latest", + "${var.region}-docker.pkg.dev/${var.project}/edvise-api/${each.key}:latest", "." ] } step { name = "gcr.io/cloud-builders/docker" - args = ["push", "${var.region}-docker.pkg.dev/${var.project}/sst-app-api/${each.key}:$COMMIT_SHA"] + args = ["push", "${var.region}-docker.pkg.dev/${var.project}/edvise-api/${each.key}:$COMMIT_SHA"] } step { name = "gcr.io/cloud-builders/docker" - args = ["push", "${var.region}-docker.pkg.dev/${var.project}/sst-app-api/${each.key}:latest"] + args = ["push", "${var.region}-docker.pkg.dev/${var.project}/edvise-api/${each.key}:latest"] } step { name = "gcr.io/cloud-builders/gcloud" @@ -113,7 +113,7 @@ resource "google_cloudbuild_trigger" "python_apps" { "deploy", "${var.environment}-${each.key}", "--image", - "${var.region}-docker.pkg.dev/${var.project}/sst-app-api/${each.key}:$COMMIT_SHA", + "${var.region}-docker.pkg.dev/${var.project}/edvise-api/${each.key}:$COMMIT_SHA", "--region", "${var.region}", ] @@ -133,7 +133,7 @@ resource "google_cloudbuild_trigger" "frontend" { for_each = var.environment == "dev" ? [1] : [] content { owner = "datakind" - name = "sst-app-ui" + name = "edvise-ui" push { branch = "develop" } @@ -144,7 +144,7 @@ resource "google_cloudbuild_trigger" "frontend" { content { ref = "refs/heads/develop" repo_type = "GITHUB" - uri = "https://github.com/datakind/sst-app-ui" + uri = "https://github.com/datakind/edvise-ui" } } build { @@ -174,29 +174,29 @@ resource "google_cloudbuild_trigger" "frontend" { "build", "--builder=gcr.io/buildpacks/builder", "--publish", - "${var.region}-docker.pkg.dev/${var.project}/sst-app-ui/frontend:$COMMIT_SHA", + "${var.region}-docker.pkg.dev/${var.project}/edvise-ui/frontend:$COMMIT_SHA", ] } step { id = "PULL and TAG latest" name = "gcr.io/cloud-builders/docker" - args = ["pull", "${var.region}-docker.pkg.dev/${var.project}/sst-app-ui/frontend:$COMMIT_SHA"] + args = ["pull", "${var.region}-docker.pkg.dev/${var.project}/edvise-ui/frontend:$COMMIT_SHA"] } step { name = "gcr.io/cloud-builders/docker" args = [ "tag", - "${var.region}-docker.pkg.dev/${var.project}/sst-app-ui/frontend:$COMMIT_SHA", - "${var.region}-docker.pkg.dev/${var.project}/sst-app-ui/frontend:latest" + "${var.region}-docker.pkg.dev/${var.project}/edvise-ui/frontend:$COMMIT_SHA", + "${var.region}-docker.pkg.dev/${var.project}/edvise-ui/frontend:latest" ] } step { name = "gcr.io/cloud-builders/docker" - args = ["push", "${var.region}-docker.pkg.dev/${var.project}/sst-app-ui/frontend:$COMMIT_SHA"] + args = ["push", "${var.region}-docker.pkg.dev/${var.project}/edvise-ui/frontend:$COMMIT_SHA"] } step { name = "gcr.io/cloud-builders/docker" - args = ["push", "${var.region}-docker.pkg.dev/${var.project}/sst-app-ui/frontend:latest"] + args = ["push", "${var.region}-docker.pkg.dev/${var.project}/edvise-ui/frontend:latest"] } step { id = "DEPLOY and RUN migration job" @@ -207,7 +207,7 @@ resource "google_cloudbuild_trigger" "frontend" { "jobs", "deploy", "${var.environment}-migrate", - "--image=${var.region}-docker.pkg.dev/${var.project}/sst-app-ui/frontend:$COMMIT_SHA", + "--image=${var.region}-docker.pkg.dev/${var.project}/edvise-ui/frontend:$COMMIT_SHA", "--region=${var.region}", "--execute-now" ] @@ -221,7 +221,7 @@ resource "google_cloudbuild_trigger" "frontend" { "deploy", "${var.environment}-frontend", "--image", - "${var.region}-docker.pkg.dev/${var.project}/sst-app-ui/frontend:$COMMIT_SHA", + "${var.region}-docker.pkg.dev/${var.project}/edvise-ui/frontend:$COMMIT_SHA", "--region", "${var.region}", ] @@ -240,7 +240,7 @@ resource "google_cloudbuild_trigger" "terraform" { source_to_build { ref = "refs/heads/develop" repo_type = "GITHUB" - uri = "https://github.com/datakind/sst-app-api" + uri = "https://github.com/datakind/edvise-api" } build { step { diff --git a/uv.lock b/uv.lock index 4c589d69..6a005fa3 100644 --- a/uv.lock +++ b/uv.lock @@ -12,11 +12,11 @@ resolution-markers = [ [[package]] name = "aiofiles" -version = "24.1.0" +version = "25.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, ] [[package]] @@ -30,7 +30,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.12.15" +version = "3.13.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -42,59 +42,59 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/dc/ef9394bde9080128ad401ac7ede185267ed637df03b51f05d14d1c99ad67/aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc", size = 703921, upload-time = "2025-07-29T05:49:43.584Z" }, - { url = "https://files.pythonhosted.org/packages/8f/42/63fccfc3a7ed97eb6e1a71722396f409c46b60a0552d8a56d7aad74e0df5/aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af", size = 480288, upload-time = "2025-07-29T05:49:47.851Z" }, - { url = "https://files.pythonhosted.org/packages/9c/a2/7b8a020549f66ea2a68129db6960a762d2393248f1994499f8ba9728bbed/aiohttp-3.12.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421", size = 468063, upload-time = "2025-07-29T05:49:49.789Z" }, - { url = "https://files.pythonhosted.org/packages/8f/f5/d11e088da9176e2ad8220338ae0000ed5429a15f3c9dfd983f39105399cd/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79", size = 1650122, upload-time = "2025-07-29T05:49:51.874Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6b/b60ce2757e2faed3d70ed45dafee48cee7bfb878785a9423f7e883f0639c/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77", size = 1624176, upload-time = "2025-07-29T05:49:53.805Z" }, - { url = "https://files.pythonhosted.org/packages/dd/de/8c9fde2072a1b72c4fadecf4f7d4be7a85b1d9a4ab333d8245694057b4c6/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c", size = 1696583, upload-time = "2025-07-29T05:49:55.338Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ad/07f863ca3d895a1ad958a54006c6dafb4f9310f8c2fdb5f961b8529029d3/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4", size = 1738896, upload-time = "2025-07-29T05:49:57.045Z" }, - { url = "https://files.pythonhosted.org/packages/20/43/2bd482ebe2b126533e8755a49b128ec4e58f1a3af56879a3abdb7b42c54f/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6", size = 1643561, upload-time = "2025-07-29T05:49:58.762Z" }, - { url = "https://files.pythonhosted.org/packages/23/40/2fa9f514c4cf4cbae8d7911927f81a1901838baf5e09a8b2c299de1acfe5/aiohttp-3.12.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2", size = 1583685, upload-time = "2025-07-29T05:50:00.375Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c3/94dc7357bc421f4fb978ca72a201a6c604ee90148f1181790c129396ceeb/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d", size = 1627533, upload-time = "2025-07-29T05:50:02.306Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3f/1f8911fe1844a07001e26593b5c255a685318943864b27b4e0267e840f95/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb", size = 1638319, upload-time = "2025-07-29T05:50:04.282Z" }, - { url = "https://files.pythonhosted.org/packages/4e/46/27bf57a99168c4e145ffee6b63d0458b9c66e58bb70687c23ad3d2f0bd17/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5", size = 1613776, upload-time = "2025-07-29T05:50:05.863Z" }, - { url = "https://files.pythonhosted.org/packages/0f/7e/1d2d9061a574584bb4ad3dbdba0da90a27fdc795bc227def3a46186a8bc1/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b", size = 1693359, upload-time = "2025-07-29T05:50:07.563Z" }, - { url = "https://files.pythonhosted.org/packages/08/98/bee429b52233c4a391980a5b3b196b060872a13eadd41c3a34be9b1469ed/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065", size = 1716598, upload-time = "2025-07-29T05:50:09.33Z" }, - { url = "https://files.pythonhosted.org/packages/57/39/b0314c1ea774df3392751b686104a3938c63ece2b7ce0ba1ed7c0b4a934f/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1", size = 1644940, upload-time = "2025-07-29T05:50:11.334Z" }, - { url = "https://files.pythonhosted.org/packages/1b/83/3dacb8d3f8f512c8ca43e3fa8a68b20583bd25636ffa4e56ee841ffd79ae/aiohttp-3.12.15-cp310-cp310-win32.whl", hash = "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a", size = 429239, upload-time = "2025-07-29T05:50:12.803Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f9/470b5daba04d558c9673ca2034f28d067f3202a40e17804425f0c331c89f/aiohttp-3.12.15-cp310-cp310-win_amd64.whl", hash = "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830", size = 452297, upload-time = "2025-07-29T05:50:14.266Z" }, - { url = "https://files.pythonhosted.org/packages/20/19/9e86722ec8e835959bd97ce8c1efa78cf361fa4531fca372551abcc9cdd6/aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117", size = 711246, upload-time = "2025-07-29T05:50:15.937Z" }, - { url = "https://files.pythonhosted.org/packages/71/f9/0a31fcb1a7d4629ac9d8f01f1cb9242e2f9943f47f5d03215af91c3c1a26/aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe", size = 483515, upload-time = "2025-07-29T05:50:17.442Z" }, - { url = "https://files.pythonhosted.org/packages/62/6c/94846f576f1d11df0c2e41d3001000527c0fdf63fce7e69b3927a731325d/aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9", size = 471776, upload-time = "2025-07-29T05:50:19.568Z" }, - { url = "https://files.pythonhosted.org/packages/f8/6c/f766d0aaafcee0447fad0328da780d344489c042e25cd58fde566bf40aed/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5", size = 1741977, upload-time = "2025-07-29T05:50:21.665Z" }, - { url = "https://files.pythonhosted.org/packages/17/e5/fb779a05ba6ff44d7bc1e9d24c644e876bfff5abe5454f7b854cace1b9cc/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728", size = 1690645, upload-time = "2025-07-29T05:50:23.333Z" }, - { url = "https://files.pythonhosted.org/packages/37/4e/a22e799c2035f5d6a4ad2cf8e7c1d1bd0923192871dd6e367dafb158b14c/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16", size = 1789437, upload-time = "2025-07-29T05:50:25.007Z" }, - { url = "https://files.pythonhosted.org/packages/28/e5/55a33b991f6433569babb56018b2fb8fb9146424f8b3a0c8ecca80556762/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0", size = 1828482, upload-time = "2025-07-29T05:50:26.693Z" }, - { url = "https://files.pythonhosted.org/packages/c6/82/1ddf0ea4f2f3afe79dffed5e8a246737cff6cbe781887a6a170299e33204/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b", size = 1730944, upload-time = "2025-07-29T05:50:28.382Z" }, - { url = "https://files.pythonhosted.org/packages/1b/96/784c785674117b4cb3877522a177ba1b5e4db9ce0fd519430b5de76eec90/aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd", size = 1668020, upload-time = "2025-07-29T05:50:30.032Z" }, - { url = "https://files.pythonhosted.org/packages/12/8a/8b75f203ea7e5c21c0920d84dd24a5c0e971fe1e9b9ebbf29ae7e8e39790/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8", size = 1716292, upload-time = "2025-07-29T05:50:31.983Z" }, - { url = "https://files.pythonhosted.org/packages/47/0b/a1451543475bb6b86a5cfc27861e52b14085ae232896a2654ff1231c0992/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50", size = 1711451, upload-time = "2025-07-29T05:50:33.989Z" }, - { url = "https://files.pythonhosted.org/packages/55/fd/793a23a197cc2f0d29188805cfc93aa613407f07e5f9da5cd1366afd9d7c/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676", size = 1691634, upload-time = "2025-07-29T05:50:35.846Z" }, - { url = "https://files.pythonhosted.org/packages/ca/bf/23a335a6670b5f5dfc6d268328e55a22651b440fca341a64fccf1eada0c6/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7", size = 1785238, upload-time = "2025-07-29T05:50:37.597Z" }, - { url = "https://files.pythonhosted.org/packages/57/4f/ed60a591839a9d85d40694aba5cef86dde9ee51ce6cca0bb30d6eb1581e7/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7", size = 1805701, upload-time = "2025-07-29T05:50:39.591Z" }, - { url = "https://files.pythonhosted.org/packages/85/e0/444747a9455c5de188c0f4a0173ee701e2e325d4b2550e9af84abb20cdba/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685", size = 1718758, upload-time = "2025-07-29T05:50:41.292Z" }, - { url = "https://files.pythonhosted.org/packages/36/ab/1006278d1ffd13a698e5dd4bfa01e5878f6bddefc296c8b62649753ff249/aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b", size = 428868, upload-time = "2025-07-29T05:50:43.063Z" }, - { url = "https://files.pythonhosted.org/packages/10/97/ad2b18700708452400278039272032170246a1bf8ec5d832772372c71f1a/aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d", size = 453273, upload-time = "2025-07-29T05:50:44.613Z" }, - { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333, upload-time = "2025-07-29T05:50:46.507Z" }, - { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948, upload-time = "2025-07-29T05:50:48.067Z" }, - { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787, upload-time = "2025-07-29T05:50:49.669Z" }, - { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590, upload-time = "2025-07-29T05:50:51.368Z" }, - { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241, upload-time = "2025-07-29T05:50:53.628Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335, upload-time = "2025-07-29T05:50:55.394Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491, upload-time = "2025-07-29T05:50:57.202Z" }, - { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929, upload-time = "2025-07-29T05:50:59.192Z" }, - { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733, upload-time = "2025-07-29T05:51:01.394Z" }, - { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790, upload-time = "2025-07-29T05:51:03.657Z" }, - { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245, upload-time = "2025-07-29T05:51:05.911Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899, upload-time = "2025-07-29T05:51:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459, upload-time = "2025-07-29T05:51:09.56Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434, upload-time = "2025-07-29T05:51:11.423Z" }, - { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045, upload-time = "2025-07-29T05:51:13.689Z" }, - { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591, upload-time = "2025-07-29T05:51:15.452Z" }, - { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266, upload-time = "2025-07-29T05:51:17.239Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994, upload-time = "2025-10-28T20:59:39.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/34/939730e66b716b76046dedfe0842995842fa906ccc4964bba414ff69e429/aiohttp-3.13.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2372b15a5f62ed37789a6b383ff7344fc5b9f243999b0cd9b629d8bc5f5b4155", size = 736471, upload-time = "2025-10-28T20:55:27.924Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/dcbdf2df7f6ca72b0bb4c0b4509701f2d8942cf54e29ca197389c214c07f/aiohttp-3.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7f8659a48995edee7229522984bd1009c1213929c769c2daa80b40fe49a180c", size = 493985, upload-time = "2025-10-28T20:55:29.456Z" }, + { url = "https://files.pythonhosted.org/packages/9d/87/71c8867e0a1d0882dcbc94af767784c3cb381c1c4db0943ab4aae4fed65e/aiohttp-3.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:939ced4a7add92296b0ad38892ce62b98c619288a081170695c6babe4f50e636", size = 489274, upload-time = "2025-10-28T20:55:31.134Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/46c24e8dae237295eaadd113edd56dee96ef6462adf19b88592d44891dc5/aiohttp-3.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6315fb6977f1d0dd41a107c527fee2ed5ab0550b7d885bc15fee20ccb17891da", size = 1668171, upload-time = "2025-10-28T20:55:36.065Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c6/4cdfb4440d0e28483681a48f69841fa5e39366347d66ef808cbdadddb20e/aiohttp-3.13.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6e7352512f763f760baaed2637055c49134fd1d35b37c2dedfac35bfe5cf8725", size = 1636036, upload-time = "2025-10-28T20:55:37.576Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/8708cf678628216fb678ab327a4e1711c576d6673998f4f43e86e9ae90dd/aiohttp-3.13.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e09a0a06348a2dd73e7213353c90d709502d9786219f69b731f6caa0efeb46f5", size = 1727975, upload-time = "2025-10-28T20:55:39.457Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2e/3ebfe12fdcb9b5f66e8a0a42dffcd7636844c8a018f261efb2419f68220b/aiohttp-3.13.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a09a6d073fb5789456545bdee2474d14395792faa0527887f2f4ec1a486a59d3", size = 1815823, upload-time = "2025-10-28T20:55:40.958Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/ca2ef819488cbb41844c6cf92ca6dd15b9441e6207c58e5ae0e0fc8d70ad/aiohttp-3.13.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b59d13c443f8e049d9e94099c7e412e34610f1f49be0f230ec656a10692a5802", size = 1669374, upload-time = "2025-10-28T20:55:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/1fe2e1179a0d91ce09c99069684aab619bf2ccde9b20bd6ca44f8837203e/aiohttp-3.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:20db2d67985d71ca033443a1ba2001c4b5693fe09b0e29f6d9358a99d4d62a8a", size = 1555315, upload-time = "2025-10-28T20:55:44.264Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2b/f3781899b81c45d7cbc7140cddb8a3481c195e7cbff8e36374759d2ab5a5/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:960c2fc686ba27b535f9fd2b52d87ecd7e4fd1cf877f6a5cba8afb5b4a8bd204", size = 1639140, upload-time = "2025-10-28T20:55:46.626Z" }, + { url = "https://files.pythonhosted.org/packages/72/27/c37e85cd3ece6f6c772e549bd5a253d0c122557b25855fb274224811e4f2/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6c00dbcf5f0d88796151e264a8eab23de2997c9303dd7c0bf622e23b24d3ce22", size = 1645496, upload-time = "2025-10-28T20:55:48.933Z" }, + { url = "https://files.pythonhosted.org/packages/66/20/3af1ab663151bd3780b123e907761cdb86ec2c4e44b2d9b195ebc91fbe37/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fed38a5edb7945f4d1bcabe2fcd05db4f6ec7e0e82560088b754f7e08d93772d", size = 1697625, upload-time = "2025-10-28T20:55:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/95/eb/ae5cab15efa365e13d56b31b0d085a62600298bf398a7986f8388f73b598/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b395bbca716c38bef3c764f187860e88c724b342c26275bc03e906142fc5964f", size = 1542025, upload-time = "2025-10-28T20:55:51.861Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2d/1683e8d67ec72d911397fe4e575688d2a9b8f6a6e03c8fdc9f3fd3d4c03f/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:204ffff2426c25dfda401ba08da85f9c59525cdc42bda26660463dd1cbcfec6f", size = 1714918, upload-time = "2025-10-28T20:55:53.515Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ffe8e0e1c57c5e542d47ffa1fcf95ef2b3ea573bf7c4d2ee877252431efc/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:05c4dd3c48fb5f15db31f57eb35374cb0c09afdde532e7fb70a75aede0ed30f6", size = 1656113, upload-time = "2025-10-28T20:55:55.438Z" }, + { url = "https://files.pythonhosted.org/packages/0d/42/d511aff5c3a2b06c09d7d214f508a4ad8ac7799817f7c3d23e7336b5e896/aiohttp-3.13.2-cp310-cp310-win32.whl", hash = "sha256:e574a7d61cf10351d734bcddabbe15ede0eaa8a02070d85446875dc11189a251", size = 432290, upload-time = "2025-10-28T20:55:56.96Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ea/1c2eb7098b5bad4532994f2b7a8228d27674035c9b3234fe02c37469ef14/aiohttp-3.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:364f55663085d658b8462a1c3f17b2b84a5c2e1ba858e1b79bff7b2e24ad1514", size = 455075, upload-time = "2025-10-28T20:55:58.373Z" }, + { url = "https://files.pythonhosted.org/packages/35/74/b321e7d7ca762638cdf8cdeceb39755d9c745aff7a64c8789be96ddf6e96/aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0", size = 743409, upload-time = "2025-10-28T20:56:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/99/3d/91524b905ec473beaf35158d17f82ef5a38033e5809fe8742e3657cdbb97/aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb", size = 497006, upload-time = "2025-10-28T20:56:01.85Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d3/7f68bc02a67716fe80f063e19adbd80a642e30682ce74071269e17d2dba1/aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9", size = 493195, upload-time = "2025-10-28T20:56:03.314Z" }, + { url = "https://files.pythonhosted.org/packages/98/31/913f774a4708775433b7375c4f867d58ba58ead833af96c8af3621a0d243/aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613", size = 1747759, upload-time = "2025-10-28T20:56:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/e8/63/04efe156f4326f31c7c4a97144f82132c3bb21859b7bb84748d452ccc17c/aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead", size = 1704456, upload-time = "2025-10-28T20:56:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/8e/02/4e16154d8e0a9cf4ae76f692941fd52543bbb148f02f098ca73cab9b1c1b/aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780", size = 1807572, upload-time = "2025-10-28T20:56:08.558Z" }, + { url = "https://files.pythonhosted.org/packages/34/58/b0583defb38689e7f06798f0285b1ffb3a6fb371f38363ce5fd772112724/aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a", size = 1895954, upload-time = "2025-10-28T20:56:10.545Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f3/083907ee3437425b4e376aa58b2c915eb1a33703ec0dc30040f7ae3368c6/aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592", size = 1747092, upload-time = "2025-10-28T20:56:12.118Z" }, + { url = "https://files.pythonhosted.org/packages/ac/61/98a47319b4e425cc134e05e5f3fc512bf9a04bf65aafd9fdcda5d57ec693/aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab", size = 1606815, upload-time = "2025-10-28T20:56:14.191Z" }, + { url = "https://files.pythonhosted.org/packages/97/4b/e78b854d82f66bb974189135d31fce265dee0f5344f64dd0d345158a5973/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30", size = 1723789, upload-time = "2025-10-28T20:56:16.101Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fc/9d2ccc794fc9b9acd1379d625c3a8c64a45508b5091c546dea273a41929e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40", size = 1718104, upload-time = "2025-10-28T20:56:17.655Z" }, + { url = "https://files.pythonhosted.org/packages/66/65/34564b8765ea5c7d79d23c9113135d1dd3609173da13084830f1507d56cf/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948", size = 1785584, upload-time = "2025-10-28T20:56:19.238Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/f6a7a426e02fc82781afd62016417b3948e2207426d90a0e478790d1c8a4/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf", size = 1595126, upload-time = "2025-10-28T20:56:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c7/8e22d5d28f94f67d2af496f14a83b3c155d915d1fe53d94b66d425ec5b42/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782", size = 1800665, upload-time = "2025-10-28T20:56:22.922Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/91133c8b68b1da9fc16555706aa7276fdf781ae2bb0876c838dd86b8116e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8", size = 1739532, upload-time = "2025-10-28T20:56:25.924Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/3747644d26a998774b21a616016620293ddefa4d63af6286f389aedac844/aiohttp-3.13.2-cp311-cp311-win32.whl", hash = "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec", size = 431876, upload-time = "2025-10-28T20:56:27.524Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/688462108c1a00eb9f05765331c107f95ae86f6b197b865d29e930b7e462/aiohttp-3.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c", size = 456205, upload-time = "2025-10-28T20:56:29.062Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/01f00e9856d0a73260e86dd8ed0c2234a466c5c1712ce1c281548df39777/aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", size = 737623, upload-time = "2025-10-28T20:56:30.797Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1b/4be39c445e2b2bd0aab4ba736deb649fabf14f6757f405f0c9685019b9e9/aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", size = 492664, upload-time = "2025-10-28T20:56:32.708Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/d35dcfea8050e131cdd731dff36434390479b4045a8d0b9d7111b0a968f1/aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", size = 491808, upload-time = "2025-10-28T20:56:34.57Z" }, + { url = "https://files.pythonhosted.org/packages/00/29/8e4609b93e10a853b65f8291e64985de66d4f5848c5637cddc70e98f01f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", size = 1738863, upload-time = "2025-10-28T20:56:36.377Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fa/4ebdf4adcc0def75ced1a0d2d227577cd7b1b85beb7edad85fcc87693c75/aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", size = 1700586, upload-time = "2025-10-28T20:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/da/04/73f5f02ff348a3558763ff6abe99c223381b0bace05cd4530a0258e52597/aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", size = 1768625, upload-time = "2025-10-28T20:56:39.75Z" }, + { url = "https://files.pythonhosted.org/packages/f8/49/a825b79ffec124317265ca7d2344a86bcffeb960743487cb11988ffb3494/aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", size = 1867281, upload-time = "2025-10-28T20:56:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/b9/48/adf56e05f81eac31edcfae45c90928f4ad50ef2e3ea72cb8376162a368f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", size = 1752431, upload-time = "2025-10-28T20:56:43.162Z" }, + { url = "https://files.pythonhosted.org/packages/30/ab/593855356eead019a74e862f21523db09c27f12fd24af72dbc3555b9bfd9/aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", size = 1562846, upload-time = "2025-10-28T20:56:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/39/0f/9f3d32271aa8dc35036e9668e31870a9d3b9542dd6b3e2c8a30931cb27ae/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", size = 1699606, upload-time = "2025-10-28T20:56:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3c/52d2658c5699b6ef7692a3f7128b2d2d4d9775f2a68093f74bca06cf01e1/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", size = 1720663, upload-time = "2025-10-28T20:56:48.528Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d4/8f8f3ff1fb7fb9e3f04fcad4e89d8a1cd8fc7d05de67e3de5b15b33008ff/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", size = 1737939, upload-time = "2025-10-28T20:56:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/03/d3/ddd348f8a27a634daae39a1b8e291ff19c77867af438af844bf8b7e3231b/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", size = 1555132, upload-time = "2025-10-28T20:56:52.568Z" }, + { url = "https://files.pythonhosted.org/packages/39/b8/46790692dc46218406f94374903ba47552f2f9f90dad554eed61bfb7b64c/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", size = 1764802, upload-time = "2025-10-28T20:56:54.292Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e4/19ce547b58ab2a385e5f0b8aa3db38674785085abcf79b6e0edd1632b12f/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", size = 1719512, upload-time = "2025-10-28T20:56:56.428Z" }, + { url = "https://files.pythonhosted.org/packages/70/30/6355a737fed29dcb6dfdd48682d5790cb5eab050f7b4e01f49b121d3acad/aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", size = 426690, upload-time = "2025-10-28T20:56:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/b10ac09069973d112de6ef980c1f6bb31cb7dcd0bc363acbdad58f927873/aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", size = 453465, upload-time = "2025-10-28T20:57:00.795Z" }, ] [[package]] @@ -112,7 +112,7 @@ wheels = [ [[package]] name = "alembic" -version = "1.16.4" +version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mako" }, @@ -120,9 +120,9 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/83/52/72e791b75c6b1efa803e491f7cbab78e963695e76d4ada05385252927e76/alembic-1.16.4.tar.gz", hash = "sha256:efab6ada0dd0fae2c92060800e0bf5c1dc26af15a10e02fb4babff164b4725e2", size = 1968161, upload-time = "2025-07-10T16:17:20.192Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/b6/2a81d7724c0c124edc5ec7a167e85858b6fd31b9611c6fb8ecf617b7e2d3/alembic-1.17.1.tar.gz", hash = "sha256:8a289f6778262df31571d29cca4c7fbacd2f0f582ea0816f4c399b6da7528486", size = 1981285, upload-time = "2025-10-29T00:23:16.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/62/96b5217b742805236614f05904541000f55422a6060a90d7fd4ce26c172d/alembic-1.16.4-py3-none-any.whl", hash = "sha256:b05e51e8e82efc1abd14ba2af6392897e145930c3e0a2faf2b0da2f7f7fd660d", size = 247026, upload-time = "2025-07-10T16:17:21.845Z" }, + { url = "https://files.pythonhosted.org/packages/a5/32/7df1d81ec2e50fb661944a35183d87e62d3f6c6d9f8aff64a4f245226d55/alembic-1.17.1-py3-none-any.whl", hash = "sha256:cbc2386e60f89608bb63f30d2d6cc66c7aaed1fe105bd862828600e5ad167023", size = 247848, upload-time = "2025-10-29T00:23:18.79Z" }, ] [[package]] @@ -136,7 +136,7 @@ wheels = [ [[package]] name = "anyio" -version = "4.10.0" +version = "4.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, @@ -144,9 +144,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, ] [[package]] @@ -198,15 +198,15 @@ wheels = [ [[package]] name = "arrow" -version = "1.3.0" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, - { name = "types-python-dateutil" }, + { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/00/0f6e8fcdb23ea632c866620cc872729ff43ed91d284c866b515c6342b173/arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85", size = 131960, upload-time = "2023-09-30T22:11:18.25Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", size = 66419, upload-time = "2023-09-30T22:11:16.072Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, ] [[package]] @@ -253,11 +253,11 @@ wheels = [ [[package]] name = "attrs" -version = "25.3.0" +version = "25.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] [[package]] @@ -303,15 +303,15 @@ wheels = [ [[package]] name = "beautifulsoup4" -version = "4.13.4" +version = "4.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/e9/df2358efd7659577435e2177bfa69cba6c33216681af51a707193dec162a/beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e", size = 625822, upload-time = "2025-09-29T10:05:42.613Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" }, + { url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392, upload-time = "2025-09-29T10:05:43.771Z" }, ] [[package]] @@ -346,19 +346,16 @@ wheels = [ [[package]] name = "bleach" -version = "6.2.0" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "packaging" }, + { name = "six" }, { name = "webencodings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/9a/0e33f5054c54d349ea62c277191c020c2d6ef1d65ab2cb1993f91ec846d1/bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f", size = 203083, upload-time = "2024-10-29T18:30:40.477Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/a3/217842324374fd3fb33db0eb4c2909ccf3ecc5a94f458088ac68581f8314/bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da", size = 195798, upload-time = "2021-08-25T14:58:31.434Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406, upload-time = "2024-10-29T18:30:38.186Z" }, -] - -[package.optional-dependencies] -css = [ - { name = "tinycss2" }, + { url = "https://files.pythonhosted.org/packages/64/cc/74d634e1e5659742973a23bb441404c53a7bedb6cd3962109ca5efb703e8/bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994", size = 157937, upload-time = "2021-08-25T15:14:13.732Z" }, ] [[package]] @@ -370,6 +367,64 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, ] +[[package]] +name = "brotli" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/10/a090475284fc4a71aed40a96f32e44a7fe5bda39687353dd977720b211b6/brotli-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b90b767916ac44e93a8e28ce6adf8d551e43affb512f2377c732d486ac6514e", size = 863089, upload-time = "2025-11-05T18:38:01.181Z" }, + { url = "https://files.pythonhosted.org/packages/03/41/17416630e46c07ac21e378c3464815dd2e120b441e641bc516ac32cc51d2/brotli-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6be67c19e0b0c56365c6a76e393b932fb0e78b3b56b711d180dd7013cb1fd984", size = 445442, upload-time = "2025-11-05T18:38:02.434Z" }, + { url = "https://files.pythonhosted.org/packages/24/31/90cc06584deb5d4fcafc0985e37741fc6b9717926a78674bbb3ce018957e/brotli-1.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bbd5b5ccd157ae7913750476d48099aaf507a79841c0d04a9db4415b14842de", size = 1532658, upload-time = "2025-11-05T18:38:03.588Z" }, + { url = "https://files.pythonhosted.org/packages/62/17/33bf0c83bcbc96756dfd712201d87342732fad70bb3472c27e833a44a4f9/brotli-1.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f3c908bcc404c90c77d5a073e55271a0a498f4e0756e48127c35d91cf155947", size = 1631241, upload-time = "2025-11-05T18:38:04.582Z" }, + { url = "https://files.pythonhosted.org/packages/48/10/f47854a1917b62efe29bc98ac18e5d4f71df03f629184575b862ef2e743b/brotli-1.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b557b29782a643420e08d75aea889462a4a8796e9a6cf5621ab05a3f7da8ef2", size = 1424307, upload-time = "2025-11-05T18:38:05.587Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b7/f88eb461719259c17483484ea8456925ee057897f8e64487d76e24e5e38d/brotli-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81da1b229b1889f25adadc929aeb9dbc4e922bd18561b65b08dd9343cfccca84", size = 1488208, upload-time = "2025-11-05T18:38:06.613Z" }, + { url = "https://files.pythonhosted.org/packages/26/59/41bbcb983a0c48b0b8004203e74706c6b6e99a04f3c7ca6f4f41f364db50/brotli-1.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ff09cd8c5eec3b9d02d2408db41be150d8891c5566addce57513bf546e3d6c6d", size = 1597574, upload-time = "2025-11-05T18:38:07.838Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e6/8c89c3bdabbe802febb4c5c6ca224a395e97913b5df0dff11b54f23c1788/brotli-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a1778532b978d2536e79c05dac2d8cd857f6c55cd0c95ace5b03740824e0e2f1", size = 1492109, upload-time = "2025-11-05T18:38:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9a/4b19d4310b2dbd545c0c33f176b0528fa68c3cd0754e34b2f2bcf56548ae/brotli-1.2.0-cp310-cp310-win32.whl", hash = "sha256:b232029d100d393ae3c603c8ffd7e3fe6f798c5e28ddca5feabb8e8fdb732997", size = 334461, upload-time = "2025-11-05T18:38:10.729Z" }, + { url = "https://files.pythonhosted.org/packages/ac/39/70981d9f47705e3c2b95c0847dfa3e7a37aa3b7c6030aedc4873081ed005/brotli-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef87b8ab2704da227e83a246356a2b179ef826f550f794b2c52cddb4efbd0196", size = 369035, upload-time = "2025-11-05T18:38:11.827Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ef/f285668811a9e1ddb47a18cb0b437d5fc2760d537a2fe8a57875ad6f8448/brotli-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744", size = 863110, upload-time = "2025-11-05T18:38:12.978Z" }, + { url = "https://files.pythonhosted.org/packages/50/62/a3b77593587010c789a9d6eaa527c79e0848b7b860402cc64bc0bc28a86c/brotli-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f", size = 445438, upload-time = "2025-11-05T18:38:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e1/7fadd47f40ce5549dc44493877db40292277db373da5053aff181656e16e/brotli-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd", size = 1534420, upload-time = "2025-11-05T18:38:15.111Z" }, + { url = "https://files.pythonhosted.org/packages/12/8b/1ed2f64054a5a008a4ccd2f271dbba7a5fb1a3067a99f5ceadedd4c1d5a7/brotli-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe", size = 1632619, upload-time = "2025-11-05T18:38:16.094Z" }, + { url = "https://files.pythonhosted.org/packages/89/5a/7071a621eb2d052d64efd5da2ef55ecdac7c3b0c6e4f9d519e9c66d987ef/brotli-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a", size = 1426014, upload-time = "2025-11-05T18:38:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/26/6d/0971a8ea435af5156acaaccec1a505f981c9c80227633851f2810abd252a/brotli-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b", size = 1489661, upload-time = "2025-11-05T18:38:18.41Z" }, + { url = "https://files.pythonhosted.org/packages/f3/75/c1baca8b4ec6c96a03ef8230fab2a785e35297632f402ebb1e78a1e39116/brotli-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3", size = 1599150, upload-time = "2025-11-05T18:38:19.792Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1a/23fcfee1c324fd48a63d7ebf4bac3a4115bdb1b00e600f80f727d850b1ae/brotli-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae", size = 1493505, upload-time = "2025-11-05T18:38:20.913Z" }, + { url = "https://files.pythonhosted.org/packages/36/e5/12904bbd36afeef53d45a84881a4810ae8810ad7e328a971ebbfd760a0b3/brotli-1.2.0-cp311-cp311-win32.whl", hash = "sha256:aa47441fa3026543513139cb8926a92a8e305ee9c71a6209ef7a97d91640ea03", size = 334451, upload-time = "2025-11-05T18:38:21.94Z" }, + { url = "https://files.pythonhosted.org/packages/02/8b/ecb5761b989629a4758c394b9301607a5880de61ee2ee5fe104b87149ebc/brotli-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24", size = 369035, upload-time = "2025-11-05T18:38:22.941Z" }, + { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543, upload-time = "2025-11-05T18:38:24.183Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288, upload-time = "2025-11-05T18:38:25.139Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071, upload-time = "2025-11-05T18:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913, upload-time = "2025-11-05T18:38:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762, upload-time = "2025-11-05T18:38:28.295Z" }, + { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494, upload-time = "2025-11-05T18:38:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302, upload-time = "2025-11-05T18:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913, upload-time = "2025-11-05T18:38:31.618Z" }, + { url = "https://files.pythonhosted.org/packages/62/28/4d00cb9bd76a6357a66fcd54b4b6d70288385584063f4b07884c1e7286ac/brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", size = 334362, upload-time = "2025-11-05T18:38:32.939Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4e/bc1dcac9498859d5e353c9b153627a3752868a9d5f05ce8dedd81a2354ab/brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", size = 369115, upload-time = "2025-11-05T18:38:33.765Z" }, +] + +[[package]] +name = "brotlicffi" +version = "1.2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/85/57c314a6b35336efbbdc13e5fc9ae13f6b60a0647cfa7c1221178ac6d8ae/brotlicffi-1.2.0.0.tar.gz", hash = "sha256:34345d8d1f9d534fcac2249e57a4c3c8801a33c9942ff9f8574f67a175e17adb", size = 476682, upload-time = "2025-11-21T18:17:57.334Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/df/a72b284d8c7bef0ed5756b41c2eb7d0219a1dd6ac6762f1c7bdbc31ef3af/brotlicffi-1.2.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:9458d08a7ccde8e3c0afedbf2c70a8263227a68dea5ab13590593f4c0a4fd5f4", size = 432340, upload-time = "2025-11-21T18:17:42.277Z" }, + { url = "https://files.pythonhosted.org/packages/74/2b/cc55a2d1d6fb4f5d458fba44a3d3f91fb4320aa14145799fd3a996af0686/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84e3d0020cf1bd8b8131f4a07819edee9f283721566fe044a20ec792ca8fd8b7", size = 1534002, upload-time = "2025-11-21T18:17:43.746Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9c/d51486bf366fc7d6735f0e46b5b96ca58dc005b250263525a1eea3cd5d21/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33cfb408d0cff64cd50bef268c0fed397c46fbb53944aa37264148614a62e990", size = 1536547, upload-time = "2025-11-21T18:17:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/293a9a0a7caf17e6e657668bebb92dfe730305999fe8c0e2703b8888789c/brotlicffi-1.2.0.0-cp38-abi3-win32.whl", hash = "sha256:23e5c912fdc6fd37143203820230374d24babd078fc054e18070a647118158f6", size = 343085, upload-time = "2025-11-21T18:17:48.887Z" }, + { url = "https://files.pythonhosted.org/packages/07/6b/6e92009df3b8b7272f85a0992b306b61c34b7ea1c4776643746e61c380ac/brotlicffi-1.2.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:f139a7cdfe4ae7859513067b736eb44d19fae1186f9e99370092f6915216451b", size = 378586, upload-time = "2025-11-21T18:17:50.531Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ec/52488a0563f1663e2ccc75834b470650f4b8bcdea3132aef3bf67219c661/brotlicffi-1.2.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fa102a60e50ddbd08de86a63431a722ea216d9bc903b000bf544149cc9b823dc", size = 402002, upload-time = "2025-11-21T18:17:51.76Z" }, + { url = "https://files.pythonhosted.org/packages/e4/63/d4aea4835fd97da1401d798d9b8ba77227974de565faea402f520b37b10f/brotlicffi-1.2.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d3c4332fc808a94e8c1035950a10d04b681b03ab585ce897ae2a360d479037c", size = 406447, upload-time = "2025-11-21T18:17:53.614Z" }, + { url = "https://files.pythonhosted.org/packages/62/4e/5554ecb2615ff035ef8678d4e419549a0f7a28b3f096b272174d656749fb/brotlicffi-1.2.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb4eb5830026b79a93bf503ad32b2c5257315e9ffc49e76b2715cffd07c8e3db", size = 402521, upload-time = "2025-11-21T18:17:54.875Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d3/b07f8f125ac52bbee5dc00ef0d526f820f67321bf4184f915f17f50a4657/brotlicffi-1.2.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3832c66e00d6d82087f20a972b2fc03e21cd99ef22705225a6f8f418a9158ecc", size = 374730, upload-time = "2025-11-21T18:17:56.334Z" }, +] + [[package]] name = "cachetools" version = "5.5.2" @@ -381,111 +436,128 @@ wheels = [ [[package]] name = "certifi" -version = "2025.8.3" +version = "2025.10.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, ] [[package]] name = "cffi" -version = "1.17.1" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, - { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, - { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, - { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, - { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, - { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, - { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, - { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, - { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, - { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, - { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, - { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, - { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, - { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, - { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, - { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, - { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, - { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, - { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, - { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, - { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, - { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, - { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, - { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, - { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, - { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, - { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, - { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, - { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, - { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, - { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, - { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, - { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] [[package]] name = "click" -version = "8.2.1" +version = "8.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, ] [[package]] @@ -511,11 +583,11 @@ pymysql = [ [[package]] name = "cloudpickle" -version = "3.1.1" +version = "3.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/39/069100b84d7418bc358d81669d5748efb14b9cceacd2f9c75f550424132f/cloudpickle-3.1.1.tar.gz", hash = "sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64", size = 22113, upload-time = "2025-01-14T17:02:05.085Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl", hash = "sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e", size = 20992, upload-time = "2025-01-14T17:02:02.417Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, ] [[package]] @@ -673,49 +745,65 @@ wheels = [ [[package]] name = "cryptography" -version = "45.0.6" +version = "46.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/0d/d13399c94234ee8f3df384819dc67e0c5ce215fb751d567a55a1f4b028c7/cryptography-45.0.6.tar.gz", hash = "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719", size = 744949, upload-time = "2025-08-05T23:59:27.93Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/29/2793d178d0eda1ca4a09a7c4e09a5185e75738cc6d526433e8663b460ea6/cryptography-45.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74", size = 7042702, upload-time = "2025-08-05T23:58:23.464Z" }, - { url = "https://files.pythonhosted.org/packages/b3/b6/cabd07410f222f32c8d55486c464f432808abaa1f12af9afcbe8f2f19030/cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f", size = 4206483, upload-time = "2025-08-05T23:58:27.132Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9e/f9c7d36a38b1cfeb1cc74849aabe9bf817990f7603ff6eb485e0d70e0b27/cryptography-45.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf", size = 4429679, upload-time = "2025-08-05T23:58:29.152Z" }, - { url = "https://files.pythonhosted.org/packages/9c/2a/4434c17eb32ef30b254b9e8b9830cee4e516f08b47fdd291c5b1255b8101/cryptography-45.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5", size = 4210553, upload-time = "2025-08-05T23:58:30.596Z" }, - { url = "https://files.pythonhosted.org/packages/ef/1d/09a5df8e0c4b7970f5d1f3aff1b640df6d4be28a64cae970d56c6cf1c772/cryptography-45.0.6-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2", size = 3894499, upload-time = "2025-08-05T23:58:32.03Z" }, - { url = "https://files.pythonhosted.org/packages/79/62/120842ab20d9150a9d3a6bdc07fe2870384e82f5266d41c53b08a3a96b34/cryptography-45.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08", size = 4458484, upload-time = "2025-08-05T23:58:33.526Z" }, - { url = "https://files.pythonhosted.org/packages/fd/80/1bc3634d45ddfed0871bfba52cf8f1ad724761662a0c792b97a951fb1b30/cryptography-45.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402", size = 4210281, upload-time = "2025-08-05T23:58:35.445Z" }, - { url = "https://files.pythonhosted.org/packages/7d/fe/ffb12c2d83d0ee625f124880a1f023b5878f79da92e64c37962bbbe35f3f/cryptography-45.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42", size = 4456890, upload-time = "2025-08-05T23:58:36.923Z" }, - { url = "https://files.pythonhosted.org/packages/8c/8e/b3f3fe0dc82c77a0deb5f493b23311e09193f2268b77196ec0f7a36e3f3e/cryptography-45.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05", size = 4333247, upload-time = "2025-08-05T23:58:38.781Z" }, - { url = "https://files.pythonhosted.org/packages/b3/a6/c3ef2ab9e334da27a1d7b56af4a2417d77e7806b2e0f90d6267ce120d2e4/cryptography-45.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453", size = 4565045, upload-time = "2025-08-05T23:58:40.415Z" }, - { url = "https://files.pythonhosted.org/packages/31/c3/77722446b13fa71dddd820a5faab4ce6db49e7e0bf8312ef4192a3f78e2f/cryptography-45.0.6-cp311-abi3-win32.whl", hash = "sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159", size = 2928923, upload-time = "2025-08-05T23:58:41.919Z" }, - { url = "https://files.pythonhosted.org/packages/38/63/a025c3225188a811b82932a4dcc8457a26c3729d81578ccecbcce2cb784e/cryptography-45.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec", size = 3403805, upload-time = "2025-08-05T23:58:43.792Z" }, - { url = "https://files.pythonhosted.org/packages/5b/af/bcfbea93a30809f126d51c074ee0fac5bd9d57d068edf56c2a73abedbea4/cryptography-45.0.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0", size = 7020111, upload-time = "2025-08-05T23:58:45.316Z" }, - { url = "https://files.pythonhosted.org/packages/98/c6/ea5173689e014f1a8470899cd5beeb358e22bb3cf5a876060f9d1ca78af4/cryptography-45.0.6-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394", size = 4198169, upload-time = "2025-08-05T23:58:47.121Z" }, - { url = "https://files.pythonhosted.org/packages/ba/73/b12995edc0c7e2311ffb57ebd3b351f6b268fed37d93bfc6f9856e01c473/cryptography-45.0.6-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9", size = 4421273, upload-time = "2025-08-05T23:58:48.557Z" }, - { url = "https://files.pythonhosted.org/packages/f7/6e/286894f6f71926bc0da67408c853dd9ba953f662dcb70993a59fd499f111/cryptography-45.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3", size = 4199211, upload-time = "2025-08-05T23:58:50.139Z" }, - { url = "https://files.pythonhosted.org/packages/de/34/a7f55e39b9623c5cb571d77a6a90387fe557908ffc44f6872f26ca8ae270/cryptography-45.0.6-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3", size = 3883732, upload-time = "2025-08-05T23:58:52.253Z" }, - { url = "https://files.pythonhosted.org/packages/f9/b9/c6d32edbcba0cd9f5df90f29ed46a65c4631c4fbe11187feb9169c6ff506/cryptography-45.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301", size = 4450655, upload-time = "2025-08-05T23:58:53.848Z" }, - { url = "https://files.pythonhosted.org/packages/77/2d/09b097adfdee0227cfd4c699b3375a842080f065bab9014248933497c3f9/cryptography-45.0.6-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5", size = 4198956, upload-time = "2025-08-05T23:58:55.209Z" }, - { url = "https://files.pythonhosted.org/packages/55/66/061ec6689207d54effdff535bbdf85cc380d32dd5377173085812565cf38/cryptography-45.0.6-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016", size = 4449859, upload-time = "2025-08-05T23:58:56.639Z" }, - { url = "https://files.pythonhosted.org/packages/41/ff/e7d5a2ad2d035e5a2af116e1a3adb4d8fcd0be92a18032917a089c6e5028/cryptography-45.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3", size = 4320254, upload-time = "2025-08-05T23:58:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/82/27/092d311af22095d288f4db89fcaebadfb2f28944f3d790a4cf51fe5ddaeb/cryptography-45.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9", size = 4554815, upload-time = "2025-08-05T23:59:00.283Z" }, - { url = "https://files.pythonhosted.org/packages/7e/01/aa2f4940262d588a8fdf4edabe4cda45854d00ebc6eaac12568b3a491a16/cryptography-45.0.6-cp37-abi3-win32.whl", hash = "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02", size = 2912147, upload-time = "2025-08-05T23:59:01.716Z" }, - { url = "https://files.pythonhosted.org/packages/0a/bc/16e0276078c2de3ceef6b5a34b965f4436215efac45313df90d55f0ba2d2/cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b", size = 3390459, upload-time = "2025-08-05T23:59:03.358Z" }, - { url = "https://files.pythonhosted.org/packages/56/d2/4482d97c948c029be08cb29854a91bd2ae8da7eb9c4152461f1244dcea70/cryptography-45.0.6-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:705bb7c7ecc3d79a50f236adda12ca331c8e7ecfbea51edd931ce5a7a7c4f012", size = 3576812, upload-time = "2025-08-05T23:59:04.833Z" }, - { url = "https://files.pythonhosted.org/packages/ec/24/55fc238fcaa122855442604b8badb2d442367dfbd5a7ca4bb0bd346e263a/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:826b46dae41a1155a0c0e66fafba43d0ede1dc16570b95e40c4d83bfcf0a451d", size = 4141694, upload-time = "2025-08-05T23:59:06.66Z" }, - { url = "https://files.pythonhosted.org/packages/f9/7e/3ea4fa6fbe51baf3903806a0241c666b04c73d2358a3ecce09ebee8b9622/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cc4d66f5dc4dc37b89cfef1bd5044387f7a1f6f0abb490815628501909332d5d", size = 4375010, upload-time = "2025-08-05T23:59:08.14Z" }, - { url = "https://files.pythonhosted.org/packages/50/42/ec5a892d82d2a2c29f80fc19ced4ba669bca29f032faf6989609cff1f8dc/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f68f833a9d445cc49f01097d95c83a850795921b3f7cc6488731e69bde3288da", size = 4141377, upload-time = "2025-08-05T23:59:09.584Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d7/246c4c973a22b9c2931999da953a2c19cae7c66b9154c2d62ffed811225e/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3b5bf5267e98661b9b888a9250d05b063220dfa917a8203744454573c7eb79db", size = 4374609, upload-time = "2025-08-05T23:59:11.923Z" }, - { url = "https://files.pythonhosted.org/packages/78/6d/c49ccf243f0a1b0781c2a8de8123ee552f0c8a417c6367a24d2ecb7c11b3/cryptography-45.0.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2384f2ab18d9be88a6e4f8972923405e2dbb8d3e16c6b43f15ca491d7831bd18", size = 3322156, upload-time = "2025-08-05T23:59:13.597Z" }, - { url = "https://files.pythonhosted.org/packages/61/69/c252de4ec047ba2f567ecb53149410219577d408c2aea9c989acae7eafce/cryptography-45.0.6-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fc022c1fa5acff6def2fc6d7819bbbd31ccddfe67d075331a65d9cfb28a20983", size = 3584669, upload-time = "2025-08-05T23:59:15.431Z" }, - { url = "https://files.pythonhosted.org/packages/e3/fe/deea71e9f310a31fe0a6bfee670955152128d309ea2d1c79e2a5ae0f0401/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3de77e4df42ac8d4e4d6cdb342d989803ad37707cf8f3fbf7b088c9cbdd46427", size = 4153022, upload-time = "2025-08-05T23:59:16.954Z" }, - { url = "https://files.pythonhosted.org/packages/60/45/a77452f5e49cb580feedba6606d66ae7b82c128947aa754533b3d1bd44b0/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:599c8d7df950aa68baa7e98f7b73f4f414c9f02d0e8104a30c0182a07732638b", size = 4386802, upload-time = "2025-08-05T23:59:18.55Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b9/a2f747d2acd5e3075fdf5c145c7c3568895daaa38b3b0c960ef830db6cdc/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:31a2b9a10530a1cb04ffd6aa1cd4d3be9ed49f7d77a4dafe198f3b382f41545c", size = 4152706, upload-time = "2025-08-05T23:59:20.044Z" }, - { url = "https://files.pythonhosted.org/packages/81/ec/381b3e8d0685a3f3f304a382aa3dfce36af2d76467da0fd4bb21ddccc7b2/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e5b3dda1b00fb41da3af4c5ef3f922a200e33ee5ba0f0bc9ecf0b0c173958385", size = 4386740, upload-time = "2025-08-05T23:59:21.525Z" }, - { url = "https://files.pythonhosted.org/packages/0a/76/cf8d69da8d0b5ecb0db406f24a63a3f69ba5e791a11b782aeeefef27ccbb/cryptography-45.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:629127cfdcdc6806dfe234734d7cb8ac54edaf572148274fa377a7d3405b0043", size = 3331874, upload-time = "2025-08-05T23:59:23.017Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, +] + +[[package]] +name = "cssselect2" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tinycss2" }, + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/86/fd7f58fc498b3166f3a7e8e0cddb6e620fe1da35b02248b1bd59e95dbaaa/cssselect2-0.8.0.tar.gz", hash = "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a", size = 35716, upload-time = "2025-03-05T14:46:07.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/e7/aa315e6a749d9b96c2504a1ba0ba031ba2d0517e972ce22682e3fccecb09/cssselect2-0.8.0-py3-none-any.whl", hash = "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e", size = 15454, upload-time = "2025-03-05T14:46:06.463Z" }, ] [[package]] @@ -727,6 +815,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] +[[package]] +name = "databricks-connect" +version = "16.1.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", + "python_full_version < '3.11' and sys_platform == 'win32'", +] +dependencies = [ + { name = "databricks-sdk", marker = "python_full_version < '3.12'" }, + { name = "googleapis-common-protos", marker = "python_full_version < '3.12'" }, + { name = "grpcio", marker = "python_full_version < '3.12'" }, + { name = "grpcio-status", marker = "python_full_version < '3.12'" }, + { name = "numpy", marker = "python_full_version < '3.12'" }, + { name = "packaging", marker = "python_full_version < '3.12'" }, + { name = "pandas", marker = "python_full_version < '3.12'" }, + { name = "py4j", marker = "python_full_version < '3.12'" }, + { name = "pyarrow", marker = "python_full_version < '3.12'" }, + { name = "setuptools", marker = "python_full_version < '3.12'" }, + { name = "six", marker = "python_full_version < '3.12'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/5d/ae8c59de242ca6ef1631e66fdf7fc413bc0d51d2521c4e1cb90104dd62d9/databricks_connect-16.1.7-py2.py3-none-any.whl", hash = "sha256:ddc7ddb8bcfb7910db67e8c4ef5ab2f938c55cf1a1b2e066ff56d2db68b13cb5", size = 2393078, upload-time = "2025-10-28T14:25:02.265Z" }, +] + +[[package]] +name = "databricks-connect" +version = "16.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform != 'win32'", + "python_full_version >= '3.12' and sys_platform == 'win32'", +] +dependencies = [ + { name = "databricks-sdk", marker = "python_full_version >= '3.12'" }, + { name = "googleapis-common-protos", marker = "python_full_version >= '3.12'" }, + { name = "grpcio", marker = "python_full_version >= '3.12'" }, + { name = "grpcio-status", marker = "python_full_version >= '3.12'" }, + { name = "numpy", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pandas", marker = "python_full_version >= '3.12'" }, + { name = "py4j", marker = "python_full_version >= '3.12'" }, + { name = "pyarrow", marker = "python_full_version >= '3.12'" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "six", marker = "python_full_version >= '3.12'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/b0/9dc0de74ca03cf16ea2058b191db4e925d98e7e5e21a64684c5e5828322b/databricks_connect-16.2.6-py2.py3-none-any.whl", hash = "sha256:3e152a443c457f01be211a94f216e6b09c673c0b4621437f4a277cee640df148", size = 2407333, upload-time = "2025-07-11T11:36:32.17Z" }, +] + [[package]] name = "databricks-sdk" version = "0.38.0" @@ -742,43 +882,49 @@ wheels = [ [[package]] name = "databricks-sql-connector" -version = "3.5.0" +version = "4.2.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "lz4" }, - { name = "numpy" }, { name = "oauthlib" }, { name = "openpyxl" }, { name = "pandas" }, - { name = "pyarrow" }, + { name = "pybreaker" }, + { name = "pyjwt" }, + { name = "python-dateutil" }, { name = "requests" }, { name = "thrift" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/6f/e6eaaeb788871b69f7d2385e26e703463fb7947c9d16471afed47faf954d/databricks_sql_connector-3.5.0.tar.gz", hash = "sha256:5d08341c81dad16920c5a2e9e2de2456f04bd08d327018bab392354c0734b122", size = 412535, upload-time = "2024-10-18T17:38:25.11Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/0c/1e8179f427044a0c769e279b2c45b72a20cff902f4e92ca1bcca50549435/databricks_sql_connector-4.2.5.tar.gz", hash = "sha256:762df7568ef1998540f96b20cad6f1aaae87d1aad54e40e528f87e4524397291", size = 187223, upload-time = "2026-02-09T11:26:29.762Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/ae/8827d2162077c3aa73b7475b6a0a9fb345f1881739f1baa43c33362ddb35/databricks_sql_connector-3.5.0-py3-none-any.whl", hash = "sha256:d88a0b7a810e7a02dfd0964e4d597ecddc7e27602db194e37bb7e05dac0b6a91", size = 429540, upload-time = "2024-10-18T17:38:23.808Z" }, + { url = "https://files.pythonhosted.org/packages/67/a7/0d6dd8323cb2249a979cf4c6a45694e975668c53b19d52d7e15490bafb4c/databricks_sql_connector-4.2.5-py3-none-any.whl", hash = "sha256:31cee10552ce77a830318ce9488fc5e67daca7abbcdf0d8d34f12a180bc55039", size = 213906, upload-time = "2026-02-09T11:26:28.566Z" }, +] + +[package.optional-dependencies] +pyarrow = [ + { name = "pyarrow" }, ] [[package]] name = "debugpy" -version = "1.8.16" +version = "1.8.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/d4/722d0bcc7986172ac2ef3c979ad56a1030e3afd44ced136d45f8142b1f4a/debugpy-1.8.16.tar.gz", hash = "sha256:31e69a1feb1cf6b51efbed3f6c9b0ef03bc46ff050679c4be7ea6d2e23540870", size = 1643809, upload-time = "2025-08-06T18:00:02.647Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/ad/71e708ff4ca377c4230530d6a7aa7992592648c122a2cd2b321cf8b35a76/debugpy-1.8.17.tar.gz", hash = "sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e", size = 1644129, upload-time = "2025-09-17T16:33:20.633Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/fd/f1b75ebc61d90882595b81d808efd3573c082e1c3407850d9dccac4ae904/debugpy-1.8.16-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:2a3958fb9c2f40ed8ea48a0d34895b461de57a1f9862e7478716c35d76f56c65", size = 2085511, upload-time = "2025-08-06T18:00:05.067Z" }, - { url = "https://files.pythonhosted.org/packages/df/5e/c5c1934352871128b30a1a144a58b5baa546e1b57bd47dbed788bad4431c/debugpy-1.8.16-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5ca7314042e8a614cc2574cd71f6ccd7e13a9708ce3c6d8436959eae56f2378", size = 3562094, upload-time = "2025-08-06T18:00:06.66Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d5/2ebe42377e5a78dc786afc25e61ee83c5628d63f32dfa41092597d52fe83/debugpy-1.8.16-cp310-cp310-win32.whl", hash = "sha256:8624a6111dc312ed8c363347a0b59c5acc6210d897e41a7c069de3c53235c9a6", size = 5234277, upload-time = "2025-08-06T18:00:08.429Z" }, - { url = "https://files.pythonhosted.org/packages/54/f8/e774ad16a60b9913213dbabb7472074c5a7b0d84f07c1f383040a9690057/debugpy-1.8.16-cp310-cp310-win_amd64.whl", hash = "sha256:fee6db83ea5c978baf042440cfe29695e1a5d48a30147abf4c3be87513609817", size = 5266011, upload-time = "2025-08-06T18:00:10.162Z" }, - { url = "https://files.pythonhosted.org/packages/63/d6/ad70ba8b49b23fa286fb21081cf732232cc19374af362051da9c7537ae52/debugpy-1.8.16-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:67371b28b79a6a12bcc027d94a06158f2fde223e35b5c4e0783b6f9d3b39274a", size = 2184063, upload-time = "2025-08-06T18:00:11.885Z" }, - { url = "https://files.pythonhosted.org/packages/aa/49/7b03e88dea9759a4c7910143f87f92beb494daaae25560184ff4ae883f9e/debugpy-1.8.16-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2abae6dd02523bec2dee16bd6b0781cccb53fd4995e5c71cc659b5f45581898", size = 3134837, upload-time = "2025-08-06T18:00:13.782Z" }, - { url = "https://files.pythonhosted.org/packages/5d/52/b348930316921de7565fbe37a487d15409041713004f3d74d03eb077dbd4/debugpy-1.8.16-cp311-cp311-win32.whl", hash = "sha256:f8340a3ac2ed4f5da59e064aa92e39edd52729a88fbde7bbaa54e08249a04493", size = 5159142, upload-time = "2025-08-06T18:00:15.391Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ef/9aa9549ce1e10cea696d980292e71672a91ee4a6a691ce5f8629e8f48c49/debugpy-1.8.16-cp311-cp311-win_amd64.whl", hash = "sha256:70f5fcd6d4d0c150a878d2aa37391c52de788c3dc680b97bdb5e529cb80df87a", size = 5183117, upload-time = "2025-08-06T18:00:17.251Z" }, - { url = "https://files.pythonhosted.org/packages/61/fb/0387c0e108d842c902801bc65ccc53e5b91d8c169702a9bbf4f7efcedf0c/debugpy-1.8.16-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:b202e2843e32e80b3b584bcebfe0e65e0392920dc70df11b2bfe1afcb7a085e4", size = 2511822, upload-time = "2025-08-06T18:00:18.526Z" }, - { url = "https://files.pythonhosted.org/packages/37/44/19e02745cae22bf96440141f94e15a69a1afaa3a64ddfc38004668fcdebf/debugpy-1.8.16-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64473c4a306ba11a99fe0bb14622ba4fbd943eb004847d9b69b107bde45aa9ea", size = 4230135, upload-time = "2025-08-06T18:00:19.997Z" }, - { url = "https://files.pythonhosted.org/packages/f3/0b/19b1ba5ee4412f303475a2c7ad5858efb99c90eae5ec627aa6275c439957/debugpy-1.8.16-cp312-cp312-win32.whl", hash = "sha256:833a61ed446426e38b0dd8be3e9d45ae285d424f5bf6cd5b2b559c8f12305508", size = 5281271, upload-time = "2025-08-06T18:00:21.281Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e0/bc62e2dc141de53bd03e2c7cb9d7011de2e65e8bdcdaa26703e4d28656ba/debugpy-1.8.16-cp312-cp312-win_amd64.whl", hash = "sha256:75f204684581e9ef3dc2f67687c3c8c183fde2d6675ab131d94084baf8084121", size = 5323149, upload-time = "2025-08-06T18:00:23.033Z" }, - { url = "https://files.pythonhosted.org/packages/52/57/ecc9ae29fa5b2d90107cd1d9bf8ed19aacb74b2264d986ae9d44fe9bdf87/debugpy-1.8.16-py2.py3-none-any.whl", hash = "sha256:19c9521962475b87da6f673514f7fd610328757ec993bf7ec0d8c96f9a325f9e", size = 5287700, upload-time = "2025-08-06T18:00:42.333Z" }, + { url = "https://files.pythonhosted.org/packages/38/36/b57c6e818d909f6e59c0182252921cf435e0951126a97e11de37e72ab5e1/debugpy-1.8.17-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:c41d2ce8bbaddcc0009cc73f65318eedfa3dbc88a8298081deb05389f1ab5542", size = 2098021, upload-time = "2025-09-17T16:33:22.556Z" }, + { url = "https://files.pythonhosted.org/packages/be/01/0363c7efdd1e9febd090bb13cee4fb1057215b157b2979a4ca5ccb678217/debugpy-1.8.17-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:1440fd514e1b815edd5861ca394786f90eb24960eb26d6f7200994333b1d79e3", size = 3087399, upload-time = "2025-09-17T16:33:24.292Z" }, + { url = "https://files.pythonhosted.org/packages/79/bc/4a984729674aa9a84856650438b9665f9a1d5a748804ac6f37932ce0d4aa/debugpy-1.8.17-cp310-cp310-win32.whl", hash = "sha256:3a32c0af575749083d7492dc79f6ab69f21b2d2ad4cd977a958a07d5865316e4", size = 5230292, upload-time = "2025-09-17T16:33:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/5d/19/2b9b3092d0cf81a5aa10c86271999453030af354d1a5a7d6e34c574515d7/debugpy-1.8.17-cp310-cp310-win_amd64.whl", hash = "sha256:a3aad0537cf4d9c1996434be68c6c9a6d233ac6f76c2a482c7803295b4e4f99a", size = 5261885, upload-time = "2025-09-17T16:33:27.592Z" }, + { url = "https://files.pythonhosted.org/packages/d8/53/3af72b5c159278c4a0cf4cffa518675a0e73bdb7d1cac0239b815502d2ce/debugpy-1.8.17-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:d3fce3f0e3de262a3b67e69916d001f3e767661c6e1ee42553009d445d1cd840", size = 2207154, upload-time = "2025-09-17T16:33:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6d/204f407df45600e2245b4a39860ed4ba32552330a0b3f5f160ae4cc30072/debugpy-1.8.17-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:c6bdf134457ae0cac6fb68205776be635d31174eeac9541e1d0c062165c6461f", size = 3170322, upload-time = "2025-09-17T16:33:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/f2/13/1b8f87d39cf83c6b713de2620c31205299e6065622e7dd37aff4808dd410/debugpy-1.8.17-cp311-cp311-win32.whl", hash = "sha256:e79a195f9e059edfe5d8bf6f3749b2599452d3e9380484cd261f6b7cd2c7c4da", size = 5155078, upload-time = "2025-09-17T16:33:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c5/c012c60a2922cc91caa9675d0ddfbb14ba59e1e36228355f41cab6483469/debugpy-1.8.17-cp311-cp311-win_amd64.whl", hash = "sha256:b532282ad4eca958b1b2d7dbcb2b7218e02cb934165859b918e3b6ba7772d3f4", size = 5179011, upload-time = "2025-09-17T16:33:35.711Z" }, + { url = "https://files.pythonhosted.org/packages/08/2b/9d8e65beb2751876c82e1aceb32f328c43ec872711fa80257c7674f45650/debugpy-1.8.17-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d", size = 2549522, upload-time = "2025-09-17T16:33:38.466Z" }, + { url = "https://files.pythonhosted.org/packages/b4/78/eb0d77f02971c05fca0eb7465b18058ba84bd957062f5eec82f941ac792a/debugpy-1.8.17-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc", size = 4309417, upload-time = "2025-09-17T16:33:41.299Z" }, + { url = "https://files.pythonhosted.org/packages/37/42/c40f1d8cc1fed1e75ea54298a382395b8b937d923fcf41ab0797a554f555/debugpy-1.8.17-cp312-cp312-win32.whl", hash = "sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf", size = 5277130, upload-time = "2025-09-17T16:33:43.554Z" }, + { url = "https://files.pythonhosted.org/packages/72/22/84263b205baad32b81b36eac076de0cdbe09fe2d0637f5b32243dc7c925b/debugpy-1.8.17-cp312-cp312-win_amd64.whl", hash = "sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464", size = 5319053, upload-time = "2025-09-17T16:33:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef", size = 5283210, upload-time = "2025-09-17T16:34:25.835Z" }, ] [[package]] @@ -810,11 +956,11 @@ wheels = [ [[package]] name = "dnspython" -version = "2.7.0" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] [[package]] @@ -831,26 +977,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, ] +[[package]] +name = "edvise" +version = "0.2.1" +source = { git = "https://github.com/datakind/edvise.git?rev=develop#22d2598617be47539a0c478595664e329f234a54" } +dependencies = [ + { name = "databricks-connect", version = "16.1.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "databricks-connect", version = "16.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "databricks-sdk" }, + { name = "faker" }, + { name = "google-cloud-storage" }, + { name = "h2o" }, + { name = "ipywidgets" }, + { name = "markdown" }, + { name = "matplotlib" }, + { name = "missingno" }, + { name = "mlflow" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pandera" }, + { name = "polars" }, + { name = "pyarrow" }, + { name = "pydantic" }, + { name = "pydyf" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "scikit-learn" }, + { name = "seaborn" }, + { name = "shap" }, + { name = "statsmodels" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "tomlkit" }, + { name = "types-markdown" }, + { name = "types-pyyaml" }, + { name = "weasyprint" }, +] + [[package]] name = "email-validator" -version = "2.2.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dnspython" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967, upload-time = "2024-06-20T11:30:30.034Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521, upload-time = "2024-06-20T11:30:28.248Z" }, -] - -[[package]] -name = "entrypoints" -version = "0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/8d/a7121ffe5f402dc015277d2d31eb82d2187334503a011c18f2e78ecbb9b2/entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4", size = 13974, upload-time = "2022-02-02T21:30:28.172Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/a8/365059bbcd4572cbc41de17fd5b682be5868b218c3c5479071865cab9078/entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f", size = 5294, upload-time = "2022-02-02T21:30:26.024Z" }, + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] [[package]] @@ -876,11 +1049,24 @@ wheels = [ [[package]] name = "executing" -version = "2.2.0" +version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693, upload-time = "2025-01-22T15:41:29.403Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702, upload-time = "2025-01-22T15:41:25.929Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "faker" +version = "33.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/9f/012fd6049fc86029951cba5112d32c7ba076c4290d7e8873b0413655b808/faker-33.1.0.tar.gz", hash = "sha256:1c925fc0e86a51fc46648b504078c88d0cd48da1da2595c4e712841cab43a1e4", size = 1850515, upload-time = "2024-11-27T23:11:46.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/9c/2bba87fbfa42503ddd9653e3546ffc4ed18b14ecab7a07ee86491b886486/Faker-33.1.0-py3-none-any.whl", hash = "sha256:d30c5f0e2796b8970de68978365247657486eb0311c5abe88d0b895b68dff05d", size = 1889127, upload-time = "2024-11-27T23:11:43.109Z" }, ] [[package]] @@ -909,16 +1095,16 @@ standard = [ [[package]] name = "fastapi-cli" -version = "0.0.8" +version = "0.0.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "rich-toolkit" }, { name = "typer" }, { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/94/3ef75d9c7c32936ecb539b9750ccbdc3d2568efd73b1cb913278375f4533/fastapi_cli-0.0.8.tar.gz", hash = "sha256:2360f2989b1ab4a3d7fc8b3a0b20e8288680d8af2e31de7c38309934d7f8a0ee", size = 16884, upload-time = "2025-07-07T14:44:09.326Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/13/11e43d630be84e51ba5510a6da6a11eb93b44b72caa796137c5dddda937b/fastapi_cli-0.0.14.tar.gz", hash = "sha256:ddfb5de0a67f77a8b3271af1460489bd4d7f4add73d11fbfac613827b0275274", size = 17994, upload-time = "2025-10-20T16:33:21.054Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/3f/6ad3103c5f59208baf4c798526daea6a74085bb35d1c161c501863470476/fastapi_cli-0.0.8-py3-none-any.whl", hash = "sha256:0ea95d882c85b9219a75a65ab27e8da17dac02873e456850fa0a726e96e985eb", size = 10770, upload-time = "2025-07-07T14:44:08.255Z" }, + { url = "https://files.pythonhosted.org/packages/40/e8/bc8bbfd93dcc8e347ce98a3e654fb0d2e5f2739afb46b98f41a30c339269/fastapi_cli-0.0.14-py3-none-any.whl", hash = "sha256:e66b9ad499ee77a4e6007545cde6de1459b7f21df199d7f29aad2adaab168eca", size = 11151, upload-time = "2025-10-20T16:33:19.318Z" }, ] [package.optional-dependencies] @@ -929,7 +1115,7 @@ standard = [ [[package]] name = "fastapi-cloud-cli" -version = "0.1.5" +version = "0.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -940,23 +1126,23 @@ dependencies = [ { name = "typer" }, { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/2e/3b6e5016affc310e5109bc580f760586eabecea0c8a7ab067611cd849ac0/fastapi_cloud_cli-0.1.5.tar.gz", hash = "sha256:341ee585eb731a6d3c3656cb91ad38e5f39809bf1a16d41de1333e38635a7937", size = 22710, upload-time = "2025-07-28T13:30:48.216Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/48/0f14d8555b750dc8c04382804e4214f1d7f55298127f3a0237ba566e69dd/fastapi_cloud_cli-0.3.1.tar.gz", hash = "sha256:8c7226c36e92e92d0c89827e8f56dbf164ab2de4444bd33aa26b6c3f7675db69", size = 24080, upload-time = "2025-10-09T11:32:58.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/a6/5aa862489a2918a096166fd98d9fe86b7fd53c607678b3fa9d8c432d88d5/fastapi_cloud_cli-0.1.5-py3-none-any.whl", hash = "sha256:d80525fb9c0e8af122370891f9fa83cf5d496e4ad47a8dd26c0496a6c85a012a", size = 18992, upload-time = "2025-07-28T13:30:47.427Z" }, + { url = "https://files.pythonhosted.org/packages/68/79/7f5a5e5513e6a737e5fb089d9c59c74d4d24dc24d581d3aa519b326bedda/fastapi_cloud_cli-0.3.1-py3-none-any.whl", hash = "sha256:7d1a98a77791a9d0757886b2ffbf11bcc6b3be93210dd15064be10b216bf7e00", size = 19711, upload-time = "2025-10-09T11:32:57.118Z" }, ] [[package]] name = "fastjsonschema" -version = "2.21.1" +version = "2.21.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/50/4b769ce1ac4071a1ef6d86b1a3fb56cdc3a37615e8c5519e1af96cdac366/fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4", size = 373939, upload-time = "2024-12-02T10:55:15.133Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/2b/0817a2b257fe88725c25589d89aec060581aabf668707a8d03b2e9e0cb2a/fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667", size = 23924, upload-time = "2024-12-02T10:55:07.599Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, ] [[package]] name = "flask" -version = "3.1.1" +version = "3.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blinker" }, @@ -966,42 +1152,49 @@ dependencies = [ { name = "markupsafe" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/de/e47735752347f4128bcf354e0da07ef311a78244eba9e3dc1d4a5ab21a98/flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e", size = 753440, upload-time = "2025-05-13T15:01:17.447Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", size = 103305, upload-time = "2025-05-13T15:01:15.591Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, ] [[package]] name = "fonttools" -version = "4.59.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/27/ec3c723bfdf86f34c5c82bf6305df3e0f0d8ea798d2d3a7cb0c0a866d286/fonttools-4.59.0.tar.gz", hash = "sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14", size = 3532521, upload-time = "2025-07-16T12:04:54.613Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/1f/3dcae710b7c4b56e79442b03db64f6c9f10c3348f7af40339dffcefb581e/fonttools-4.59.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96", size = 2761846, upload-time = "2025-07-16T12:03:33.267Z" }, - { url = "https://files.pythonhosted.org/packages/eb/0e/ae3a1884fa1549acac1191cc9ec039142f6ac0e9cbc139c2e6a3dab967da/fonttools-4.59.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df", size = 2332060, upload-time = "2025-07-16T12:03:36.472Z" }, - { url = "https://files.pythonhosted.org/packages/75/46/58bff92a7216829159ac7bdb1d05a48ad1b8ab8c539555f12d29fdecfdd4/fonttools-4.59.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482", size = 4852354, upload-time = "2025-07-16T12:03:39.102Z" }, - { url = "https://files.pythonhosted.org/packages/05/57/767e31e48861045d89691128bd81fd4c62b62150f9a17a666f731ce4f197/fonttools-4.59.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64", size = 4781132, upload-time = "2025-07-16T12:03:41.415Z" }, - { url = "https://files.pythonhosted.org/packages/d7/78/adb5e9b0af5c6ce469e8b0e112f144eaa84b30dd72a486e9c778a9b03b31/fonttools-4.59.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db", size = 4832901, upload-time = "2025-07-16T12:03:43.115Z" }, - { url = "https://files.pythonhosted.org/packages/ac/92/bc3881097fbf3d56d112bec308c863c058e5d4c9c65f534e8ae58450ab8a/fonttools-4.59.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d", size = 4940140, upload-time = "2025-07-16T12:03:44.781Z" }, - { url = "https://files.pythonhosted.org/packages/4a/54/39cdb23f0eeda2e07ae9cb189f2b6f41da89aabc682d3a387b3ff4a4ed29/fonttools-4.59.0-cp310-cp310-win32.whl", hash = "sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f", size = 2215890, upload-time = "2025-07-16T12:03:46.961Z" }, - { url = "https://files.pythonhosted.org/packages/d8/eb/f8388d9e19f95d8df2449febe9b1a38ddd758cfdb7d6de3a05198d785d61/fonttools-4.59.0-cp310-cp310-win_amd64.whl", hash = "sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e", size = 2260191, upload-time = "2025-07-16T12:03:48.908Z" }, - { url = "https://files.pythonhosted.org/packages/06/96/520733d9602fa1bf6592e5354c6721ac6fc9ea72bc98d112d0c38b967199/fonttools-4.59.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c", size = 2782387, upload-time = "2025-07-16T12:03:51.424Z" }, - { url = "https://files.pythonhosted.org/packages/87/6a/170fce30b9bce69077d8eec9bea2cfd9f7995e8911c71be905e2eba6368b/fonttools-4.59.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5", size = 2342194, upload-time = "2025-07-16T12:03:53.295Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b6/7c8166c0066856f1408092f7968ac744060cf72ca53aec9036106f57eeca/fonttools-4.59.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705", size = 5032333, upload-time = "2025-07-16T12:03:55.177Z" }, - { url = "https://files.pythonhosted.org/packages/eb/0c/707c5a19598eafcafd489b73c4cb1c142102d6197e872f531512d084aa76/fonttools-4.59.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464", size = 4974422, upload-time = "2025-07-16T12:03:57.406Z" }, - { url = "https://files.pythonhosted.org/packages/f6/e7/6d33737d9fe632a0f59289b6f9743a86d2a9d0673de2a0c38c0f54729822/fonttools-4.59.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38", size = 5010631, upload-time = "2025-07-16T12:03:59.449Z" }, - { url = "https://files.pythonhosted.org/packages/63/e1/a4c3d089ab034a578820c8f2dff21ef60daf9668034a1e4fb38bb1cc3398/fonttools-4.59.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6", size = 5122198, upload-time = "2025-07-16T12:04:01.542Z" }, - { url = "https://files.pythonhosted.org/packages/09/77/ca82b9c12fa4de3c520b7760ee61787640cf3fde55ef1b0bfe1de38c8153/fonttools-4.59.0-cp311-cp311-win32.whl", hash = "sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757", size = 2214216, upload-time = "2025-07-16T12:04:03.515Z" }, - { url = "https://files.pythonhosted.org/packages/ab/25/5aa7ca24b560b2f00f260acf32c4cf29d7aaf8656e159a336111c18bc345/fonttools-4.59.0-cp311-cp311-win_amd64.whl", hash = "sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0", size = 2261879, upload-time = "2025-07-16T12:04:05.015Z" }, - { url = "https://files.pythonhosted.org/packages/e2/77/b1c8af22f4265e951cd2e5535dbef8859efcef4fb8dee742d368c967cddb/fonttools-4.59.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b", size = 2767562, upload-time = "2025-07-16T12:04:06.895Z" }, - { url = "https://files.pythonhosted.org/packages/ff/5a/aeb975699588176bb357e8b398dfd27e5d3a2230d92b81ab8cbb6187358d/fonttools-4.59.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2", size = 2335168, upload-time = "2025-07-16T12:04:08.695Z" }, - { url = "https://files.pythonhosted.org/packages/54/97/c6101a7e60ae138c4ef75b22434373a0da50a707dad523dd19a4889315bf/fonttools-4.59.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b", size = 4909850, upload-time = "2025-07-16T12:04:10.761Z" }, - { url = "https://files.pythonhosted.org/packages/bd/6c/fa4d18d641054f7bff878cbea14aa9433f292b9057cb1700d8e91a4d5f4f/fonttools-4.59.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1", size = 4955131, upload-time = "2025-07-16T12:04:12.846Z" }, - { url = "https://files.pythonhosted.org/packages/20/5c/331947fc1377deb928a69bde49f9003364f5115e5cbe351eea99e39412a2/fonttools-4.59.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e", size = 4899667, upload-time = "2025-07-16T12:04:14.558Z" }, - { url = "https://files.pythonhosted.org/packages/8a/46/b66469dfa26b8ff0baa7654b2cc7851206c6d57fe3abdabbaab22079a119/fonttools-4.59.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e", size = 5051349, upload-time = "2025-07-16T12:04:16.388Z" }, - { url = "https://files.pythonhosted.org/packages/2e/05/ebfb6b1f3a4328ab69787d106a7d92ccde77ce66e98659df0f9e3f28d93d/fonttools-4.59.0-cp312-cp312-win32.whl", hash = "sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b", size = 2201315, upload-time = "2025-07-16T12:04:18.557Z" }, - { url = "https://files.pythonhosted.org/packages/09/45/d2bdc9ea20bbadec1016fd0db45696d573d7a26d95ab5174ffcb6d74340b/fonttools-4.59.0-cp312-cp312-win_amd64.whl", hash = "sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01", size = 2249408, upload-time = "2025-07-16T12:04:20.489Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9c/df0ef2c51845a13043e5088f7bb988ca6cd5bb82d5d4203d6a158aa58cf2/fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d", size = 1128050, upload-time = "2025-07-16T12:04:52.687Z" }, +version = "4.60.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/70/03e9d89a053caff6ae46053890eba8e4a5665a7c5638279ed4492e6d4b8b/fonttools-4.60.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28", size = 2810747, upload-time = "2025-09-29T21:10:59.653Z" }, + { url = "https://files.pythonhosted.org/packages/6f/41/449ad5aff9670ab0df0f61ee593906b67a36d7e0b4d0cd7fa41ac0325bf5/fonttools-4.60.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15", size = 2346909, upload-time = "2025-09-29T21:11:02.882Z" }, + { url = "https://files.pythonhosted.org/packages/9a/18/e5970aa96c8fad1cb19a9479cc3b7602c0c98d250fcdc06a5da994309c50/fonttools-4.60.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c", size = 4864572, upload-time = "2025-09-29T21:11:05.096Z" }, + { url = "https://files.pythonhosted.org/packages/ce/20/9b2b4051b6ec6689480787d506b5003f72648f50972a92d04527a456192c/fonttools-4.60.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea", size = 4794635, upload-time = "2025-09-29T21:11:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/10/52/c791f57347c1be98f8345e3dca4ac483eb97666dd7c47f3059aeffab8b59/fonttools-4.60.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652", size = 4843878, upload-time = "2025-09-29T21:11:10.893Z" }, + { url = "https://files.pythonhosted.org/packages/69/e9/35c24a8d01644cee8c090a22fad34d5b61d1e0a8ecbc9945ad785ebf2e9e/fonttools-4.60.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a", size = 4954555, upload-time = "2025-09-29T21:11:13.24Z" }, + { url = "https://files.pythonhosted.org/packages/f7/86/fb1e994971be4bdfe3a307de6373ef69a9df83fb66e3faa9c8114893d4cc/fonttools-4.60.1-cp310-cp310-win32.whl", hash = "sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce", size = 2232019, upload-time = "2025-09-29T21:11:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/40/84/62a19e2bd56f0e9fb347486a5b26376bade4bf6bbba64dda2c103bd08c94/fonttools-4.60.1-cp310-cp310-win_amd64.whl", hash = "sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038", size = 2276803, upload-time = "2025-09-29T21:11:18.152Z" }, + { url = "https://files.pythonhosted.org/packages/ea/85/639aa9bface1537e0fb0f643690672dde0695a5bbbc90736bc571b0b1941/fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f", size = 2831872, upload-time = "2025-09-29T21:11:20.329Z" }, + { url = "https://files.pythonhosted.org/packages/6b/47/3c63158459c95093be9618794acb1067b3f4d30dcc5c3e8114b70e67a092/fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2", size = 2356990, upload-time = "2025-09-29T21:11:22.754Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/1934b537c86fcf99f9761823f1fc37a98fbd54568e8e613f29a90fed95a9/fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914", size = 5042189, upload-time = "2025-09-29T21:11:25.061Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d2/9f4e4c4374dd1daa8367784e1bd910f18ba886db1d6b825b12edf6db3edc/fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1", size = 4978683, upload-time = "2025-09-29T21:11:27.693Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c4/0fb2dfd1ecbe9a07954cc13414713ed1eab17b1c0214ef07fc93df234a47/fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d", size = 5021372, upload-time = "2025-09-29T21:11:30.257Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d5/495fc7ae2fab20223cc87179a8f50f40f9a6f821f271ba8301ae12bb580f/fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa", size = 5132562, upload-time = "2025-09-29T21:11:32.737Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fa/021dab618526323c744e0206b3f5c8596a2e7ae9aa38db5948a131123e83/fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258", size = 2230288, upload-time = "2025-09-29T21:11:35.015Z" }, + { url = "https://files.pythonhosted.org/packages/bb/78/0e1a6d22b427579ea5c8273e1c07def2f325b977faaf60bb7ddc01456cb1/fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf", size = 2278184, upload-time = "2025-09-29T21:11:37.434Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f7/a10b101b7a6f8836a5adb47f2791f2075d044a6ca123f35985c42edc82d8/fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc", size = 2832953, upload-time = "2025-09-29T21:11:39.616Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/7bd094b59c926acf2304d2151354ddbeb74b94812f3dc943c231db09cb41/fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877", size = 2352706, upload-time = "2025-09-29T21:11:41.826Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ca/4bb48a26ed95a1e7eba175535fe5805887682140ee0a0d10a88e1de84208/fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c", size = 4923716, upload-time = "2025-09-29T21:11:43.893Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9f/2cb82999f686c1d1ddf06f6ae1a9117a880adbec113611cc9d22b2fdd465/fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401", size = 4968175, upload-time = "2025-09-29T21:11:46.439Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/be569699e37d166b78e6218f2cde8c550204f2505038cdd83b42edc469b9/fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903", size = 4911031, upload-time = "2025-09-29T21:11:48.977Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9f/89411cc116effaec5260ad519162f64f9c150e5522a27cbb05eb62d0c05b/fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed", size = 5062966, upload-time = "2025-09-29T21:11:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/62/a1/f888221934b5731d46cb9991c7a71f30cb1f97c0ef5fcf37f8da8fce6c8e/fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6", size = 2218750, upload-time = "2025-09-29T21:11:56.601Z" }, + { url = "https://files.pythonhosted.org/packages/88/8f/a55b5550cd33cd1028601df41acd057d4be20efa5c958f417b0c0613924d/fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383", size = 2267026, upload-time = "2025-09-29T21:11:58.852Z" }, + { url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" }, +] + +[package.optional-dependencies] +woff = [ + { name = "brotli", marker = "platform_python_implementation == 'CPython'" }, + { name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" }, + { name = "zopfli" }, ] [[package]] @@ -1015,62 +1208,59 @@ wheels = [ [[package]] name = "frozenlist" -version = "1.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/36/0da0a49409f6b47cc2d060dc8c9040b897b5902a8a4e37d9bc1deb11f680/frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a", size = 81304, upload-time = "2025-06-09T22:59:46.226Z" }, - { url = "https://files.pythonhosted.org/packages/77/f0/77c11d13d39513b298e267b22eb6cb559c103d56f155aa9a49097221f0b6/frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61", size = 47735, upload-time = "2025-06-09T22:59:48.133Z" }, - { url = "https://files.pythonhosted.org/packages/37/12/9d07fa18971a44150593de56b2f2947c46604819976784bcf6ea0d5db43b/frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d", size = 46775, upload-time = "2025-06-09T22:59:49.564Z" }, - { url = "https://files.pythonhosted.org/packages/70/34/f73539227e06288fcd1f8a76853e755b2b48bca6747e99e283111c18bcd4/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e", size = 224644, upload-time = "2025-06-09T22:59:51.35Z" }, - { url = "https://files.pythonhosted.org/packages/fb/68/c1d9c2f4a6e438e14613bad0f2973567586610cc22dcb1e1241da71de9d3/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9", size = 222125, upload-time = "2025-06-09T22:59:52.884Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d0/98e8f9a515228d708344d7c6986752be3e3192d1795f748c24bcf154ad99/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c", size = 233455, upload-time = "2025-06-09T22:59:54.74Z" }, - { url = "https://files.pythonhosted.org/packages/79/df/8a11bcec5600557f40338407d3e5bea80376ed1c01a6c0910fcfdc4b8993/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981", size = 227339, upload-time = "2025-06-09T22:59:56.187Z" }, - { url = "https://files.pythonhosted.org/packages/50/82/41cb97d9c9a5ff94438c63cc343eb7980dac4187eb625a51bdfdb7707314/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615", size = 212969, upload-time = "2025-06-09T22:59:57.604Z" }, - { url = "https://files.pythonhosted.org/packages/13/47/f9179ee5ee4f55629e4f28c660b3fdf2775c8bfde8f9c53f2de2d93f52a9/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50", size = 222862, upload-time = "2025-06-09T22:59:59.498Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/df81e41ec6b953902c8b7e3a83bee48b195cb0e5ec2eabae5d8330c78038/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa", size = 222492, upload-time = "2025-06-09T23:00:01.026Z" }, - { url = "https://files.pythonhosted.org/packages/84/17/30d6ea87fa95a9408245a948604b82c1a4b8b3e153cea596421a2aef2754/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577", size = 238250, upload-time = "2025-06-09T23:00:03.401Z" }, - { url = "https://files.pythonhosted.org/packages/8f/00/ecbeb51669e3c3df76cf2ddd66ae3e48345ec213a55e3887d216eb4fbab3/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59", size = 218720, upload-time = "2025-06-09T23:00:05.282Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c0/c224ce0e0eb31cc57f67742071bb470ba8246623c1823a7530be0e76164c/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e", size = 232585, upload-time = "2025-06-09T23:00:07.962Z" }, - { url = "https://files.pythonhosted.org/packages/55/3c/34cb694abf532f31f365106deebdeac9e45c19304d83cf7d51ebbb4ca4d1/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd", size = 234248, upload-time = "2025-06-09T23:00:09.428Z" }, - { url = "https://files.pythonhosted.org/packages/98/c0/2052d8b6cecda2e70bd81299e3512fa332abb6dcd2969b9c80dfcdddbf75/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718", size = 221621, upload-time = "2025-06-09T23:00:11.32Z" }, - { url = "https://files.pythonhosted.org/packages/c5/bf/7dcebae315436903b1d98ffb791a09d674c88480c158aa171958a3ac07f0/frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e", size = 39578, upload-time = "2025-06-09T23:00:13.526Z" }, - { url = "https://files.pythonhosted.org/packages/8f/5f/f69818f017fa9a3d24d1ae39763e29b7f60a59e46d5f91b9c6b21622f4cd/frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464", size = 43830, upload-time = "2025-06-09T23:00:14.98Z" }, - { url = "https://files.pythonhosted.org/packages/34/7e/803dde33760128acd393a27eb002f2020ddb8d99d30a44bfbaab31c5f08a/frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a", size = 82251, upload-time = "2025-06-09T23:00:16.279Z" }, - { url = "https://files.pythonhosted.org/packages/75/a9/9c2c5760b6ba45eae11334db454c189d43d34a4c0b489feb2175e5e64277/frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750", size = 48183, upload-time = "2025-06-09T23:00:17.698Z" }, - { url = "https://files.pythonhosted.org/packages/47/be/4038e2d869f8a2da165f35a6befb9158c259819be22eeaf9c9a8f6a87771/frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd", size = 47107, upload-time = "2025-06-09T23:00:18.952Z" }, - { url = "https://files.pythonhosted.org/packages/79/26/85314b8a83187c76a37183ceed886381a5f992975786f883472fcb6dc5f2/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2", size = 237333, upload-time = "2025-06-09T23:00:20.275Z" }, - { url = "https://files.pythonhosted.org/packages/1f/fd/e5b64f7d2c92a41639ffb2ad44a6a82f347787abc0c7df5f49057cf11770/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f", size = 231724, upload-time = "2025-06-09T23:00:21.705Z" }, - { url = "https://files.pythonhosted.org/packages/20/fb/03395c0a43a5976af4bf7534759d214405fbbb4c114683f434dfdd3128ef/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30", size = 245842, upload-time = "2025-06-09T23:00:23.148Z" }, - { url = "https://files.pythonhosted.org/packages/d0/15/c01c8e1dffdac5d9803507d824f27aed2ba76b6ed0026fab4d9866e82f1f/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98", size = 239767, upload-time = "2025-06-09T23:00:25.103Z" }, - { url = "https://files.pythonhosted.org/packages/14/99/3f4c6fe882c1f5514b6848aa0a69b20cb5e5d8e8f51a339d48c0e9305ed0/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86", size = 224130, upload-time = "2025-06-09T23:00:27.061Z" }, - { url = "https://files.pythonhosted.org/packages/4d/83/220a374bd7b2aeba9d0725130665afe11de347d95c3620b9b82cc2fcab97/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae", size = 235301, upload-time = "2025-06-09T23:00:29.02Z" }, - { url = "https://files.pythonhosted.org/packages/03/3c/3e3390d75334a063181625343e8daab61b77e1b8214802cc4e8a1bb678fc/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8", size = 234606, upload-time = "2025-06-09T23:00:30.514Z" }, - { url = "https://files.pythonhosted.org/packages/23/1e/58232c19608b7a549d72d9903005e2d82488f12554a32de2d5fb59b9b1ba/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31", size = 248372, upload-time = "2025-06-09T23:00:31.966Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a4/e4a567e01702a88a74ce8a324691e62a629bf47d4f8607f24bf1c7216e7f/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7", size = 229860, upload-time = "2025-06-09T23:00:33.375Z" }, - { url = "https://files.pythonhosted.org/packages/73/a6/63b3374f7d22268b41a9db73d68a8233afa30ed164c46107b33c4d18ecdd/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5", size = 245893, upload-time = "2025-06-09T23:00:35.002Z" }, - { url = "https://files.pythonhosted.org/packages/6d/eb/d18b3f6e64799a79673c4ba0b45e4cfbe49c240edfd03a68be20002eaeaa/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898", size = 246323, upload-time = "2025-06-09T23:00:36.468Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f5/720f3812e3d06cd89a1d5db9ff6450088b8f5c449dae8ffb2971a44da506/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56", size = 233149, upload-time = "2025-06-09T23:00:37.963Z" }, - { url = "https://files.pythonhosted.org/packages/69/68/03efbf545e217d5db8446acfd4c447c15b7c8cf4dbd4a58403111df9322d/frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7", size = 39565, upload-time = "2025-06-09T23:00:39.753Z" }, - { url = "https://files.pythonhosted.org/packages/58/17/fe61124c5c333ae87f09bb67186d65038834a47d974fc10a5fadb4cc5ae1/frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d", size = 44019, upload-time = "2025-06-09T23:00:40.988Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload-time = "2025-06-09T23:00:42.24Z" }, - { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload-time = "2025-06-09T23:00:43.481Z" }, - { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload-time = "2025-06-09T23:00:44.793Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload-time = "2025-06-09T23:00:46.125Z" }, - { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload-time = "2025-06-09T23:00:47.73Z" }, - { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload-time = "2025-06-09T23:00:49.742Z" }, - { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload-time = "2025-06-09T23:00:51.352Z" }, - { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload-time = "2025-06-09T23:00:52.855Z" }, - { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload-time = "2025-06-09T23:00:54.43Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload-time = "2025-06-09T23:00:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload-time = "2025-06-09T23:00:58.468Z" }, - { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload-time = "2025-06-09T23:01:00.015Z" }, - { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload-time = "2025-06-09T23:01:01.474Z" }, - { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload-time = "2025-06-09T23:01:02.961Z" }, - { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload-time = "2025-06-09T23:01:05.095Z" }, - { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload-time = "2025-06-09T23:01:06.54Z" }, - { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload-time = "2025-06-09T23:01:07.752Z" }, - { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { 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/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { 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/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { 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/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] [[package]] @@ -1099,7 +1289,7 @@ wheels = [ [[package]] name = "google-api-core" -version = "2.25.1" +version = "2.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, @@ -1108,41 +1298,41 @@ dependencies = [ { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/21/e9d043e88222317afdbdb567165fdbc3b0aad90064c7e0c9eb0ad9955ad8/google_api_core-2.25.1.tar.gz", hash = "sha256:d2aaa0b13c78c61cb3f4282c464c046e45fbd75755683c9c525e6e8f7ed0a5e8", size = 165443, upload-time = "2025-06-12T20:52:20.439Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/da/83d7043169ac2c8c7469f0e375610d78ae2160134bf1b80634c482fa079c/google_api_core-2.28.1.tar.gz", hash = "sha256:2b405df02d68e68ce0fbc138559e6036559e685159d148ae5861013dc201baf8", size = 176759, upload-time = "2025-10-28T21:34:51.529Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/4b/ead00905132820b623732b175d66354e9d3e69fcf2a5dcdab780664e7896/google_api_core-2.25.1-py3-none-any.whl", hash = "sha256:8a2a56c1fef82987a524371f99f3bd0143702fecc670c72e600c1cda6bf8dbb7", size = 160807, upload-time = "2025-06-12T20:52:19.334Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d4/90197b416cb61cefd316964fd9e7bd8324bcbafabf40eef14a9f20b81974/google_api_core-2.28.1-py3-none-any.whl", hash = "sha256:4021b0f8ceb77a6fb4de6fde4502cecab45062e66ff4f2895169e0b35bc9466c", size = 173706, upload-time = "2025-10-28T21:34:50.151Z" }, ] [[package]] name = "google-auth" -version = "2.40.3" +version = "2.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/6b/22a77135757c3a7854c9f008ffed6bf4e8851616d77faf13147e9ab5aae6/google_auth-2.42.1.tar.gz", hash = "sha256:30178b7a21aa50bffbdc1ffcb34ff770a2f65c712170ecd5446c4bef4dc2b94e", size = 295541, upload-time = "2025-10-30T16:42:19.381Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" }, + { url = "https://files.pythonhosted.org/packages/92/05/adeb6c495aec4f9d93f9e2fc29eeef6e14d452bba11d15bdb874ce1d5b10/google_auth-2.42.1-py2.py3-none-any.whl", hash = "sha256:eb73d71c91fc95dbd221a2eb87477c278a355e7367a35c0d84e6b0e5f9b4ad11", size = 222550, upload-time = "2025-10-30T16:42:17.878Z" }, ] [[package]] name = "google-cloud-core" -version = "2.4.3" +version = "2.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, { name = "google-auth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/b8/2b53838d2acd6ec6168fd284a990c76695e84c65deee79c9f3a4276f6b4f/google_cloud_core-2.4.3.tar.gz", hash = "sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53", size = 35861, upload-time = "2025-03-10T21:05:38.948Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/03/ef0bc99d0e0faf4fdbe67ac445e18cdaa74824fd93cd069e7bb6548cb52d/google_cloud_core-2.5.0.tar.gz", hash = "sha256:7c1b7ef5c92311717bd05301aa1a91ffbc565673d3b0b4163a52d8413a186963", size = 36027, upload-time = "2025-10-29T23:17:39.513Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/86/bda7241a8da2d28a754aad2ba0f6776e35b67e37c36ae0c45d49370f1014/google_cloud_core-2.4.3-py2.py3-none-any.whl", hash = "sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e", size = 29348, upload-time = "2025-03-10T21:05:37.785Z" }, + { url = "https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl", hash = "sha256:67d977b41ae6c7211ee830c7912e41003ea8194bff15ae7d72fd6f51e57acabc", size = 29469, upload-time = "2025-10-29T23:17:38.548Z" }, ] [[package]] name = "google-cloud-storage" -version = "2.18.2" +version = "2.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, @@ -1152,9 +1342,9 @@ dependencies = [ { name = "google-resumable-media" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/b7/1554cdeb55d9626a4b8720746cba8119af35527b12e1780164f9ba0f659a/google_cloud_storage-2.18.2.tar.gz", hash = "sha256:aaf7acd70cdad9f274d29332673fcab98708d0e1f4dceb5a5356aaef06af4d99", size = 5532864, upload-time = "2024-08-08T21:59:02.358Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/76/4d965702e96bb67976e755bed9828fa50306dca003dbee08b67f41dd265e/google_cloud_storage-2.19.0.tar.gz", hash = "sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2", size = 5535488, upload-time = "2024-12-05T01:35:06.49Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/da/95db7bd4f0bd1644378ac1702c565c0210b004754d925a74f526a710c087/google_cloud_storage-2.18.2-py2.py3-none-any.whl", hash = "sha256:97a4d45c368b7d401ed48c4fdfe86e1e1cb96401c9e199e419d289e2c0370166", size = 130466, upload-time = "2024-08-08T21:58:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/d5/94/6db383d8ee1adf45dc6c73477152b82731fa4c4a46d9c1932cc8757e0fd4/google_cloud_storage-2.19.0-py2.py3-none-any.whl", hash = "sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba", size = 131787, upload-time = "2024-12-05T01:35:04.736Z" }, ] [[package]] @@ -1199,14 +1389,14 @@ wheels = [ [[package]] name = "googleapis-common-protos" -version = "1.70.0" +version = "1.71.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903, upload-time = "2025-04-14T10:17:02.924Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/43/b25abe02db2911397819003029bef768f68a974f2ece483e6084d1a5f754/googleapis_common_protos-1.71.0.tar.gz", hash = "sha256:1aec01e574e29da63c80ba9f7bbf1ccfaacf1da877f23609fe236ca7c72a2e2e", size = 146454, upload-time = "2025-10-20T14:58:08.732Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530, upload-time = "2025-04-14T10:17:01.271Z" }, + { url = "https://files.pythonhosted.org/packages/25/e8/eba9fece11d57a71e3e22ea672742c8f3cf23b35730c9e96db768b295216/googleapis_common_protos-1.71.0-py3-none-any.whl", hash = "sha256:59034a1d849dc4d18971997a72ac56246570afdd17f9369a0ff68218d50ab78c", size = 294576, upload-time = "2025-10-20T14:56:21.295Z" }, ] [[package]] @@ -1226,11 +1416,11 @@ wheels = [ [[package]] name = "graphql-core" -version = "3.2.6" +version = "3.2.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c4/16/7574029da84834349b60ed71614d66ca3afe46e9bf9c7b9562102acb7d4f/graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab", size = 505353, upload-time = "2025-01-26T16:36:27.374Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/9b/037a640a2983b09aed4a823f9cf1729e6d780b0671f854efa4727a7affbe/graphql_core-3.2.7.tar.gz", hash = "sha256:27b6904bdd3b43f2a0556dad5d579bdfdeab1f38e8e8788e555bdcb586a6f62c", size = 513484, upload-time = "2025-11-01T22:30:40.436Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/4f/7297663840621022bc73c22d7d9d80dbc78b4db6297f764b545cd5dd462d/graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f", size = 203416, upload-time = "2025-01-26T16:36:24.868Z" }, + { url = "https://files.pythonhosted.org/packages/0a/14/933037032608787fb92e365883ad6a741c235e0ff992865ec5d904a38f1e/graphql_core-3.2.7-py3-none-any.whl", hash = "sha256:17fc8f3ca4a42913d8e24d9ac9f08deddf0a0b2483076575757f6c412ead2ec0", size = 207262, upload-time = "2025-11-01T22:30:38.912Z" }, ] [[package]] @@ -1259,6 +1449,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" }, { url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" }, { url = "https://files.pythonhosted.org/packages/a1/8d/88f3ebd2bc96bf7747093696f4335a0a8a4c5acfcf1b757717c0d2474ba3/greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", size = 1137126, upload-time = "2025-08-07T13:18:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/f1/29/74242b7d72385e29bcc5563fba67dad94943d7cd03552bac320d597f29b2/greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7", size = 1544904, upload-time = "2025-11-04T12:42:04.763Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e2/1572b8eeab0f77df5f6729d6ab6b141e4a84ee8eb9bc8c1e7918f94eda6d/greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8", size = 1611228, upload-time = "2025-11-04T12:42:08.423Z" }, { url = "https://files.pythonhosted.org/packages/d6/6f/b60b0291d9623c496638c582297ead61f43c4b72eef5e9c926ef4565ec13/greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", size = 298654, upload-time = "2025-08-07T13:50:00.469Z" }, { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, @@ -1268,6 +1460,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, { url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" }, + { url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" }, { url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" }, { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, @@ -1277,9 +1471,66 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, + { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" }, { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, ] +[[package]] +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/17/ff4795dc9a34b6aee6ec379f1b66438a3789cd1315aac0cbab60d92f74b3/grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc", size = 5840037, upload-time = "2025-10-21T16:20:25.069Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ff/35f9b96e3fa2f12e1dcd58a4513a2e2294a001d64dec81677361b7040c9a/grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde", size = 11836482, upload-time = "2025-10-21T16:20:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1c/8374990f9545e99462caacea5413ed783014b3b66ace49e35c533f07507b/grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3", size = 6407178, upload-time = "2025-10-21T16:20:32.733Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/36fd7d7c75a6c12542c90a6d647a27935a1ecaad03e0ffdb7c42db6b04d2/grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990", size = 7075684, upload-time = "2025-10-21T16:20:35.435Z" }, + { url = "https://files.pythonhosted.org/packages/38/f7/e3cdb252492278e004722306c5a8935eae91e64ea11f0af3437a7de2e2b7/grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af", size = 6611133, upload-time = "2025-10-21T16:20:37.541Z" }, + { url = "https://files.pythonhosted.org/packages/7e/20/340db7af162ccd20a0893b5f3c4a5d676af7b71105517e62279b5b61d95a/grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2", size = 7195507, upload-time = "2025-10-21T16:20:39.643Z" }, + { url = "https://files.pythonhosted.org/packages/10/f0/b2160addc1487bd8fa4810857a27132fb4ce35c1b330c2f3ac45d697b106/grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6", size = 8160651, upload-time = "2025-10-21T16:20:42.492Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2c/ac6f98aa113c6ef111b3f347854e99ebb7fb9d8f7bb3af1491d438f62af4/grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3", size = 7620568, upload-time = "2025-10-21T16:20:45.995Z" }, + { url = "https://files.pythonhosted.org/packages/90/84/7852f7e087285e3ac17a2703bc4129fafee52d77c6c82af97d905566857e/grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b", size = 3998879, upload-time = "2025-10-21T16:20:48.592Z" }, + { url = "https://files.pythonhosted.org/packages/10/30/d3d2adcbb6dd3ff59d6ac3df6ef830e02b437fb5c90990429fd180e52f30/grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b", size = 4706892, upload-time = "2025-10-21T16:20:50.697Z" }, + { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" }, + { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" }, + { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" }, + { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" }, + { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" }, + { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" }, + { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" }, + { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" }, + { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" }, + { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" }, +] + +[[package]] +name = "grpcio-status" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/46/e9f19d5be65e8423f886813a2a9d0056ba94757b0c5007aa59aed1a961fa/grpcio_status-1.76.0.tar.gz", hash = "sha256:25fcbfec74c15d1a1cb5da3fab8ee9672852dc16a5a9eeb5baf7d7a9952943cd", size = 13679, upload-time = "2025-10-21T16:28:52.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/cc/27ba60ad5a5f2067963e6a858743500df408eb5855e98be778eaef8c9b02/grpcio_status-1.76.0-py3-none-any.whl", hash = "sha256:380568794055a8efbbd8871162df92012e0228a5f6dffaf57f2a00c534103b18", size = 14425, upload-time = "2025-10-21T16:28:40.853Z" }, +] + [[package]] name = "gunicorn" version = "22.0.0" @@ -1301,6 +1552,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "h2o" +version = "3.46.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "tabulate" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/11/1bbc9dab18a3c9648a6b14ddfe8d19954a64f6eca7e923f8eae1d29e9b5a/h2o-3.46.0.7-py2.py3-none-any.whl", hash = "sha256:58e15c5cf3e134876fd242b07e38309f68614702ec299b3c6ffbd53484b51e79", size = 265885843, upload-time = "2025-03-27T20:59:49.937Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -1316,31 +1579,31 @@ wheels = [ [[package]] name = "httptools" -version = "0.6.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload-time = "2024-10-16T19:45:08.902Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/6f/972f8eb0ea7d98a1c6be436e2142d51ad2a64ee18e02b0e7ff1f62171ab1/httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0", size = 198780, upload-time = "2024-10-16T19:44:06.882Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b0/17c672b4bc5c7ba7f201eada4e96c71d0a59fbc185e60e42580093a86f21/httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da", size = 103297, upload-time = "2024-10-16T19:44:08.129Z" }, - { url = "https://files.pythonhosted.org/packages/92/5e/b4a826fe91971a0b68e8c2bd4e7db3e7519882f5a8ccdb1194be2b3ab98f/httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1", size = 443130, upload-time = "2024-10-16T19:44:09.45Z" }, - { url = "https://files.pythonhosted.org/packages/b0/51/ce61e531e40289a681a463e1258fa1e05e0be54540e40d91d065a264cd8f/httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50", size = 442148, upload-time = "2024-10-16T19:44:11.539Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9e/270b7d767849b0c96f275c695d27ca76c30671f8eb8cc1bab6ced5c5e1d0/httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959", size = 415949, upload-time = "2024-10-16T19:44:13.388Z" }, - { url = "https://files.pythonhosted.org/packages/81/86/ced96e3179c48c6f656354e106934e65c8963d48b69be78f355797f0e1b3/httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4", size = 417591, upload-time = "2024-10-16T19:44:15.258Z" }, - { url = "https://files.pythonhosted.org/packages/75/73/187a3f620ed3175364ddb56847d7a608a6fc42d551e133197098c0143eca/httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c", size = 88344, upload-time = "2024-10-16T19:44:16.54Z" }, - { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029, upload-time = "2024-10-16T19:44:18.427Z" }, - { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492, upload-time = "2024-10-16T19:44:19.515Z" }, - { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891, upload-time = "2024-10-16T19:44:21.067Z" }, - { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788, upload-time = "2024-10-16T19:44:22.958Z" }, - { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214, upload-time = "2024-10-16T19:44:24.513Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120, upload-time = "2024-10-16T19:44:26.295Z" }, - { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565, upload-time = "2024-10-16T19:44:29.188Z" }, - { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683, upload-time = "2024-10-16T19:44:30.175Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337, upload-time = "2024-10-16T19:44:31.786Z" }, - { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796, upload-time = "2024-10-16T19:44:32.825Z" }, - { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837, upload-time = "2024-10-16T19:44:33.974Z" }, - { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289, upload-time = "2024-10-16T19:44:35.111Z" }, - { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779, upload-time = "2024-10-16T19:44:36.253Z" }, - { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634, upload-time = "2024-10-16T19:44:37.357Z" }, +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" }, + { url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" }, + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, ] [[package]] @@ -1360,11 +1623,11 @@ wheels = [ [[package]] name = "idna" -version = "3.10" +version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] @@ -1381,23 +1644,23 @@ wheels = [ [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "ipykernel" -version = "6.30.1" +version = "6.31.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "appnope", marker = "sys_platform == 'darwin'" }, { name = "comm" }, { name = "debugpy" }, { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ipython", version = "9.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "matplotlib-inline" }, @@ -1408,9 +1671,9 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/76/11082e338e0daadc89c8ff866185de11daf67d181901038f9e139d109761/ipykernel-6.30.1.tar.gz", hash = "sha256:6abb270161896402e76b91394fcdce5d1be5d45f456671e5080572f8505be39b", size = 166260, upload-time = "2025-08-04T15:47:35.018Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/1d/d5ba6edbfe6fae4c3105bca3a9c889563cc752c7f2de45e333164c7f4846/ipykernel-6.31.0.tar.gz", hash = "sha256:2372ce8bc1ff4f34e58cafed3a0feb2194b91fc7cad0fc72e79e47b45ee9e8f6", size = 167493, upload-time = "2025-10-20T11:42:39.948Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/c7/b445faca8deb954fe536abebff4ece5b097b923de482b26e78448c89d1dd/ipykernel-6.30.1-py3-none-any.whl", hash = "sha256:aa6b9fb93dca949069d8b85b6c79b2518e32ac583ae9c7d37c51d119e18b3fb4", size = 117484, upload-time = "2025-08-04T15:47:32.622Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl", hash = "sha256:abe5386f6ced727a70e0eb0cf1da801fa7c5fa6ff82147747d5a0406cd8c94af", size = 117003, upload-time = "2025-10-20T11:42:37.502Z" }, ] [[package]] @@ -1441,7 +1704,7 @@ wheels = [ [[package]] name = "ipython" -version = "9.4.0" +version = "9.6.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12' and sys_platform != 'win32'", @@ -1462,9 +1725,9 @@ dependencies = [ { name = "traitlets", marker = "python_full_version >= '3.11'" }, { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/80/406f9e3bde1c1fd9bf5a0be9d090f8ae623e401b7670d8f6fdf2ab679891/ipython-9.4.0.tar.gz", hash = "sha256:c033c6d4e7914c3d9768aabe76bbe87ba1dc66a92a05db6bfa1125d81f2ee270", size = 4385338, upload-time = "2025-07-01T11:11:30.606Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/34/29b18c62e39ee2f7a6a3bba7efd952729d8aadd45ca17efc34453b717665/ipython-9.6.0.tar.gz", hash = "sha256:5603d6d5d356378be5043e69441a072b50a5b33b4503428c77b04cb8ce7bc731", size = 4396932, upload-time = "2025-09-29T10:55:53.948Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/f8/0031ee2b906a15a33d6bfc12dd09c3dfa966b3cb5b284ecfb7549e6ac3c4/ipython-9.4.0-py3-none-any.whl", hash = "sha256:25850f025a446d9b359e8d296ba175a36aedd32e83ca9b5060430fe16801f066", size = 611021, upload-time = "2025-07-01T11:11:27.85Z" }, + { url = "https://files.pythonhosted.org/packages/48/c5/d5e07995077e48220269c28a221e168c91123ad5ceee44d548f54a057fc0/ipython-9.6.0-py3-none-any.whl", hash = "sha256:5f77efafc886d2f023442479b8149e7d86547ad0a979e9da9f045d252f648196", size = 616170, upload-time = "2025-09-29T10:55:47.676Z" }, ] [[package]] @@ -1479,6 +1742,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, ] +[[package]] +name = "ipywidgets" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "comm" }, + { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jupyterlab-widgets" }, + { name = "traitlets" }, + { name = "widgetsnbextension" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/ae/c5ce1edc1afe042eadb445e95b0671b03cee61895264357956e61c0d2ac0/ipywidgets-8.1.8.tar.gz", hash = "sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668", size = 116739, upload-time = "2025-11-01T21:18:12.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" }, +] + [[package]] name = "isoduration" version = "20.11.0" @@ -1493,11 +1773,11 @@ wheels = [ [[package]] name = "isort" -version = "6.0.1" +version = "6.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955, upload-time = "2025-02-26T21:13:16.955Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/82/fa43935523efdfcce6abbae9da7f372b627b27142c3419fcf13bf5b0c397/isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481", size = 824325, upload-time = "2025-10-01T16:26:45.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" }, + { url = "https://files.pythonhosted.org/packages/7f/cc/9b681a170efab4868a032631dea1e8446d8ec718a7f657b94d49d1a12643/isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784", size = 94329, upload-time = "2025-10-01T16:26:43.291Z" }, ] [[package]] @@ -1535,20 +1815,20 @@ wheels = [ [[package]] name = "joblib" -version = "1.5.1" +version = "1.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475, upload-time = "2025-05-23T12:04:37.097Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55", size = 331077, upload-time = "2025-08-27T12:15:46.575Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" }, ] [[package]] name = "json5" -version = "0.12.0" +version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/be/c6c745ec4c4539b25a278b70e29793f10382947df0d9efba2fa09120895d/json5-0.12.0.tar.gz", hash = "sha256:0b4b6ff56801a1c7dc817b0241bca4ce474a0e6a163bfef3fc594d3fd263ff3a", size = 51907, upload-time = "2025-04-03T16:33:13.201Z" } +sdist = { url = "https://files.pythonhosted.org/packages/12/ae/929aee9619e9eba9015207a9d2c1c54db18311da7eb4dcf6d41ad6f0eb67/json5-0.12.1.tar.gz", hash = "sha256:b2743e77b3242f8d03c143dd975a6ec7c52e2f2afe76ed934e53503dd4ad4990", size = 52191, upload-time = "2025-08-12T19:47:42.583Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/9f/3500910d5a98549e3098807493851eeef2b89cdd3032227558a104dfe926/json5-0.12.0-py3-none-any.whl", hash = "sha256:6d37aa6c08b0609f16e1ec5ff94697e2cbbfbad5ac112afa05794da9ab7810db", size = 36079, upload-time = "2025-04-03T16:33:11.927Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/05328bd2621be49a6fed9e3030b1e51a2d04537d3f816d211b9cc53c5262/json5-0.12.1-py3-none-any.whl", hash = "sha256:d9c9b3bc34a5f54d43c35e11ef7cb87d8bdd098c6ace87117a7b7e83e705c1d5", size = 36119, upload-time = "2025-08-12T19:47:41.131Z" }, ] [[package]] @@ -1571,7 +1851,7 @@ wheels = [ [[package]] name = "jsonschema" -version = "4.25.0" +version = "4.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -1579,9 +1859,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830, upload-time = "2025-07-18T15:39:45.11Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, ] [package.optional-dependencies] @@ -1599,14 +1879,14 @@ format-nongpl = [ [[package]] name = "jsonschema-specifications" -version = "2025.4.1" +version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] [[package]] @@ -1627,16 +1907,15 @@ wheels = [ [[package]] name = "jupyter-core" -version = "5.8.1" +version = "5.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "platformdirs" }, - { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/1b/72906d554acfeb588332eaaa6f61577705e9ec752ddb486f302dafa292d9/jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941", size = 88923, upload-time = "2025-05-27T07:38:16.655Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/57/6bffd4b20b88da3800c5d691e0337761576ee688eb01299eae865689d2df/jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0", size = 28880, upload-time = "2025-05-27T07:38:15.137Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, ] [[package]] @@ -1660,19 +1939,19 @@ wheels = [ [[package]] name = "jupyter-lsp" -version = "2.2.6" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-server" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/3d/40bdb41b665d3302390ed1356cebd5917c10769d1f190ee4ca595900840e/jupyter_lsp-2.2.6.tar.gz", hash = "sha256:0566bd9bb04fd9e6774a937ed01522b555ba78be37bebef787c8ab22de4c0361", size = 48948, upload-time = "2025-07-18T21:35:19.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/5a/9066c9f8e94ee517133cd98dba393459a16cd48bba71a82f16a65415206c/jupyter_lsp-2.3.0.tar.gz", hash = "sha256:458aa59339dc868fb784d73364f17dbce8836e906cd75fd471a325cba02e0245", size = 54823, upload-time = "2025-08-27T17:47:34.671Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/7c/12f68daf85b469b4896d5e4a629baa33c806d61de75ac5b39d8ef27ec4a2/jupyter_lsp-2.2.6-py3-none-any.whl", hash = "sha256:283783752bf0b459ee7fa88effa72104d87dd343b82d5c06cf113ef755b15b6d", size = 69371, upload-time = "2025-07-18T21:35:16.585Z" }, + { url = "https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl", hash = "sha256:e914a3cb2addf48b1c7710914771aaf1819d46b2e5a79b0f917b5478ec93f34f", size = 76687, upload-time = "2025-08-27T17:47:33.15Z" }, ] [[package]] name = "jupyter-server" -version = "2.16.0" +version = "2.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1684,7 +1963,7 @@ dependencies = [ { name = "jupyter-server-terminals" }, { name = "nbconvert" }, { name = "nbformat" }, - { name = "overrides" }, + { name = "overrides", marker = "python_full_version < '3.12'" }, { name = "packaging" }, { name = "prometheus-client" }, { name = "pywinpty", marker = "os_name == 'nt'" }, @@ -1695,9 +1974,9 @@ dependencies = [ { name = "traitlets" }, { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/c8/ba2bbcd758c47f1124c4ca14061e8ce60d9c6fd537faee9534a95f83521a/jupyter_server-2.16.0.tar.gz", hash = "sha256:65d4b44fdf2dcbbdfe0aa1ace4a842d4aaf746a2b7b168134d5aaed35621b7f6", size = 728177, upload-time = "2025-05-12T16:44:46.245Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/ac/e040ec363d7b6b1f11304cc9f209dac4517ece5d5e01821366b924a64a50/jupyter_server-2.17.0.tar.gz", hash = "sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5", size = 731949, upload-time = "2025-08-21T14:42:54.042Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/1f/5ebbced977171d09a7b0c08a285ff9a20aafb9c51bde07e52349ff1ddd71/jupyter_server-2.16.0-py3-none-any.whl", hash = "sha256:3d8db5be3bc64403b1c65b400a1d7f4647a5ce743f3b20dbdefe8ddb7b55af9e", size = 386904, upload-time = "2025-05-12T16:44:43.335Z" }, + { url = "https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl", hash = "sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f", size = 388221, upload-time = "2025-08-21T14:42:52.034Z" }, ] [[package]] @@ -1715,7 +1994,7 @@ wheels = [ [[package]] name = "jupyterlab" -version = "4.4.5" +version = "4.4.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-lru" }, @@ -1733,9 +2012,9 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/89/695805a6564bafe08ef2505f3c473ae7140b8ba431d381436f11bdc2c266/jupyterlab-4.4.5.tar.gz", hash = "sha256:0bd6c18e6a3c3d91388af6540afa3d0bb0b2e76287a7b88ddf20ab41b336e595", size = 23037079, upload-time = "2025-07-20T09:21:30.151Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/5d/75c42a48ff5fc826a7dff3fe4004cda47c54f9d981c351efacfbc9139d3c/jupyterlab-4.4.10.tar.gz", hash = "sha256:521c017508af4e1d6d9d8a9d90f47a11c61197ad63b2178342489de42540a615", size = 22969303, upload-time = "2025-10-22T14:50:58.768Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/74/e144ce85b34414e44b14c1f6bf2e3bfe17964c8e5670ebdc7629f2e53672/jupyterlab-4.4.5-py3-none-any.whl", hash = "sha256:e76244cceb2d1fb4a99341f3edc866f2a13a9e14c50368d730d75d8017be0863", size = 12267763, upload-time = "2025-07-20T09:21:26.37Z" }, + { url = "https://files.pythonhosted.org/packages/f7/46/1eaa5db8d54a594bdade67afbcae42e9a2da676628be3eb39f36dcff6390/jupyterlab-4.4.10-py3-none-any.whl", hash = "sha256:65939ab4c8dcd0c42185c2d0d1a9d60b254dc8c46fc4fdb286b63c51e9358e07", size = 12293385, upload-time = "2025-10-22T14:50:54.075Z" }, ] [[package]] @@ -1749,7 +2028,7 @@ wheels = [ [[package]] name = "jupyterlab-server" -version = "2.27.3" +version = "2.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, @@ -1760,9 +2039,18 @@ dependencies = [ { name = "packaging" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/c9/a883ce65eb27905ce77ace410d83587c82ea64dc85a48d1f7ed52bcfa68d/jupyterlab_server-2.27.3.tar.gz", hash = "sha256:eb36caca59e74471988f0ae25c77945610b887f777255aa21f8065def9e51ed4", size = 76173, upload-time = "2024-07-16T17:02:04.149Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/2c/90153f189e421e93c4bb4f9e3f59802a1f01abd2ac5cf40b152d7f735232/jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c", size = 76996, upload-time = "2025-10-22T13:59:18.37Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/09/2032e7d15c544a0e3cd831c51d77a8ca57f7555b2e1b2922142eddb02a84/jupyterlab_server-2.27.3-py3-none-any.whl", hash = "sha256:e697488f66c3db49df675158a77b3b017520d772c6e1548c7d9bcc5df7944ee4", size = 59700, upload-time = "2024-07-16T17:02:01.115Z" }, + { url = "https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968", size = 59830, upload-time = "2025-10-22T13:59:16.767Z" }, +] + +[[package]] +name = "jupyterlab-widgets" +version = "3.0.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/2d/ef58fed122b268c69c0aa099da20bc67657cdfb2e222688d5731bd5b971d/jupyterlab_widgets-3.0.16.tar.gz", hash = "sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0", size = 897423, upload-time = "2025-11-01T21:11:29.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, ] [[package]] @@ -1824,43 +2112,63 @@ wheels = [ [[package]] name = "lark" -version = "1.2.2" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.46.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/60/bc7622aefb2aee1c0b4ba23c1446d3e30225c8770b38d7aedbfb65ca9d5a/lark-1.2.2.tar.gz", hash = "sha256:ca807d0162cd16cef15a8feecb862d7319e7a09bdb13aef927968e45040fed80", size = 252132, upload-time = "2024-08-13T19:49:00.652Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/cd/08ae687ba099c7e3d21fe2ea536500563ef1943c5105bf6ab4ee3829f68e/llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb", size = 193456, upload-time = "2025-12-08T18:15:36.295Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/00/d90b10b962b4277f5e64a78b6609968859ff86889f5b898c1a778c06ec00/lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c", size = 111036, upload-time = "2024-08-13T19:48:58.603Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a4/3959e1c61c5ca9db7921e5fd115b344c29b9d57a5dadd87bef97963ca1a5/llvmlite-0.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4323177e936d61ae0f73e653e2e614284d97d14d5dd12579adc92b6c2b0597b0", size = 37232766, upload-time = "2025-12-08T18:14:34.765Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a5/a4d916f1015106e1da876028606a8e87fd5d5c840f98c87bc2d5153b6a2f/llvmlite-0.46.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a2d461cb89537b7c20feb04c46c32e12d5ad4f0896c9dfc0f60336219ff248e", size = 56275176, upload-time = "2025-12-08T18:14:37.944Z" }, + { url = "https://files.pythonhosted.org/packages/79/7f/a7f2028805dac8c1a6fae7bda4e739b7ebbcd45b29e15bf6d21556fcd3d5/llvmlite-0.46.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b1f6595a35b7b39c3518b85a28bf18f45e075264e4b2dce3f0c2a4f232b4a910", size = 55128629, upload-time = "2025-12-08T18:14:41.674Z" }, + { url = "https://files.pythonhosted.org/packages/b2/bc/4689e1ba0c073c196b594471eb21be0aa51d9e64b911728aa13cd85ef0ae/llvmlite-0.46.0-cp310-cp310-win_amd64.whl", hash = "sha256:e7a34d4aa6f9a97ee006b504be6d2b8cb7f755b80ab2f344dda1ef992f828559", size = 38138651, upload-time = "2025-12-08T18:14:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a1/2ad4b2367915faeebe8447f0a057861f646dbf5fbbb3561db42c65659cf3/llvmlite-0.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82f3d39b16f19aa1a56d5fe625883a6ab600d5cc9ea8906cca70ce94cabba067", size = 37232766, upload-time = "2025-12-08T18:14:48.836Z" }, + { url = "https://files.pythonhosted.org/packages/12/b5/99cf8772fdd846c07da4fd70f07812a3c8fd17ea2409522c946bb0f2b277/llvmlite-0.46.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a3df43900119803bbc52720e758c76f316a9a0f34612a886862dfe0a5591a17e", size = 56275175, upload-time = "2025-12-08T18:14:51.604Z" }, + { url = "https://files.pythonhosted.org/packages/38/f2/ed806f9c003563732da156139c45d970ee435bd0bfa5ed8de87ba972b452/llvmlite-0.46.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de183fefc8022d21b0aa37fc3e90410bc3524aed8617f0ff76732fc6c3af5361", size = 55128630, upload-time = "2025-12-08T18:14:55.107Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/8f5a37a65fc9b7b17408508145edd5f86263ad69c19d3574e818f533a0eb/llvmlite-0.46.0-cp311-cp311-win_amd64.whl", hash = "sha256:e8b10bc585c58bdffec9e0c309bb7d51be1f2f15e169a4b4d42f2389e431eb93", size = 38138652, upload-time = "2025-12-08T18:14:58.171Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f8/4db016a5e547d4e054ff2f3b99203d63a497465f81ab78ec8eb2ff7b2304/llvmlite-0.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9588ad4c63b4f0175a3984b85494f0c927c6b001e3a246a3a7fb3920d9a137", size = 37232767, upload-time = "2025-12-08T18:15:00.737Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/4890a7c14b4fa54400945cb52ac3cd88545bbdb973c440f98ca41591cdc5/llvmlite-0.46.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3535bd2bb6a2d7ae4012681ac228e5132cdb75fefb1bcb24e33f2f3e0c865ed4", size = 56275176, upload-time = "2025-12-08T18:15:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/6a/07/3d31d39c1a1a08cd5337e78299fca77e6aebc07c059fbd0033e3edfab45c/llvmlite-0.46.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cbfd366e60ff87ea6cc62f50bc4cd800ebb13ed4c149466f50cf2163a473d1e", size = 55128630, upload-time = "2025-12-08T18:15:07.196Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6b/d139535d7590a1bba1ceb68751bef22fadaa5b815bbdf0e858e3875726b2/llvmlite-0.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:398b39db462c39563a97b912d4f2866cd37cba60537975a09679b28fbbc0fb38", size = 38138940, upload-time = "2025-12-08T18:15:10.162Z" }, ] [[package]] name = "lz4" -version = "4.4.4" +version = "4.4.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c6/5a/945f5086326d569f14c84ac6f7fcc3229f0b9b1e8cc536b951fd53dfb9e1/lz4-4.4.4.tar.gz", hash = "sha256:070fd0627ec4393011251a094e08ed9fdcc78cb4e7ab28f507638eee4e39abda", size = 171884, upload-time = "2025-04-01T22:55:58.62Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/80/4054e99cda2e003097f59aeb3ad470128f3298db5065174a84564d2d6983/lz4-4.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f170abb8416c4efca48e76cac2c86c3185efdf841aecbe5c190121c42828ced0", size = 220896, upload-time = "2025-04-01T22:55:13.577Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4e/f92424d5734e772b05ddbeec739e2566e2a2336995b36a180e1dd9411e9a/lz4-4.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d33a5105cd96ebd32c3e78d7ece6123a9d2fb7c18b84dec61f27837d9e0c496c", size = 189679, upload-time = "2025-04-01T22:55:15.471Z" }, - { url = "https://files.pythonhosted.org/packages/a2/70/71ffd496067cba6ba352e10b89c0e9cee3e4bc4717ba866b6aa350f4c7ac/lz4-4.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ebbc5b76b4f0018988825a7e9ce153be4f0d4eba34e6c1f2fcded120573e88", size = 1237940, upload-time = "2025-04-01T22:55:16.498Z" }, - { url = "https://files.pythonhosted.org/packages/6e/59/cf34d1e232b11e1ae7122300be00529f369a7cd80f74ac351d58c4c4eedf/lz4-4.4.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc64d6dfa7a89397529b22638939e70d85eaedc1bd68e30a29c78bfb65d4f715", size = 1264105, upload-time = "2025-04-01T22:55:17.606Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f6/3a00a98ff5b872d572cc6e9c88e0f6275bea0f3ed1dc1b8f8b736c85784c/lz4-4.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a355223a284f42a723c120ce68827de66d5cb872a38732b3d5abbf544fa2fe26", size = 1184179, upload-time = "2025-04-01T22:55:19.206Z" }, - { url = "https://files.pythonhosted.org/packages/bc/de/6aeb602786174bad290609c0c988afb1077b74a80eaea23ebc3b5de6e2fa/lz4-4.4.4-cp310-cp310-win32.whl", hash = "sha256:b28228197775b7b5096898851d59ef43ccaf151136f81d9c436bc9ba560bc2ba", size = 88265, upload-time = "2025-04-01T22:55:20.215Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b5/1f52c8b17d02ae637f85911c0135ca08be1c9bbdfb3e7de1c4ae7af0bac6/lz4-4.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:45e7c954546de4f85d895aa735989d77f87dd649f503ce1c8a71a151b092ed36", size = 99916, upload-time = "2025-04-01T22:55:21.332Z" }, - { url = "https://files.pythonhosted.org/packages/01/e7/123587e7dae6cdba48393e4fdad2b9412f43f51346afe9ca6f697029de11/lz4-4.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:e3fc90f766401684740978cd781d73b9685bd81b5dbf7257542ef9de4612e4d2", size = 89746, upload-time = "2025-04-01T22:55:22.205Z" }, - { url = "https://files.pythonhosted.org/packages/28/e8/63843dc5ecb1529eb38e1761ceed04a0ad52a9ad8929ab8b7930ea2e4976/lz4-4.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ddfc7194cd206496c445e9e5b0c47f970ce982c725c87bd22de028884125b68f", size = 220898, upload-time = "2025-04-01T22:55:23.085Z" }, - { url = "https://files.pythonhosted.org/packages/e4/94/c53de5f07c7dc11cf459aab2a1d754f5df5f693bfacbbe1e4914bfd02f1e/lz4-4.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:714f9298c86f8e7278f1c6af23e509044782fa8220eb0260f8f8f1632f820550", size = 189685, upload-time = "2025-04-01T22:55:24.413Z" }, - { url = "https://files.pythonhosted.org/packages/fe/59/c22d516dd0352f2a3415d1f665ccef2f3e74ecec3ca6a8f061a38f97d50d/lz4-4.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8474c91de47733856c6686df3c4aca33753741da7e757979369c2c0d32918ba", size = 1239225, upload-time = "2025-04-01T22:55:25.737Z" }, - { url = "https://files.pythonhosted.org/packages/81/af/665685072e71f3f0e626221b7922867ec249cd8376aca761078c8f11f5da/lz4-4.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80dd27d7d680ea02c261c226acf1d41de2fd77af4fb2da62b278a9376e380de0", size = 1265881, upload-time = "2025-04-01T22:55:26.817Z" }, - { url = "https://files.pythonhosted.org/packages/90/04/b4557ae381d3aa451388a29755cc410066f5e2f78c847f66f154f4520a68/lz4-4.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b7d6dddfd01b49aedb940fdcaf32f41dc58c926ba35f4e31866aeec2f32f4f4", size = 1185593, upload-time = "2025-04-01T22:55:27.896Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e4/03636979f4e8bf92c557f998ca98ee4e6ef92e92eaf0ed6d3c7f2524e790/lz4-4.4.4-cp311-cp311-win32.whl", hash = "sha256:4134b9fd70ac41954c080b772816bb1afe0c8354ee993015a83430031d686a4c", size = 88259, upload-time = "2025-04-01T22:55:29.03Z" }, - { url = "https://files.pythonhosted.org/packages/07/f0/9efe53b4945441a5d2790d455134843ad86739855b7e6199977bf6dc8898/lz4-4.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:f5024d3ca2383470f7c4ef4d0ed8eabad0b22b23eeefde1c192cf1a38d5e9f78", size = 99916, upload-time = "2025-04-01T22:55:29.933Z" }, - { url = "https://files.pythonhosted.org/packages/87/c8/1675527549ee174b9e1db089f7ddfbb962a97314657269b1e0344a5eaf56/lz4-4.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:6ea715bb3357ea1665f77874cf8f55385ff112553db06f3742d3cdcec08633f7", size = 89741, upload-time = "2025-04-01T22:55:31.184Z" }, - { url = "https://files.pythonhosted.org/packages/f7/2d/5523b4fabe11cd98f040f715728d1932eb7e696bfe94391872a823332b94/lz4-4.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:23ae267494fdd80f0d2a131beff890cf857f1b812ee72dbb96c3204aab725553", size = 220669, upload-time = "2025-04-01T22:55:32.032Z" }, - { url = "https://files.pythonhosted.org/packages/91/06/1a5bbcacbfb48d8ee5b6eb3fca6aa84143a81d92946bdb5cd6b005f1863e/lz4-4.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fff9f3a1ed63d45cb6514bfb8293005dc4141341ce3500abdfeb76124c0b9b2e", size = 189661, upload-time = "2025-04-01T22:55:33.413Z" }, - { url = "https://files.pythonhosted.org/packages/fa/08/39eb7ac907f73e11a69a11576a75a9e36406b3241c0ba41453a7eb842abb/lz4-4.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ea7f07329f85a8eda4d8cf937b87f27f0ac392c6400f18bea2c667c8b7f8ecc", size = 1238775, upload-time = "2025-04-01T22:55:34.835Z" }, - { url = "https://files.pythonhosted.org/packages/e9/26/05840fbd4233e8d23e88411a066ab19f1e9de332edddb8df2b6a95c7fddc/lz4-4.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ccab8f7f7b82f9fa9fc3b0ba584d353bd5aa818d5821d77d5b9447faad2aaad", size = 1265143, upload-time = "2025-04-01T22:55:35.933Z" }, - { url = "https://files.pythonhosted.org/packages/b7/5d/5f2db18c298a419932f3ab2023deb689863cf8fd7ed875b1c43492479af2/lz4-4.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43e9d48b2daf80e486213128b0763deed35bbb7a59b66d1681e205e1702d735", size = 1185032, upload-time = "2025-04-01T22:55:37.454Z" }, - { url = "https://files.pythonhosted.org/packages/c4/e6/736ab5f128694b0f6aac58343bcf37163437ac95997276cd0be3ea4c3342/lz4-4.4.4-cp312-cp312-win32.whl", hash = "sha256:33e01e18e4561b0381b2c33d58e77ceee850a5067f0ece945064cbaac2176962", size = 88284, upload-time = "2025-04-01T22:55:38.536Z" }, - { url = "https://files.pythonhosted.org/packages/40/b8/243430cb62319175070e06e3a94c4c7bd186a812e474e22148ae1290d47d/lz4-4.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d21d1a2892a2dcc193163dd13eaadabb2c1b803807a5117d8f8588b22eaf9f12", size = 99918, upload-time = "2025-04-01T22:55:39.628Z" }, - { url = "https://files.pythonhosted.org/packages/6c/e1/0686c91738f3e6c2e1a243e0fdd4371667c4d2e5009b0a3605806c2aa020/lz4-4.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:2f4f2965c98ab254feddf6b5072854a6935adab7bc81412ec4fe238f07b85f62", size = 89736, upload-time = "2025-04-01T22:55:40.5Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/57/51/f1b86d93029f418033dddf9b9f79c8d2641e7454080478ee2aab5123173e/lz4-4.4.5.tar.gz", hash = "sha256:5f0b9e53c1e82e88c10d7c180069363980136b9d7a8306c4dca4f760d60c39f0", size = 172886, upload-time = "2025-11-03T13:02:36.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/45/2466d73d79e3940cad4b26761f356f19fd33f4409c96f100e01a5c566909/lz4-4.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d221fa421b389ab2345640a508db57da36947a437dfe31aeddb8d5c7b646c22d", size = 207396, upload-time = "2025-11-03T13:01:24.965Z" }, + { url = "https://files.pythonhosted.org/packages/72/12/7da96077a7e8918a5a57a25f1254edaf76aefb457666fcc1066deeecd609/lz4-4.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7dc1e1e2dbd872f8fae529acd5e4839efd0b141eaa8ae7ce835a9fe80fbad89f", size = 207154, upload-time = "2025-11-03T13:01:26.922Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0e/0fb54f84fd1890d4af5bc0a3c1fa69678451c1a6bd40de26ec0561bb4ec5/lz4-4.4.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e928ec2d84dc8d13285b4a9288fd6246c5cde4f5f935b479f50d986911f085e3", size = 1291053, upload-time = "2025-11-03T13:01:28.396Z" }, + { url = "https://files.pythonhosted.org/packages/15/45/8ce01cc2715a19c9e72b0e423262072c17d581a8da56e0bd4550f3d76a79/lz4-4.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daffa4807ef54b927451208f5f85750c545a4abbff03d740835fc444cd97f758", size = 1278586, upload-time = "2025-11-03T13:01:29.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/34/7be9b09015e18510a09b8d76c304d505a7cbc66b775ec0b8f61442316818/lz4-4.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a2b7504d2dffed3fd19d4085fe1cc30cf221263fd01030819bdd8d2bb101cf1", size = 1367315, upload-time = "2025-11-03T13:01:31.054Z" }, + { url = "https://files.pythonhosted.org/packages/2a/94/52cc3ec0d41e8d68c985ec3b2d33631f281d8b748fb44955bc0384c2627b/lz4-4.4.5-cp310-cp310-win32.whl", hash = "sha256:0846e6e78f374156ccf21c631de80967e03cc3c01c373c665789dc0c5431e7fc", size = 88173, upload-time = "2025-11-03T13:01:32.643Z" }, + { url = "https://files.pythonhosted.org/packages/ca/35/c3c0bdc409f551404355aeeabc8da343577d0e53592368062e371a3620e1/lz4-4.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:7c4e7c44b6a31de77d4dc9772b7d2561937c9588a734681f70ec547cfbc51ecd", size = 99492, upload-time = "2025-11-03T13:01:33.813Z" }, + { url = "https://files.pythonhosted.org/packages/1d/02/4d88de2f1e97f9d05fd3d278fe412b08969bc94ff34942f5a3f09318144a/lz4-4.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:15551280f5656d2206b9b43262799c89b25a25460416ec554075a8dc568e4397", size = 91280, upload-time = "2025-11-03T13:01:35.081Z" }, + { url = "https://files.pythonhosted.org/packages/93/5b/6edcd23319d9e28b1bedf32768c3d1fd56eed8223960a2c47dacd2cec2af/lz4-4.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6da84a26b3aa5da13a62e4b89ab36a396e9327de8cd48b436a3467077f8ccd4", size = 207391, upload-time = "2025-11-03T13:01:36.644Z" }, + { url = "https://files.pythonhosted.org/packages/34/36/5f9b772e85b3d5769367a79973b8030afad0d6b724444083bad09becd66f/lz4-4.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61d0ee03e6c616f4a8b69987d03d514e8896c8b1b7cc7598ad029e5c6aedfd43", size = 207146, upload-time = "2025-11-03T13:01:37.928Z" }, + { url = "https://files.pythonhosted.org/packages/04/f4/f66da5647c0d72592081a37c8775feacc3d14d2625bbdaabd6307c274565/lz4-4.4.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:33dd86cea8375d8e5dd001e41f321d0a4b1eb7985f39be1b6a4f466cd480b8a7", size = 1292623, upload-time = "2025-11-03T13:01:39.341Z" }, + { url = "https://files.pythonhosted.org/packages/85/fc/5df0f17467cdda0cad464a9197a447027879197761b55faad7ca29c29a04/lz4-4.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:609a69c68e7cfcfa9d894dc06be13f2e00761485b62df4e2472f1b66f7b405fb", size = 1279982, upload-time = "2025-11-03T13:01:40.816Z" }, + { url = "https://files.pythonhosted.org/packages/25/3b/b55cb577aa148ed4e383e9700c36f70b651cd434e1c07568f0a86c9d5fbb/lz4-4.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75419bb1a559af00250b8f1360d508444e80ed4b26d9d40ec5b09fe7875cb989", size = 1368674, upload-time = "2025-11-03T13:01:42.118Z" }, + { url = "https://files.pythonhosted.org/packages/fb/31/e97e8c74c59ea479598e5c55cbe0b1334f03ee74ca97726e872944ed42df/lz4-4.4.5-cp311-cp311-win32.whl", hash = "sha256:12233624f1bc2cebc414f9efb3113a03e89acce3ab6f72035577bc61b270d24d", size = 88168, upload-time = "2025-11-03T13:01:43.282Z" }, + { url = "https://files.pythonhosted.org/packages/18/47/715865a6c7071f417bef9b57c8644f29cb7a55b77742bd5d93a609274e7e/lz4-4.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:8a842ead8ca7c0ee2f396ca5d878c4c40439a527ebad2b996b0444f0074ed004", size = 99491, upload-time = "2025-11-03T13:01:44.167Z" }, + { url = "https://files.pythonhosted.org/packages/14/e7/ac120c2ca8caec5c945e6356ada2aa5cfabd83a01e3170f264a5c42c8231/lz4-4.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:83bc23ef65b6ae44f3287c38cbf82c269e2e96a26e560aa551735883388dcc4b", size = 91271, upload-time = "2025-11-03T13:01:45.016Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/016e4f6de37d806f7cc8f13add0a46c9a7cfc41a5ddc2bc831d7954cf1ce/lz4-4.4.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df5aa4cead2044bab83e0ebae56e0944cc7fcc1505c7787e9e1057d6d549897e", size = 207163, upload-time = "2025-11-03T13:01:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/8d/df/0fadac6e5bd31b6f34a1a8dbd4db6a7606e70715387c27368586455b7fc9/lz4-4.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d0bf51e7745484d2092b3a51ae6eb58c3bd3ce0300cf2b2c14f76c536d5697a", size = 207150, upload-time = "2025-11-03T13:01:47.205Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/34e36cc49bb16ca73fb57fbd4c5eaa61760c6b64bce91fcb4e0f4a97f852/lz4-4.4.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7b62f94b523c251cf32aa4ab555f14d39bd1a9df385b72443fd76d7c7fb051f5", size = 1292045, upload-time = "2025-11-03T13:01:48.667Z" }, + { url = "https://files.pythonhosted.org/packages/90/1c/b1d8e3741e9fc89ed3b5f7ef5f22586c07ed6bb04e8343c2e98f0fa7ff04/lz4-4.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c3ea562c3af274264444819ae9b14dbbf1ab070aff214a05e97db6896c7597e", size = 1279546, upload-time = "2025-11-03T13:01:50.159Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/e3867222474f6c1b76e89f3bd914595af69f55bf2c1866e984c548afdc15/lz4-4.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e", size = 1368249, upload-time = "2025-11-03T13:01:51.273Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e7/d667d337367686311c38b580d1ca3d5a23a6617e129f26becd4f5dc458df/lz4-4.4.5-cp312-cp312-win32.whl", hash = "sha256:214e37cfe270948ea7eb777229e211c601a3e0875541c1035ab408fbceaddf50", size = 88189, upload-time = "2025-11-03T13:01:52.605Z" }, + { url = "https://files.pythonhosted.org/packages/a5/0b/a54cd7406995ab097fceb907c7eb13a6ddd49e0b231e448f1a81a50af65c/lz4-4.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:713a777de88a73425cf08eb11f742cd2c98628e79a8673d6a52e3c5f0c116f33", size = 99497, upload-time = "2025-11-03T13:01:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7e/dc28a952e4bfa32ca16fa2eb026e7a6ce5d1411fcd5986cd08c74ec187b9/lz4-4.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:a88cbb729cc333334ccfb52f070463c21560fca63afcf636a9f160a55fac3301", size = 91279, upload-time = "2025-11-03T13:01:54.419Z" }, ] [[package]] @@ -1877,11 +2185,11 @@ wheels = [ [[package]] name = "markdown" -version = "3.8.2" +version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, + { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, ] [[package]] @@ -1898,45 +2206,48 @@ wheels = [ [[package]] name = "markupsafe" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, - { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, - { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, - { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, - { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, - { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, - { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, - { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, - { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, ] [[package]] name = "matplotlib" -version = "3.10.5" +version = "3.10.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -1950,46 +2261,46 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/91/f2939bb60b7ebf12478b030e0d7f340247390f402b3b189616aad790c366/matplotlib-3.10.5.tar.gz", hash = "sha256:352ed6ccfb7998a00881692f38b4ca083c691d3e275b4145423704c34c909076", size = 34804044, upload-time = "2025-07-31T18:09:33.805Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/89/5355cdfe43242cb4d1a64a67cb6831398b665ad90e9702c16247cbd8d5ab/matplotlib-3.10.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5d4773a6d1c106ca05cb5a5515d277a6bb96ed09e5c8fab6b7741b8fcaa62c8f", size = 8229094, upload-time = "2025-07-31T18:07:36.507Z" }, - { url = "https://files.pythonhosted.org/packages/34/bc/ba802650e1c69650faed261a9df004af4c6f21759d7a1ec67fe972f093b3/matplotlib-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc88af74e7ba27de6cbe6faee916024ea35d895ed3d61ef6f58c4ce97da7185a", size = 8091464, upload-time = "2025-07-31T18:07:38.864Z" }, - { url = "https://files.pythonhosted.org/packages/ac/64/8d0c8937dee86c286625bddb1902efacc3e22f2b619f5b5a8df29fe5217b/matplotlib-3.10.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:64c4535419d5617f7363dad171a5a59963308e0f3f813c4bed6c9e6e2c131512", size = 8653163, upload-time = "2025-07-31T18:07:41.141Z" }, - { url = "https://files.pythonhosted.org/packages/11/dc/8dfc0acfbdc2fc2336c72561b7935cfa73db9ca70b875d8d3e1b3a6f371a/matplotlib-3.10.5-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a277033048ab22d34f88a3c5243938cef776493f6201a8742ed5f8b553201343", size = 9490635, upload-time = "2025-07-31T18:07:42.936Z" }, - { url = "https://files.pythonhosted.org/packages/54/02/e3fdfe0f2e9fb05f3a691d63876639dbf684170fdcf93231e973104153b4/matplotlib-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e4a6470a118a2e93022ecc7d3bd16b3114b2004ea2bf014fff875b3bc99b70c6", size = 9539036, upload-time = "2025-07-31T18:07:45.18Z" }, - { url = "https://files.pythonhosted.org/packages/c1/29/82bf486ff7f4dbedfb11ccc207d0575cbe3be6ea26f75be514252bde3d70/matplotlib-3.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:7e44cada61bec8833c106547786814dd4a266c1b2964fd25daa3804f1b8d4467", size = 8093529, upload-time = "2025-07-31T18:07:49.553Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c7/1f2db90a1d43710478bb1e9b57b162852f79234d28e4f48a28cc415aa583/matplotlib-3.10.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dcfc39c452c6a9f9028d3e44d2d721484f665304857188124b505b2c95e1eecf", size = 8239216, upload-time = "2025-07-31T18:07:51.947Z" }, - { url = "https://files.pythonhosted.org/packages/82/6d/ca6844c77a4f89b1c9e4d481c412e1d1dbabf2aae2cbc5aa2da4a1d6683e/matplotlib-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:903352681b59f3efbf4546985142a9686ea1d616bb054b09a537a06e4b892ccf", size = 8102130, upload-time = "2025-07-31T18:07:53.65Z" }, - { url = "https://files.pythonhosted.org/packages/1d/1e/5e187a30cc673a3e384f3723e5f3c416033c1d8d5da414f82e4e731128ea/matplotlib-3.10.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:080c3676a56b8ee1c762bcf8fca3fe709daa1ee23e6ef06ad9f3fc17332f2d2a", size = 8666471, upload-time = "2025-07-31T18:07:55.304Z" }, - { url = "https://files.pythonhosted.org/packages/03/c0/95540d584d7d645324db99a845ac194e915ef75011a0d5e19e1b5cee7e69/matplotlib-3.10.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b4984d5064a35b6f66d2c11d668565f4389b1119cc64db7a4c1725bc11adffc", size = 9500518, upload-time = "2025-07-31T18:07:57.199Z" }, - { url = "https://files.pythonhosted.org/packages/ba/2e/e019352099ea58b4169adb9c6e1a2ad0c568c6377c2b677ee1f06de2adc7/matplotlib-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3967424121d3a46705c9fa9bdb0931de3228f13f73d7bb03c999c88343a89d89", size = 9552372, upload-time = "2025-07-31T18:07:59.41Z" }, - { url = "https://files.pythonhosted.org/packages/b7/81/3200b792a5e8b354f31f4101ad7834743ad07b6d620259f2059317b25e4d/matplotlib-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:33775bbeb75528555a15ac29396940128ef5613cf9a2d31fb1bfd18b3c0c0903", size = 8100634, upload-time = "2025-07-31T18:08:01.801Z" }, - { url = "https://files.pythonhosted.org/packages/52/46/a944f6f0c1f5476a0adfa501969d229ce5ae60cf9a663be0e70361381f89/matplotlib-3.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:c61333a8e5e6240e73769d5826b9a31d8b22df76c0778f8480baf1b4b01c9420", size = 7978880, upload-time = "2025-07-31T18:08:03.407Z" }, - { url = "https://files.pythonhosted.org/packages/66/1e/c6f6bcd882d589410b475ca1fc22e34e34c82adff519caf18f3e6dd9d682/matplotlib-3.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:00b6feadc28a08bd3c65b2894f56cf3c94fc8f7adcbc6ab4516ae1e8ed8f62e2", size = 8253056, upload-time = "2025-07-31T18:08:05.385Z" }, - { url = "https://files.pythonhosted.org/packages/53/e6/d6f7d1b59413f233793dda14419776f5f443bcccb2dfc84b09f09fe05dbe/matplotlib-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee98a5c5344dc7f48dc261b6ba5d9900c008fc12beb3fa6ebda81273602cc389", size = 8110131, upload-time = "2025-07-31T18:08:07.293Z" }, - { url = "https://files.pythonhosted.org/packages/66/2b/bed8a45e74957549197a2ac2e1259671cd80b55ed9e1fe2b5c94d88a9202/matplotlib-3.10.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a17e57e33de901d221a07af32c08870ed4528db0b6059dce7d7e65c1122d4bea", size = 8669603, upload-time = "2025-07-31T18:08:09.064Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a7/315e9435b10d057f5e52dfc603cd353167ae28bb1a4e033d41540c0067a4/matplotlib-3.10.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97b9d6443419085950ee4a5b1ee08c363e5c43d7176e55513479e53669e88468", size = 9508127, upload-time = "2025-07-31T18:08:10.845Z" }, - { url = "https://files.pythonhosted.org/packages/7f/d9/edcbb1f02ca99165365d2768d517898c22c6040187e2ae2ce7294437c413/matplotlib-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ceefe5d40807d29a66ae916c6a3915d60ef9f028ce1927b84e727be91d884369", size = 9566926, upload-time = "2025-07-31T18:08:13.186Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d9/6dd924ad5616c97b7308e6320cf392c466237a82a2040381163b7500510a/matplotlib-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:c04cba0f93d40e45b3c187c6c52c17f24535b27d545f757a2fffebc06c12b98b", size = 8107599, upload-time = "2025-07-31T18:08:15.116Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f3/522dc319a50f7b0279fbe74f86f7a3506ce414bc23172098e8d2bdf21894/matplotlib-3.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:a41bcb6e2c8e79dc99c5511ae6f7787d2fb52efd3d805fff06d5d4f667db16b2", size = 7978173, upload-time = "2025-07-31T18:08:21.518Z" }, - { url = "https://files.pythonhosted.org/packages/e4/eb/7d4c5de49eb78294e1a8e2be8a6ecff8b433e921b731412a56cd1abd3567/matplotlib-3.10.5-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b5fa2e941f77eb579005fb804026f9d0a1082276118d01cc6051d0d9626eaa7f", size = 8222360, upload-time = "2025-07-31T18:09:21.813Z" }, - { url = "https://files.pythonhosted.org/packages/16/8a/e435db90927b66b16d69f8f009498775f4469f8de4d14b87856965e58eba/matplotlib-3.10.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1fc0d2a3241cdcb9daaca279204a3351ce9df3c0e7e621c7e04ec28aaacaca30", size = 8087462, upload-time = "2025-07-31T18:09:23.504Z" }, - { url = "https://files.pythonhosted.org/packages/0b/dd/06c0e00064362f5647f318e00b435be2ff76a1bdced97c5eaf8347311fbe/matplotlib-3.10.5-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8dee65cb1424b7dc982fe87895b5613d4e691cc57117e8af840da0148ca6c1d7", size = 8659802, upload-time = "2025-07-31T18:09:25.256Z" }, - { url = "https://files.pythonhosted.org/packages/dc/d6/e921be4e1a5f7aca5194e1f016cb67ec294548e530013251f630713e456d/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:160e125da27a749481eaddc0627962990f6029811dbeae23881833a011a0907f", size = 8233224, upload-time = "2025-07-31T18:09:27.512Z" }, - { url = "https://files.pythonhosted.org/packages/ec/74/a2b9b04824b9c349c8f1b2d21d5af43fa7010039427f2b133a034cb09e59/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac3d50760394d78a3c9be6b28318fe22b494c4fcf6407e8fd4794b538251899b", size = 8098539, upload-time = "2025-07-31T18:09:29.629Z" }, - { url = "https://files.pythonhosted.org/packages/fc/66/cd29ebc7f6c0d2a15d216fb572573e8fc38bd5d6dec3bd9d7d904c0949f7/matplotlib-3.10.5-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c49465bf689c4d59d174d0c7795fb42a21d4244d11d70e52b8011987367ac61", size = 8672192, upload-time = "2025-07-31T18:09:31.407Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/87/3932d5778ab4c025db22710b61f49ccaed3956c5cf46ffb2ffa7492b06d9/matplotlib-3.10.7-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ac81eee3b7c266dd92cee1cd658407b16c57eed08c7421fa354ed68234de380", size = 8247141, upload-time = "2025-10-09T00:26:06.023Z" }, + { url = "https://files.pythonhosted.org/packages/45/a8/bfed45339160102bce21a44e38a358a1134a5f84c26166de03fb4a53208f/matplotlib-3.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:667ecd5d8d37813a845053d8f5bf110b534c3c9f30e69ebd25d4701385935a6d", size = 8107995, upload-time = "2025-10-09T00:26:08.669Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3c/5692a2d9a5ba848fda3f48d2b607037df96460b941a59ef236404b39776b/matplotlib-3.10.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc1c51b846aca49a5a8b44fbba6a92d583a35c64590ad9e1e950dc88940a4297", size = 8680503, upload-time = "2025-10-09T00:26:10.607Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/86ace53c48b05d0e6e9c127b2ace097434901f3e7b93f050791c8243201a/matplotlib-3.10.7-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a11c2e9e72e7de09b7b72e62f3df23317c888299c875e2b778abf1eda8c0a42", size = 9514982, upload-time = "2025-10-09T00:26:12.594Z" }, + { url = "https://files.pythonhosted.org/packages/a6/81/ead71e2824da8f72640a64166d10e62300df4ae4db01a0bac56c5b39fa51/matplotlib-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f19410b486fdd139885ace124e57f938c1e6a3210ea13dd29cab58f5d4bc12c7", size = 9566429, upload-time = "2025-10-09T00:26:14.758Z" }, + { url = "https://files.pythonhosted.org/packages/65/7d/954b3067120456f472cce8fdcacaf4a5fcd522478db0c37bb243c7cb59dd/matplotlib-3.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:b498e9e4022f93de2d5a37615200ca01297ceebbb56fe4c833f46862a490f9e3", size = 8108174, upload-time = "2025-10-09T00:26:17.015Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bc/0fb489005669127ec13f51be0c6adc074d7cf191075dab1da9fe3b7a3cfc/matplotlib-3.10.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:53b492410a6cd66c7a471de6c924f6ede976e963c0f3097a3b7abfadddc67d0a", size = 8257507, upload-time = "2025-10-09T00:26:19.073Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/d42588ad895279ff6708924645b5d2ed54a7fb2dc045c8a804e955aeace1/matplotlib-3.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9749313deb729f08207718d29c86246beb2ea3fdba753595b55901dee5d2fd6", size = 8119565, upload-time = "2025-10-09T00:26:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/10/b7/4aa196155b4d846bd749cf82aa5a4c300cf55a8b5e0dfa5b722a63c0f8a0/matplotlib-3.10.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2222c7ba2cbde7fe63032769f6eb7e83ab3227f47d997a8453377709b7fe3a5a", size = 8692668, upload-time = "2025-10-09T00:26:22.967Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e7/664d2b97016f46683a02d854d730cfcf54ff92c1dafa424beebef50f831d/matplotlib-3.10.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e91f61a064c92c307c5a9dc8c05dc9f8a68f0a3be199d9a002a0622e13f874a1", size = 9521051, upload-time = "2025-10-09T00:26:25.041Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a3/37aef1404efa615f49b5758a5e0261c16dd88f389bc1861e722620e4a754/matplotlib-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f1851eab59ca082c95df5a500106bad73672645625e04538b3ad0f69471ffcc", size = 9576878, upload-time = "2025-10-09T00:26:27.478Z" }, + { url = "https://files.pythonhosted.org/packages/33/cd/b145f9797126f3f809d177ca378de57c45413c5099c5990de2658760594a/matplotlib-3.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:6516ce375109c60ceec579e699524e9d504cd7578506f01150f7a6bc174a775e", size = 8115142, upload-time = "2025-10-09T00:26:29.774Z" }, + { url = "https://files.pythonhosted.org/packages/2e/39/63bca9d2b78455ed497fcf51a9c71df200a11048f48249038f06447fa947/matplotlib-3.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:b172db79759f5f9bc13ef1c3ef8b9ee7b37b0247f987fbbbdaa15e4f87fd46a9", size = 7992439, upload-time = "2025-10-09T00:26:40.32Z" }, + { url = "https://files.pythonhosted.org/packages/be/b3/09eb0f7796932826ec20c25b517d568627754f6c6462fca19e12c02f2e12/matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748", size = 8272389, upload-time = "2025-10-09T00:26:42.474Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/1ae80ddafb8652fd8046cb5c8460ecc8d4afccb89e2c6d6bec61e04e1eaf/matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f", size = 8128247, upload-time = "2025-10-09T00:26:44.77Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/95ae2e242d4a5c98bd6e90e36e128d71cf1c7e39b0874feaed3ef782e789/matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0", size = 8696996, upload-time = "2025-10-09T00:26:46.792Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3d/5b559efc800bd05cb2033aa85f7e13af51958136a48327f7c261801ff90a/matplotlib-3.10.7-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11ae579ac83cdf3fb72573bb89f70e0534de05266728740d478f0f818983c695", size = 9530153, upload-time = "2025-10-09T00:26:49.07Z" }, + { url = "https://files.pythonhosted.org/packages/88/57/eab4a719fd110312d3c220595d63a3c85ec2a39723f0f4e7fa7e6e3f74ba/matplotlib-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4c14b6acd16cddc3569a2d515cfdd81c7a68ac5639b76548cfc1a9e48b20eb65", size = 9593093, upload-time = "2025-10-09T00:26:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/31/3c/80816f027b3a4a28cd2a0a6ef7f89a2db22310e945cd886ec25bfb399221/matplotlib-3.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:0d8c32b7ea6fb80b1aeff5a2ceb3fb9778e2759e899d9beff75584714afcc5ee", size = 8122771, upload-time = "2025-10-09T00:26:53.296Z" }, + { url = "https://files.pythonhosted.org/packages/de/77/ef1fc78bfe99999b2675435cc52120887191c566b25017d78beaabef7f2d/matplotlib-3.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:5f3f6d315dcc176ba7ca6e74c7768fb7e4cf566c49cb143f6bc257b62e634ed8", size = 7992812, upload-time = "2025-10-09T00:26:54.882Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6c/a9bcf03e9afb2a873e0a5855f79bce476d1023f26f8212969f2b7504756c/matplotlib-3.10.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5c09cf8f2793f81368f49f118b6f9f937456362bee282eac575cca7f84cda537", size = 8241204, upload-time = "2025-10-09T00:27:48.806Z" }, + { url = "https://files.pythonhosted.org/packages/5b/fd/0e6f5aa762ed689d9fa8750b08f1932628ffa7ed30e76423c399d19407d2/matplotlib-3.10.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:de66744b2bb88d5cd27e80dfc2ec9f0517d0a46d204ff98fe9e5f2864eb67657", size = 8104607, upload-time = "2025-10-09T00:27:50.876Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a9/21c9439d698fac5f0de8fc68b2405b738ed1f00e1279c76f2d9aa5521ead/matplotlib-3.10.7-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53cc80662dd197ece414dd5b66e07370201515a3eaf52e7c518c68c16814773b", size = 8682257, upload-time = "2025-10-09T00:27:52.597Z" }, + { url = "https://files.pythonhosted.org/packages/58/8f/76d5dc21ac64a49e5498d7f0472c0781dae442dd266a67458baec38288ec/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15112bcbaef211bd663fa935ec33313b948e214454d949b723998a43357b17b0", size = 8252283, upload-time = "2025-10-09T00:27:54.739Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/9c5d4c2317feb31d819e38c9f947c942f42ebd4eb935fc6fd3518a11eaa7/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2a959c640cdeecdd2ec3136e8ea0441da59bcaf58d67e9c590740addba2cb68", size = 8116733, upload-time = "2025-10-09T00:27:56.406Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cc/3fe688ff1355010937713164caacf9ed443675ac48a997bab6ed23b3f7c0/matplotlib-3.10.7-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3886e47f64611046bc1db523a09dd0a0a6bed6081e6f90e13806dd1d1d1b5e91", size = 8693919, upload-time = "2025-10-09T00:27:58.41Z" }, ] [[package]] name = "matplotlib-inline" -version = "0.1.7" +version = "0.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, ] [[package]] @@ -2010,21 +2321,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "missingno" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "seaborn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/f9/e20220b851bc6d7abf70b403a3080d6af946ed4b13b2bf91ef4e74d2aec9/missingno-0.5.2.tar.gz", hash = "sha256:4a4baa9ca9f9e4e0d9402455df26b656632e94b99e87fa64c0cdbbbc722837ac", size = 17489, upload-time = "2023-02-26T20:10:29.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/22/cd5cf999af21c2f97486622c551ac3d07361ced8125121e907f588ff5f24/missingno-0.5.2-py3-none-any.whl", hash = "sha256:55782621ce09ba0f0a1d08e5bd6d6fe1946469fb03951fadf7d209911ca5b072", size = 8704, upload-time = "2023-02-26T20:10:26.042Z" }, +] + [[package]] name = "mistune" -version = "3.1.3" +version = "3.1.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/79/bda47f7dd7c3c55770478d6d02c9960c430b0cf1773b72366ff89126ea31/mistune-3.1.3.tar.gz", hash = "sha256:a7035c21782b2becb6be62f8f25d3df81ccb4d6fa477a6525b15af06539f02a0", size = 94347, upload-time = "2025-03-19T14:27:24.955Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/02/a7fb8b21d4d55ac93cdcde9d3638da5dd0ebdd3a4fed76c7725e10b81cbe/mistune-3.1.4.tar.gz", hash = "sha256:b5a7f801d389f724ec702840c11d8fc48f2b33519102fc7ee739e8177b672164", size = 94588, upload-time = "2025-08-29T07:20:43.594Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/4d/23c4e4f09da849e127e9f123241946c23c1e30f45a88366879e064211815/mistune-3.1.3-py3-none-any.whl", hash = "sha256:1a32314113cff28aa6432e99e522677c8587fd83e3d51c29b82a52409c842bd9", size = 53410, upload-time = "2025-03-19T14:27:23.451Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f0/8282d9641415e9e33df173516226b404d367a0fc55e1a60424a152913abc/mistune-3.1.4-py3-none-any.whl", hash = "sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d", size = 53481, upload-time = "2025-08-29T07:20:42.218Z" }, ] [[package]] name = "mlflow" -version = "2.15.1" +version = "2.22.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "alembic" }, @@ -2039,113 +2366,114 @@ dependencies = [ { name = "numpy" }, { name = "pandas" }, { name = "pyarrow" }, - { name = "querystring-parser" }, { name = "scikit-learn" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sqlalchemy" }, { name = "waitress", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/e8/98ffb4ac7213ff4d7de7e6ceebdb906270357a1803735dce07637fca4cb0/mlflow-2.15.1.tar.gz", hash = "sha256:88da13f547cedce992d4614a4547f44bcdaf369e893179dd9a8bfa60338011bf", size = 25732254, upload-time = "2024-08-06T10:43:32.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/56/4aaea65472c25dd463ed0855c1d673749cd9050e5c8214642d17434b441a/mlflow-2.22.4.tar.gz", hash = "sha256:cb8cb3b82ec696dc613bcc347b023c20fc0ed6a82170b36d0ded01d3ba06da97", size = 28377569, upload-time = "2025-12-05T13:20:56.105Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/7a/7d5594ddcaaff7a92caed1d7822cfe52ed01fe06c94b4ad88bcfef579c32/mlflow-2.15.1-py3-none-any.whl", hash = "sha256:f998b8ec9df9199284f52e79ea5dd0b2b76b327ed7f060531e44f1ecd197c5d9", size = 26284155, upload-time = "2024-08-06T10:43:27.255Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0b/bf491b0604f2608e97b53b8cc33220fd20855ac4762d18d0ddf0d3ae3a6c/mlflow-2.22.4-py3-none-any.whl", hash = "sha256:c37b312060737cc9197c4a956c730fa6c292580787fe464efe736c339e87649a", size = 29004180, upload-time = "2025-12-05T13:20:52.703Z" }, ] [[package]] name = "mlflow-skinny" -version = "2.15.1" +version = "2.22.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "click" }, { name = "cloudpickle" }, { name = "databricks-sdk" }, - { name = "entrypoints" }, + { name = "fastapi" }, { name = "gitpython" }, { name = "importlib-metadata" }, { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, { name = "packaging" }, { name = "protobuf" }, - { name = "pytz" }, + { name = "pydantic" }, { name = "pyyaml" }, { name = "requests" }, { name = "sqlparse" }, + { name = "typing-extensions" }, + { name = "uvicorn" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/dd/b0406075c04572fa934ef2115b2f3b5529b24b1c1dd335bac0fd5c3194c8/mlflow_skinny-2.15.1.tar.gz", hash = "sha256:302f49757ffc8bdfc517b06f5252a02634203fec5e5ce95ad876a36af8403907", size = 5171535, upload-time = "2024-08-06T10:45:35.135Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/73/de6cfdd1bd48fd896c33844b863931bf7215f9401e01e4554019aca0fa94/mlflow_skinny-2.22.4.tar.gz", hash = "sha256:d75ef4c6f38b745d84aef4d6dcb26331c8a3c784ee5a284ec89186398c8d927b", size = 5892192, upload-time = "2025-12-05T12:50:03.045Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/a1/3812743e5dd83317d0469a46d737f0ab5c084fecfecc03a1ac8a7e7ec0d8/mlflow_skinny-2.15.1-py3-none-any.whl", hash = "sha256:a48c6f56106b104dc7221bad91af75a150b927d15210a41928cc8ecba086470a", size = 5497955, upload-time = "2024-08-06T10:45:32.689Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d1/549a995e261ca708c60fe0b63dfa4d1842fc58b04eb9c78cd678aebe1e7e/mlflow_skinny-2.22.4-py3-none-any.whl", hash = "sha256:3622115f53806d99fc42b0c2e45f225b16948584feeec7f233e484f08fe6c7f2", size = 6270862, upload-time = "2025-12-05T12:50:00.406Z" }, ] [[package]] name = "multidict" -version = "6.6.4" +version = "6.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload-time = "2025-08-11T12:08:48.217Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/6b/86f353088c1358e76fd30b0146947fddecee812703b604ee901e85cd2a80/multidict-6.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f", size = 77054, upload-time = "2025-08-11T12:06:02.99Z" }, - { url = "https://files.pythonhosted.org/packages/19/5d/c01dc3d3788bb877bd7f5753ea6eb23c1beeca8044902a8f5bfb54430f63/multidict-6.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb", size = 44914, upload-time = "2025-08-11T12:06:05.264Z" }, - { url = "https://files.pythonhosted.org/packages/46/44/964dae19ea42f7d3e166474d8205f14bb811020e28bc423d46123ddda763/multidict-6.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495", size = 44601, upload-time = "2025-08-11T12:06:06.627Z" }, - { url = "https://files.pythonhosted.org/packages/31/20/0616348a1dfb36cb2ab33fc9521de1f27235a397bf3f59338e583afadd17/multidict-6.6.4-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8", size = 224821, upload-time = "2025-08-11T12:06:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/14/26/5d8923c69c110ff51861af05bd27ca6783011b96725d59ccae6d9daeb627/multidict-6.6.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7", size = 242608, upload-time = "2025-08-11T12:06:09.697Z" }, - { url = "https://files.pythonhosted.org/packages/5c/cc/e2ad3ba9459aa34fa65cf1f82a5c4a820a2ce615aacfb5143b8817f76504/multidict-6.6.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796", size = 222324, upload-time = "2025-08-11T12:06:10.905Z" }, - { url = "https://files.pythonhosted.org/packages/19/db/4ed0f65701afbc2cb0c140d2d02928bb0fe38dd044af76e58ad7c54fd21f/multidict-6.6.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db", size = 253234, upload-time = "2025-08-11T12:06:12.658Z" }, - { url = "https://files.pythonhosted.org/packages/94/c1/5160c9813269e39ae14b73debb907bfaaa1beee1762da8c4fb95df4764ed/multidict-6.6.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0", size = 251613, upload-time = "2025-08-11T12:06:13.97Z" }, - { url = "https://files.pythonhosted.org/packages/05/a9/48d1bd111fc2f8fb98b2ed7f9a115c55a9355358432a19f53c0b74d8425d/multidict-6.6.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877", size = 241649, upload-time = "2025-08-11T12:06:15.204Z" }, - { url = "https://files.pythonhosted.org/packages/85/2a/f7d743df0019408768af8a70d2037546a2be7b81fbb65f040d76caafd4c5/multidict-6.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace", size = 239238, upload-time = "2025-08-11T12:06:16.467Z" }, - { url = "https://files.pythonhosted.org/packages/cb/b8/4f4bb13323c2d647323f7919201493cf48ebe7ded971717bfb0f1a79b6bf/multidict-6.6.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6", size = 233517, upload-time = "2025-08-11T12:06:18.107Z" }, - { url = "https://files.pythonhosted.org/packages/33/29/4293c26029ebfbba4f574febd2ed01b6f619cfa0d2e344217d53eef34192/multidict-6.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb", size = 243122, upload-time = "2025-08-11T12:06:19.361Z" }, - { url = "https://files.pythonhosted.org/packages/20/60/a1c53628168aa22447bfde3a8730096ac28086704a0d8c590f3b63388d0c/multidict-6.6.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb", size = 248992, upload-time = "2025-08-11T12:06:20.661Z" }, - { url = "https://files.pythonhosted.org/packages/a3/3b/55443a0c372f33cae5d9ec37a6a973802884fa0ab3586659b197cf8cc5e9/multidict-6.6.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987", size = 243708, upload-time = "2025-08-11T12:06:21.891Z" }, - { url = "https://files.pythonhosted.org/packages/7c/60/a18c6900086769312560b2626b18e8cca22d9e85b1186ba77f4755b11266/multidict-6.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f", size = 237498, upload-time = "2025-08-11T12:06:23.206Z" }, - { url = "https://files.pythonhosted.org/packages/11/3d/8bdd8bcaff2951ce2affccca107a404925a2beafedd5aef0b5e4a71120a6/multidict-6.6.4-cp310-cp310-win32.whl", hash = "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f", size = 41415, upload-time = "2025-08-11T12:06:24.77Z" }, - { url = "https://files.pythonhosted.org/packages/c0/53/cab1ad80356a4cd1b685a254b680167059b433b573e53872fab245e9fc95/multidict-6.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0", size = 46046, upload-time = "2025-08-11T12:06:25.893Z" }, - { url = "https://files.pythonhosted.org/packages/cf/9a/874212b6f5c1c2d870d0a7adc5bb4cfe9b0624fa15cdf5cf757c0f5087ae/multidict-6.6.4-cp310-cp310-win_arm64.whl", hash = "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729", size = 43147, upload-time = "2025-08-11T12:06:27.534Z" }, - { url = "https://files.pythonhosted.org/packages/6b/7f/90a7f01e2d005d6653c689039977f6856718c75c5579445effb7e60923d1/multidict-6.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c", size = 76472, upload-time = "2025-08-11T12:06:29.006Z" }, - { url = "https://files.pythonhosted.org/packages/54/a3/bed07bc9e2bb302ce752f1dabc69e884cd6a676da44fb0e501b246031fdd/multidict-6.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb", size = 44634, upload-time = "2025-08-11T12:06:30.374Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4b/ceeb4f8f33cf81277da464307afeaf164fb0297947642585884f5cad4f28/multidict-6.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e", size = 44282, upload-time = "2025-08-11T12:06:31.958Z" }, - { url = "https://files.pythonhosted.org/packages/03/35/436a5da8702b06866189b69f655ffdb8f70796252a8772a77815f1812679/multidict-6.6.4-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded", size = 229696, upload-time = "2025-08-11T12:06:33.087Z" }, - { url = "https://files.pythonhosted.org/packages/b6/0e/915160be8fecf1fca35f790c08fb74ca684d752fcba62c11daaf3d92c216/multidict-6.6.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683", size = 246665, upload-time = "2025-08-11T12:06:34.448Z" }, - { url = "https://files.pythonhosted.org/packages/08/ee/2f464330acd83f77dcc346f0b1a0eaae10230291450887f96b204b8ac4d3/multidict-6.6.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a", size = 225485, upload-time = "2025-08-11T12:06:35.672Z" }, - { url = "https://files.pythonhosted.org/packages/71/cc/9a117f828b4d7fbaec6adeed2204f211e9caf0a012692a1ee32169f846ae/multidict-6.6.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9", size = 257318, upload-time = "2025-08-11T12:06:36.98Z" }, - { url = "https://files.pythonhosted.org/packages/25/77/62752d3dbd70e27fdd68e86626c1ae6bccfebe2bb1f84ae226363e112f5a/multidict-6.6.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50", size = 254689, upload-time = "2025-08-11T12:06:38.233Z" }, - { url = "https://files.pythonhosted.org/packages/00/6e/fac58b1072a6fc59af5e7acb245e8754d3e1f97f4f808a6559951f72a0d4/multidict-6.6.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52", size = 246709, upload-time = "2025-08-11T12:06:39.517Z" }, - { url = "https://files.pythonhosted.org/packages/01/ef/4698d6842ef5e797c6db7744b0081e36fb5de3d00002cc4c58071097fac3/multidict-6.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6", size = 243185, upload-time = "2025-08-11T12:06:40.796Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c9/d82e95ae1d6e4ef396934e9b0e942dfc428775f9554acf04393cce66b157/multidict-6.6.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e", size = 237838, upload-time = "2025-08-11T12:06:42.595Z" }, - { url = "https://files.pythonhosted.org/packages/57/cf/f94af5c36baaa75d44fab9f02e2a6bcfa0cd90acb44d4976a80960759dbc/multidict-6.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3", size = 246368, upload-time = "2025-08-11T12:06:44.304Z" }, - { url = "https://files.pythonhosted.org/packages/4a/fe/29f23460c3d995f6a4b678cb2e9730e7277231b981f0b234702f0177818a/multidict-6.6.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c", size = 253339, upload-time = "2025-08-11T12:06:45.597Z" }, - { url = "https://files.pythonhosted.org/packages/29/b6/fd59449204426187b82bf8a75f629310f68c6adc9559dc922d5abe34797b/multidict-6.6.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b", size = 246933, upload-time = "2025-08-11T12:06:46.841Z" }, - { url = "https://files.pythonhosted.org/packages/19/52/d5d6b344f176a5ac3606f7a61fb44dc746e04550e1a13834dff722b8d7d6/multidict-6.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f", size = 242225, upload-time = "2025-08-11T12:06:48.588Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d3/5b2281ed89ff4d5318d82478a2a2450fcdfc3300da48ff15c1778280ad26/multidict-6.6.4-cp311-cp311-win32.whl", hash = "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2", size = 41306, upload-time = "2025-08-11T12:06:49.95Z" }, - { url = "https://files.pythonhosted.org/packages/74/7d/36b045c23a1ab98507aefd44fd8b264ee1dd5e5010543c6fccf82141ccef/multidict-6.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e", size = 46029, upload-time = "2025-08-11T12:06:51.082Z" }, - { url = "https://files.pythonhosted.org/packages/0f/5e/553d67d24432c5cd52b49047f2d248821843743ee6d29a704594f656d182/multidict-6.6.4-cp311-cp311-win_arm64.whl", hash = "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf", size = 43017, upload-time = "2025-08-11T12:06:52.243Z" }, - { url = "https://files.pythonhosted.org/packages/05/f6/512ffd8fd8b37fb2680e5ac35d788f1d71bbaf37789d21a820bdc441e565/multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", size = 76516, upload-time = "2025-08-11T12:06:53.393Z" }, - { url = "https://files.pythonhosted.org/packages/99/58/45c3e75deb8855c36bd66cc1658007589662ba584dbf423d01df478dd1c5/multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", size = 45394, upload-time = "2025-08-11T12:06:54.555Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/e8c4472a93a26e4507c0b8e1f0762c0d8a32de1328ef72fd704ef9cc5447/multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", size = 43591, upload-time = "2025-08-11T12:06:55.672Z" }, - { url = "https://files.pythonhosted.org/packages/05/51/edf414f4df058574a7265034d04c935aa84a89e79ce90fcf4df211f47b16/multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", size = 237215, upload-time = "2025-08-11T12:06:57.213Z" }, - { url = "https://files.pythonhosted.org/packages/c8/45/8b3d6dbad8cf3252553cc41abea09ad527b33ce47a5e199072620b296902/multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", size = 258299, upload-time = "2025-08-11T12:06:58.946Z" }, - { url = "https://files.pythonhosted.org/packages/3c/e8/8ca2e9a9f5a435fc6db40438a55730a4bf4956b554e487fa1b9ae920f825/multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", size = 242357, upload-time = "2025-08-11T12:07:00.301Z" }, - { url = "https://files.pythonhosted.org/packages/0f/84/80c77c99df05a75c28490b2af8f7cba2a12621186e0a8b0865d8e745c104/multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", size = 268369, upload-time = "2025-08-11T12:07:01.638Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e9/920bfa46c27b05fb3e1ad85121fd49f441492dca2449c5bcfe42e4565d8a/multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", size = 269341, upload-time = "2025-08-11T12:07:02.943Z" }, - { url = "https://files.pythonhosted.org/packages/af/65/753a2d8b05daf496f4a9c367fe844e90a1b2cac78e2be2c844200d10cc4c/multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", size = 256100, upload-time = "2025-08-11T12:07:04.564Z" }, - { url = "https://files.pythonhosted.org/packages/09/54/655be13ae324212bf0bc15d665a4e34844f34c206f78801be42f7a0a8aaa/multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", size = 253584, upload-time = "2025-08-11T12:07:05.914Z" }, - { url = "https://files.pythonhosted.org/packages/5c/74/ab2039ecc05264b5cec73eb018ce417af3ebb384ae9c0e9ed42cb33f8151/multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", size = 251018, upload-time = "2025-08-11T12:07:08.301Z" }, - { url = "https://files.pythonhosted.org/packages/af/0a/ccbb244ac848e56c6427f2392741c06302bbfba49c0042f1eb3c5b606497/multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", size = 251477, upload-time = "2025-08-11T12:07:10.248Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b0/0ed49bba775b135937f52fe13922bc64a7eaf0a3ead84a36e8e4e446e096/multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", size = 263575, upload-time = "2025-08-11T12:07:11.928Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d9/7fb85a85e14de2e44dfb6a24f03c41e2af8697a6df83daddb0e9b7569f73/multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", size = 259649, upload-time = "2025-08-11T12:07:13.244Z" }, - { url = "https://files.pythonhosted.org/packages/03/9e/b3a459bcf9b6e74fa461a5222a10ff9b544cb1cd52fd482fb1b75ecda2a2/multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", size = 251505, upload-time = "2025-08-11T12:07:14.57Z" }, - { url = "https://files.pythonhosted.org/packages/86/a2/8022f78f041dfe6d71e364001a5cf987c30edfc83c8a5fb7a3f0974cff39/multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", size = 41888, upload-time = "2025-08-11T12:07:15.904Z" }, - { url = "https://files.pythonhosted.org/packages/c7/eb/d88b1780d43a56db2cba24289fa744a9d216c1a8546a0dc3956563fd53ea/multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", size = 46072, upload-time = "2025-08-11T12:07:17.045Z" }, - { url = "https://files.pythonhosted.org/packages/9f/16/b929320bf5750e2d9d4931835a4c638a19d2494a5b519caaaa7492ebe105/multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", size = 43222, upload-time = "2025-08-11T12:07:18.328Z" }, - { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/63/7bdd4adc330abcca54c85728db2327130e49e52e8c3ce685cec44e0f2e9f/multidict-6.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349", size = 77153, upload-time = "2025-10-06T14:48:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/b6c35ff175ed1a3142222b78455ee31be71a8396ed3ab5280fbe3ebe4e85/multidict-6.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e", size = 44993, upload-time = "2025-10-06T14:48:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1f/064c77877c5fa6df6d346e68075c0f6998547afe952d6471b4c5f6a7345d/multidict-6.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3", size = 44607, upload-time = "2025-10-06T14:48:29.581Z" }, + { url = "https://files.pythonhosted.org/packages/04/7a/bf6aa92065dd47f287690000b3d7d332edfccb2277634cadf6a810463c6a/multidict-6.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046", size = 241847, upload-time = "2025-10-06T14:48:32.107Z" }, + { url = "https://files.pythonhosted.org/packages/94/39/297a8de920f76eda343e4ce05f3b489f0ab3f9504f2576dfb37b7c08ca08/multidict-6.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32", size = 242616, upload-time = "2025-10-06T14:48:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/39/3a/d0eee2898cfd9d654aea6cb8c4addc2f9756e9a7e09391cfe55541f917f7/multidict-6.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73", size = 222333, upload-time = "2025-10-06T14:48:35.9Z" }, + { url = "https://files.pythonhosted.org/packages/05/48/3b328851193c7a4240815b71eea165b49248867bbb6153a0aee227a0bb47/multidict-6.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc", size = 253239, upload-time = "2025-10-06T14:48:37.302Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ca/0706a98c8d126a89245413225ca4a3fefc8435014de309cf8b30acb68841/multidict-6.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62", size = 251618, upload-time = "2025-10-06T14:48:38.963Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/9c7992f245554d8b173f6f0a048ad24b3e645d883f096857ec2c0822b8bd/multidict-6.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84", size = 241655, upload-time = "2025-10-06T14:48:40.312Z" }, + { url = "https://files.pythonhosted.org/packages/31/79/26a85991ae67efd1c0b1fc2e0c275b8a6aceeb155a68861f63f87a798f16/multidict-6.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0", size = 239245, upload-time = "2025-10-06T14:48:41.848Z" }, + { url = "https://files.pythonhosted.org/packages/14/1e/75fa96394478930b79d0302eaf9a6c69f34005a1a5251ac8b9c336486ec9/multidict-6.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e", size = 233523, upload-time = "2025-10-06T14:48:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5e/085544cb9f9c4ad2b5d97467c15f856df8d9bac410cffd5c43991a5d878b/multidict-6.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4", size = 243129, upload-time = "2025-10-06T14:48:45.225Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c3/e9d9e2f20c9474e7a8fcef28f863c5cbd29bb5adce6b70cebe8bdad0039d/multidict-6.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648", size = 248999, upload-time = "2025-10-06T14:48:46.703Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3f/df171b6efa3239ae33b97b887e42671cd1d94d460614bfb2c30ffdab3b95/multidict-6.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111", size = 243711, upload-time = "2025-10-06T14:48:48.146Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2f/9b5564888c4e14b9af64c54acf149263721a283aaf4aa0ae89b091d5d8c1/multidict-6.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36", size = 237504, upload-time = "2025-10-06T14:48:49.447Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3a/0bd6ca0f7d96d790542d591c8c3354c1e1b6bfd2024d4d92dc3d87485ec7/multidict-6.7.0-cp310-cp310-win32.whl", hash = "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85", size = 41422, upload-time = "2025-10-06T14:48:50.789Z" }, + { url = "https://files.pythonhosted.org/packages/00/35/f6a637ea2c75f0d3b7c7d41b1189189acff0d9deeb8b8f35536bb30f5e33/multidict-6.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7", size = 46050, upload-time = "2025-10-06T14:48:51.938Z" }, + { url = "https://files.pythonhosted.org/packages/e7/b8/f7bf8329b39893d02d9d95cf610c75885d12fc0f402b1c894e1c8e01c916/multidict-6.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0", size = 43153, upload-time = "2025-10-06T14:48:53.146Z" }, + { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, + { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, + { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, + { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, + { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, + { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, + { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, + { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, + { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, + { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, + { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, ] [[package]] name = "mypy" -version = "1.17.1" +version = "1.18.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, @@ -2153,27 +2481,27 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/a9/3d7aa83955617cdf02f94e50aab5c830d205cfa4320cf124ff64acce3a8e/mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972", size = 11003299, upload-time = "2025-07-31T07:54:06.425Z" }, - { url = "https://files.pythonhosted.org/packages/83/e8/72e62ff837dd5caaac2b4a5c07ce769c8e808a00a65e5d8f94ea9c6f20ab/mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7", size = 10125451, upload-time = "2025-07-31T07:53:52.974Z" }, - { url = "https://files.pythonhosted.org/packages/7d/10/f3f3543f6448db11881776f26a0ed079865926b0c841818ee22de2c6bbab/mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df", size = 11916211, upload-time = "2025-07-31T07:53:18.879Z" }, - { url = "https://files.pythonhosted.org/packages/06/bf/63e83ed551282d67bb3f7fea2cd5561b08d2bb6eb287c096539feb5ddbc5/mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390", size = 12652687, upload-time = "2025-07-31T07:53:30.544Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/68f2eeef11facf597143e85b694a161868b3b006a5fbad50e09ea117ef24/mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94", size = 12896322, upload-time = "2025-07-31T07:53:50.74Z" }, - { url = "https://files.pythonhosted.org/packages/a3/87/8e3e9c2c8bd0d7e071a89c71be28ad088aaecbadf0454f46a540bda7bca6/mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b", size = 9507962, upload-time = "2025-07-31T07:53:08.431Z" }, - { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" }, - { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" }, - { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" }, - { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" }, - { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" }, - { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" }, - { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" }, - { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" }, - { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" }, - { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" }, - { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" }, - { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" }, + { url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" }, + { url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4f/90dc8c15c1441bf31cf0f9918bb077e452618708199e530f4cbd5cede6ff/mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed", size = 9766753, upload-time = "2025-09-19T00:10:49.161Z" }, + { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, + { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, + { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, + { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, + { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, ] [[package]] @@ -2206,7 +2534,7 @@ version = "7.16.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, - { name = "bleach", extra = ["css"] }, + { name = "bleach" }, { name = "defusedxml" }, { name = "jinja2" }, { name = "jupyter-core" }, @@ -2261,6 +2589,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, ] +[[package]] +name = "numba" +version = "0.63.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/60/0145d479b2209bd8fdae5f44201eceb8ce5a23e0ed54c71f57db24618665/numba-0.63.1.tar.gz", hash = "sha256:b320aa675d0e3b17b40364935ea52a7b1c670c9037c39cf92c49502a75902f4b", size = 2761666, upload-time = "2025-12-10T02:57:39.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/ce/5283d4ffa568f795bb0fd61ee1f0efc0c6094b94209259167fc8d4276bde/numba-0.63.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6d6bf5bf00f7db629305caaec82a2ffb8abe2bf45eaad0d0738dc7de4113779", size = 2680810, upload-time = "2025-12-10T02:56:55.269Z" }, + { url = "https://files.pythonhosted.org/packages/0f/72/a8bda517e26d912633b32626333339b7c769ea73a5c688365ea5f88fd07e/numba-0.63.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08653d0dfc9cc9c4c9a8fba29ceb1f2d5340c3b86c4a7e5e07e42b643bc6a2f4", size = 3739735, upload-time = "2025-12-10T02:56:57.922Z" }, + { url = "https://files.pythonhosted.org/packages/ca/17/1913b7c1173b2db30fb7a9696892a7c4c59aeee777a9af6859e9e01bac51/numba-0.63.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09eebf5650246ce2a4e9a8d38270e2d4b0b0ae978103bafb38ed7adc5ea906e", size = 3446707, upload-time = "2025-12-10T02:56:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/b4/77/703db56c3061e9fdad5e79c91452947fdeb2ec0bdfe4affe9b144e7025e0/numba-0.63.1-cp310-cp310-win_amd64.whl", hash = "sha256:f8bba17421d865d8c0f7be2142754ebce53e009daba41c44cf6909207d1a8d7d", size = 2747374, upload-time = "2025-12-10T02:57:07.908Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/5f8614c165d2e256fbc6c57028519db6f32e4982475a372bbe550ea0454c/numba-0.63.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b33db00f18ccc790ee9911ce03fcdfe9d5124637d1ecc266f5ae0df06e02fec3", size = 2680501, upload-time = "2025-12-10T02:57:09.797Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9d/d0afc4cf915edd8eadd9b2ab5b696242886ee4f97720d9322650d66a88c6/numba-0.63.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d31ea186a78a7c0f6b1b2a3fe68057fdb291b045c52d86232b5383b6cf4fc25", size = 3744945, upload-time = "2025-12-10T02:57:11.697Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/d82f38f2ab73f3be6f838a826b545b80339762ee8969c16a8bf1d39395a8/numba-0.63.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed3bb2fbdb651d6aac394388130a7001aab6f4541837123a4b4ab8b02716530c", size = 3450827, upload-time = "2025-12-10T02:57:13.709Z" }, + { url = "https://files.pythonhosted.org/packages/18/3f/a9b106e93c5bd7434e65f044bae0d204e20aa7f7f85d72ceb872c7c04216/numba-0.63.1-cp311-cp311-win_amd64.whl", hash = "sha256:1ecbff7688f044b1601be70113e2fb1835367ee0b28ffa8f3adf3a05418c5c87", size = 2747262, upload-time = "2025-12-10T02:57:15.664Z" }, + { url = "https://files.pythonhosted.org/packages/14/9c/c0974cd3d00ff70d30e8ff90522ba5fbb2bcee168a867d2321d8d0457676/numba-0.63.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2819cd52afa5d8d04e057bdfd54367575105f8829350d8fb5e4066fb7591cc71", size = 2680981, upload-time = "2025-12-10T02:57:17.579Z" }, + { url = "https://files.pythonhosted.org/packages/cb/70/ea2bc45205f206b7a24ee68a159f5097c9ca7e6466806e7c213587e0c2b1/numba-0.63.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5cfd45dbd3d409e713b1ccfdc2ee72ca82006860254429f4ef01867fdba5845f", size = 3801656, upload-time = "2025-12-10T02:57:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/4f4ba4fd0f99825cbf3cdefd682ca3678be1702b63362011de6e5f71f831/numba-0.63.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69a599df6976c03b7ecf15d05302696f79f7e6d10d620367407517943355bcb0", size = 3501857, upload-time = "2025-12-10T02:57:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/af/fd/6540456efa90b5f6604a86ff50dabefb187e43557e9081adcad3be44f048/numba-0.63.1-cp312-cp312-win_amd64.whl", hash = "sha256:bbad8c63e4fc7eb3cdb2c2da52178e180419f7969f9a685f283b313a70b92af3", size = 2750282, upload-time = "2025-12-10T02:57:22.474Z" }, +] + [[package]] name = "numpy" version = "1.26.4" @@ -2316,42 +2668,42 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.36.0" +version = "1.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/d2/c782c88b8afbf961d6972428821c302bd1e9e7bc361352172f0ca31296e2/opentelemetry_api-1.36.0.tar.gz", hash = "sha256:9a72572b9c416d004d492cbc6e61962c0501eaf945ece9b5a0f56597d8348aa0", size = 64780, upload-time = "2025-07-29T15:12:06.02Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242, upload-time = "2025-10-16T08:35:50.25Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/ee/6b08dde0a022c463b88f55ae81149584b125a42183407dc1045c486cc870/opentelemetry_api-1.36.0-py3-none-any.whl", hash = "sha256:02f20bcacf666e1333b6b1f04e647dc1d5111f86b8e510238fcc56d7762cda8c", size = 65564, upload-time = "2025-07-29T15:11:47.998Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947, upload-time = "2025-10-16T08:35:30.23Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.36.0" +version = "1.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/85/8567a966b85a2d3f971c4d42f781c305b2b91c043724fa08fd37d158e9dc/opentelemetry_sdk-1.36.0.tar.gz", hash = "sha256:19c8c81599f51b71670661ff7495c905d8fdf6976e41622d5245b791b06fa581", size = 162557, upload-time = "2025-07-29T15:12:16.76Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942, upload-time = "2025-10-16T08:36:02.257Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/59/7bed362ad1137ba5886dac8439e84cd2df6d087be7c09574ece47ae9b22c/opentelemetry_sdk-1.36.0-py3-none-any.whl", hash = "sha256:19fe048b42e98c5c1ffe85b569b7073576ad4ce0bcb6e9b4c6a39e890a6c45fb", size = 119995, upload-time = "2025-07-29T15:12:03.181Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349, upload-time = "2025-10-16T08:35:46.995Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.57b0" +version = "0.59b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/31/67dfa252ee88476a29200b0255bda8dfc2cf07b56ad66dc9a6221f7dc787/opentelemetry_semantic_conventions-0.57b0.tar.gz", hash = "sha256:609a4a79c7891b4620d64c7aac6898f872d790d75f22019913a660756f27ff32", size = 124225, upload-time = "2025-07-29T15:12:17.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861, upload-time = "2025-10-16T08:36:03.346Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/75/7d591371c6c39c73de5ce5da5a2cc7b72d1d1cd3f8f4638f553c01c37b11/opentelemetry_semantic_conventions-0.57b0-py3-none-any.whl", hash = "sha256:757f7e76293294f124c827e514c2a3144f191ef175b069ce8d1211e1e38e9e78", size = 201627, upload-time = "2025-07-29T15:12:04.174Z" }, + { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954, upload-time = "2025-10-16T08:35:48.054Z" }, ] [[package]] @@ -2409,18 +2761,19 @@ wheels = [ [[package]] name = "pandera" -version = "0.25.0" +version = "0.23.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "numpy" }, { name = "packaging" }, + { name = "pandas" }, { name = "pydantic" }, { name = "typeguard" }, - { name = "typing-extensions" }, { name = "typing-inspect" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/c1/02f78cd18cd32a009405c847dcf430a97d1a8c162f6e8872acae928c8f20/pandera-0.25.0.tar.gz", hash = "sha256:af3bbaa163672c91b83d59d70715f25c4134dbccfc8bc89a642a2f0e23db951e", size = 555391, upload-time = "2025-07-08T19:20:22.106Z" } +sdist = { url = "https://files.pythonhosted.org/packages/70/1a/bcb3daa41bf6a46dc7c218cb0558922f1e711176bbb4ce649ed610a35f3f/pandera-0.23.1.tar.gz", hash = "sha256:2afed504ad61bbd2960dc7f85dd6be0b28d230a626f392b8ba05c291b970ae06", size = 513325, upload-time = "2025-03-08T02:25:29.404Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/e0/234707103c742555e1c23bff51f3f0e496c144cd76fcf5a6b800dfe193f2/pandera-0.25.0-py3-none-any.whl", hash = "sha256:365a555accc46404466641203e297722d424d74a1315f077ab899e1344f82303", size = 293336, upload-time = "2025-07-08T19:20:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/79/9e/79bb285d4dbee32f4786b659bfbddb2d56edf3c9bc2b7ed16474903584ed/pandera-0.23.1-py3-none-any.whl", hash = "sha256:37f75cfb5e34db88afc07adf3ea07ba8738bca709f0e67799c8f1b2fc6b9c977", size = 264213, upload-time = "2025-03-08T02:25:27.672Z" }, ] [[package]] @@ -2448,11 +2801,11 @@ wheels = [ [[package]] name = "parso" -version = "0.8.4" +version = "0.8.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609, upload-time = "2024-04-05T09:43:55.897Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650, upload-time = "2024-04-05T09:43:53.299Z" }, + { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, ] [[package]] @@ -2473,6 +2826,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] +[[package]] +name = "patsy" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/44/ed13eccdd0519eff265f44b670d46fbb0ec813e2274932dc1c0e48520f7d/patsy-1.0.2.tar.gz", hash = "sha256:cdc995455f6233e90e22de72c37fcadb344e7586fb83f06696f54d92f8ce74c0", size = 399942, upload-time = "2025-10-20T16:17:37.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/70/ba4b949bdc0490ab78d545459acd7702b211dfccf7eb89bbc1060f52818d/patsy-1.0.2-py2.py3-none-any.whl", hash = "sha256:37bfddbc58fcf0362febb5f54f10743f8b21dd2aa73dec7e7ef59d1b02ae668a", size = 233301, upload-time = "2025-10-20T16:17:36.563Z" }, +] + [[package]] name = "pexpect" version = "4.9.0" @@ -2487,66 +2852,59 @@ wheels = [ [[package]] name = "pillow" -version = "11.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, - { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" }, - { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" }, - { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" }, - { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" }, - { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" }, - { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" }, - { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" }, - { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" }, - { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" }, - { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, - { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, - { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, - { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, - { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, - { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, - { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, - { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, - { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, - { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, - { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, - { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, - { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, - { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, - { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, - { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, - { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, - { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, - { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, - { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, - { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" }, - { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" }, - { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" }, - { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, - { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, - { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, - { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, - { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/08/26e68b6b5da219c2a2cb7b563af008b53bb8e6b6fcb3fa40715fcdb2523a/pillow-12.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b", size = 5289809, upload-time = "2025-10-15T18:21:27.791Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/4e58fb097fb74c7b4758a680aacd558810a417d1edaa7000142976ef9d2f/pillow-12.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1", size = 4650606, upload-time = "2025-10-15T18:21:29.823Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/1fa492aa9f77b3bc6d471c468e62bfea1823056bf7e5e4f1914d7ab2565e/pillow-12.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363", size = 6221023, upload-time = "2025-10-15T18:21:31.415Z" }, + { url = "https://files.pythonhosted.org/packages/c1/09/4de7cd03e33734ccd0c876f0251401f1314e819cbfd89a0fcb6e77927cc6/pillow-12.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca", size = 8024937, upload-time = "2025-10-15T18:21:33.453Z" }, + { url = "https://files.pythonhosted.org/packages/2e/69/0688e7c1390666592876d9d474f5e135abb4acb39dcb583c4dc5490f1aff/pillow-12.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e", size = 6334139, upload-time = "2025-10-15T18:21:35.395Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1c/880921e98f525b9b44ce747ad1ea8f73fd7e992bafe3ca5e5644bf433dea/pillow-12.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782", size = 7026074, upload-time = "2025-10-15T18:21:37.219Z" }, + { url = "https://files.pythonhosted.org/packages/28/03/96f718331b19b355610ef4ebdbbde3557c726513030665071fd025745671/pillow-12.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10", size = 6448852, upload-time = "2025-10-15T18:21:39.168Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a0/6a193b3f0cc9437b122978d2c5cbce59510ccf9a5b48825096ed7472da2f/pillow-12.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa", size = 7117058, upload-time = "2025-10-15T18:21:40.997Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c4/043192375eaa4463254e8e61f0e2ec9a846b983929a8d0a7122e0a6d6fff/pillow-12.0.0-cp310-cp310-win32.whl", hash = "sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275", size = 6295431, upload-time = "2025-10-15T18:21:42.518Z" }, + { url = "https://files.pythonhosted.org/packages/92/c6/c2f2fc7e56301c21827e689bb8b0b465f1b52878b57471a070678c0c33cd/pillow-12.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d", size = 7000412, upload-time = "2025-10-15T18:21:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d2/5f675067ba82da7a1c238a73b32e3fd78d67f9d9f80fbadd33a40b9c0481/pillow-12.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7", size = 2435903, upload-time = "2025-10-15T18:21:46.29Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" }, + { url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" }, + { url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" }, + { url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" }, + { url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" }, + { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" }, + { url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" }, + { url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" }, + { url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" }, ] [[package]] name = "platformdirs" -version = "4.3.8" +version = "4.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, ] [[package]] @@ -2558,82 +2916,92 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "polars" +version = "0.20.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/cb/447d8ba0d38df42bf247ade1fb7ad1ba61f09a95144ee8c4ab0314d38703/polars-0.20.31.tar.gz", hash = "sha256:00f62dec6bf43a4e2a5db58b99bf0e79699fe761c80ae665868eaea5168f3bbb", size = 3666354, upload-time = "2024-06-01T11:22:23.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/74/ca08d8b5d067159541c4419f0cbd0b474bd44a89f97e79e0b4b3fd5b24b5/polars-0.20.31-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:86454ade5ed302bbf87f145cfcb1b14f7a5765a9440e448659e1f3dba6ac4e79", size = 27645039, upload-time = "2024-06-01T11:21:12.336Z" }, + { url = "https://files.pythonhosted.org/packages/d0/71/984ed5f67c824c9b547665454ee438e0540a1ce2e8eca4d2021eeaf826aa/polars-0.20.31-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:67f2fe842262b7e1b9371edad21b760f6734d28b74c78dda88dff1bf031b9499", size = 24750031, upload-time = "2024-06-01T11:21:19.859Z" }, + { url = "https://files.pythonhosted.org/packages/90/7d/7541e559d7fce232ba34340b0953cac9af2344853d675dc2de01a4d3abc7/polars-0.20.31-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24b82441f93409e0e8abd6f427b029db102f02b8de328cee9a680f84b84e3736", size = 28792870, upload-time = "2024-06-01T11:21:25.301Z" }, + { url = "https://files.pythonhosted.org/packages/72/6a/6bf5da56542ae976140dd30be950149146c361eb8dd6471fdb6d50ae7581/polars-0.20.31-cp38-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:87f43bce4d41abf8c8c5658d881e4b8378e5c61010a696bfea8b4106b908e916", size = 26994077, upload-time = "2024-06-01T11:21:30.082Z" }, + { url = "https://files.pythonhosted.org/packages/37/3e/d8f460c420254b094df5b2fa24e1d5571611540309eb66dad46405fb9b47/polars-0.20.31-cp38-abi3-win_amd64.whl", hash = "sha256:2d7567c9fd9d3b9aa93387ca9880d9e8f7acea3c0a0555c03d8c0c2f0715d43c", size = 28847550, upload-time = "2024-06-01T11:21:35.203Z" }, +] + [[package]] name = "prometheus-client" -version = "0.22.1" +version = "0.23.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/cf/40dde0a2be27cc1eb41e333d1a674a74ce8b8b0457269cc640fd42b07cf7/prometheus_client-0.22.1.tar.gz", hash = "sha256:190f1331e783cf21eb60bca559354e0a4d4378facecf78f5428c39b675d20d28", size = 69746, upload-time = "2025-06-02T14:29:01.152Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481, upload-time = "2025-09-18T20:47:25.043Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/ae/ec06af4fe3ee72d16973474f122541746196aaa16cea6f66d18b963c6177/prometheus_client-0.22.1-py3-none-any.whl", hash = "sha256:cca895342e308174341b2cbf99a56bef291fbc0ef7b9e5412a0f26d653ba7094", size = 58694, upload-time = "2025-06-02T14:29:00.068Z" }, + { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" }, ] [[package]] name = "prompt-toolkit" -version = "3.0.51" +version = "3.0.52" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] [[package]] name = "propcache" -version = "0.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/14/510deed325e262afeb8b360043c5d7c960da7d3ecd6d6f9496c9c56dc7f4/propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770", size = 73178, upload-time = "2025-06-09T22:53:40.126Z" }, - { url = "https://files.pythonhosted.org/packages/cd/4e/ad52a7925ff01c1325653a730c7ec3175a23f948f08626a534133427dcff/propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3", size = 43133, upload-time = "2025-06-09T22:53:41.965Z" }, - { url = "https://files.pythonhosted.org/packages/63/7c/e9399ba5da7780871db4eac178e9c2e204c23dd3e7d32df202092a1ed400/propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3", size = 43039, upload-time = "2025-06-09T22:53:43.268Z" }, - { url = "https://files.pythonhosted.org/packages/22/e1/58da211eb8fdc6fc854002387d38f415a6ca5f5c67c1315b204a5d3e9d7a/propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e", size = 201903, upload-time = "2025-06-09T22:53:44.872Z" }, - { url = "https://files.pythonhosted.org/packages/c4/0a/550ea0f52aac455cb90111c8bab995208443e46d925e51e2f6ebdf869525/propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220", size = 213362, upload-time = "2025-06-09T22:53:46.707Z" }, - { url = "https://files.pythonhosted.org/packages/5a/af/9893b7d878deda9bb69fcf54600b247fba7317761b7db11fede6e0f28bd0/propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb", size = 210525, upload-time = "2025-06-09T22:53:48.547Z" }, - { url = "https://files.pythonhosted.org/packages/7c/bb/38fd08b278ca85cde36d848091ad2b45954bc5f15cce494bb300b9285831/propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614", size = 198283, upload-time = "2025-06-09T22:53:50.067Z" }, - { url = "https://files.pythonhosted.org/packages/78/8c/9fe55bd01d362bafb413dfe508c48753111a1e269737fa143ba85693592c/propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50", size = 191872, upload-time = "2025-06-09T22:53:51.438Z" }, - { url = "https://files.pythonhosted.org/packages/54/14/4701c33852937a22584e08abb531d654c8bcf7948a8f87ad0a4822394147/propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339", size = 199452, upload-time = "2025-06-09T22:53:53.229Z" }, - { url = "https://files.pythonhosted.org/packages/16/44/447f2253d859602095356007657ee535e0093215ea0b3d1d6a41d16e5201/propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0", size = 191567, upload-time = "2025-06-09T22:53:54.541Z" }, - { url = "https://files.pythonhosted.org/packages/f2/b3/e4756258749bb2d3b46defcff606a2f47410bab82be5824a67e84015b267/propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2", size = 193015, upload-time = "2025-06-09T22:53:56.44Z" }, - { url = "https://files.pythonhosted.org/packages/1e/df/e6d3c7574233164b6330b9fd697beeac402afd367280e6dc377bb99b43d9/propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7", size = 204660, upload-time = "2025-06-09T22:53:57.839Z" }, - { url = "https://files.pythonhosted.org/packages/b2/53/e4d31dd5170b4a0e2e6b730f2385a96410633b4833dc25fe5dffd1f73294/propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b", size = 206105, upload-time = "2025-06-09T22:53:59.638Z" }, - { url = "https://files.pythonhosted.org/packages/7f/fe/74d54cf9fbe2a20ff786e5f7afcfde446588f0cf15fb2daacfbc267b866c/propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c", size = 196980, upload-time = "2025-06-09T22:54:01.071Z" }, - { url = "https://files.pythonhosted.org/packages/22/ec/c469c9d59dada8a7679625e0440b544fe72e99311a4679c279562051f6fc/propcache-0.3.2-cp310-cp310-win32.whl", hash = "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70", size = 37679, upload-time = "2025-06-09T22:54:03.003Z" }, - { url = "https://files.pythonhosted.org/packages/38/35/07a471371ac89d418f8d0b699c75ea6dca2041fbda360823de21f6a9ce0a/propcache-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9", size = 41459, upload-time = "2025-06-09T22:54:04.134Z" }, - { url = "https://files.pythonhosted.org/packages/80/8d/e8b436717ab9c2cfc23b116d2c297305aa4cd8339172a456d61ebf5669b8/propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be", size = 74207, upload-time = "2025-06-09T22:54:05.399Z" }, - { url = "https://files.pythonhosted.org/packages/d6/29/1e34000e9766d112171764b9fa3226fa0153ab565d0c242c70e9945318a7/propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f", size = 43648, upload-time = "2025-06-09T22:54:08.023Z" }, - { url = "https://files.pythonhosted.org/packages/46/92/1ad5af0df781e76988897da39b5f086c2bf0f028b7f9bd1f409bb05b6874/propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9", size = 43496, upload-time = "2025-06-09T22:54:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/ce/e96392460f9fb68461fabab3e095cb00c8ddf901205be4eae5ce246e5b7e/propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf", size = 217288, upload-time = "2025-06-09T22:54:10.466Z" }, - { url = "https://files.pythonhosted.org/packages/c5/2a/866726ea345299f7ceefc861a5e782b045545ae6940851930a6adaf1fca6/propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9", size = 227456, upload-time = "2025-06-09T22:54:11.828Z" }, - { url = "https://files.pythonhosted.org/packages/de/03/07d992ccb6d930398689187e1b3c718339a1c06b8b145a8d9650e4726166/propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66", size = 225429, upload-time = "2025-06-09T22:54:13.823Z" }, - { url = "https://files.pythonhosted.org/packages/5d/e6/116ba39448753b1330f48ab8ba927dcd6cf0baea8a0ccbc512dfb49ba670/propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df", size = 213472, upload-time = "2025-06-09T22:54:15.232Z" }, - { url = "https://files.pythonhosted.org/packages/a6/85/f01f5d97e54e428885a5497ccf7f54404cbb4f906688a1690cd51bf597dc/propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2", size = 204480, upload-time = "2025-06-09T22:54:17.104Z" }, - { url = "https://files.pythonhosted.org/packages/e3/79/7bf5ab9033b8b8194cc3f7cf1aaa0e9c3256320726f64a3e1f113a812dce/propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7", size = 214530, upload-time = "2025-06-09T22:54:18.512Z" }, - { url = "https://files.pythonhosted.org/packages/31/0b/bd3e0c00509b609317df4a18e6b05a450ef2d9a963e1d8bc9c9415d86f30/propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95", size = 205230, upload-time = "2025-06-09T22:54:19.947Z" }, - { url = "https://files.pythonhosted.org/packages/7a/23/fae0ff9b54b0de4e819bbe559508da132d5683c32d84d0dc2ccce3563ed4/propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e", size = 206754, upload-time = "2025-06-09T22:54:21.716Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7f/ad6a3c22630aaa5f618b4dc3c3598974a72abb4c18e45a50b3cdd091eb2f/propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e", size = 218430, upload-time = "2025-06-09T22:54:23.17Z" }, - { url = "https://files.pythonhosted.org/packages/5b/2c/ba4f1c0e8a4b4c75910742f0d333759d441f65a1c7f34683b4a74c0ee015/propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf", size = 223884, upload-time = "2025-06-09T22:54:25.539Z" }, - { url = "https://files.pythonhosted.org/packages/88/e4/ebe30fc399e98572019eee82ad0caf512401661985cbd3da5e3140ffa1b0/propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e", size = 211480, upload-time = "2025-06-09T22:54:26.892Z" }, - { url = "https://files.pythonhosted.org/packages/96/0a/7d5260b914e01d1d0906f7f38af101f8d8ed0dc47426219eeaf05e8ea7c2/propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897", size = 37757, upload-time = "2025-06-09T22:54:28.241Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2d/89fe4489a884bc0da0c3278c552bd4ffe06a1ace559db5ef02ef24ab446b/propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39", size = 41500, upload-time = "2025-06-09T22:54:29.4Z" }, - { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload-time = "2025-06-09T22:54:30.551Z" }, - { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload-time = "2025-06-09T22:54:32.296Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload-time = "2025-06-09T22:54:33.929Z" }, - { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload-time = "2025-06-09T22:54:35.186Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload-time = "2025-06-09T22:54:36.708Z" }, - { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload-time = "2025-06-09T22:54:38.062Z" }, - { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload-time = "2025-06-09T22:54:39.634Z" }, - { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload-time = "2025-06-09T22:54:41.565Z" }, - { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload-time = "2025-06-09T22:54:43.038Z" }, - { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload-time = "2025-06-09T22:54:44.376Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload-time = "2025-06-09T22:54:46.243Z" }, - { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload-time = "2025-06-09T22:54:47.63Z" }, - { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload-time = "2025-06-09T22:54:48.982Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload-time = "2025-06-09T22:54:50.424Z" }, - { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload-time = "2025-06-09T22:54:52.072Z" }, - { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload-time = "2025-06-09T22:54:53.234Z" }, - { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { 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/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { 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/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { 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/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { 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/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { 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" }, ] [[package]] @@ -2650,31 +3018,31 @@ wheels = [ [[package]] name = "protobuf" -version = "5.29.5" +version = "6.33.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/29/d09e70352e4e88c9c7a198d5645d7277811448d76c23b00345670f7c8a38/protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84", size = 425226, upload-time = "2025-05-28T23:51:59.82Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/11/6e40e9fc5bba02988a214c07cf324595789ca7820160bfd1f8be96e48539/protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079", size = 422963, upload-time = "2025-05-28T23:51:41.204Z" }, - { url = "https://files.pythonhosted.org/packages/81/7f/73cefb093e1a2a7c3ffd839e6f9fcafb7a427d300c7f8aef9c64405d8ac6/protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc", size = 434818, upload-time = "2025-05-28T23:51:44.297Z" }, - { url = "https://files.pythonhosted.org/packages/dd/73/10e1661c21f139f2c6ad9b23040ff36fee624310dc28fba20d33fdae124c/protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671", size = 418091, upload-time = "2025-05-28T23:51:45.907Z" }, - { url = "https://files.pythonhosted.org/packages/6c/04/98f6f8cf5b07ab1294c13f34b4e69b3722bb609c5b701d6c169828f9f8aa/protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015", size = 319824, upload-time = "2025-05-28T23:51:47.545Z" }, - { url = "https://files.pythonhosted.org/packages/85/e4/07c80521879c2d15f321465ac24c70efe2381378c00bf5e56a0f4fbac8cd/protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61", size = 319942, upload-time = "2025-05-28T23:51:49.11Z" }, - { url = "https://files.pythonhosted.org/packages/7e/cc/7e77861000a0691aeea8f4566e5d3aa716f2b1dece4a24439437e41d3d25/protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5", size = 172823, upload-time = "2025-05-28T23:51:58.157Z" }, + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, ] [[package]] name = "psutil" -version = "7.0.0" +version = "7.1.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", size = 489059, upload-time = "2025-11-02T12:25:54.619Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, - { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, - { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, - { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, + { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359, upload-time = "2025-11-02T12:26:25.284Z" }, + { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171, upload-time = "2025-11-02T12:26:27.23Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261, upload-time = "2025-11-02T12:26:29.48Z" }, + { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635, upload-time = "2025-11-02T12:26:31.74Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633, upload-time = "2025-11-02T12:26:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" }, ] [[package]] @@ -2696,35 +3064,41 @@ wheels = [ ] [[package]] -name = "pyarrow" -version = "15.0.2" +name = "py4j" +version = "0.10.9.7" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, +sdist = { url = "https://files.pythonhosted.org/packages/1e/f2/b34255180c72c36ff7097f7c2cdca02abcbd89f5eebf7c7c41262a9a0637/py4j-0.10.9.7.tar.gz", hash = "sha256:0b6e5315bb3ada5cf62ac651d107bb2ebc02def3dee9d9548e3baac644ea8dbb", size = 1508234, upload-time = "2022-08-12T22:49:09.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/30/a58b32568f1623aaad7db22aa9eafc4c6c194b429ff35bdc55ca2726da47/py4j-0.10.9.7-py2.py3-none-any.whl", hash = "sha256:85defdfd2b2376eb3abf5ca6474b51ab7e0de341c75a02f46dc9b5976f5a5c1b", size = 200481, upload-time = "2022-08-12T22:49:07.05Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/a1/b7c9bacfd17a9d1d8d025db2fc39112e0b1a629ea401880e4e97632dbc4c/pyarrow-15.0.2.tar.gz", hash = "sha256:9c9bc803cb3b7bfacc1e96ffbfd923601065d9d3f911179d81e72d99fd74a3d9", size = 1064226, upload-time = "2024-03-18T16:58:06.866Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/fc/9e58e43f41d161bf3b3bcc580170b3b0bdac8c0f1603a65b967cf94b6bf4/pyarrow-15.0.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:88b340f0a1d05b5ccc3d2d986279045655b1fe8e41aba6ca44ea28da0d1455d8", size = 27150472, upload-time = "2024-03-18T16:53:30.164Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f4/d39bdce9661621df9bdb511c3f72c81817edc8bc6365672b22a5de41004a/pyarrow-15.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eaa8f96cecf32da508e6c7f69bb8401f03745c050c1dd42ec2596f2e98deecac", size = 24196261, upload-time = "2024-03-18T16:53:37.402Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b2/de978e01592192695c7449c6fa28f2269bf74808b533a177c90ee6295bdd/pyarrow-15.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23c6753ed4f6adb8461e7c383e418391b8d8453c5d67e17f416c3a5d5709afbd", size = 36153060, upload-time = "2024-03-18T16:53:46.902Z" }, - { url = "https://files.pythonhosted.org/packages/01/e0/13aada7b0af1039554e675bd8c878acb3d86bab690e5a6b05fc8547a9cf2/pyarrow-15.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f639c059035011db8c0497e541a8a45d98a58dbe34dc8fadd0ef128f2cee46e5", size = 38402930, upload-time = "2024-03-18T16:53:55.894Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f9/7f82c25c89828f38ebc2ce2f7d6b544107bc7502255ed92ac398be69cc19/pyarrow-15.0.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:290e36a59a0993e9a5224ed2fb3e53375770f07379a0ea03ee2fce2e6d30b423", size = 35655190, upload-time = "2024-03-18T16:54:04.8Z" }, - { url = "https://files.pythonhosted.org/packages/e9/0e/0d30e6fd1e0fc9cc267381520f9386a56b2b51c4066d8f9a0d4a5a2e0b44/pyarrow-15.0.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:06c2bb2a98bc792f040bef31ad3e9be6a63d0cb39189227c08a7d955db96816e", size = 38331501, upload-time = "2024-03-18T16:54:14.322Z" }, - { url = "https://files.pythonhosted.org/packages/ec/85/abca962d99950aad803bd755baf020a8183ca3be1319bb205f52bbbcce16/pyarrow-15.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:f7a197f3670606a960ddc12adbe8075cea5f707ad7bf0dffa09637fdbb89f76c", size = 24814742, upload-time = "2024-03-18T16:54:21.932Z" }, - { url = "https://files.pythonhosted.org/packages/34/50/93f6104e79bec6e1af4356f5164695a0b6338f230e1273706ec9eb836bea/pyarrow-15.0.2-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:5f8bc839ea36b1f99984c78e06e7a06054693dc2af8920f6fb416b5bca9944e4", size = 27187122, upload-time = "2024-03-18T16:54:29.514Z" }, - { url = "https://files.pythonhosted.org/packages/47/cb/be17c4879e60e683761be281d955923d586a572fbc2503e08f08ca713349/pyarrow-15.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5e81dfb4e519baa6b4c80410421528c214427e77ca0ea9461eb4097c328fa33", size = 24217346, upload-time = "2024-03-18T16:54:36.41Z" }, - { url = "https://files.pythonhosted.org/packages/ac/f6/57d67d7729643ebc80f0df18420b9fc1857ca418d1b2bb3bc5be2fd2119e/pyarrow-15.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a4f240852b302a7af4646c8bfe9950c4691a419847001178662a98915fd7ee7", size = 36151795, upload-time = "2024-03-18T16:54:44.674Z" }, - { url = "https://files.pythonhosted.org/packages/ff/42/df219f3a1e06c2dd63599243384d6ba2a02a44a976801fbc9601264ff562/pyarrow-15.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e7d9cfb5a1e648e172428c7a42b744610956f3b70f524aa3a6c02a448ba853e", size = 38398065, upload-time = "2024-03-18T16:54:53.221Z" }, - { url = "https://files.pythonhosted.org/packages/4a/37/a32de321c7270df01b709f554903acf4edaaef373310ff116302224348a9/pyarrow-15.0.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2d4f905209de70c0eb5b2de6763104d5a9a37430f137678edfb9a675bac9cd98", size = 35672270, upload-time = "2024-03-18T16:55:02.175Z" }, - { url = "https://files.pythonhosted.org/packages/61/94/0b28417737ea56a4819603c0024c8b24365f85154bb938785352e09bea55/pyarrow-15.0.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:90adb99e8ce5f36fbecbbc422e7dcbcbed07d985eed6062e459e23f9e71fd197", size = 38346410, upload-time = "2024-03-18T16:55:10.399Z" }, - { url = "https://files.pythonhosted.org/packages/96/2f/0092154f3e1ebbc814de1f8a9075543d77a7ecc691fbad407df174799abe/pyarrow-15.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:b116e7fd7889294cbd24eb90cd9bdd3850be3738d61297855a71ac3b8124ee38", size = 24799922, upload-time = "2024-03-18T16:55:17.261Z" }, - { url = "https://files.pythonhosted.org/packages/d2/84/a24b15ca90f3ae49bdb15c5b10c000475be539da677e8d6495318c65457d/pyarrow-15.0.2-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:25335e6f1f07fdaa026a61c758ee7d19ce824a866b27bba744348fa73bb5a440", size = 27100546, upload-time = "2024-03-18T16:55:23.939Z" }, - { url = "https://files.pythonhosted.org/packages/7b/cb/15f9c73da8e37253a5312b6803e77ef240eaf8e89e47e0310b020a5b94f0/pyarrow-15.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:90f19e976d9c3d8e73c80be84ddbe2f830b6304e4c576349d9360e335cd627fc", size = 24186578, upload-time = "2024-03-18T16:55:30.268Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0d/082945e14f11f74a5c2318336f99018d48f8aea111817dd082eb7eda6754/pyarrow-15.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a22366249bf5fd40ddacc4f03cd3160f2d7c247692945afb1899bab8a140ddfb", size = 36150968, upload-time = "2024-03-18T16:55:38.479Z" }, - { url = "https://files.pythonhosted.org/packages/71/8a/c5f28f99a44e0913f0f86e315f04b51b3757a2353dedaa916c7997b4cb51/pyarrow-15.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2a335198f886b07e4b5ea16d08ee06557e07db54a8400cc0d03c7f6a22f785f", size = 38412265, upload-time = "2024-03-18T16:55:47.131Z" }, - { url = "https://files.pythonhosted.org/packages/61/07/9910553bd6227ba86be5313665b8e1572449e17502e61c9954b529b96f1e/pyarrow-15.0.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e6d459c0c22f0b9c810a3917a1de3ee704b021a5fb8b3bacf968eece6df098f", size = 35652118, upload-time = "2024-03-18T16:55:55.171Z" }, - { url = "https://files.pythonhosted.org/packages/f5/87/6270d60494909a45beac5afcb49f67b6a2f19ea07e25d130c62ae4e02bdc/pyarrow-15.0.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:033b7cad32198754d93465dcfb71d0ba7cb7cd5c9afd7052cab7214676eec38b", size = 38344967, upload-time = "2024-03-18T16:56:03.575Z" }, - { url = "https://files.pythonhosted.org/packages/cd/93/c2d3384aba712a0eb503f3940132189e81e97fb320844651783f45f15722/pyarrow-15.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:29850d050379d6e8b5a693098f4de7fd6a2bea4365bfd073d7c57c57b95041ee", size = 25277837, upload-time = "2024-03-18T16:56:10.276Z" }, + +[[package]] +name = "pyarrow" +version = "19.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/09/a9046344212690f0632b9c709f9bf18506522feb333c894d0de81d62341a/pyarrow-19.0.1.tar.gz", hash = "sha256:3bf266b485df66a400f282ac0b6d1b500b9d2ae73314a153dbe97d6d5cc8a99e", size = 1129437, upload-time = "2025-02-18T18:55:57.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/01/b23b514d86b839956238d3f8ef206fd2728eee87ff1b8ce150a5678d9721/pyarrow-19.0.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:fc28912a2dc924dddc2087679cc8b7263accc71b9ff025a1362b004711661a69", size = 30688914, upload-time = "2025-02-18T18:51:37.575Z" }, + { url = "https://files.pythonhosted.org/packages/c6/68/218ff7cf4a0652a933e5f2ed11274f724dd43b9813cb18dd72c0a35226a2/pyarrow-19.0.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fca15aabbe9b8355800d923cc2e82c8ef514af321e18b437c3d782aa884eaeec", size = 32102866, upload-time = "2025-02-18T18:51:44.358Z" }, + { url = "https://files.pythonhosted.org/packages/98/01/c295050d183014f4a2eb796d7d2bbfa04b6cccde7258bb68aacf6f18779b/pyarrow-19.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad76aef7f5f7e4a757fddcdcf010a8290958f09e3470ea458c80d26f4316ae89", size = 41147682, upload-time = "2025-02-18T18:51:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/40/17/a6c3db0b5f3678f33bbb552d2acbc16def67f89a72955b67b0109af23eb0/pyarrow-19.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d03c9d6f2a3dffbd62671ca070f13fc527bb1867b4ec2b98c7eeed381d4f389a", size = 42179192, upload-time = "2025-02-18T18:51:56.265Z" }, + { url = "https://files.pythonhosted.org/packages/cf/75/c7c8e599300d8cebb6cb339014800e1c720c9db2a3fcb66aa64ec84bac72/pyarrow-19.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:65cf9feebab489b19cdfcfe4aa82f62147218558d8d3f0fc1e9dea0ab8e7905a", size = 40517272, upload-time = "2025-02-18T18:52:02.969Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c9/68ab123ee1528699c4d5055f645ecd1dd68ff93e4699527249d02f55afeb/pyarrow-19.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:41f9706fbe505e0abc10e84bf3a906a1338905cbbcf1177b71486b03e6ea6608", size = 42069036, upload-time = "2025-02-18T18:52:10.173Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/d5cfd7654084e6c0d9c3ce949e5d9e0ccad569ae1e2d5a68a3ec03b2be89/pyarrow-19.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6cb2335a411b713fdf1e82a752162f72d4a7b5dbc588e32aa18383318b05866", size = 25277951, upload-time = "2025-02-18T18:52:15.459Z" }, + { url = "https://files.pythonhosted.org/packages/a0/55/f1a8d838ec07fe3ca53edbe76f782df7b9aafd4417080eebf0b42aab0c52/pyarrow-19.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:cc55d71898ea30dc95900297d191377caba257612f384207fe9f8293b5850f90", size = 30713987, upload-time = "2025-02-18T18:52:20.463Z" }, + { url = "https://files.pythonhosted.org/packages/13/12/428861540bb54c98a140ae858a11f71d041ef9e501e6b7eb965ca7909505/pyarrow-19.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:7a544ec12de66769612b2d6988c36adc96fb9767ecc8ee0a4d270b10b1c51e00", size = 32135613, upload-time = "2025-02-18T18:52:25.29Z" }, + { url = "https://files.pythonhosted.org/packages/2f/8a/23d7cc5ae2066c6c736bce1db8ea7bc9ac3ef97ac7e1c1667706c764d2d9/pyarrow-19.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0148bb4fc158bfbc3d6dfe5001d93ebeed253793fff4435167f6ce1dc4bddeae", size = 41149147, upload-time = "2025-02-18T18:52:30.975Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7a/845d151bb81a892dfb368bf11db584cf8b216963ccce40a5cf50a2492a18/pyarrow-19.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f24faab6ed18f216a37870d8c5623f9c044566d75ec586ef884e13a02a9d62c5", size = 42178045, upload-time = "2025-02-18T18:52:36.859Z" }, + { url = "https://files.pythonhosted.org/packages/a7/31/e7282d79a70816132cf6cae7e378adfccce9ae10352d21c2fecf9d9756dd/pyarrow-19.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:4982f8e2b7afd6dae8608d70ba5bd91699077323f812a0448d8b7abdff6cb5d3", size = 40532998, upload-time = "2025-02-18T18:52:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/b8/82/20f3c290d6e705e2ee9c1fa1d5a0869365ee477e1788073d8b548da8b64c/pyarrow-19.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:49a3aecb62c1be1d822f8bf629226d4a96418228a42f5b40835c1f10d42e4db6", size = 42084055, upload-time = "2025-02-18T18:52:48.749Z" }, + { url = "https://files.pythonhosted.org/packages/ff/77/e62aebd343238863f2c9f080ad2ef6ace25c919c6ab383436b5b81cbeef7/pyarrow-19.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:008a4009efdb4ea3d2e18f05cd31f9d43c388aad29c636112c2966605ba33466", size = 25283133, upload-time = "2025-02-18T18:52:54.549Z" }, + { url = "https://files.pythonhosted.org/packages/78/b4/94e828704b050e723f67d67c3535cf7076c7432cd4cf046e4bb3b96a9c9d/pyarrow-19.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:80b2ad2b193e7d19e81008a96e313fbd53157945c7be9ac65f44f8937a55427b", size = 30670749, upload-time = "2025-02-18T18:53:00.062Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3b/4692965e04bb1df55e2c314c4296f1eb12b4f3052d4cf43d29e076aedf66/pyarrow-19.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:ee8dec072569f43835932a3b10c55973593abc00936c202707a4ad06af7cb294", size = 32128007, upload-time = "2025-02-18T18:53:06.581Z" }, + { url = "https://files.pythonhosted.org/packages/22/f7/2239af706252c6582a5635c35caa17cb4d401cd74a87821ef702e3888957/pyarrow-19.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d5d1ec7ec5324b98887bdc006f4d2ce534e10e60f7ad995e7875ffa0ff9cb14", size = 41144566, upload-time = "2025-02-18T18:53:11.958Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/c9661b2b2849cfefddd9fd65b64e093594b231b472de08ff658f76c732b2/pyarrow-19.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ad4c0eb4e2a9aeb990af6c09e6fa0b195c8c0e7b272ecc8d4d2b6574809d34", size = 42202991, upload-time = "2025-02-18T18:53:17.678Z" }, + { url = "https://files.pythonhosted.org/packages/fe/4f/a2c0ed309167ef436674782dfee4a124570ba64299c551e38d3fdaf0a17b/pyarrow-19.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d383591f3dcbe545f6cc62daaef9c7cdfe0dff0fb9e1c8121101cabe9098cfa6", size = 40507986, upload-time = "2025-02-18T18:53:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/27/2e/29bb28a7102a6f71026a9d70d1d61df926887e36ec797f2e6acfd2dd3867/pyarrow-19.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b4c4156a625f1e35d6c0b2132635a237708944eb41df5fbe7d50f20d20c17832", size = 42087026, upload-time = "2025-02-18T18:53:33.063Z" }, + { url = "https://files.pythonhosted.org/packages/16/33/2a67c0f783251106aeeee516f4806161e7b481f7d744d0d643d2f30230a5/pyarrow-19.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:5bd1618ae5e5476b7654c7b55a6364ae87686d4724538c24185bbb2952679960", size = 25250108, upload-time = "2025-02-18T18:53:38.462Z" }, ] [[package]] @@ -2748,13 +3122,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, ] +[[package]] +name = "pybreaker" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/89/fbf98e383f1ec6d117af2cd983efdb3eb7018b63834c427025764194cac2/pybreaker-1.4.1.tar.gz", hash = "sha256:8df2d245c73ba40c8242c56ffb4f12138fbadc23e296224740c2028ea9dc1178", size = 15555, upload-time = "2025-09-21T15:12:04.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/75/e64d3d40a741e2be21d69154f4e5c43a66f0c603c5ef11f49e01429a5932/pybreaker-1.4.1-py3-none-any.whl", hash = "sha256:b4dab4a05195b7f2a64a6c1a6c4ba7a96534ef56ea7210e6bcb59f28897160e0", size = 12915, upload-time = "2025-09-21T15:12:02.284Z" }, +] + [[package]] name = "pycparser" -version = "2.22" +version = "2.23" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, ] [[package]] @@ -2781,7 +3164,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.11.7" +version = "2.12.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -2789,9 +3172,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" }, ] [package.optional-dependencies] @@ -2801,72 +3184,87 @@ email = [ [[package]] name = "pydantic-core" -version = "2.33.2" +version = "2.41.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, - { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, - { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, - { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, - { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, - { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, - { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, - { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, - { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, - { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, - { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, - { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, - { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, - { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, - { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, - { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, - { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, - { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, - { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, - { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, - { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, - { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, - { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, - { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, - { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, - { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, - { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, - { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, - { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, - { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, - { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, - { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, - { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, - { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, - { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, - { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, - { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, - { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, - { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/3d/9b8ca77b0f76fcdbf8bc6b72474e264283f461284ca84ac3fde570c6c49a/pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e", size = 2111197, upload-time = "2025-10-14T10:19:43.303Z" }, + { url = "https://files.pythonhosted.org/packages/59/92/b7b0fe6ed4781642232755cb7e56a86e2041e1292f16d9ae410a0ccee5ac/pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b", size = 1917909, upload-time = "2025-10-14T10:19:45.194Z" }, + { url = "https://files.pythonhosted.org/packages/52/8c/3eb872009274ffa4fb6a9585114e161aa1a0915af2896e2d441642929fe4/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd", size = 1969905, upload-time = "2025-10-14T10:19:46.567Z" }, + { url = "https://files.pythonhosted.org/packages/f4/21/35adf4a753bcfaea22d925214a0c5b880792e3244731b3f3e6fec0d124f7/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945", size = 2051938, upload-time = "2025-10-14T10:19:48.237Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d0/cdf7d126825e36d6e3f1eccf257da8954452934ede275a8f390eac775e89/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706", size = 2250710, upload-time = "2025-10-14T10:19:49.619Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1c/af1e6fd5ea596327308f9c8d1654e1285cc3d8de0d584a3c9d7705bf8a7c/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba", size = 2367445, upload-time = "2025-10-14T10:19:51.269Z" }, + { url = "https://files.pythonhosted.org/packages/d3/81/8cece29a6ef1b3a92f956ea6da6250d5b2d2e7e4d513dd3b4f0c7a83dfea/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b", size = 2072875, upload-time = "2025-10-14T10:19:52.671Z" }, + { url = "https://files.pythonhosted.org/packages/e3/37/a6a579f5fc2cd4d5521284a0ab6a426cc6463a7b3897aeb95b12f1ba607b/pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d", size = 2191329, upload-time = "2025-10-14T10:19:54.214Z" }, + { url = "https://files.pythonhosted.org/packages/ae/03/505020dc5c54ec75ecba9f41119fd1e48f9e41e4629942494c4a8734ded1/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700", size = 2151658, upload-time = "2025-10-14T10:19:55.843Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5d/2c0d09fb53aa03bbd2a214d89ebfa6304be7df9ed86ee3dc7770257f41ee/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6", size = 2316777, upload-time = "2025-10-14T10:19:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/ea/4b/c2c9c8f5e1f9c864b57d08539d9d3db160e00491c9f5ee90e1bfd905e644/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9", size = 2320705, upload-time = "2025-10-14T10:19:59.016Z" }, + { url = "https://files.pythonhosted.org/packages/28/c3/a74c1c37f49c0a02c89c7340fafc0ba816b29bd495d1a31ce1bdeacc6085/pydantic_core-2.41.4-cp310-cp310-win32.whl", hash = "sha256:0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57", size = 1975464, upload-time = "2025-10-14T10:20:00.581Z" }, + { url = "https://files.pythonhosted.org/packages/d6/23/5dd5c1324ba80303368f7569e2e2e1a721c7d9eb16acb7eb7b7f85cb1be2/pydantic_core-2.41.4-cp310-cp310-win_amd64.whl", hash = "sha256:a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc", size = 2024497, upload-time = "2025-10-14T10:20:03.018Z" }, + { url = "https://files.pythonhosted.org/packages/62/4c/f6cbfa1e8efacd00b846764e8484fe173d25b8dab881e277a619177f3384/pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80", size = 2109062, upload-time = "2025-10-14T10:20:04.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/40b72d3868896bfcd410e1bd7e516e762d326201c48e5b4a06446f6cf9e8/pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae", size = 1916301, upload-time = "2025-10-14T10:20:06.857Z" }, + { url = "https://files.pythonhosted.org/packages/94/4d/d203dce8bee7faeca791671c88519969d98d3b4e8f225da5b96dad226fc8/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827", size = 1968728, upload-time = "2025-10-14T10:20:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/65/f5/6a66187775df87c24d526985b3a5d78d861580ca466fbd9d4d0e792fcf6c/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f", size = 2050238, upload-time = "2025-10-14T10:20:09.766Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b9/78336345de97298cf53236b2f271912ce11f32c1e59de25a374ce12f9cce/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def", size = 2249424, upload-time = "2025-10-14T10:20:11.732Z" }, + { url = "https://files.pythonhosted.org/packages/99/bb/a4584888b70ee594c3d374a71af5075a68654d6c780369df269118af7402/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2", size = 2366047, upload-time = "2025-10-14T10:20:13.647Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8d/17fc5de9d6418e4d2ae8c675f905cdafdc59d3bf3bf9c946b7ab796a992a/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8", size = 2071163, upload-time = "2025-10-14T10:20:15.307Z" }, + { url = "https://files.pythonhosted.org/packages/54/e7/03d2c5c0b8ed37a4617430db68ec5e7dbba66358b629cd69e11b4d564367/pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265", size = 2190585, upload-time = "2025-10-14T10:20:17.3Z" }, + { url = "https://files.pythonhosted.org/packages/be/fc/15d1c9fe5ad9266a5897d9b932b7f53d7e5cfc800573917a2c5d6eea56ec/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c", size = 2150109, upload-time = "2025-10-14T10:20:19.143Z" }, + { url = "https://files.pythonhosted.org/packages/26/ef/e735dd008808226c83ba56972566138665b71477ad580fa5a21f0851df48/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a", size = 2315078, upload-time = "2025-10-14T10:20:20.742Z" }, + { url = "https://files.pythonhosted.org/packages/90/00/806efdcf35ff2ac0f938362350cd9827b8afb116cc814b6b75cf23738c7c/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e", size = 2318737, upload-time = "2025-10-14T10:20:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/41/7e/6ac90673fe6cb36621a2283552897838c020db343fa86e513d3f563b196f/pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03", size = 1974160, upload-time = "2025-10-14T10:20:23.817Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9d/7c5e24ee585c1f8b6356e1d11d40ab807ffde44d2db3b7dfd6d20b09720e/pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e", size = 2021883, upload-time = "2025-10-14T10:20:25.48Z" }, + { url = "https://files.pythonhosted.org/packages/33/90/5c172357460fc28b2871eb4a0fb3843b136b429c6fa827e4b588877bf115/pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db", size = 1968026, upload-time = "2025-10-14T10:20:27.039Z" }, + { url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043, upload-time = "2025-10-14T10:20:28.561Z" }, + { url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699, upload-time = "2025-10-14T10:20:30.217Z" }, + { url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121, upload-time = "2025-10-14T10:20:32.246Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590, upload-time = "2025-10-14T10:20:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869, upload-time = "2025-10-14T10:20:35.965Z" }, + { url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169, upload-time = "2025-10-14T10:20:37.627Z" }, + { url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165, upload-time = "2025-10-14T10:20:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067, upload-time = "2025-10-14T10:20:41.015Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997, upload-time = "2025-10-14T10:20:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187, upload-time = "2025-10-14T10:20:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204, upload-time = "2025-10-14T10:20:46.781Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536, upload-time = "2025-10-14T10:20:48.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132, upload-time = "2025-10-14T10:20:50.421Z" }, + { url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483, upload-time = "2025-10-14T10:20:52.35Z" }, + { url = "https://files.pythonhosted.org/packages/b0/12/5ba58daa7f453454464f92b3ca7b9d7c657d8641c48e370c3ebc9a82dd78/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a1b2cfec3879afb742a7b0bcfa53e4f22ba96571c9e54d6a3afe1052d17d843b", size = 2122139, upload-time = "2025-10-14T10:22:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/21/fb/6860126a77725c3108baecd10fd3d75fec25191d6381b6eb2ac660228eac/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:d175600d975b7c244af6eb9c9041f10059f20b8bbffec9e33fdd5ee3f67cdc42", size = 1936674, upload-time = "2025-10-14T10:22:49.555Z" }, + { url = "https://files.pythonhosted.org/packages/de/be/57dcaa3ed595d81f8757e2b44a38240ac5d37628bce25fb20d02c7018776/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f184d657fa4947ae5ec9c47bd7e917730fa1cbb78195037e32dcbab50aca5ee", size = 1956398, upload-time = "2025-10-14T10:22:52.19Z" }, + { url = "https://files.pythonhosted.org/packages/2f/1d/679a344fadb9695f1a6a294d739fbd21d71fa023286daeea8c0ed49e7c2b/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed810568aeffed3edc78910af32af911c835cc39ebbfacd1f0ab5dd53028e5c", size = 2138674, upload-time = "2025-10-14T10:22:54.499Z" }, + { url = "https://files.pythonhosted.org/packages/c4/48/ae937e5a831b7c0dc646b2ef788c27cd003894882415300ed21927c21efa/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537", size = 2112087, upload-time = "2025-10-14T10:22:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/5e/db/6db8073e3d32dae017da7e0d16a9ecb897d0a4d92e00634916e486097961/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94", size = 1920387, upload-time = "2025-10-14T10:22:59.342Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c1/dd3542d072fcc336030d66834872f0328727e3b8de289c662faa04aa270e/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c", size = 1951495, upload-time = "2025-10-14T10:23:02.089Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c6/db8d13a1f8ab3f1eb08c88bd00fd62d44311e3456d1e85c0e59e0a0376e7/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335", size = 2139008, upload-time = "2025-10-14T10:23:04.539Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d4/912e976a2dd0b49f31c98a060ca90b353f3b73ee3ea2fd0030412f6ac5ec/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00", size = 2106739, upload-time = "2025-10-14T10:23:06.934Z" }, + { url = "https://files.pythonhosted.org/packages/71/f0/66ec5a626c81eba326072d6ee2b127f8c139543f1bf609b4842978d37833/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9", size = 1932549, upload-time = "2025-10-14T10:23:09.24Z" }, + { url = "https://files.pythonhosted.org/packages/c4/af/625626278ca801ea0a658c2dcf290dc9f21bb383098e99e7c6a029fccfc0/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2", size = 2135093, upload-time = "2025-10-14T10:23:11.626Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/2fba049f54e0f4975fef66be654c597a1d005320fa141863699180c7697d/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258", size = 2187971, upload-time = "2025-10-14T10:23:14.437Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/65ab839a2dfcd3b949202f9d920c34f9de5a537c3646662bdf2f7d999680/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347", size = 2147939, upload-time = "2025-10-14T10:23:16.831Z" }, + { url = "https://files.pythonhosted.org/packages/44/58/627565d3d182ce6dfda18b8e1c841eede3629d59c9d7cbc1e12a03aeb328/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa", size = 2311400, upload-time = "2025-10-14T10:23:19.234Z" }, + { url = "https://files.pythonhosted.org/packages/24/06/8a84711162ad5a5f19a88cead37cca81b4b1f294f46260ef7334ae4f24d3/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a", size = 2316840, upload-time = "2025-10-14T10:23:21.738Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8b/b7bb512a4682a2f7fbfae152a755d37351743900226d29bd953aaf870eaa/pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d", size = 2149135, upload-time = "2025-10-14T10:23:24.379Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7d/138e902ed6399b866f7cfe4435d22445e16fff888a1c00560d9dc79a780f/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5", size = 2104721, upload-time = "2025-10-14T10:23:26.906Z" }, + { url = "https://files.pythonhosted.org/packages/47/13/0525623cf94627f7b53b4c2034c81edc8491cbfc7c28d5447fa318791479/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2", size = 1931608, upload-time = "2025-10-14T10:23:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f9/744bc98137d6ef0a233f808bfc9b18cf94624bf30836a18d3b05d08bf418/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd", size = 2132986, upload-time = "2025-10-14T10:23:32.057Z" }, + { url = "https://files.pythonhosted.org/packages/17/c8/629e88920171173f6049386cc71f893dff03209a9ef32b4d2f7e7c264bcf/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c", size = 2187516, upload-time = "2025-10-14T10:23:34.871Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/4f2734688d98488782218ca61bcc118329bf5de05bb7fe3adc7dd79b0b86/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405", size = 2146146, upload-time = "2025-10-14T10:23:37.342Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f2/ab385dbd94a052c62224b99cf99002eee99dbec40e10006c78575aead256/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8", size = 2311296, upload-time = "2025-10-14T10:23:40.145Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8e/e4f12afe1beeb9823bba5375f8f258df0cc61b056b0195fb1cf9f62a1a58/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308", size = 2315386, upload-time = "2025-10-14T10:23:42.624Z" }, + { url = "https://files.pythonhosted.org/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f", size = 2147775, upload-time = "2025-10-14T10:23:45.406Z" }, +] + +[[package]] +name = "pydyf" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/c2/97fc6ce4ce0045080dc99446def812081b57750ed8aa67bfdfafa4561fe5/pydyf-0.11.0.tar.gz", hash = "sha256:394dddf619cca9d0c55715e3c55ea121a9bf9cbc780cdc1201a2427917b86b64", size = 17769, upload-time = "2024-07-12T12:26:51.95Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/ac/d5db977deaf28c6ecbc61bbca269eb3e8f0b3a1f55c8549e5333e606e005/pydyf-0.11.0-py3-none-any.whl", hash = "sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3", size = 8104, upload-time = "2024-07-12T12:26:49.896Z" }, ] [[package]] @@ -2889,7 +3287,7 @@ wheels = [ [[package]] name = "pylint" -version = "3.3.8" +version = "3.3.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "astroid" }, @@ -2901,52 +3299,66 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tomlkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/58/1f614a84d3295c542e9f6e2c764533eea3f318f4592dc1ea06c797114767/pylint-3.3.8.tar.gz", hash = "sha256:26698de19941363037e2937d3db9ed94fb3303fdadf7d98847875345a8bb6b05", size = 1523947, upload-time = "2025-08-09T09:12:57.234Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/9d/81c84a312d1fa8133b0db0c76148542a98349298a01747ab122f9314b04e/pylint-3.3.9.tar.gz", hash = "sha256:d312737d7b25ccf6b01cc4ac629b5dcd14a0fcf3ec392735ac70f137a9d5f83a", size = 1525946, upload-time = "2025-10-05T18:41:43.786Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/1a/711e93a7ab6c392e349428ea56e794a3902bb4e0284c1997cff2d7efdbc1/pylint-3.3.8-py3-none-any.whl", hash = "sha256:7ef94aa692a600e82fabdd17102b73fc226758218c97473c7ad67bd4cb905d83", size = 523153, upload-time = "2025-08-09T09:12:54.836Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a7/69460c4a6af7575449e615144aa2205b89408dc2969b87bc3df2f262ad0b/pylint-3.3.9-py3-none-any.whl", hash = "sha256:01f9b0462c7730f94786c283f3e52a1fbdf0494bbe0971a78d7277ef46a751e7", size = 523465, upload-time = "2025-10-05T18:41:41.766Z" }, ] [[package]] name = "pymysql" -version = "1.1.1" +version = "1.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/ce59b5e5ed4ce8512f879ff1fa5ab699d211ae2495f1adaa5fbba2a1eada/pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0", size = 47678, upload-time = "2024-05-21T11:03:43.722Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/ae/1fe3fcd9f959efa0ebe200b8de88b5a5ce3e767e38c7ac32fb179f16a388/pymysql-1.1.2.tar.gz", hash = "sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03", size = 48258, upload-time = "2025-08-24T12:55:55.146Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/94/e4181a1f6286f545507528c78016e00065ea913276888db2262507693ce5/PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c", size = 44972, upload-time = "2024-05-21T11:03:41.216Z" }, + { url = "https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl", hash = "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9", size = 45300, upload-time = "2025-08-24T12:55:53.394Z" }, ] [[package]] name = "pynacl" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/22/27582568be639dfe22ddb3902225f91f2f17ceff88ce80e4db396c8986da/PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", size = 3392854, upload-time = "2022-01-07T22:05:41.134Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/c6/a3124dee667a423f2c637cfd262a54d67d8ccf3e160f3c50f622a85b7723/pynacl-1.6.0.tar.gz", hash = "sha256:cb36deafe6e2bce3b286e5d1f3e1c246e0ccdb8808ddb4550bb2792f2df298f2", size = 3505641, upload-time = "2025-09-10T23:39:22.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/75/0b8ede18506041c0bf23ac4d8e2971b4161cd6ce630b177d0a08eb0d8857/PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", size = 349920, upload-time = "2022-01-07T22:05:49.156Z" }, - { url = "https://files.pythonhosted.org/packages/59/bb/fddf10acd09637327a97ef89d2a9d621328850a72f1fdc8c08bdf72e385f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", size = 601722, upload-time = "2022-01-07T22:05:50.989Z" }, - { url = "https://files.pythonhosted.org/packages/5d/70/87a065c37cca41a75f2ce113a5a2c2aa7533be648b184ade58971b5f7ccc/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", size = 680087, upload-time = "2022-01-07T22:05:52.539Z" }, - { url = "https://files.pythonhosted.org/packages/ee/87/f1bb6a595f14a327e8285b9eb54d41fef76c585a0edef0a45f6fc95de125/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", size = 856678, upload-time = "2022-01-07T22:05:54.251Z" }, - { url = "https://files.pythonhosted.org/packages/66/28/ca86676b69bf9f90e710571b67450508484388bfce09acf8a46f0b8c785f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", size = 1133660, upload-time = "2022-01-07T22:05:56.056Z" }, - { url = "https://files.pythonhosted.org/packages/3d/85/c262db650e86812585e2bc59e497a8f59948a005325a11bbbc9ecd3fe26b/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", size = 663824, upload-time = "2022-01-07T22:05:57.434Z" }, - { url = "https://files.pythonhosted.org/packages/fd/1a/cc308a884bd299b651f1633acb978e8596c71c33ca85e9dc9fa33a5399b9/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", size = 1117912, upload-time = "2022-01-07T22:05:58.665Z" }, - { url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624, upload-time = "2022-01-07T22:06:00.085Z" }, - { url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141, upload-time = "2022-01-07T22:06:01.861Z" }, + { url = "https://files.pythonhosted.org/packages/63/37/87c72df19857c5b3b47ace6f211a26eb862ada495cc96daa372d96048fca/pynacl-1.6.0-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:f4b3824920e206b4f52abd7de621ea7a44fd3cb5c8daceb7c3612345dfc54f2e", size = 382610, upload-time = "2025-09-10T23:38:49.459Z" }, + { url = "https://files.pythonhosted.org/packages/0c/64/3ce958a5817fd3cc6df4ec14441c43fd9854405668d73babccf77f9597a3/pynacl-1.6.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:16dd347cdc8ae0b0f6187a2608c0af1c8b7ecbbe6b4a06bff8253c192f696990", size = 798744, upload-time = "2025-09-10T23:38:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/e4/8a/3f0dd297a0a33fa3739c255feebd0206bb1df0b44c52fbe2caf8e8bc4425/pynacl-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16c60daceee88d04f8d41d0a4004a7ed8d9a5126b997efd2933e08e93a3bd850", size = 1397879, upload-time = "2025-09-10T23:39:00.44Z" }, + { url = "https://files.pythonhosted.org/packages/41/94/028ff0434a69448f61348d50d2c147dda51aabdd4fbc93ec61343332174d/pynacl-1.6.0-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25720bad35dfac34a2bcdd61d9e08d6bfc6041bebc7751d9c9f2446cf1e77d64", size = 833907, upload-time = "2025-09-10T23:38:50.936Z" }, + { url = "https://files.pythonhosted.org/packages/52/bc/a5cff7f8c30d5f4c26a07dfb0bcda1176ab8b2de86dda3106c00a02ad787/pynacl-1.6.0-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bfaa0a28a1ab718bad6239979a5a57a8d1506d0caf2fba17e524dbb409441cf", size = 1436649, upload-time = "2025-09-10T23:38:52.783Z" }, + { url = "https://files.pythonhosted.org/packages/7a/20/c397be374fd5d84295046e398de4ba5f0722dc14450f65db76a43c121471/pynacl-1.6.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ef214b90556bb46a485b7da8258e59204c244b1b5b576fb71848819b468c44a7", size = 817142, upload-time = "2025-09-10T23:38:54.4Z" }, + { url = "https://files.pythonhosted.org/packages/12/30/5efcef3406940cda75296c6d884090b8a9aad2dcc0c304daebb5ae99fb4a/pynacl-1.6.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:49c336dd80ea54780bcff6a03ee1a476be1612423010472e60af83452aa0f442", size = 1401794, upload-time = "2025-09-10T23:38:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/be/e1/a8fe1248cc17ccb03b676d80fa90763760a6d1247da434844ea388d0816c/pynacl-1.6.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f3482abf0f9815e7246d461fab597aa179b7524628a4bc36f86a7dc418d2608d", size = 772161, upload-time = "2025-09-10T23:39:01.93Z" }, + { url = "https://files.pythonhosted.org/packages/a3/76/8a62702fb657d6d9104ce13449db221a345665d05e6a3fdefb5a7cafd2ad/pynacl-1.6.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:140373378e34a1f6977e573033d1dd1de88d2a5d90ec6958c9485b2fd9f3eb90", size = 1370720, upload-time = "2025-09-10T23:39:03.531Z" }, + { url = "https://files.pythonhosted.org/packages/6d/38/9e9e9b777a1c4c8204053733e1a0269672c0bd40852908c9ad6b6eaba82c/pynacl-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6b393bc5e5a0eb86bb85b533deb2d2c815666665f840a09e0aa3362bb6088736", size = 791252, upload-time = "2025-09-10T23:39:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d972ce3d92ae05c9091363cf185e8646933f91c376e97b8be79ea6e96c22/pynacl-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a25cfede801f01e54179b8ff9514bd7b5944da560b7040939732d1804d25419", size = 1362910, upload-time = "2025-09-10T23:39:06.924Z" }, + { url = "https://files.pythonhosted.org/packages/35/2c/ee0b373a1861f66a7ca8bdb999331525615061320dd628527a50ba8e8a60/pynacl-1.6.0-cp38-abi3-win32.whl", hash = "sha256:dcdeb41c22ff3c66eef5e63049abf7639e0db4edee57ba70531fc1b6b133185d", size = 226461, upload-time = "2025-09-10T23:39:11.894Z" }, + { url = "https://files.pythonhosted.org/packages/75/f7/41b6c0b9dd9970173b6acc026bab7b4c187e4e5beef2756d419ad65482da/pynacl-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:cf831615cc16ba324240de79d925eacae8265b7691412ac6b24221db157f6bd1", size = 238802, upload-time = "2025-09-10T23:39:08.966Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0f/462326910c6172fa2c6ed07922b22ffc8e77432b3affffd9e18f444dbfbb/pynacl-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:84709cea8f888e618c21ed9a0efdb1a59cc63141c403db8bf56c469b71ad56f2", size = 183846, upload-time = "2025-09-10T23:39:10.552Z" }, ] [[package]] name = "pyparsing" -version = "3.2.3" +version = "3.2.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, + { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, +] + +[[package]] +name = "pyphen" +version = "0.17.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/56/e4d7e1bd70d997713649c5ce530b2d15a5fc2245a74ca820fc2d51d89d4d/pyphen-0.17.2.tar.gz", hash = "sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3", size = 2079470, upload-time = "2025-01-20T13:18:36.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/1f/c2142d2edf833a90728e5cdeb10bdbdc094dde8dbac078cee0cf33f5e11b/pyphen-0.17.2-py3-none-any.whl", hash = "sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd", size = 2079358, upload-time = "2025-01-20T13:18:29.629Z" }, ] [[package]] name = "pytest" -version = "8.4.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -2957,9 +3369,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] [[package]] @@ -2985,11 +3397,11 @@ wheels = [ [[package]] name = "python-json-logger" -version = "3.3.0" +version = "4.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/de/d3144a0bceede957f961e975f3752760fbe390d57fbe194baf709d8f1f7b/python_json_logger-3.3.0.tar.gz", hash = "sha256:12b7e74b17775e7d565129296105bbe3910842d9d0eb083fc83a6a617aa8df84", size = 16642, upload-time = "2025-03-07T07:08:27.301Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/20/0f2523b9e50a8052bc6a8b732dfc8568abbdc42010aef03a2d750bdab3b2/python_json_logger-3.3.0-py3-none-any.whl", hash = "sha256:dd980fae8cffb24c13caf6e158d3d61c0d6d22342f932cb6e9deedab3d35eec7", size = 15163, upload-time = "2025-03-07T07:08:25.627Z" }, + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, ] [[package]] @@ -3028,195 +3440,165 @@ wheels = [ [[package]] name = "pywinpty" -version = "2.0.15" +version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/7c/917f9c4681bb8d34bfbe0b79d36bbcd902651aeab48790df3d30ba0202fb/pywinpty-2.0.15.tar.gz", hash = "sha256:312cf39153a8736c617d45ce8b6ad6cd2107de121df91c455b10ce6bba7a39b2", size = 29017, upload-time = "2025-02-03T21:53:23.265Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/bb/a7cc2967c5c4eceb6cc49cfe39447d4bfc56e6c865e7c2249b6eb978935f/pywinpty-3.0.2.tar.gz", hash = "sha256:1505cc4cb248af42cb6285a65c9c2086ee9e7e574078ee60933d5d7fa86fb004", size = 30669, upload-time = "2025-10-03T21:16:29.205Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/b7/855db919ae526d2628f3f2e6c281c4cdff7a9a8af51bb84659a9f07b1861/pywinpty-2.0.15-cp310-cp310-win_amd64.whl", hash = "sha256:8e7f5de756a615a38b96cd86fa3cd65f901ce54ce147a3179c45907fa11b4c4e", size = 1405161, upload-time = "2025-02-03T21:56:25.008Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ac/6884dcb7108af66ad53f73ef4dad096e768c9203a6e6ce5e6b0c4a46e238/pywinpty-2.0.15-cp311-cp311-win_amd64.whl", hash = "sha256:9a6bcec2df2707aaa9d08b86071970ee32c5026e10bcc3cc5f6f391d85baf7ca", size = 1405249, upload-time = "2025-02-03T21:55:47.114Z" }, - { url = "https://files.pythonhosted.org/packages/88/e5/9714def18c3a411809771a3fbcec70bffa764b9675afb00048a620fca604/pywinpty-2.0.15-cp312-cp312-win_amd64.whl", hash = "sha256:83a8f20b430bbc5d8957249f875341a60219a4e971580f2ba694fbfb54a45ebc", size = 1405243, upload-time = "2025-02-03T21:56:52.476Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f5/b17ae550841949c217ad557ee445b4a14e9c0b506ae51ee087eff53428a6/pywinpty-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:65db57fd3387d71e8372b6a54269cbcd0f6dfa6d4616a29e0af749ec19f5c558", size = 2050330, upload-time = "2025-10-03T21:20:15.656Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a1/409c1651c9f874d598c10f51ff586c416625601df4bca315d08baec4c3e3/pywinpty-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:327790d70e4c841ebd9d0f295a780177149aeb405bca44c7115a3de5c2054b23", size = 2050304, upload-time = "2025-10-03T21:19:29.466Z" }, + { url = "https://files.pythonhosted.org/packages/02/4e/1098484e042c9485f56f16eb2b69b43b874bd526044ee401512234cf9e04/pywinpty-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:99fdd9b455f0ad6419aba6731a7a0d2f88ced83c3c94a80ff9533d95fa8d8a9e", size = 2050391, upload-time = "2025-10-03T21:19:01.642Z" }, ] [[package]] name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, ] [[package]] name = "pyzmq" -version = "27.0.1" +version = "27.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/5f/557d2032a2f471edbcc227da724c24a1c05887b5cda1e3ae53af98b9e0a5/pyzmq-27.0.1.tar.gz", hash = "sha256:45c549204bc20e7484ffd2555f6cf02e572440ecf2f3bdd60d4404b20fddf64b", size = 281158, upload-time = "2025-08-03T05:05:40.352Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/0b/ccf4d0b152a6a11f0fc01e73978202fe0e8fe0e91e20941598e83a170bee/pyzmq-27.0.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:90a4da42aa322de8a3522461e3b5fe999935763b27f69a02fced40f4e3cf9682", size = 1329293, upload-time = "2025-08-03T05:02:56.001Z" }, - { url = "https://files.pythonhosted.org/packages/bc/76/48706d291951b1300d3cf985e503806901164bf1581f27c4b6b22dbab2fa/pyzmq-27.0.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e648dca28178fc879c814cf285048dd22fd1f03e1104101106505ec0eea50a4d", size = 905953, upload-time = "2025-08-03T05:02:59.061Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8a/df3135b96712068d184c53120c7dbf3023e5e362a113059a4f85cd36c6a0/pyzmq-27.0.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bca8abc31799a6f3652d13f47e0b0e1cab76f9125f2283d085a3754f669b607", size = 666165, upload-time = "2025-08-03T05:03:00.789Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ed/341a7148e08d2830f480f53ab3d136d88fc5011bb367b516d95d0ebb46dd/pyzmq-27.0.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:092f4011b26d6b0201002f439bd74b38f23f3aefcb358621bdc3b230afc9b2d5", size = 853756, upload-time = "2025-08-03T05:03:03.347Z" }, - { url = "https://files.pythonhosted.org/packages/c2/bc/d26fe010477c3e901f0f5a3e70446950dde9aa217f1d1a13534eb0fccfe5/pyzmq-27.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f02f30a4a6b3efe665ab13a3dd47109d80326c8fd286311d1ba9f397dc5f247", size = 1654870, upload-time = "2025-08-03T05:03:05.331Z" }, - { url = "https://files.pythonhosted.org/packages/32/21/9b488086bf3f55b2eb26db09007a3962f62f3b81c5c6295a6ff6aaebd69c/pyzmq-27.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f293a1419266e3bf3557d1f8778f9e1ffe7e6b2c8df5c9dca191caf60831eb74", size = 2033444, upload-time = "2025-08-03T05:03:07.318Z" }, - { url = "https://files.pythonhosted.org/packages/3d/53/85b64a792223cd43393d25e03c8609df41aac817ea5ce6a27eceeed433ee/pyzmq-27.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ce181dd1a7c6c012d0efa8ab603c34b5ee9d86e570c03415bbb1b8772eeb381c", size = 1891289, upload-time = "2025-08-03T05:03:08.96Z" }, - { url = "https://files.pythonhosted.org/packages/23/5b/078aae8fe1c4cdba1a77a598870c548fd52b4d4a11e86b8116bbef47d9f3/pyzmq-27.0.1-cp310-cp310-win32.whl", hash = "sha256:f65741cc06630652e82aa68ddef4986a3ab9073dd46d59f94ce5f005fa72037c", size = 566693, upload-time = "2025-08-03T05:03:10.711Z" }, - { url = "https://files.pythonhosted.org/packages/24/e1/4471fff36416ebf1ffe43577b9c7dcf2ff4798f2171f0d169640a48d2305/pyzmq-27.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:44909aa3ed2234d69fe81e1dade7be336bcfeab106e16bdaa3318dcde4262b93", size = 631649, upload-time = "2025-08-03T05:03:12.232Z" }, - { url = "https://files.pythonhosted.org/packages/e8/4c/8edac8dd56f223124aa40403d2c097bbad9b0e2868a67cad9a2a029863aa/pyzmq-27.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:4401649bfa0a38f0f8777f8faba7cd7eb7b5b8ae2abc7542b830dd09ad4aed0d", size = 559274, upload-time = "2025-08-03T05:03:13.728Z" }, - { url = "https://files.pythonhosted.org/packages/ae/18/a8e0da6ababbe9326116fb1c890bf1920eea880e8da621afb6bc0f39a262/pyzmq-27.0.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:9729190bd770314f5fbba42476abf6abe79a746eeda11d1d68fd56dd70e5c296", size = 1332721, upload-time = "2025-08-03T05:03:15.237Z" }, - { url = "https://files.pythonhosted.org/packages/75/a4/9431ba598651d60ebd50dc25755402b770322cf8432adcc07d2906e53a54/pyzmq-27.0.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:696900ef6bc20bef6a242973943574f96c3f97d2183c1bd3da5eea4f559631b1", size = 908249, upload-time = "2025-08-03T05:03:16.933Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/e624e1793689e4e685d2ee21c40277dd4024d9d730af20446d88f69be838/pyzmq-27.0.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96a63aecec22d3f7fdea3c6c98df9e42973f5856bb6812c3d8d78c262fee808", size = 668649, upload-time = "2025-08-03T05:03:18.49Z" }, - { url = "https://files.pythonhosted.org/packages/6c/29/0652a39d4e876e0d61379047ecf7752685414ad2e253434348246f7a2a39/pyzmq-27.0.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c512824360ea7490390566ce00bee880e19b526b312b25cc0bc30a0fe95cb67f", size = 856601, upload-time = "2025-08-03T05:03:20.194Z" }, - { url = "https://files.pythonhosted.org/packages/36/2d/8d5355d7fc55bb6e9c581dd74f58b64fa78c994079e3a0ea09b1b5627cde/pyzmq-27.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dfb2bb5e0f7198eaacfb6796fb0330afd28f36d985a770745fba554a5903595a", size = 1657750, upload-time = "2025-08-03T05:03:22.055Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f4/cd032352d5d252dc6f5ee272a34b59718ba3af1639a8a4ef4654f9535cf5/pyzmq-27.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4f6886c59ba93ffde09b957d3e857e7950c8fe818bd5494d9b4287bc6d5bc7f1", size = 2034312, upload-time = "2025-08-03T05:03:23.578Z" }, - { url = "https://files.pythonhosted.org/packages/e4/1a/c050d8b6597200e97a4bd29b93c769d002fa0b03083858227e0376ad59bc/pyzmq-27.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b99ea9d330e86ce1ff7f2456b33f1bf81c43862a5590faf4ef4ed3a63504bdab", size = 1893632, upload-time = "2025-08-03T05:03:25.167Z" }, - { url = "https://files.pythonhosted.org/packages/6a/29/173ce21d5097e7fcf284a090e8beb64fc683c6582b1f00fa52b1b7e867ce/pyzmq-27.0.1-cp311-cp311-win32.whl", hash = "sha256:571f762aed89025ba8cdcbe355fea56889715ec06d0264fd8b6a3f3fa38154ed", size = 566587, upload-time = "2025-08-03T05:03:26.769Z" }, - { url = "https://files.pythonhosted.org/packages/53/ab/22bd33e7086f0a2cc03a5adabff4bde414288bb62a21a7820951ef86ec20/pyzmq-27.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee16906c8025fa464bea1e48128c048d02359fb40bebe5333103228528506530", size = 632873, upload-time = "2025-08-03T05:03:28.685Z" }, - { url = "https://files.pythonhosted.org/packages/90/14/3e59b4a28194285ceeff725eba9aa5ba8568d1cb78aed381dec1537c705a/pyzmq-27.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:ba068f28028849da725ff9185c24f832ccf9207a40f9b28ac46ab7c04994bd41", size = 558918, upload-time = "2025-08-03T05:03:30.085Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9b/c0957041067c7724b310f22c398be46399297c12ed834c3bc42200a2756f/pyzmq-27.0.1-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:af7ebce2a1e7caf30c0bb64a845f63a69e76a2fadbc1cac47178f7bb6e657bdd", size = 1305432, upload-time = "2025-08-03T05:03:32.177Z" }, - { url = "https://files.pythonhosted.org/packages/8e/55/bd3a312790858f16b7def3897a0c3eb1804e974711bf7b9dcb5f47e7f82c/pyzmq-27.0.1-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8f617f60a8b609a13099b313e7e525e67f84ef4524b6acad396d9ff153f6e4cd", size = 895095, upload-time = "2025-08-03T05:03:33.918Z" }, - { url = "https://files.pythonhosted.org/packages/20/50/fc384631d8282809fb1029a4460d2fe90fa0370a0e866a8318ed75c8d3bb/pyzmq-27.0.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d59dad4173dc2a111f03e59315c7bd6e73da1a9d20a84a25cf08325b0582b1a", size = 651826, upload-time = "2025-08-03T05:03:35.818Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0a/2356305c423a975000867de56888b79e44ec2192c690ff93c3109fd78081/pyzmq-27.0.1-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5b6133c8d313bde8bd0d123c169d22525300ff164c2189f849de495e1344577", size = 839751, upload-time = "2025-08-03T05:03:37.265Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1b/81e95ad256ca7e7ccd47f5294c1c6da6e2b64fbace65b84fe8a41470342e/pyzmq-27.0.1-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:58cca552567423f04d06a075f4b473e78ab5bdb906febe56bf4797633f54aa4e", size = 1641359, upload-time = "2025-08-03T05:03:38.799Z" }, - { url = "https://files.pythonhosted.org/packages/50/63/9f50ec965285f4e92c265c8f18344e46b12803666d8b73b65d254d441435/pyzmq-27.0.1-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:4b9d8e26fb600d0d69cc9933e20af08552e97cc868a183d38a5c0d661e40dfbb", size = 2020281, upload-time = "2025-08-03T05:03:40.338Z" }, - { url = "https://files.pythonhosted.org/packages/02/4a/19e3398d0dc66ad2b463e4afa1fc541d697d7bc090305f9dfb948d3dfa29/pyzmq-27.0.1-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2329f0c87f0466dce45bba32b63f47018dda5ca40a0085cc5c8558fea7d9fc55", size = 1877112, upload-time = "2025-08-03T05:03:42.012Z" }, - { url = "https://files.pythonhosted.org/packages/bf/42/c562e9151aa90ed1d70aac381ea22a929d6b3a2ce4e1d6e2e135d34fd9c6/pyzmq-27.0.1-cp312-abi3-win32.whl", hash = "sha256:57bb92abdb48467b89c2d21da1ab01a07d0745e536d62afd2e30d5acbd0092eb", size = 558177, upload-time = "2025-08-03T05:03:43.979Z" }, - { url = "https://files.pythonhosted.org/packages/40/96/5c50a7d2d2b05b19994bf7336b97db254299353dd9b49b565bb71b485f03/pyzmq-27.0.1-cp312-abi3-win_amd64.whl", hash = "sha256:ff3f8757570e45da7a5bedaa140489846510014f7a9d5ee9301c61f3f1b8a686", size = 618923, upload-time = "2025-08-03T05:03:45.438Z" }, - { url = "https://files.pythonhosted.org/packages/13/33/1ec89c8f21c89d21a2eaff7def3676e21d8248d2675705e72554fb5a6f3f/pyzmq-27.0.1-cp312-abi3-win_arm64.whl", hash = "sha256:df2c55c958d3766bdb3e9d858b911288acec09a9aab15883f384fc7180df5bed", size = 552358, upload-time = "2025-08-03T05:03:46.887Z" }, - { url = "https://files.pythonhosted.org/packages/6f/87/fc96f224dd99070fe55d0afc37ac08d7d4635d434e3f9425b232867e01b9/pyzmq-27.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:544b995a6a1976fad5d7ff01409b4588f7608ccc41be72147700af91fd44875d", size = 835950, upload-time = "2025-08-03T05:05:04.193Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b6/802d96017f176c3a7285603d9ed2982550095c136c6230d3e0b53f52c7e5/pyzmq-27.0.1-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0f772eea55cccce7f45d6ecdd1d5049c12a77ec22404f6b892fae687faa87bee", size = 799876, upload-time = "2025-08-03T05:05:06.263Z" }, - { url = "https://files.pythonhosted.org/packages/4e/52/49045c6528007cce385f218f3a674dc84fc8b3265330d09e57c0a59b41f4/pyzmq-27.0.1-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9d63d66059114a6756d09169c9209ffceabacb65b9cb0f66e6fc344b20b73e6", size = 567402, upload-time = "2025-08-03T05:05:08.028Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fe/c29ac0d5a817543ecf0cb18f17195805bad0da567a1c64644aacf11b2779/pyzmq-27.0.1-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1da8e645c655d86f0305fb4c65a0d848f461cd90ee07d21f254667287b5dbe50", size = 747030, upload-time = "2025-08-03T05:05:10.116Z" }, - { url = "https://files.pythonhosted.org/packages/17/d1/cc1fbfb65b4042016e4e035b2548cdfe0945c817345df83aa2d98490e7fc/pyzmq-27.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1843fd0daebcf843fe6d4da53b8bdd3fc906ad3e97d25f51c3fed44436d82a49", size = 544567, upload-time = "2025-08-03T05:05:11.856Z" }, - { url = "https://files.pythonhosted.org/packages/b4/1a/49f66fe0bc2b2568dd4280f1f520ac8fafd73f8d762140e278d48aeaf7b9/pyzmq-27.0.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7fb0ee35845bef1e8c4a152d766242164e138c239e3182f558ae15cb4a891f94", size = 835949, upload-time = "2025-08-03T05:05:13.798Z" }, - { url = "https://files.pythonhosted.org/packages/49/94/443c1984b397eab59b14dd7ae8bc2ac7e8f32dbc646474453afcaa6508c4/pyzmq-27.0.1-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f379f11e138dfd56c3f24a04164f871a08281194dd9ddf656a278d7d080c8ad0", size = 799875, upload-time = "2025-08-03T05:05:15.632Z" }, - { url = "https://files.pythonhosted.org/packages/30/f1/fd96138a0f152786a2ba517e9c6a8b1b3516719e412a90bb5d8eea6b660c/pyzmq-27.0.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b978c0678cffbe8860ec9edc91200e895c29ae1ac8a7085f947f8e8864c489fb", size = 567403, upload-time = "2025-08-03T05:05:17.326Z" }, - { url = "https://files.pythonhosted.org/packages/16/57/34e53ef2b55b1428dac5aabe3a974a16c8bda3bf20549ba500e3ff6cb426/pyzmq-27.0.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ebccf0d760bc92a4a7c751aeb2fef6626144aace76ee8f5a63abeb100cae87f", size = 747032, upload-time = "2025-08-03T05:05:19.074Z" }, - { url = "https://files.pythonhosted.org/packages/81/b7/769598c5ae336fdb657946950465569cf18803140fe89ce466d7f0a57c11/pyzmq-27.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:77fed80e30fa65708546c4119840a46691290efc231f6bfb2ac2a39b52e15811", size = 544566, upload-time = "2025-08-03T05:05:20.798Z" }, -] - -[[package]] -name = "querystring-parser" -version = "1.2.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4a/fa/f54f5662e0eababf0c49e92fd94bf178888562c0e7b677c8941bbbcd1bd6/querystring_parser-1.2.4.tar.gz", hash = "sha256:644fce1cffe0530453b43a83a38094dbe422ccba8c9b2f2a1c00280e14ca8a62", size = 5454, upload-time = "2019-07-22T17:58:29.235Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/6b/572b2590fd55114118bf08bde63c0a421dcc82d593700f3e2ad89908a8a9/querystring_parser-1.2.4-py2.py3-none-any.whl", hash = "sha256:d2fa90765eaf0de96c8b087872991a10238e89ba015ae59fedfed6bd61c242a0", size = 7908, upload-time = "2020-10-21T22:33:33.17Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" }, + { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, + { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, + { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, ] [[package]] name = "rapidfuzz" -version = "3.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/6895abc3a3d056b9698da3199b04c0e56226d530ae44a470edabf8b664f0/rapidfuzz-3.13.0.tar.gz", hash = "sha256:d2eaf3839e52cbcc0accbe9817a67b4b0fcf70aaeb229cfddc1c28061f9ce5d8", size = 57904226, upload-time = "2025-04-03T20:38:51.226Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/27/ca10b3166024ae19a7e7c21f73c58dfd4b7fef7420e5497ee64ce6b73453/rapidfuzz-3.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aafc42a1dc5e1beeba52cd83baa41372228d6d8266f6d803c16dbabbcc156255", size = 1998899, upload-time = "2025-04-03T20:35:08.764Z" }, - { url = "https://files.pythonhosted.org/packages/f0/38/c4c404b13af0315483a6909b3a29636e18e1359307fb74a333fdccb3730d/rapidfuzz-3.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:85c9a131a44a95f9cac2eb6e65531db014e09d89c4f18c7b1fa54979cb9ff1f3", size = 1449949, upload-time = "2025-04-03T20:35:11.26Z" }, - { url = "https://files.pythonhosted.org/packages/12/ae/15c71d68a6df6b8e24595421fdf5bcb305888318e870b7be8d935a9187ee/rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d7cec4242d30dd521ef91c0df872e14449d1dffc2a6990ede33943b0dae56c3", size = 1424199, upload-time = "2025-04-03T20:35:12.954Z" }, - { url = "https://files.pythonhosted.org/packages/dc/9a/765beb9e14d7b30d12e2d6019e8b93747a0bedbc1d0cce13184fa3825426/rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e297c09972698c95649e89121e3550cee761ca3640cd005e24aaa2619175464e", size = 5352400, upload-time = "2025-04-03T20:35:15.421Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b8/49479fe6f06b06cd54d6345ed16de3d1ac659b57730bdbe897df1e059471/rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef0f5f03f61b0e5a57b1df7beafd83df993fd5811a09871bad6038d08e526d0d", size = 1652465, upload-time = "2025-04-03T20:35:18.43Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d8/08823d496b7dd142a7b5d2da04337df6673a14677cfdb72f2604c64ead69/rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8cf5f7cd6e4d5eb272baf6a54e182b2c237548d048e2882258336533f3f02b7", size = 1616590, upload-time = "2025-04-03T20:35:20.482Z" }, - { url = "https://files.pythonhosted.org/packages/38/d4/5cfbc9a997e544f07f301c54d42aac9e0d28d457d543169e4ec859b8ce0d/rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9256218ac8f1a957806ec2fb9a6ddfc6c32ea937c0429e88cf16362a20ed8602", size = 3086956, upload-time = "2025-04-03T20:35:22.756Z" }, - { url = "https://files.pythonhosted.org/packages/25/1e/06d8932a72fa9576095234a15785136407acf8f9a7dbc8136389a3429da1/rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1bdd2e6d0c5f9706ef7595773a81ca2b40f3b33fd7f9840b726fb00c6c4eb2e", size = 2494220, upload-time = "2025-04-03T20:35:25.563Z" }, - { url = "https://files.pythonhosted.org/packages/03/16/5acf15df63119d5ca3d9a54b82807866ff403461811d077201ca351a40c3/rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5280be8fd7e2bee5822e254fe0a5763aa0ad57054b85a32a3d9970e9b09bbcbf", size = 7585481, upload-time = "2025-04-03T20:35:27.426Z" }, - { url = "https://files.pythonhosted.org/packages/e1/cf/ebade4009431ea8e715e59e882477a970834ddaacd1a670095705b86bd0d/rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd742c03885db1fce798a1cd87a20f47f144ccf26d75d52feb6f2bae3d57af05", size = 2894842, upload-time = "2025-04-03T20:35:29.457Z" }, - { url = "https://files.pythonhosted.org/packages/a7/bd/0732632bd3f906bf613229ee1b7cbfba77515db714a0e307becfa8a970ae/rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5435fcac94c9ecf0504bf88a8a60c55482c32e18e108d6079a0089c47f3f8cf6", size = 3438517, upload-time = "2025-04-03T20:35:31.381Z" }, - { url = "https://files.pythonhosted.org/packages/83/89/d3bd47ec9f4b0890f62aea143a1e35f78f3d8329b93d9495b4fa8a3cbfc3/rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:93a755266856599be4ab6346273f192acde3102d7aa0735e2f48b456397a041f", size = 4412773, upload-time = "2025-04-03T20:35:33.425Z" }, - { url = "https://files.pythonhosted.org/packages/b3/57/1a152a07883e672fc117c7f553f5b933f6e43c431ac3fd0e8dae5008f481/rapidfuzz-3.13.0-cp310-cp310-win32.whl", hash = "sha256:3abe6a4e8eb4cfc4cda04dd650a2dc6d2934cbdeda5def7e6fd1c20f6e7d2a0b", size = 1842334, upload-time = "2025-04-03T20:35:35.648Z" }, - { url = "https://files.pythonhosted.org/packages/a7/68/7248addf95b6ca51fc9d955161072285da3059dd1472b0de773cff910963/rapidfuzz-3.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:e8ddb58961401da7d6f55f185512c0d6bd24f529a637078d41dd8ffa5a49c107", size = 1624392, upload-time = "2025-04-03T20:35:37.294Z" }, - { url = "https://files.pythonhosted.org/packages/68/23/f41c749f2c61ed1ed5575eaf9e73ef9406bfedbf20a3ffa438d15b5bf87e/rapidfuzz-3.13.0-cp310-cp310-win_arm64.whl", hash = "sha256:c523620d14ebd03a8d473c89e05fa1ae152821920c3ff78b839218ff69e19ca3", size = 865584, upload-time = "2025-04-03T20:35:39.005Z" }, - { url = "https://files.pythonhosted.org/packages/87/17/9be9eff5a3c7dfc831c2511262082c6786dca2ce21aa8194eef1cb71d67a/rapidfuzz-3.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d395a5cad0c09c7f096433e5fd4224d83b53298d53499945a9b0e5a971a84f3a", size = 1999453, upload-time = "2025-04-03T20:35:40.804Z" }, - { url = "https://files.pythonhosted.org/packages/75/67/62e57896ecbabe363f027d24cc769d55dd49019e576533ec10e492fcd8a2/rapidfuzz-3.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7b3eda607a019169f7187328a8d1648fb9a90265087f6903d7ee3a8eee01805", size = 1450881, upload-time = "2025-04-03T20:35:42.734Z" }, - { url = "https://files.pythonhosted.org/packages/96/5c/691c5304857f3476a7b3df99e91efc32428cbe7d25d234e967cc08346c13/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98e0bfa602e1942d542de077baf15d658bd9d5dcfe9b762aff791724c1c38b70", size = 1422990, upload-time = "2025-04-03T20:35:45.158Z" }, - { url = "https://files.pythonhosted.org/packages/46/81/7a7e78f977496ee2d613154b86b203d373376bcaae5de7bde92f3ad5a192/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bef86df6d59667d9655905b02770a0c776d2853971c0773767d5ef8077acd624", size = 5342309, upload-time = "2025-04-03T20:35:46.952Z" }, - { url = "https://files.pythonhosted.org/packages/51/44/12fdd12a76b190fe94bf38d252bb28ddf0ab7a366b943e792803502901a2/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fedd316c165beed6307bf754dee54d3faca2c47e1f3bcbd67595001dfa11e969", size = 1656881, upload-time = "2025-04-03T20:35:49.954Z" }, - { url = "https://files.pythonhosted.org/packages/27/ae/0d933e660c06fcfb087a0d2492f98322f9348a28b2cc3791a5dbadf6e6fb/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5158da7f2ec02a930be13bac53bb5903527c073c90ee37804090614cab83c29e", size = 1608494, upload-time = "2025-04-03T20:35:51.646Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2c/4b2f8aafdf9400e5599b6ed2f14bc26ca75f5a923571926ccbc998d4246a/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b6f913ee4618ddb6d6f3e387b76e8ec2fc5efee313a128809fbd44e65c2bbb2", size = 3072160, upload-time = "2025-04-03T20:35:53.472Z" }, - { url = "https://files.pythonhosted.org/packages/60/7d/030d68d9a653c301114101c3003b31ce01cf2c3224034cd26105224cd249/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d25fdbce6459ccbbbf23b4b044f56fbd1158b97ac50994eaae2a1c0baae78301", size = 2491549, upload-time = "2025-04-03T20:35:55.391Z" }, - { url = "https://files.pythonhosted.org/packages/8e/cd/7040ba538fc6a8ddc8816a05ecf46af9988b46c148ddd7f74fb0fb73d012/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25343ccc589a4579fbde832e6a1e27258bfdd7f2eb0f28cb836d6694ab8591fc", size = 7584142, upload-time = "2025-04-03T20:35:57.71Z" }, - { url = "https://files.pythonhosted.org/packages/c1/96/85f7536fbceb0aa92c04a1c37a3fc4fcd4e80649e9ed0fb585382df82edc/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a9ad1f37894e3ffb76bbab76256e8a8b789657183870be11aa64e306bb5228fd", size = 2896234, upload-time = "2025-04-03T20:35:59.969Z" }, - { url = "https://files.pythonhosted.org/packages/55/fd/460e78438e7019f2462fe9d4ecc880577ba340df7974c8a4cfe8d8d029df/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5dc71ef23845bb6b62d194c39a97bb30ff171389c9812d83030c1199f319098c", size = 3437420, upload-time = "2025-04-03T20:36:01.91Z" }, - { url = "https://files.pythonhosted.org/packages/cc/df/c3c308a106a0993befd140a414c5ea78789d201cf1dfffb8fd9749718d4f/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b7f4c65facdb94f44be759bbd9b6dda1fa54d0d6169cdf1a209a5ab97d311a75", size = 4410860, upload-time = "2025-04-03T20:36:04.352Z" }, - { url = "https://files.pythonhosted.org/packages/75/ee/9d4ece247f9b26936cdeaae600e494af587ce9bf8ddc47d88435f05cfd05/rapidfuzz-3.13.0-cp311-cp311-win32.whl", hash = "sha256:b5104b62711565e0ff6deab2a8f5dbf1fbe333c5155abe26d2cfd6f1849b6c87", size = 1843161, upload-time = "2025-04-03T20:36:06.802Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5a/d00e1f63564050a20279015acb29ecaf41646adfacc6ce2e1e450f7f2633/rapidfuzz-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:9093cdeb926deb32a4887ebe6910f57fbcdbc9fbfa52252c10b56ef2efb0289f", size = 1629962, upload-time = "2025-04-03T20:36:09.133Z" }, - { url = "https://files.pythonhosted.org/packages/3b/74/0a3de18bc2576b794f41ccd07720b623e840fda219ab57091897f2320fdd/rapidfuzz-3.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:f70f646751b6aa9d05be1fb40372f006cc89d6aad54e9d79ae97bd1f5fce5203", size = 866631, upload-time = "2025-04-03T20:36:11.022Z" }, - { url = "https://files.pythonhosted.org/packages/13/4b/a326f57a4efed8f5505b25102797a58e37ee11d94afd9d9422cb7c76117e/rapidfuzz-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a1a6a906ba62f2556372282b1ef37b26bca67e3d2ea957277cfcefc6275cca7", size = 1989501, upload-time = "2025-04-03T20:36:13.43Z" }, - { url = "https://files.pythonhosted.org/packages/b7/53/1f7eb7ee83a06c400089ec7cb841cbd581c2edd7a4b21eb2f31030b88daa/rapidfuzz-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fd0975e015b05c79a97f38883a11236f5a24cca83aa992bd2558ceaa5652b26", size = 1445379, upload-time = "2025-04-03T20:36:16.439Z" }, - { url = "https://files.pythonhosted.org/packages/07/09/de8069a4599cc8e6d194e5fa1782c561151dea7d5e2741767137e2a8c1f0/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d4e13593d298c50c4f94ce453f757b4b398af3fa0fd2fde693c3e51195b7f69", size = 1405986, upload-time = "2025-04-03T20:36:18.447Z" }, - { url = "https://files.pythonhosted.org/packages/5d/77/d9a90b39c16eca20d70fec4ca377fbe9ea4c0d358c6e4736ab0e0e78aaf6/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed6f416bda1c9133000009d84d9409823eb2358df0950231cc936e4bf784eb97", size = 5310809, upload-time = "2025-04-03T20:36:20.324Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7d/14da291b0d0f22262d19522afaf63bccf39fc027c981233fb2137a57b71f/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dc82b6ed01acb536b94a43996a94471a218f4d89f3fdd9185ab496de4b2a981", size = 1629394, upload-time = "2025-04-03T20:36:22.256Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e4/79ed7e4fa58f37c0f8b7c0a62361f7089b221fe85738ae2dbcfb815e985a/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9d824de871daa6e443b39ff495a884931970d567eb0dfa213d234337343835f", size = 1600544, upload-time = "2025-04-03T20:36:24.207Z" }, - { url = "https://files.pythonhosted.org/packages/4e/20/e62b4d13ba851b0f36370060025de50a264d625f6b4c32899085ed51f980/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d18228a2390375cf45726ce1af9d36ff3dc1f11dce9775eae1f1b13ac6ec50f", size = 3052796, upload-time = "2025-04-03T20:36:26.279Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8d/55fdf4387dec10aa177fe3df8dbb0d5022224d95f48664a21d6b62a5299d/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5fe634c9482ec5d4a6692afb8c45d370ae86755e5f57aa6c50bfe4ca2bdd87", size = 2464016, upload-time = "2025-04-03T20:36:28.525Z" }, - { url = "https://files.pythonhosted.org/packages/9b/be/0872f6a56c0f473165d3b47d4170fa75263dc5f46985755aa9bf2bbcdea1/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:694eb531889f71022b2be86f625a4209c4049e74be9ca836919b9e395d5e33b3", size = 7556725, upload-time = "2025-04-03T20:36:30.629Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f3/6c0750e484d885a14840c7a150926f425d524982aca989cdda0bb3bdfa57/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11b47b40650e06147dee5e51a9c9ad73bb7b86968b6f7d30e503b9f8dd1292db", size = 2859052, upload-time = "2025-04-03T20:36:32.836Z" }, - { url = "https://files.pythonhosted.org/packages/6f/98/5a3a14701b5eb330f444f7883c9840b43fb29c575e292e09c90a270a6e07/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:98b8107ff14f5af0243f27d236bcc6e1ef8e7e3b3c25df114e91e3a99572da73", size = 3390219, upload-time = "2025-04-03T20:36:35.062Z" }, - { url = "https://files.pythonhosted.org/packages/e9/7d/f4642eaaeb474b19974332f2a58471803448be843033e5740965775760a5/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b836f486dba0aceb2551e838ff3f514a38ee72b015364f739e526d720fdb823a", size = 4377924, upload-time = "2025-04-03T20:36:37.363Z" }, - { url = "https://files.pythonhosted.org/packages/8e/83/fa33f61796731891c3e045d0cbca4436a5c436a170e7f04d42c2423652c3/rapidfuzz-3.13.0-cp312-cp312-win32.whl", hash = "sha256:4671ee300d1818d7bdfd8fa0608580d7778ba701817216f0c17fb29e6b972514", size = 1823915, upload-time = "2025-04-03T20:36:39.451Z" }, - { url = "https://files.pythonhosted.org/packages/03/25/5ee7ab6841ca668567d0897905eebc79c76f6297b73bf05957be887e9c74/rapidfuzz-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e2065f68fb1d0bf65adc289c1bdc45ba7e464e406b319d67bb54441a1b9da9e", size = 1616985, upload-time = "2025-04-03T20:36:41.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/5e/3f0fb88db396cb692aefd631e4805854e02120a2382723b90dcae720bcc6/rapidfuzz-3.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:65cc97c2fc2c2fe23586599686f3b1ceeedeca8e598cfcc1b7e56dc8ca7e2aa7", size = 860116, upload-time = "2025-04-03T20:36:43.915Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e1/f5d85ae3c53df6f817ca70dbdd37c83f31e64caced5bb867bec6b43d1fdf/rapidfuzz-3.13.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe5790a36d33a5d0a6a1f802aa42ecae282bf29ac6f7506d8e12510847b82a45", size = 1904437, upload-time = "2025-04-03T20:38:00.255Z" }, - { url = "https://files.pythonhosted.org/packages/db/d7/ded50603dddc5eb182b7ce547a523ab67b3bf42b89736f93a230a398a445/rapidfuzz-3.13.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:cdb33ee9f8a8e4742c6b268fa6bd739024f34651a06b26913381b1413ebe7590", size = 1383126, upload-time = "2025-04-03T20:38:02.676Z" }, - { url = "https://files.pythonhosted.org/packages/c4/48/6f795e793babb0120b63a165496d64f989b9438efbeed3357d9a226ce575/rapidfuzz-3.13.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c99b76b93f7b495eee7dcb0d6a38fb3ce91e72e99d9f78faa5664a881cb2b7d", size = 1365565, upload-time = "2025-04-03T20:38:06.646Z" }, - { url = "https://files.pythonhosted.org/packages/f0/50/0062a959a2d72ed17815824e40e2eefdb26f6c51d627389514510a7875f3/rapidfuzz-3.13.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6af42f2ede8b596a6aaf6d49fdee3066ca578f4856b85ab5c1e2145de367a12d", size = 5251719, upload-time = "2025-04-03T20:38:09.191Z" }, - { url = "https://files.pythonhosted.org/packages/e7/02/bd8b70cd98b7a88e1621264778ac830c9daa7745cd63e838bd773b1aeebd/rapidfuzz-3.13.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c0efa73afbc5b265aca0d8a467ae2a3f40d6854cbe1481cb442a62b7bf23c99", size = 2991095, upload-time = "2025-04-03T20:38:12.554Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8d/632d895cdae8356826184864d74a5f487d40cb79f50a9137510524a1ba86/rapidfuzz-3.13.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7ac21489de962a4e2fc1e8f0b0da4aa1adc6ab9512fd845563fecb4b4c52093a", size = 1553888, upload-time = "2025-04-03T20:38:15.357Z" }, - { url = "https://files.pythonhosted.org/packages/88/df/6060c5a9c879b302bd47a73fc012d0db37abf6544c57591bcbc3459673bd/rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1ba007f4d35a45ee68656b2eb83b8715e11d0f90e5b9f02d615a8a321ff00c27", size = 1905935, upload-time = "2025-04-03T20:38:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/a2/6c/a0b819b829e20525ef1bd58fc776fb8d07a0c38d819e63ba2b7c311a2ed4/rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d7a217310429b43be95b3b8ad7f8fc41aba341109dc91e978cd7c703f928c58f", size = 1383714, upload-time = "2025-04-03T20:38:20.628Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c1/3da3466cc8a9bfb9cd345ad221fac311143b6a9664b5af4adb95b5e6ce01/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:558bf526bcd777de32b7885790a95a9548ffdcce68f704a81207be4a286c1095", size = 1367329, upload-time = "2025-04-03T20:38:23.01Z" }, - { url = "https://files.pythonhosted.org/packages/da/f0/9f2a9043bfc4e66da256b15d728c5fc2d865edf0028824337f5edac36783/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:202a87760f5145140d56153b193a797ae9338f7939eb16652dd7ff96f8faf64c", size = 5251057, upload-time = "2025-04-03T20:38:25.52Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ff/af2cb1d8acf9777d52487af5c6b34ce9d13381a753f991d95ecaca813407/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcccc08f671646ccb1e413c773bb92e7bba789e3a1796fd49d23c12539fe2e4", size = 2992401, upload-time = "2025-04-03T20:38:28.196Z" }, - { url = "https://files.pythonhosted.org/packages/c1/c5/c243b05a15a27b946180db0d1e4c999bef3f4221505dff9748f1f6c917be/rapidfuzz-3.13.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f219f1e3c3194d7a7de222f54450ce12bc907862ff9a8962d83061c1f923c86", size = 1553782, upload-time = "2025-04-03T20:38:30.778Z" }, +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/28/9d808fe62375b9aab5ba92fa9b29371297b067c2790b2d7cda648b1e2f8d/rapidfuzz-3.14.3.tar.gz", hash = "sha256:2491937177868bc4b1e469087601d53f925e8d270ccc21e07404b4b5814b7b5f", size = 57863900, upload-time = "2025-11-01T11:54:52.321Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/d1/0efa42a602ed466d3ca1c462eed5d62015c3fd2a402199e2c4b87aa5aa25/rapidfuzz-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9fcd4d751a4fffa17aed1dde41647923c72c74af02459ad1222e3b0022da3a1", size = 1952376, upload-time = "2025-11-01T11:52:29.175Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/37a169bb28b23850a164e6624b1eb299e1ad73c9e7c218ee15744e68d628/rapidfuzz-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ad73afb688b36864a8d9b7344a9cf6da186c471e5790cbf541a635ee0f457f2", size = 1390903, upload-time = "2025-11-01T11:52:31.239Z" }, + { url = "https://files.pythonhosted.org/packages/3c/91/b37207cbbdb6eaafac3da3f55ea85287b27745cb416e75e15769b7d8abe8/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5fb2d978a601820d2cfd111e2c221a9a7bfdf84b41a3ccbb96ceef29f2f1ac7", size = 1385655, upload-time = "2025-11-01T11:52:32.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bb/ca53e518acf43430be61f23b9c5987bd1e01e74fcb7a9ee63e00f597aefb/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d83b8b712fa37e06d59f29a4b49e2e9e8635e908fbc21552fe4d1163db9d2a1", size = 3164708, upload-time = "2025-11-01T11:52:34.618Z" }, + { url = "https://files.pythonhosted.org/packages/df/e1/7667bf2db3e52adb13cb933dd4a6a2efc66045d26fa150fc0feb64c26d61/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:dc8c07801df5206b81ed6bd6c35cb520cf9b6c64b9b0d19d699f8633dc942897", size = 1221106, upload-time = "2025-11-01T11:52:36.069Z" }, + { url = "https://files.pythonhosted.org/packages/05/8a/84d9f2d46a2c8eb2ccae81747c4901fa10fe4010aade2d57ce7b4b8e02ec/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c71ce6d4231e5ef2e33caa952bfe671cb9fd42e2afb11952df9fad41d5c821f9", size = 2406048, upload-time = "2025-11-01T11:52:37.936Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a9/a0b7b7a1b81a020c034eb67c8e23b7e49f920004e295378de3046b0d99e1/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0e38828d1381a0cceb8a4831212b2f673d46f5129a1897b0451c883eaf4a1747", size = 2527020, upload-time = "2025-11-01T11:52:39.657Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/416df7d108b99b4942ba04dd4cf73c45c3aadb3ef003d95cad78b1d12eb9/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da2a007434323904719158e50f3076a4dadb176ce43df28ed14610c773cc9825", size = 4273958, upload-time = "2025-11-01T11:52:41.017Z" }, + { url = "https://files.pythonhosted.org/packages/81/d0/b81e041c17cd475002114e0ab8800e4305e60837882cb376a621e520d70f/rapidfuzz-3.14.3-cp310-cp310-win32.whl", hash = "sha256:fce3152f94afcfd12f3dd8cf51e48fa606e3cb56719bccebe3b401f43d0714f9", size = 1725043, upload-time = "2025-11-01T11:52:42.465Z" }, + { url = "https://files.pythonhosted.org/packages/09/6b/64ad573337d81d64bc78a6a1df53a72a71d54d43d276ce0662c2e95a1f35/rapidfuzz-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:37d3c653af15cd88592633e942f5407cb4c64184efab163c40fcebad05f25141", size = 1542273, upload-time = "2025-11-01T11:52:44.005Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5e/faf76e259bc15808bc0b86028f510215c3d755b6c3a3911113079485e561/rapidfuzz-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:cc594bbcd3c62f647dfac66800f307beaee56b22aaba1c005e9c4c40ed733923", size = 814875, upload-time = "2025-11-01T11:52:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/5b0a33ad3332ee1213068c66f7c14e9e221be90bab434f0cb4defa9d6660/rapidfuzz-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea2d113e260a5da0c4003e0a5e9fdf24a9dc2bb9eaa43abd030a1e46ce7837d", size = 1953885, upload-time = "2025-11-01T11:52:47.75Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ab/f1181f500c32c8fcf7c966f5920c7e56b9b1d03193386d19c956505c312d/rapidfuzz-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e6c31a4aa68cfa75d7eede8b0ed24b9e458447db604c2db53f358be9843d81d3", size = 1390200, upload-time = "2025-11-01T11:52:49.491Z" }, + { url = "https://files.pythonhosted.org/packages/14/2a/0f2de974ececad873865c6bb3ea3ad07c976ac293d5025b2d73325aac1d4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02821366d928e68ddcb567fed8723dad7ea3a979fada6283e6914d5858674850", size = 1389319, upload-time = "2025-11-01T11:52:51.224Z" }, + { url = "https://files.pythonhosted.org/packages/ed/69/309d8f3a0bb3031fd9b667174cc4af56000645298af7c2931be5c3d14bb4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe8df315ab4e6db4e1be72c5170f8e66021acde22cd2f9d04d2058a9fd8162e", size = 3178495, upload-time = "2025-11-01T11:52:53.005Z" }, + { url = "https://files.pythonhosted.org/packages/10/b7/f9c44a99269ea5bf6fd6a40b84e858414b6e241288b9f2b74af470d222b1/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:769f31c60cd79420188fcdb3c823227fc4a6deb35cafec9d14045c7f6743acae", size = 1228443, upload-time = "2025-11-01T11:52:54.991Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0a/3b3137abac7f19c9220e14cd7ce993e35071a7655e7ef697785a3edfea1a/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54fa03062124e73086dae66a3451c553c1e20a39c077fd704dc7154092c34c63", size = 2411998, upload-time = "2025-11-01T11:52:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b6/983805a844d44670eaae63831024cdc97ada4e9c62abc6b20703e81e7f9b/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:834d1e818005ed0d4ae38f6b87b86fad9b0a74085467ece0727d20e15077c094", size = 2530120, upload-time = "2025-11-01T11:52:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/b4/cc/2c97beb2b1be2d7595d805682472f1b1b844111027d5ad89b65e16bdbaaa/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:948b00e8476a91f510dd1ec07272efc7d78c275d83b630455559671d4e33b678", size = 4283129, upload-time = "2025-11-01T11:53:00.188Z" }, + { url = "https://files.pythonhosted.org/packages/4d/03/2f0e5e94941045aefe7eafab72320e61285c07b752df9884ce88d6b8b835/rapidfuzz-3.14.3-cp311-cp311-win32.whl", hash = "sha256:43d0305c36f504232f18ea04e55f2059bb89f169d3119c4ea96a0e15b59e2a91", size = 1724224, upload-time = "2025-11-01T11:53:02.149Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/5fa23e204435803875daefda73fd61baeabc3c36b8fc0e34c1705aab8c7b/rapidfuzz-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:ef6bf930b947bd0735c550683939a032090f1d688dfd8861d6b45307b96fd5c5", size = 1544259, upload-time = "2025-11-01T11:53:03.66Z" }, + { url = "https://files.pythonhosted.org/packages/48/35/d657b85fcc615a42661b98ac90ce8e95bd32af474603a105643963749886/rapidfuzz-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:f3eb0ff3b75d6fdccd40b55e7414bb859a1cda77c52762c9c82b85569f5088e7", size = 814734, upload-time = "2025-11-01T11:53:05.008Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8e/3c215e860b458cfbedb3ed73bc72e98eb7e0ed72f6b48099604a7a3260c2/rapidfuzz-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:685c93ea961d135893b5984a5a9851637d23767feabe414ec974f43babbd8226", size = 1945306, upload-time = "2025-11-01T11:53:06.452Z" }, + { url = "https://files.pythonhosted.org/packages/36/d9/31b33512015c899f4a6e6af64df8dfe8acddf4c8b40a4b3e0e6e1bcd00e5/rapidfuzz-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa7c8f26f009f8c673fbfb443792f0cf8cf50c4e18121ff1e285b5e08a94fbdb", size = 1390788, upload-time = "2025-11-01T11:53:08.721Z" }, + { url = "https://files.pythonhosted.org/packages/a9/67/2ee6f8de6e2081ccd560a571d9c9063184fe467f484a17fa90311a7f4a2e/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57f878330c8d361b2ce76cebb8e3e1dc827293b6abf404e67d53260d27b5d941", size = 1374580, upload-time = "2025-11-01T11:53:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/83/80d22997acd928eda7deadc19ccd15883904622396d6571e935993e0453a/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c5f545f454871e6af05753a0172849c82feaf0f521c5ca62ba09e1b382d6382", size = 3154947, upload-time = "2025-11-01T11:53:12.093Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cf/9f49831085a16384695f9fb096b99662f589e30b89b4a589a1ebc1a19d34/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:07aa0b5d8863e3151e05026a28e0d924accf0a7a3b605da978f0359bb804df43", size = 1223872, upload-time = "2025-11-01T11:53:13.664Z" }, + { url = "https://files.pythonhosted.org/packages/c8/0f/41ee8034e744b871c2e071ef0d360686f5ccfe5659f4fd96c3ec406b3c8b/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73b07566bc7e010e7b5bd490fb04bb312e820970180df6b5655e9e6224c137db", size = 2392512, upload-time = "2025-11-01T11:53:15.109Z" }, + { url = "https://files.pythonhosted.org/packages/da/86/280038b6b0c2ccec54fb957c732ad6b41cc1fd03b288d76545b9cf98343f/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6de00eb84c71476af7d3110cf25d8fe7c792d7f5fa86764ef0b4ca97e78ca3ed", size = 2521398, upload-time = "2025-11-01T11:53:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7b/05c26f939607dca0006505e3216248ae2de631e39ef94dd63dbbf0860021/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d7843a1abf0091773a530636fdd2a49a41bcae22f9910b86b4f903e76ddc82dc", size = 4259416, upload-time = "2025-11-01T11:53:19.34Z" }, + { url = "https://files.pythonhosted.org/packages/40/eb/9e3af4103d91788f81111af1b54a28de347cdbed8eaa6c91d5e98a889aab/rapidfuzz-3.14.3-cp312-cp312-win32.whl", hash = "sha256:dea97ac3ca18cd3ba8f3d04b5c1fe4aa60e58e8d9b7793d3bd595fdb04128d7a", size = 1709527, upload-time = "2025-11-01T11:53:20.949Z" }, + { url = "https://files.pythonhosted.org/packages/b8/63/d06ecce90e2cf1747e29aeab9f823d21e5877a4c51b79720b2d3be7848f8/rapidfuzz-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:b5100fd6bcee4d27f28f4e0a1c6b5127bc8ba7c2a9959cad9eab0bf4a7ab3329", size = 1538989, upload-time = "2025-11-01T11:53:22.428Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6d/beee32dcda64af8128aab3ace2ccb33d797ed58c434c6419eea015fec779/rapidfuzz-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:4e49c9e992bc5fc873bd0fff7ef16a4405130ec42f2ce3d2b735ba5d3d4eb70f", size = 811161, upload-time = "2025-11-01T11:53:23.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/33/b5bd6475c7c27164b5becc9b0e3eb978f1e3640fea590dd3dced6006ee83/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7cf174b52cb3ef5d49e45d0a1133b7e7d0ecf770ed01f97ae9962c5c91d97d23", size = 1888499, upload-time = "2025-11-01T11:54:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/30/d2/89d65d4db4bb931beade9121bc71ad916b5fa9396e807d11b33731494e8e/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:442cba39957a008dfc5bdef21a9c3f4379e30ffb4e41b8555dbaf4887eca9300", size = 1336747, upload-time = "2025-11-01T11:54:43.957Z" }, + { url = "https://files.pythonhosted.org/packages/85/33/cd87d92b23f0b06e8914a61cea6850c6d495ca027f669fab7a379041827a/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1faa0f8f76ba75fd7b142c984947c280ef6558b5067af2ae9b8729b0a0f99ede", size = 1352187, upload-time = "2025-11-01T11:54:45.518Z" }, + { url = "https://files.pythonhosted.org/packages/22/20/9d30b4a1ab26aac22fff17d21dec7e9089ccddfe25151d0a8bb57001dc3d/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e6eefec45625c634926a9fd46c9e4f31118ac8f3156fff9494422cee45207e6", size = 3101472, upload-time = "2025-11-01T11:54:47.255Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ad/fa2d3e5c29a04ead7eaa731c7cd1f30f9ec3c77b3a578fdf90280797cbcb/rapidfuzz-3.14.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56fefb4382bb12250f164250240b9dd7772e41c5c8ae976fd598a32292449cc5", size = 1511361, upload-time = "2025-11-01T11:54:49.057Z" }, ] [[package]] name = "referencing" -version = "0.36.2" +version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] [[package]] name = "requests" -version = "2.32.4" +version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -3224,9 +3606,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[package]] @@ -3264,176 +3646,163 @@ wheels = [ [[package]] name = "rich" -version = "14.1.0" +version = "14.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, ] [[package]] name = "rich-toolkit" -version = "0.15.0" +version = "0.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/65/36/cdb3d51371ad0cccbf1541506304783bd72d55790709b8eb68c0d401a13a/rich_toolkit-0.15.0.tar.gz", hash = "sha256:3f5730e9f2d36d0bfe01cf723948b7ecf4cc355d2b71e2c00e094f7963128c09", size = 115118, upload-time = "2025-08-11T10:55:37.909Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/33/1a18839aaa8feef7983590c05c22c9c09d245ada6017d118325bbfcc7651/rich_toolkit-0.15.1.tar.gz", hash = "sha256:6f9630eb29f3843d19d48c3bd5706a086d36d62016687f9d0efa027ddc2dd08a", size = 115322, upload-time = "2025-09-04T09:28:11.789Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/e4/b0794eefb3cf78566b15e5bf576492c1d4a92ce5f6da55675bc11e9ef5d8/rich_toolkit-0.15.0-py3-none-any.whl", hash = "sha256:ddb91008283d4a7989fd8ff0324a48773a7a2276229c6a3070755645538ef1bb", size = 29062, upload-time = "2025-08-11T10:55:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/c8/49/42821d55ead7b5a87c8d121edf323cb393d8579f63e933002ade900b784f/rich_toolkit-0.15.1-py3-none-any.whl", hash = "sha256:36a0b1d9a135d26776e4b78f1d5c2655da6e0ef432380b5c6b523c8d8ab97478", size = 29412, upload-time = "2025-09-04T09:28:10.587Z" }, ] [[package]] name = "rignore" -version = "0.6.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/46/05a94dc55ac03cf931d18e43b86ecee5ee054cb88b7853fffd741e35009c/rignore-0.6.4.tar.gz", hash = "sha256:e893fdd2d7fdcfa9407d0b7600ef2c2e2df97f55e1c45d4a8f54364829ddb0ab", size = 11633, upload-time = "2025-07-19T19:24:46.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/27/55ec2871e42c0a01669f7741598a5948f04bd32f3975478a0bead9e7e251/rignore-0.6.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c201375cfe76e56e61fcdfe50d0882aafb49544b424bfc828e0508dc9fbc431b", size = 888088, upload-time = "2025-07-19T19:23:50.776Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/6be3d7adf91f7d67f08833a29dea4f7c345554b385f9a797c397f6685f29/rignore-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4962d537e377394292c4828e1e9c620618dd8daa49ba746abe533733a89f8644", size = 824159, upload-time = "2025-07-19T19:23:44.395Z" }, - { url = "https://files.pythonhosted.org/packages/99/b7/fbb56b8cfa27971f9a19e87769dae0cb648343226eddda94ded32be2afc3/rignore-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a6dd2f213cff6ca3c4d257fa3f5b0c7d4f6c23fe83bf292425fbe8d0c9c908a", size = 892493, upload-time = "2025-07-19T19:22:32.061Z" }, - { url = "https://files.pythonhosted.org/packages/d5/cf/21f130801c29c1fcf22f00a41d7530cef576819ee1a26c86bdb7bb06a0f2/rignore-0.6.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:64d379193f86a21fc93762783f36651927f54d5eea54c4922fdccb5e37076ed2", size = 872810, upload-time = "2025-07-19T19:22:45.554Z" }, - { url = "https://files.pythonhosted.org/packages/e4/4a/474a627263ef13a0ac28a0ce3a20932fbe41f6043f7280da47c7aca1f586/rignore-0.6.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53c4f8682cf645b7a9160e0f1786af3201ed54a020bb4abd515c970043387127", size = 1160488, upload-time = "2025-07-19T19:22:58.359Z" }, - { url = "https://files.pythonhosted.org/packages/0b/c7/a10c180f77cbb456ab483c28e52efd6166cee787f11d21cb1d369b89e961/rignore-0.6.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af1246e672bd835a17d3ae91579b3c235ec55b10924ef22608d3e9ec90fa2699", size = 938780, upload-time = "2025-07-19T19:23:10.604Z" }, - { url = "https://files.pythonhosted.org/packages/32/68/8e67701e8cc9f157f12b3742e14f14e395c7f3a497720c7f6aab7e5cdec4/rignore-0.6.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82eed48fbc3097af418862e3c5c26fa81aa993e0d8b5f3a0a9a29cc6975eedff", size = 950347, upload-time = "2025-07-19T19:23:33.759Z" }, - { url = "https://files.pythonhosted.org/packages/1e/11/8eef123a2d029ed697b119806a0ca8a99d9457500c40b4d26cd21860eb89/rignore-0.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df1215a071d42fd857fb6363c13803fbd915d48eaeaa9b103fb2266ba89c8995", size = 976679, upload-time = "2025-07-19T19:23:23.813Z" }, - { url = "https://files.pythonhosted.org/packages/09/7e/9584f4e4b3c1587ae09f286a14dab2376895d782be632289d151cb952432/rignore-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:82f2d318e66756066ed664015d8ca720078ab1d319377f1f61e3f4d01325faea", size = 1067469, upload-time = "2025-07-19T19:23:57.616Z" }, - { url = "https://files.pythonhosted.org/packages/c3/2c/d3515693b89c47761822219bb519cefd0cd45a38ff82c35a4ccdd8e95deb/rignore-0.6.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e7d4258fc81051097c4d4c6ad17f0100c40088dbd2c6c31fc3c888a1d5a16190", size = 1136199, upload-time = "2025-07-19T19:24:09.922Z" }, - { url = "https://files.pythonhosted.org/packages/e7/39/94ea41846547ebb87d16527a3e978c8918632a060f77669a492f8a90b8b9/rignore-0.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a0d0b9ec7929df8fd35ae89cb56619850dc140869139d61a2f4fa2941d2d1878", size = 1111179, upload-time = "2025-07-19T19:24:21.908Z" }, - { url = "https://files.pythonhosted.org/packages/ce/77/9acda68c7cea4d5dd027ef63163e0be30008f635acd75ea801e4c443fcdd/rignore-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8883d079b948ffcd56b67572831c9b8949eca7fe2e8f7bdbf7691c7a9388f054", size = 1121143, upload-time = "2025-07-19T19:24:33.958Z" }, - { url = "https://files.pythonhosted.org/packages/05/67/d1489e9224f33b9a87b7f870650bcab582ee3452df286bcb2fbb6a7ba257/rignore-0.6.4-cp310-cp310-win32.whl", hash = "sha256:5aeac5b354e15eb9f7857b02ad2af12ae2c2ed25a61921b0bd7e272774530f77", size = 643131, upload-time = "2025-07-19T19:24:54.437Z" }, - { url = "https://files.pythonhosted.org/packages/5d/d1/7d668bed51d3f0895e875e57c8e42f421635cdbcb96652ab24f297c9c5cf/rignore-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:90419f881d05a1febb0578a175aa3e51d149ded1875421ed75a8af4392b7fe56", size = 721109, upload-time = "2025-07-19T19:24:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/be/11/66992d271dbc44eac33f3b6b871855bc17e511b9279a2a0982b44c2b0c01/rignore-0.6.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:85f684dfc2c497e35ad34ffd6744a3bcdcac273ec1dbe7d0464bfa20f3331434", size = 888239, upload-time = "2025-07-19T19:23:51.835Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1b/a9bde714e474043f97a06097925cf11e4597f9453adc267427d05ff9f38e/rignore-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23954acc6debc852dbccbffbb70f0e26b12d230239e1ad0638eb5540694d0308", size = 824348, upload-time = "2025-07-19T19:23:45.54Z" }, - { url = "https://files.pythonhosted.org/packages/db/58/dabba227fee6553f9be069f58128419b6d4954c784c4cd566cfe59955c1f/rignore-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2bf793bd58dbf3dee063a758b23ea446b5f037370405ecefc78e1e8923fc658", size = 892419, upload-time = "2025-07-19T19:22:33.763Z" }, - { url = "https://files.pythonhosted.org/packages/2c/fa/e3c16368ee32d6d1146cf219b127fd5c7e6baf22cad7a7a5967782ff3b20/rignore-0.6.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1eaeaa5a904e098604ea2012383a721de06211c8b4013abf0d41c3cfeb982f4f", size = 873285, upload-time = "2025-07-19T19:22:46.67Z" }, - { url = "https://files.pythonhosted.org/packages/78/9d/ef43d760dc3d18011d8482692b478785a846bba64157844b3068e428739c/rignore-0.6.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a48bdbeb03093e3fac2b40d62a718c59b5bb4f29cfdc8e7cbb360e1ea7bf0056", size = 1160457, upload-time = "2025-07-19T19:22:59.457Z" }, - { url = "https://files.pythonhosted.org/packages/95/de/eca1b035705e0b4e6c630fd1fcec45d14cf354a4acea88cf29ea0a322fea/rignore-0.6.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c5f9452d116be405f0967160b449c46ac929b50eaf527f33ee4680e3716e39", size = 938833, upload-time = "2025-07-19T19:23:11.657Z" }, - { url = "https://files.pythonhosted.org/packages/d4/2d/58912efa4137e989616d679a5390b53e93d5150be47217dd686ff60cd4cd/rignore-0.6.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cf1039bfbdaa0f9710a6fb75436c25ca26d364881ec4d1e66d466bb36a7fb98", size = 950603, upload-time = "2025-07-19T19:23:35.245Z" }, - { url = "https://files.pythonhosted.org/packages/6f/3d/9827cc1c7674d8d884d3d231a224a2db8ea8eae075a1611dfdcd0c301e20/rignore-0.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:136629eb0ec2b6ac6ab34e71ce8065a07106fe615a53eceefc30200d528a4612", size = 976867, upload-time = "2025-07-19T19:23:24.919Z" }, - { url = "https://files.pythonhosted.org/packages/75/47/9dcee35e24897b62d66f7578f127bc91465c942a9d702d516d3fe7dcaa00/rignore-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:35e3d0ebaf01086e6454c3fecae141e2db74a5ddf4a97c72c69428baeff0b7d4", size = 1067603, upload-time = "2025-07-19T19:23:58.765Z" }, - { url = "https://files.pythonhosted.org/packages/4b/68/f66e7c0b0fc009f3e19ba8e6c3078a227285e3aecd9f6498d39df808cdfd/rignore-0.6.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ed1f9010fa1ef5ea0b69803d1dfb4b7355921779e03a30396034c52691658bc", size = 1136289, upload-time = "2025-07-19T19:24:11.136Z" }, - { url = "https://files.pythonhosted.org/packages/a6/b7/6fff161fe3ae5c0e0a0dded9a428e41d31c7fefc4e57c7553b9ffb064139/rignore-0.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c16e9e898ed0afe2e20fa8d6412e02bd13f039f7e0d964a289368efd4d9ad320", size = 1111566, upload-time = "2025-07-19T19:24:23.065Z" }, - { url = "https://files.pythonhosted.org/packages/1f/c5/a5978ad65074a08dad46233a3333d154ae9cb9339325f3c181002a174746/rignore-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7e6bc0bdcd404a7a8268629e8e99967127bb41e02d9eb09a471364c4bc25e215", size = 1121142, upload-time = "2025-07-19T19:24:35.151Z" }, - { url = "https://files.pythonhosted.org/packages/e8/af/91f084374b95dc2477a4bd066957beb3b61b551f2364b4f7f5bc52c9e4c7/rignore-0.6.4-cp311-cp311-win32.whl", hash = "sha256:fdd59bd63d2a49cc6d4f3598f285552ccb1a41e001df1012e0e0345cf2cabf79", size = 643031, upload-time = "2025-07-19T19:24:55.541Z" }, - { url = "https://files.pythonhosted.org/packages/07/3a/31672aa957aebba8903005313697127bbbad9db3afcfc9857150301fab1d/rignore-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:7bf5be0e8a01845e57b5faa47ef9c623bb2070aa2f743c2fc73321ffaae45701", size = 721003, upload-time = "2025-07-19T19:24:48.867Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6c/e5af4383cdd7829ef9aa63ac82a6507983e02dbc7c2e7b9aa64b7b8e2c7a/rignore-0.6.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:74720d074b79f32449d5d212ce732e0144a294a184246d1f1e7bcc1fc5c83b69", size = 885885, upload-time = "2025-07-19T19:23:53.236Z" }, - { url = "https://files.pythonhosted.org/packages/89/3e/1b02a868830e464769aa417ee195ac352fe71ff818df8ce50c4b998edb9c/rignore-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a8184fcf567bd6b6d7b85a0c138d98dd40f63054141c96b175844414c5530d7", size = 819736, upload-time = "2025-07-19T19:23:46.565Z" }, - { url = "https://files.pythonhosted.org/packages/e0/75/b9be0c523d97c09f3c6508a67ce376aba4efe41c333c58903a0d7366439a/rignore-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcb0d7d7ecc3fbccf6477bb187c04a091579ea139f15f139abe0b3b48bdfef69", size = 892779, upload-time = "2025-07-19T19:22:35.167Z" }, - { url = "https://files.pythonhosted.org/packages/91/f4/3064b06233697f2993485d132f06fe95061fef71631485da75aed246c4fd/rignore-0.6.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feac73377a156fb77b3df626c76f7e5893d9b4e9e886ac8c0f9d44f1206a2a91", size = 872116, upload-time = "2025-07-19T19:22:47.828Z" }, - { url = "https://files.pythonhosted.org/packages/99/94/cb8e7af9a3c0a665f10e2366144e0ebc66167cf846aca5f1ac31b3661598/rignore-0.6.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:465179bc30beb1f7a3439e428739a2b5777ed26660712b8c4e351b15a7c04483", size = 1163345, upload-time = "2025-07-19T19:23:00.557Z" }, - { url = "https://files.pythonhosted.org/packages/86/6b/49faa7ad85ceb6ccef265df40091d9992232d7f6055fa664fe0a8b13781c/rignore-0.6.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a4877b4dca9cf31a4d09845b300c677c86267657540d0b4d3e6d0ce3110e6e9", size = 939967, upload-time = "2025-07-19T19:23:13.494Z" }, - { url = "https://files.pythonhosted.org/packages/80/c8/b91afda10bd5ca1e3a80463340b899c0dc26a7750a9f3c94f668585c7f40/rignore-0.6.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:456456802b1e77d1e2d149320ee32505b8183e309e228129950b807d204ddd17", size = 949717, upload-time = "2025-07-19T19:23:36.404Z" }, - { url = "https://files.pythonhosted.org/packages/3f/f1/88bfdde58ae3fb1c1a92bb801f492eea8eafcdaf05ab9b75130023a4670b/rignore-0.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c1ff2fc223f1d9473d36923160af37bf765548578eb9d47a2f52e90da8ae408", size = 975534, upload-time = "2025-07-19T19:23:25.988Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8f/a80b4a2e48ceba56ba19e096d41263d844757e10aa36ede212571b5d8117/rignore-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e445fbc214ae18e0e644a78086ea5d0f579e210229a4fbe86367d11a4cd03c11", size = 1067837, upload-time = "2025-07-19T19:23:59.888Z" }, - { url = "https://files.pythonhosted.org/packages/7d/90/0905597af0e78748909ef58418442a480ddd93e9fc89b0ca9ab170c357c0/rignore-0.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e07d9c5270fc869bc431aadcfb6ed0447f89b8aafaa666914c077435dc76a123", size = 1134959, upload-time = "2025-07-19T19:24:12.396Z" }, - { url = "https://files.pythonhosted.org/packages/cc/7d/0fa29adf9183b61947ce6dc8a1a9779a8ea16573f557be28ec893f6ddbaa/rignore-0.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7a6ccc0ea83d2c0c6df6b166f2acacedcc220a516436490f41e99a5ae73b6019", size = 1109708, upload-time = "2025-07-19T19:24:24.176Z" }, - { url = "https://files.pythonhosted.org/packages/4e/a7/92892ed86b2e36da403dd3a0187829f2d880414cef75bd612bfdf4dedebc/rignore-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:536392c5ec91755db48389546c833c4ab1426fe03e5a8522992b54ef8a244e7e", size = 1120546, upload-time = "2025-07-19T19:24:36.377Z" }, - { url = "https://files.pythonhosted.org/packages/31/1b/d29ae1fe901d523741d6d1d3ffe0d630734dd0ed6b047628a69c1e15ea44/rignore-0.6.4-cp312-cp312-win32.whl", hash = "sha256:f5f9dca46fc41c0a1e236767f68be9d63bdd2726db13a0ae3a30f68414472969", size = 642005, upload-time = "2025-07-19T19:24:56.671Z" }, - { url = "https://files.pythonhosted.org/packages/1a/41/a224944824688995374e4525115ce85fecd82442fc85edd5bcd81f4f256d/rignore-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:e02eecb9e1b9f9bf7c9030ae73308a777bed3b2486204cc74dfcfbe699ab1497", size = 720358, upload-time = "2025-07-19T19:24:49.959Z" }, - { url = "https://files.pythonhosted.org/packages/85/4d/5a69ea5ae7de78eddf0a0699b6dbd855f87c1436673425461188ea39662f/rignore-0.6.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40f493eef4b191777ba6d16879e3f73836142e04480d2e2f483675d652e6b559", size = 895408, upload-time = "2025-07-19T19:22:42.16Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c3/b6cdf9b676d6774c5de3ca04a5f4dbaffae3bb06bdee395e095be24f098e/rignore-0.6.4-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6790635e4df35333e27cd9e8b31d1d559826cf8b52f2c374b81ab698ac0140cf", size = 873042, upload-time = "2025-07-19T19:22:54.663Z" }, - { url = "https://files.pythonhosted.org/packages/80/25/61182149b2f2ca86c22c6253b361ec0e983e60e913ca75588a7d559b41eb/rignore-0.6.4-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e326dab28787f07c6987c04686d4ad9d4b1e1caca1a15b85d443f91af2e133d2", size = 1162036, upload-time = "2025-07-19T19:23:06.916Z" }, - { url = "https://files.pythonhosted.org/packages/db/44/7fe55c2b7adc8c90dc8709ef2fac25fa526b0c8bfd1090af4e6b33c2e42f/rignore-0.6.4-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd24cb0f58c6036b0f64ac6fc3f759b7f0de5506fa9f5a65e9d57f8cf44a026d", size = 940381, upload-time = "2025-07-19T19:23:19.364Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a3/8cc0c9a9db980a1589007d0fedcaf41475820e0cd4950a5f6eeb8ebc0ee0/rignore-0.6.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36cb95b0acae3c88b99a39f4246b395fd983848f3ec85ff26531d638b6584a45", size = 951924, upload-time = "2025-07-19T19:23:42.209Z" }, - { url = "https://files.pythonhosted.org/packages/07/f2/4f2c88307c84801d6c772c01e8d856deaa8e85117180b88aaa0f41d4f86f/rignore-0.6.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dfc954973429ce545d06163d87a6bae0ccea5703adbc957ee3d332c9592a58eb", size = 976515, upload-time = "2025-07-19T19:23:31.524Z" }, - { url = "https://files.pythonhosted.org/packages/a4/bd/f701ddf897cf5e3f394107e6dad147216b3a0d84e9d53d7a5fed7cc97d26/rignore-0.6.4-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:cbed37d7c128b58ab9ade80e131efc4a48b6d045cd0bd1d3254cbb6b4a0ad67e", size = 1069896, upload-time = "2025-07-19T19:24:06.24Z" }, - { url = "https://files.pythonhosted.org/packages/00/52/1ae54afad26aafcfee1b44a36b27bb0dd63f1c23081e1599dbf681368925/rignore-0.6.4-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:a0db910ef867d6ca2d52fefd22d8b6b63b20ec61661e2ad57e5c425a4e39431a", size = 1136337, upload-time = "2025-07-19T19:24:18.529Z" }, - { url = "https://files.pythonhosted.org/packages/85/9a/3b74aabb69ed118d0b493afa62d1aacc3bf12b8f11bf682a3c02174c3068/rignore-0.6.4-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d664443a0a71d0a7d669adf32be59c4249bbff8b2810960f1b91d413ee4cf6b8", size = 1111677, upload-time = "2025-07-19T19:24:30.21Z" }, - { url = "https://files.pythonhosted.org/packages/70/7d/bd0f6c1bc89c80b116b526b77cdd5263c0ad218d5416aebf4ca9cce9ca73/rignore-0.6.4-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:b9f6f1d91429b4a6772152848815cf1459663796b7b899a0e15d9198e32c9371", size = 1122823, upload-time = "2025-07-19T19:24:42.476Z" }, - { url = "https://files.pythonhosted.org/packages/33/a1/daaa2df10dfa6d87c896a5783c8407c284530d5a056307d1f55a8ef0c533/rignore-0.6.4-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b3da26d5a35ab15525b68d30b7352ad2247321f5201fc7e50ba6d547f78d5ea", size = 895772, upload-time = "2025-07-19T19:22:43.423Z" }, - { url = "https://files.pythonhosted.org/packages/35/e6/65130a50cd3ed11c967034dfd653e160abb7879fb4ee338a1cccaeda7acd/rignore-0.6.4-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43028f3587558231d9fa68accff58c901dc50fd7bbc5764d3ee3df95290f6ebf", size = 873093, upload-time = "2025-07-19T19:22:55.745Z" }, - { url = "https://files.pythonhosted.org/packages/32/c4/02ead1274ce935c59f2bb3deaaaa339df9194bc40e3c2d8d623e31e47ec4/rignore-0.6.4-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc56f1fcab7740751b98fead67b98ba64896424d8c834ea22089568db4e36dfa", size = 1162199, upload-time = "2025-07-19T19:23:08.376Z" }, - { url = "https://files.pythonhosted.org/packages/78/0c/94a4edce0e80af69f200cc35d8da4c727c52d28f0c9d819b388849ae8ef6/rignore-0.6.4-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6033f2280898535a5f69935e08830a4e49ff1e29ef2c3f9a2b9ced59de06fdbf", size = 940176, upload-time = "2025-07-19T19:23:20.862Z" }, - { url = "https://files.pythonhosted.org/packages/43/92/21ec579c999a3ed4d1b2a5926a9d0edced7c65d8ac353bc9120d49b05a64/rignore-0.6.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f5ac0c4e6a24be88f3821e101ef4665e9e1dc015f9e45109f32fed71dbcdafa", size = 951632, upload-time = "2025-07-19T19:23:43.32Z" }, - { url = "https://files.pythonhosted.org/packages/67/c4/72e7ba244222b9efdeb18f9974d6f1e30cf5a2289e1b482a1e8b3ebee90f/rignore-0.6.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8906ac8dd585ece83b1346e0470260a1951058cc0ef5a17542069bde4aa3f42f", size = 976923, upload-time = "2025-07-19T19:23:32.678Z" }, - { url = "https://files.pythonhosted.org/packages/8e/14/e754c12bc953c7fa309687cd30a6ea95e5721168fb0b2a99a34bff24be5c/rignore-0.6.4-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:14d095622969504a2e56f666286202dad583f08d3347b7be2d647ddfd7a9bf47", size = 1069861, upload-time = "2025-07-19T19:24:07.671Z" }, - { url = "https://files.pythonhosted.org/packages/a6/24/ba2bdaf04a19b5331c051b9d480e8daca832bed4aeaa156d6d679044c06c/rignore-0.6.4-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:30f3d688df7eb4850318f1b5864d14f2c5fe5dbf3803ed0fc8329d2a7ad560dc", size = 1136368, upload-time = "2025-07-19T19:24:19.68Z" }, - { url = "https://files.pythonhosted.org/packages/83/48/7cf52353299e02aa629150007fa75f4b91d99b4f2fa536f2e24ead810116/rignore-0.6.4-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:028f62a7b0a6235bb3f03c9e7f342352e7fa4b3f08c761c72f9de8faee40ed9c", size = 1111714, upload-time = "2025-07-19T19:24:31.717Z" }, - { url = "https://files.pythonhosted.org/packages/84/9c/3881ad34f01942af0cf713e25e476bf851e04e389cc3ff146c3b459ab861/rignore-0.6.4-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:7e6c425603db2c147eace4f752ca3cd4551e7568c9d332175d586c68bcbe3d8d", size = 1122433, upload-time = "2025-07-19T19:24:43.973Z" }, +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/04/ecf19d701a0c9ddacbe0a01024acc5642045ff68afedd6c6e5822255e0ae/rignore-0.7.4.tar.gz", hash = "sha256:84c1c6bdfae8b28ef2c5fa5cc7b08d7e9cf1df1f4079c16bc669da4937634907", size = 54982, upload-time = "2025-11-03T10:06:07.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/f5/683f4abdcad60254e4c941a8fed430aa075428f96d8bc17eca99dfd7b64e/rignore-0.7.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc4b26519cc07c89e9213bdd20dbb456a9f772610e6b2d06fb13f00abc37be7", size = 899191, upload-time = "2025-11-03T10:03:17.724Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a6/ebdffc5713ffd7036b15c9030500272626187e23357f7fb13b615afe70a3/rignore-0.7.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c184ffc0ae4898b0221350a0f418aa702f6b74713f1fed5d8144bba80fac8c5b", size = 873972, upload-time = "2025-11-03T10:03:34.843Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7f/b049be89bed218a3390b4f5055aaf4a3e988c9dccba41589f33d06bb9b38/rignore-0.7.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f0c646b1299b3855654d3210de41742db3b5139617ebb03e0cd05f6e1d2b021", size = 1167920, upload-time = "2025-11-03T10:03:50.243Z" }, + { url = "https://files.pythonhosted.org/packages/d1/56/eadb7fb82b850fb1c8136f4c1a9a7d99dc507aeafe4de65a7f2751e9686a/rignore-0.7.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:845a2232f2abbc246ff750cee36255e0e8382e2a2e8bacf5ede3f391a0c60747", size = 942710, upload-time = "2025-11-03T10:04:06.769Z" }, + { url = "https://files.pythonhosted.org/packages/13/a1/f4c68cdb75b0bc1c2cc658ad7e2a183391b485f5fdb1c9a52eade11ef1e4/rignore-0.7.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70e4b297707f619c2c25ec8b5ce0376e4c6ea4a44b52b438c08e3eac39200b97", size = 959808, upload-time = "2025-11-03T10:04:38.841Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d0/b8ab2f0fa404b0c2bc5f204ab63d81f0019ee7e998cf5eb215b60f46ea37/rignore-0.7.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6b6361963bad54627e5efd6c75924d2e0a71df1b5fe5a539f6d6589b0f2ee9fb", size = 984905, upload-time = "2025-11-03T10:04:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d3/be1dd4d94c48232ad8a193ba4b63f4022f7b14d6f9b8ade2613304958249/rignore-0.7.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a6fae1e963f371c3677f866cd3593f504fa7d489bc1a812f06312253b122ab2", size = 1079187, upload-time = "2025-11-03T10:05:04.525Z" }, + { url = "https://files.pythonhosted.org/packages/1f/74/57865ec77d9239827059c7943f71829c5d3e82ca4e071c1aeba27576448e/rignore-0.7.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0225547de2b78e1147dcc34e5991ff15bd30306f09b24d78031e51bac7c6ef75", size = 1138968, upload-time = "2025-11-03T10:05:19.269Z" }, + { url = "https://files.pythonhosted.org/packages/84/8b/b97d501eaf276b960ab8af013a554601d561eb5a7fc59dfb1ed3719e3913/rignore-0.7.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5890bec12a156d2fabdf17ef2aeff34835bdc66b430daeb62d312f9943dd99ec", size = 1119031, upload-time = "2025-11-03T10:05:35.362Z" }, + { url = "https://files.pythonhosted.org/packages/0f/af/5798f286141aa89b9061dcf2e9c156f234d90ab8dbdf93b9516ca5b7e37b/rignore-0.7.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29059794f1a192fb388dcc849d9417ec6366185bd1d5e38c1f50f9f3f501f851", size = 1128505, upload-time = "2025-11-03T10:05:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/70668e64ec9bbe5122d3bf6f0f5c787be95a5018de64db011c6b5eb7e1bd/rignore-0.7.4-cp310-cp310-win32.whl", hash = "sha256:ba6a560208d219bb9e06212bcc6707f027069a223ff2a57bd1a382688bfbe9b0", size = 646814, upload-time = "2025-11-03T10:06:22.643Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/a8a62710c7d413df3f64d210c71fab977b8740f9fee486aa99d883f450a8/rignore-0.7.4-cp310-cp310-win_amd64.whl", hash = "sha256:76cff6199928f890430d5f50233608b00d53e13f7f38877cbb8b4612facda4ac", size = 727000, upload-time = "2025-11-03T10:06:13.3Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d6/c05c480c6eaad8e77cba509a53cf83bf13e982e925fb588ecf1c799421fb/rignore-0.7.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4dd18a33edeea1db75df7d6a23f52b52a8e2637cef947c2af3351e305d5c90e", size = 891778, upload-time = "2025-11-03T10:04:57.947Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3e/e6d582c2e98f857d61537fb42666bda3d3d50eff220f1484806eacbbe1cb/rignore-0.7.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bff0709015c409ba278a7c56f9e76f4fba50eb3120a03a14323a7b626be401c0", size = 823737, upload-time = "2025-11-03T10:04:52.944Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/405c83c0b73b5b00997a01c71c469d0d0c3337fb833b58f1dd3139eb622e/rignore-0.7.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30038864631035403c7015fa5ab314ecab59b3066d6d3ea40fe7acb4dff1ff23", size = 899465, upload-time = "2025-11-03T10:03:19.774Z" }, + { url = "https://files.pythonhosted.org/packages/08/a5/bb760af49ca5b0846e71ab1dc8d49864ce842ccc2ab1fdf3928cfe50de8a/rignore-0.7.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14451d33e21cfec2b034883b209181d80224536b022b5f4bfd9b089d3a8d243f", size = 873678, upload-time = "2025-11-03T10:03:36.036Z" }, + { url = "https://files.pythonhosted.org/packages/99/dd/79bd4a38210db11f8c0767171695ec8b8caf48ca34d56138f1cc5b15fb02/rignore-0.7.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bbbc7b05e7c838173fb8b36845bdf247ac2c5e9f109a6d061519c46694e2085", size = 1167585, upload-time = "2025-11-03T10:03:51.778Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3a/2caca3f68addb5ec9bed3c0897849b1d7fa08ea8f183cd49e60141e236df/rignore-0.7.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ac1f278d43d8a460e59a56e07ad9cdb6c64dd1fd2a2ac2dbcd8feaad8fc4220", size = 941476, upload-time = "2025-11-03T10:04:08.42Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0f/6228fd3e55e41897999564b5c8db3db5b9b776438544f9bac2f6a3108cb3/rignore-0.7.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1ae1e3c4c285e14623a0070a3d7de6fe251babfec2dc594cd208b5e99bc5960", size = 959446, upload-time = "2025-11-03T10:04:40.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/ed2704668c075c72b965729866a28e0217e6c1ac71b1883ed14e0752182b/rignore-0.7.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68b0baeb026b216b42dcbc7f3544065fe5d8a56a0fb6e2389374b300d099b4fa", size = 985183, upload-time = "2025-11-03T10:04:23.738Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f6/7d3e028dad6c079663864c19a841faf7cd94ecd0299aaf9eb4aa2b1f362f/rignore-0.7.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ef79218c0d8ff5f82d064a84eb79fec6b838446f64d2f029cce2df7a805b6997", size = 1079293, upload-time = "2025-11-03T10:05:05.8Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b3/674a9bbee38e9a7a66b4f26df6c64f6fbaa688c250846e0bcd7383692aa2/rignore-0.7.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0cabe580cdc16b4f0c33b49e7ff2ecad568f2ceae0a18b85b8635e478c5f4d6e", size = 1138950, upload-time = "2025-11-03T10:05:20.869Z" }, + { url = "https://files.pythonhosted.org/packages/92/f5/06143260c5056da47033b2fb03c68c2c6c3fe032bbf014d68ad0f5854f57/rignore-0.7.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b11f7735b0d0ca5bae0bf7c0a3bd5ff67c7e06fab7ef78820838ff1e8019728b", size = 1118759, upload-time = "2025-11-03T10:05:37.005Z" }, + { url = "https://files.pythonhosted.org/packages/e9/83/8242a4053ab894f4d5ab8783f18c9aa3b125d5157452a0f2c4a048573661/rignore-0.7.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f80d3a90525522e89ea7204f656c0feaa60f478057d02fa523f605206fbd2da7", size = 1128271, upload-time = "2025-11-03T10:05:52.681Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c1/cd8f51a2958e71819340b495841ccc7bcc1a17b170725d812c609f22467f/rignore-0.7.4-cp311-cp311-win32.whl", hash = "sha256:053659a88a8faf3764d7d955234164434412f185a32d35804fcdffb05944a7f4", size = 646547, upload-time = "2025-11-03T10:06:24.056Z" }, + { url = "https://files.pythonhosted.org/packages/c2/22/6f61ac041d0550d2b868c2a585de27f445cdf1d45072976c9499c26e72a3/rignore-0.7.4-cp311-cp311-win_amd64.whl", hash = "sha256:607ddb1ba81167efcf3348b76a5b3b4f74b1646e8aa4c2e6c97eac96f30de87b", size = 727133, upload-time = "2025-11-03T10:06:14.514Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/23e1b6bc9d02a7672297002af12a438fac1b1f47ed2923bd2e762aef5d94/rignore-0.7.4-cp311-cp311-win_arm64.whl", hash = "sha256:b5316ea236e0a35969c926a983007d07985425749c53b6e5afa502cc9d65f04a", size = 657601, upload-time = "2025-11-03T10:06:08.003Z" }, + { url = "https://files.pythonhosted.org/packages/68/a9/1f9ceedbf7f529f6357bb8f9ac53e2cf615a2eb9203c820df905a84922a4/rignore-0.7.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:29136c02bcc327b20f8d092fcd14e906a66a7ecce9fae5e336964ec9f92b9d9a", size = 889682, upload-time = "2025-11-03T10:04:59.531Z" }, + { url = "https://files.pythonhosted.org/packages/f4/fd/546599973247213bcf7334ecdc05d886ceac8e9e506a4b97454c2c2455ac/rignore-0.7.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b53afbe413cd32c4f87cab813c0dc933ace26a9c984a082cf64d24f2f2bb0e0", size = 820247, upload-time = "2025-11-03T10:04:54.183Z" }, + { url = "https://files.pythonhosted.org/packages/64/03/f00b5197f556b51aee40b040712e074b0b7f530df302de34aeb35707da97/rignore-0.7.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213aaa9910d4916e8fdf98790f802c8075c3b4eb7d0e2e9d7e13f82c52b2e231", size = 897900, upload-time = "2025-11-03T10:03:21.628Z" }, + { url = "https://files.pythonhosted.org/packages/02/c3/2e0eb3bc46147f78a8b0da72dc964ca9b90edeaef616cc1e841c7bf0d940/rignore-0.7.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81db4625f33eb8c2cae95d93d864a25bd6392b40e482bc6f738471909aeafe3e", size = 873707, upload-time = "2025-11-03T10:03:37.684Z" }, + { url = "https://files.pythonhosted.org/packages/3c/40/8930d0e865d5212c2f56b33815c442f71290fb763050d231b6e91f07d8ad/rignore-0.7.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8a66e96cc65183c5eb4199a14cffa7cec202bb547ac728442c21a3531c72c64", size = 1169036, upload-time = "2025-11-03T10:03:53.447Z" }, + { url = "https://files.pythonhosted.org/packages/23/79/e6ece7fabcaee80d478486f3eb9d84e63ccc6e66f37e7d66b1369d551656/rignore-0.7.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a07cfc21fdaf193dd227d3545cc3b7c23cbbca0048e605dd05f18bcd24d0934", size = 943032, upload-time = "2025-11-03T10:04:09.624Z" }, + { url = "https://files.pythonhosted.org/packages/a3/bd/67827efa0fb04e2810832b869792909968733bd84bee33673b1deced2ddc/rignore-0.7.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39d4d8ba2efc8cbadcfc0d3d3361501a6e8fb33e7581ef0e1ce089dcaa4c85dc", size = 959790, upload-time = "2025-11-03T10:04:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0f/0b08c034c6d7408d8d8f98b2ce55802426f67bcf280c16f6571b40b3666f/rignore-0.7.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c8d8f4eba2b68df87a99e014f92ec1575d81b6aa00b4b283dcf04264b971e285", size = 984062, upload-time = "2025-11-03T10:04:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/e6e33d1519d4c8e279dc7ce75503825919c937902d28f8bd046d51ccdc96/rignore-0.7.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e71fe8e1140a672907e8bda9167412817de5d47ebf76edf210916680c01e9962", size = 1078421, upload-time = "2025-11-03T10:05:07.081Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c4/a0ef8ae07bdece7791692372fae83c15de423a6c8daf36781b523f80ac7a/rignore-0.7.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:39e920c625b4de28bab0c8dcb7fe7896c4f387f461040995044e4e9ff8bcddf0", size = 1139038, upload-time = "2025-11-03T10:05:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c1/931b656efeb5a5eeb65cc002e4b2afc4eec044048ed8fa0d1e94164fd145/rignore-0.7.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:96dbb625bdd655595c6da6f7ac19dcc7aa93f20009e5e42d2a9ea67e12327015", size = 1117649, upload-time = "2025-11-03T10:05:38.597Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/d7542eb1cf14a58a46064c08f11d1365e70673533842daf5f23c84ecb4db/rignore-0.7.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:66590d1dee4385b421bae2c6f255d6f98d7a65fa0b0280e17b5f98db695e8589", size = 1127980, upload-time = "2025-11-03T10:05:54.015Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/d33555f429fde87656bae3a1ed118590785a23b1bcfeb247546fa19bc89b/rignore-0.7.4-cp312-cp312-win32.whl", hash = "sha256:accdbb74ce54af5ce5f1b6b2a0b01237990e0dc583ba4c69619e66a640b1a170", size = 646147, upload-time = "2025-11-03T10:06:25.347Z" }, + { url = "https://files.pythonhosted.org/packages/8f/74/546b9342294f1f53c3fdfbedd136c00a35a3bc3a3be81b1f967ba0d79711/rignore-0.7.4-cp312-cp312-win_amd64.whl", hash = "sha256:04a7423d8d80725aba721e4135f41b9fa15f59bd7b3e74d035e670c4c63c7fd6", size = 726163, upload-time = "2025-11-03T10:06:15.792Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/7d2d38cf567b51e8d94f21d4221283115cbbd8031fe683805b837404fbea/rignore-0.7.4-cp312-cp312-win_arm64.whl", hash = "sha256:0ca32536b64717d0af6ce4638913df41ffbf3e71c061b36e33e3ad670f87605e", size = 656297, upload-time = "2025-11-03T10:06:09.326Z" }, + { url = "https://files.pythonhosted.org/packages/15/ef/d00a7c2a67d0409fbc937159dcce7a4f7f7db1b11c95e4719213d3499dc1/rignore-0.7.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8572bc52b6e90c79b858513bc3dff8000add064dc8b1bb19e86fdce2ed11d709", size = 900868, upload-time = "2025-11-03T10:03:31.191Z" }, + { url = "https://files.pythonhosted.org/packages/d0/3f/15641976c1674d4c8a69485c5081c09fdbf8773efa86110ae6da1fd7fe55/rignore-0.7.4-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eed763f91f55060193789b1642b706853fe13437f1efe82b89d00a6bde4833bc", size = 877032, upload-time = "2025-11-03T10:03:45.499Z" }, + { url = "https://files.pythonhosted.org/packages/51/a5/cdb7986f55bac454a52c506f1453ddc32878ea370d2747f897e26f5267b2/rignore-0.7.4-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af178d7e1421e6e4da590d27a76da7bb71f0bca70d2176397fe50012fcf823ec", size = 1171228, upload-time = "2025-11-03T10:04:01.97Z" }, + { url = "https://files.pythonhosted.org/packages/27/39/54e71461c227f260e86cc0583a8cc912d9e4d54f8f7416d1eeb3b005f9a1/rignore-0.7.4-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90b3d7b8d4517f384b36285a0d08efd7f1359b97adb85e87aab7b70db0c0b325", size = 943477, upload-time = "2025-11-03T10:04:18.098Z" }, + { url = "https://files.pythonhosted.org/packages/a5/14/45f508496a4b1a0e4a30fd7452a62767d36f2b24ea15aa1f4dd087b686f5/rignore-0.7.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a112c341ed8d4e30451aad3e7244bd3c9380b8b27032d18943656a6e536a106b", size = 961669, upload-time = "2025-11-03T10:04:49.207Z" }, + { url = "https://files.pythonhosted.org/packages/97/16/10622702cb3270c2ee87f9be21cb414c8a396c77ba63da5573a89a23b7b3/rignore-0.7.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7658c1f75bee29e9e88e3bb66e21c8801c7d6a39685b94dbc0e3c2508d513ab5", size = 987075, upload-time = "2025-11-03T10:04:34.493Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c0/24a8803ef644e9ef41c038c2bf33ea0bac34268932d8a000788e65909285/rignore-0.7.4-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:55cac2342a1e748bca0b11c9503f00443f5c8be4a325213016c75dcd241016ba", size = 1080325, upload-time = "2025-11-03T10:05:15.294Z" }, + { url = "https://files.pythonhosted.org/packages/c9/80/32cc8ff27ef107207cfea5e0850c3f726d37ed31349d0a1ddb8dff9e3e69/rignore-0.7.4-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:60bf875116a3154082e2e5bb19fe16bf9afa2163ab870724e39126cfb02d1acf", size = 1141084, upload-time = "2025-11-03T10:05:30.776Z" }, + { url = "https://files.pythonhosted.org/packages/79/20/c7214f17d714be88e49cb7e2df36014f7e61d6e612730c8202a953a113f1/rignore-0.7.4-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:4925846a134849440ed040a1fe12aab493add84c06fd6803aeb7c6d3a330de88", size = 1121184, upload-time = "2025-11-03T10:05:47.113Z" }, + { url = "https://files.pythonhosted.org/packages/8e/23/64d4b493cbaba74ac6dc10edce28effe141c9fa9ba418d60e3bcd5fc44a6/rignore-0.7.4-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:12b672e7786fdc4b419d39c28a1edd24a325212b954c6649ead156226b41280c", size = 1130192, upload-time = "2025-11-03T10:06:02.519Z" }, + { url = "https://files.pythonhosted.org/packages/11/ad/28bfdb72d3e15009ead8c41f5bd77e532d746a9edc6662197871982670d1/rignore-0.7.4-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:760b63adb499eb1d8b2b4f8c57f966252b780946cd55ec1d0c2d80b7bf6ce2de", size = 899784, upload-time = "2025-11-03T10:03:32.36Z" }, + { url = "https://files.pythonhosted.org/packages/37/c8/365c75addfc594bdc00c636c04dd8c02a8baa2e61ea0ad6bf5bc1d81775e/rignore-0.7.4-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1a5c33d2805a9c5996f5e0cc6730f8e5a646e18d03439c50e21201cef519f914", size = 874984, upload-time = "2025-11-03T10:03:46.724Z" }, + { url = "https://files.pythonhosted.org/packages/4d/94/d1c5c3b18a9d3454d72a7304c35ffd97ed631eaa4926ed0d4f74e285f682/rignore-0.7.4-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:448fdedb86962fb4f25d79ff61e9ad4de63eb09ac4166f618bc55c5a7e558352", size = 1170522, upload-time = "2025-11-03T10:04:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/10/02/cd9519d1e36035273c97b576cac1dbf88fcb8e2c64fa1399c0c7da011633/rignore-0.7.4-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1a8cbd0ce1cfca428c13a9bc1e74f8390858e54275a5ee8678e3dce765e0936", size = 942348, upload-time = "2025-11-03T10:04:19.447Z" }, + { url = "https://files.pythonhosted.org/packages/a5/56/615e307ad12a01685254c4a6fe57675b95e67ee7436c628e7743eb1c4712/rignore-0.7.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a67806555c5121a86603f30d3dacd53e8b2481bb441cd93f8f005315c190b45", size = 961028, upload-time = "2025-11-03T10:04:50.47Z" }, + { url = "https://files.pythonhosted.org/packages/de/62/ca56faee83943148c69767d137e12a3af46cca7666742cad24efbc083b65/rignore-0.7.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e459d09117df4e14e4d99b0d9b2d751239f00f929363053631c572c3bac21f70", size = 986154, upload-time = "2025-11-03T10:04:36.024Z" }, + { url = "https://files.pythonhosted.org/packages/da/4d/d102d0f999aba82736424ff5bec483f277addd4a3ad7e3bd7a300c3460ea/rignore-0.7.4-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:20bf75aaae0836e6a68dac0551fb0f8791af6d93928a39ecc40d44bb22a812c6", size = 1079645, upload-time = "2025-11-03T10:05:16.684Z" }, + { url = "https://files.pythonhosted.org/packages/b7/3a/96bbe8c8dc69c540e589b15af76086decac58244f54f2260599dfcdb2e4e/rignore-0.7.4-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:38069630fd6835996de06a45c286773d4e1d3c9021d7c6f9bbb840b58cc58bf3", size = 1140030, upload-time = "2025-11-03T10:05:32.396Z" }, + { url = "https://files.pythonhosted.org/packages/b9/16/1796c19161b942e514638e0cc281acf1800650268600c01db27319374b29/rignore-0.7.4-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:2cb54c6396ff82d89217be02dac6cc382b5389f98d19997c2c966df55b7bebb7", size = 1120349, upload-time = "2025-11-03T10:05:48.394Z" }, + { url = "https://files.pythonhosted.org/packages/53/ad/4f1dbc3592ff5ad9d22852fa21993a614e1877408b9c501f61d583994a63/rignore-0.7.4-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5d58e2cb510563b4b1c6431c67c906c75827348c2bceab37380b18d099948fef", size = 1129465, upload-time = "2025-11-03T10:06:03.939Z" }, ] [[package]] name = "rpds-py" -version = "0.27.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/d9/991a0dee12d9fc53ed027e26a26a64b151d77252ac477e22666b9688bc16/rpds_py-0.27.0.tar.gz", hash = "sha256:8b23cf252f180cda89220b378d917180f29d313cd6a07b2431c0d3b776aae86f", size = 27420, upload-time = "2025-08-07T08:26:39.624Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/2d/ad2e37dee3f45580f7fa0066c412a521f9bee53d2718b0e9436d308a1ecd/rpds_py-0.27.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:130c1ffa5039a333f5926b09e346ab335f0d4ec393b030a18549a7c7e7c2cea4", size = 371511, upload-time = "2025-08-07T08:23:06.205Z" }, - { url = "https://files.pythonhosted.org/packages/f5/67/57b4b2479193fde9dd6983a13c2550b5f9c3bcdf8912dffac2068945eb14/rpds_py-0.27.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a4cf32a26fa744101b67bfd28c55d992cd19438aff611a46cac7f066afca8fd4", size = 354718, upload-time = "2025-08-07T08:23:08.222Z" }, - { url = "https://files.pythonhosted.org/packages/a3/be/c2b95ec4b813eb11f3a3c3d22f22bda8d3a48a074a0519cde968c4d102cf/rpds_py-0.27.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64a0fe3f334a40b989812de70160de6b0ec7e3c9e4a04c0bbc48d97c5d3600ae", size = 381518, upload-time = "2025-08-07T08:23:09.696Z" }, - { url = "https://files.pythonhosted.org/packages/a5/d2/5a7279bc2b93b20bd50865a2269016238cee45f7dc3cc33402a7f41bd447/rpds_py-0.27.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a0ff7ee28583ab30a52f371b40f54e7138c52ca67f8ca17ccb7ccf0b383cb5f", size = 396694, upload-time = "2025-08-07T08:23:11.105Z" }, - { url = "https://files.pythonhosted.org/packages/65/e9/bac8b3714bd853c5bcb466e04acfb9a5da030d77e0ddf1dfad9afb791c31/rpds_py-0.27.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15ea4d2e182345dd1b4286593601d766411b43f868924afe297570658c31a62b", size = 514813, upload-time = "2025-08-07T08:23:12.215Z" }, - { url = "https://files.pythonhosted.org/packages/1d/aa/293115e956d7d13b7d2a9e9a4121f74989a427aa125f00ce4426ca8b7b28/rpds_py-0.27.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36184b44bf60a480863e51021c26aca3dfe8dd2f5eeabb33622b132b9d8b8b54", size = 402246, upload-time = "2025-08-07T08:23:13.699Z" }, - { url = "https://files.pythonhosted.org/packages/88/59/2d6789bb898fb3e2f0f7b82b7bcf27f579ebcb6cc36c24f4e208f7f58a5b/rpds_py-0.27.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b78430703cfcf5f5e86eb74027a1ed03a93509273d7c705babb547f03e60016", size = 383661, upload-time = "2025-08-07T08:23:15.231Z" }, - { url = "https://files.pythonhosted.org/packages/0c/55/add13a593a7a81243a9eed56d618d3d427be5dc1214931676e3f695dfdc1/rpds_py-0.27.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:dbd749cff1defbde270ca346b69b3baf5f1297213ef322254bf2a28537f0b046", size = 401691, upload-time = "2025-08-07T08:23:16.681Z" }, - { url = "https://files.pythonhosted.org/packages/04/09/3e8b2aad494ffaca571e4e19611a12cc18fcfd756d9274f3871a2d822445/rpds_py-0.27.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bde37765564cd22a676dd8101b657839a1854cfaa9c382c5abf6ff7accfd4ae", size = 416529, upload-time = "2025-08-07T08:23:17.863Z" }, - { url = "https://files.pythonhosted.org/packages/a4/6d/bd899234728f1d8f72c9610f50fdf1c140ecd0a141320e1f1d0f6b20595d/rpds_py-0.27.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1d66f45b9399036e890fb9c04e9f70c33857fd8f58ac8db9f3278cfa835440c3", size = 558673, upload-time = "2025-08-07T08:23:18.99Z" }, - { url = "https://files.pythonhosted.org/packages/79/f4/f3e02def5193fb899d797c232f90d6f8f0f2b9eca2faef6f0d34cbc89b2e/rpds_py-0.27.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d85d784c619370d9329bbd670f41ff5f2ae62ea4519761b679d0f57f0f0ee267", size = 588426, upload-time = "2025-08-07T08:23:20.541Z" }, - { url = "https://files.pythonhosted.org/packages/e3/0c/88e716cd8fd760e5308835fe298255830de4a1c905fd51760b9bb40aa965/rpds_py-0.27.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5df559e9e7644d9042f626f2c3997b555f347d7a855a15f170b253f6c5bfe358", size = 554552, upload-time = "2025-08-07T08:23:21.714Z" }, - { url = "https://files.pythonhosted.org/packages/2b/a9/0a8243c182e7ac59b901083dff7e671feba6676a131bfff3f8d301cd2b36/rpds_py-0.27.0-cp310-cp310-win32.whl", hash = "sha256:b8a4131698b6992b2a56015f51646711ec5d893a0b314a4b985477868e240c87", size = 218081, upload-time = "2025-08-07T08:23:23.273Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e7/202ff35852312760148be9e08fe2ba6900aa28e7a46940a313eae473c10c/rpds_py-0.27.0-cp310-cp310-win_amd64.whl", hash = "sha256:cbc619e84a5e3ab2d452de831c88bdcad824414e9c2d28cd101f94dbdf26329c", size = 230077, upload-time = "2025-08-07T08:23:24.308Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/49d515434c1752e40f5e35b985260cf27af052593378580a2f139a5be6b8/rpds_py-0.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dbc2ab5d10544eb485baa76c63c501303b716a5c405ff2469a1d8ceffaabf622", size = 371577, upload-time = "2025-08-07T08:23:25.379Z" }, - { url = "https://files.pythonhosted.org/packages/e1/6d/bf2715b2fee5087fa13b752b5fd573f1a93e4134c74d275f709e38e54fe7/rpds_py-0.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7ec85994f96a58cf7ed288caa344b7fe31fd1d503bdf13d7331ead5f70ab60d5", size = 354959, upload-time = "2025-08-07T08:23:26.767Z" }, - { url = "https://files.pythonhosted.org/packages/a3/5c/e7762808c746dd19733a81373c10da43926f6a6adcf4920a21119697a60a/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:190d7285cd3bb6d31d37a0534d7359c1ee191eb194c511c301f32a4afa5a1dd4", size = 381485, upload-time = "2025-08-07T08:23:27.869Z" }, - { url = "https://files.pythonhosted.org/packages/40/51/0d308eb0b558309ca0598bcba4243f52c4cd20e15fe991b5bd75824f2e61/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c10d92fb6d7fd827e44055fcd932ad93dac6a11e832d51534d77b97d1d85400f", size = 396816, upload-time = "2025-08-07T08:23:29.424Z" }, - { url = "https://files.pythonhosted.org/packages/5c/aa/2d585ec911d78f66458b2c91252134ca0c7c70f687a72c87283173dc0c96/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd2c1d27ebfe6a015cfa2005b7fe8c52d5019f7bbdd801bc6f7499aab9ae739e", size = 514950, upload-time = "2025-08-07T08:23:30.576Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ef/aced551cc1148179557aed84343073adadf252c91265263ee6203458a186/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4790c9d5dd565ddb3e9f656092f57268951398cef52e364c405ed3112dc7c7c1", size = 402132, upload-time = "2025-08-07T08:23:32.428Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ac/cf644803d8d417653fe2b3604186861d62ea6afaef1b2284045741baef17/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4300e15e7d03660f04be84a125d1bdd0e6b2f674bc0723bc0fd0122f1a4585dc", size = 383660, upload-time = "2025-08-07T08:23:33.829Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ec/caf47c55ce02b76cbaeeb2d3b36a73da9ca2e14324e3d75cf72b59dcdac5/rpds_py-0.27.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:59195dc244fc183209cf8a93406889cadde47dfd2f0a6b137783aa9c56d67c85", size = 401730, upload-time = "2025-08-07T08:23:34.97Z" }, - { url = "https://files.pythonhosted.org/packages/0b/71/c1f355afdcd5b99ffc253422aa4bdcb04ccf1491dcd1bda3688a0c07fd61/rpds_py-0.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fae4a01ef8c4cb2bbe92ef2063149596907dc4a881a8d26743b3f6b304713171", size = 416122, upload-time = "2025-08-07T08:23:36.062Z" }, - { url = "https://files.pythonhosted.org/packages/38/0f/f4b5b1eda724ed0e04d2b26d8911cdc131451a7ee4c4c020a1387e5c6ded/rpds_py-0.27.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e3dc8d4ede2dbae6c0fc2b6c958bf51ce9fd7e9b40c0f5b8835c3fde44f5807d", size = 558771, upload-time = "2025-08-07T08:23:37.478Z" }, - { url = "https://files.pythonhosted.org/packages/93/c0/5f8b834db2289ab48d5cffbecbb75e35410103a77ac0b8da36bf9544ec1c/rpds_py-0.27.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3782fb753aa825b4ccabc04292e07897e2fd941448eabf666856c5530277626", size = 587876, upload-time = "2025-08-07T08:23:38.662Z" }, - { url = "https://files.pythonhosted.org/packages/d2/dd/1a1df02ab8eb970115cff2ae31a6f73916609b900dc86961dc382b8c2e5e/rpds_py-0.27.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:887ab1f12b0d227e9260558a4a2320024b20102207ada65c43e1ffc4546df72e", size = 554359, upload-time = "2025-08-07T08:23:39.897Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e4/95a014ab0d51ab6e3bebbdb476a42d992d2bbf9c489d24cff9fda998e925/rpds_py-0.27.0-cp311-cp311-win32.whl", hash = "sha256:5d6790ff400254137b81b8053b34417e2c46921e302d655181d55ea46df58cf7", size = 218084, upload-time = "2025-08-07T08:23:41.086Z" }, - { url = "https://files.pythonhosted.org/packages/49/78/f8d5b71ec65a0376b0de31efcbb5528ce17a9b7fdd19c3763303ccfdedec/rpds_py-0.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:e24d8031a2c62f34853756d9208eeafa6b940a1efcbfe36e8f57d99d52bb7261", size = 230085, upload-time = "2025-08-07T08:23:42.143Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d3/84429745184091e06b4cc70f8597408e314c2d2f7f5e13249af9ffab9e3d/rpds_py-0.27.0-cp311-cp311-win_arm64.whl", hash = "sha256:08680820d23df1df0a0260f714d12966bc6c42d02e8055a91d61e03f0c47dda0", size = 222112, upload-time = "2025-08-07T08:23:43.233Z" }, - { url = "https://files.pythonhosted.org/packages/cd/17/e67309ca1ac993fa1888a0d9b2f5ccc1f67196ace32e76c9f8e1dbbbd50c/rpds_py-0.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:19c990fdf5acecbf0623e906ae2e09ce1c58947197f9bced6bbd7482662231c4", size = 362611, upload-time = "2025-08-07T08:23:44.773Z" }, - { url = "https://files.pythonhosted.org/packages/93/2e/28c2fb84aa7aa5d75933d1862d0f7de6198ea22dfd9a0cca06e8a4e7509e/rpds_py-0.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c27a7054b5224710fcfb1a626ec3ff4f28bcb89b899148c72873b18210e446b", size = 347680, upload-time = "2025-08-07T08:23:46.014Z" }, - { url = "https://files.pythonhosted.org/packages/44/3e/9834b4c8f4f5fe936b479e623832468aa4bd6beb8d014fecaee9eac6cdb1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09965b314091829b378b60607022048953e25f0b396c2b70e7c4c81bcecf932e", size = 384600, upload-time = "2025-08-07T08:23:48Z" }, - { url = "https://files.pythonhosted.org/packages/19/78/744123c7b38865a965cd9e6f691fde7ef989a00a256fa8bf15b75240d12f/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14f028eb47f59e9169bfdf9f7ceafd29dd64902141840633683d0bad5b04ff34", size = 400697, upload-time = "2025-08-07T08:23:49.407Z" }, - { url = "https://files.pythonhosted.org/packages/32/97/3c3d32fe7daee0a1f1a678b6d4dfb8c4dcf88197fa2441f9da7cb54a8466/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6168af0be75bba990a39f9431cdfae5f0ad501f4af32ae62e8856307200517b8", size = 517781, upload-time = "2025-08-07T08:23:50.557Z" }, - { url = "https://files.pythonhosted.org/packages/b2/be/28f0e3e733680aa13ecec1212fc0f585928a206292f14f89c0b8a684cad1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab47fe727c13c09d0e6f508e3a49e545008e23bf762a245b020391b621f5b726", size = 406449, upload-time = "2025-08-07T08:23:51.732Z" }, - { url = "https://files.pythonhosted.org/packages/95/ae/5d15c83e337c082d0367053baeb40bfba683f42459f6ebff63a2fd7e5518/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa01b3d5e3b7d97efab65bd3d88f164e289ec323a8c033c5c38e53ee25c007e", size = 386150, upload-time = "2025-08-07T08:23:52.822Z" }, - { url = "https://files.pythonhosted.org/packages/bf/65/944e95f95d5931112829e040912b25a77b2e7ed913ea5fe5746aa5c1ce75/rpds_py-0.27.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:6c135708e987f46053e0a1246a206f53717f9fadfba27174a9769ad4befba5c3", size = 406100, upload-time = "2025-08-07T08:23:54.339Z" }, - { url = "https://files.pythonhosted.org/packages/21/a4/1664b83fae02894533cd11dc0b9f91d673797c2185b7be0f7496107ed6c5/rpds_py-0.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc327f4497b7087d06204235199daf208fd01c82d80465dc5efa4ec9df1c5b4e", size = 421345, upload-time = "2025-08-07T08:23:55.832Z" }, - { url = "https://files.pythonhosted.org/packages/7c/26/b7303941c2b0823bfb34c71378249f8beedce57301f400acb04bb345d025/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e57906e38583a2cba67046a09c2637e23297618dc1f3caddbc493f2be97c93f", size = 561891, upload-time = "2025-08-07T08:23:56.951Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c8/48623d64d4a5a028fa99576c768a6159db49ab907230edddc0b8468b998b/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f4f69d7a4300fbf91efb1fb4916421bd57804c01ab938ab50ac9c4aa2212f03", size = 591756, upload-time = "2025-08-07T08:23:58.146Z" }, - { url = "https://files.pythonhosted.org/packages/b3/51/18f62617e8e61cc66334c9fb44b1ad7baae3438662098efbc55fb3fda453/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4c4fbbcff474e1e5f38be1bf04511c03d492d42eec0babda5d03af3b5589374", size = 557088, upload-time = "2025-08-07T08:23:59.6Z" }, - { url = "https://files.pythonhosted.org/packages/bd/4c/e84c3a276e2496a93d245516be6b49e20499aa8ca1c94d59fada0d79addc/rpds_py-0.27.0-cp312-cp312-win32.whl", hash = "sha256:27bac29bbbf39601b2aab474daf99dbc8e7176ca3389237a23944b17f8913d97", size = 221926, upload-time = "2025-08-07T08:24:00.695Z" }, - { url = "https://files.pythonhosted.org/packages/83/89/9d0fbcef64340db0605eb0a0044f258076f3ae0a3b108983b2c614d96212/rpds_py-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a06aa1197ec0281eb1d7daf6073e199eb832fe591ffa329b88bae28f25f5fe5", size = 233235, upload-time = "2025-08-07T08:24:01.846Z" }, - { url = "https://files.pythonhosted.org/packages/c9/b0/e177aa9f39cbab060f96de4a09df77d494f0279604dc2f509263e21b05f9/rpds_py-0.27.0-cp312-cp312-win_arm64.whl", hash = "sha256:e14aab02258cb776a108107bd15f5b5e4a1bbaa61ef33b36693dfab6f89d54f9", size = 223315, upload-time = "2025-08-07T08:24:03.337Z" }, - { url = "https://files.pythonhosted.org/packages/47/55/287068956f9ba1cb40896d291213f09fdd4527630709058b45a592bc09dc/rpds_py-0.27.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:46f48482c1a4748ab2773f75fffbdd1951eb59794e32788834b945da857c47a8", size = 371566, upload-time = "2025-08-07T08:25:43.95Z" }, - { url = "https://files.pythonhosted.org/packages/a2/fb/443af59cbe552e89680bb0f1d1ba47f6387b92083e28a45b8c8863b86c5a/rpds_py-0.27.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:419dd9c98bcc9fb0242be89e0c6e922df333b975d4268faa90d58499fd9c9ebe", size = 355781, upload-time = "2025-08-07T08:25:45.256Z" }, - { url = "https://files.pythonhosted.org/packages/ad/f0/35f48bb073b5ca42b1dcc55cb148f4a3bd4411a3e584f6a18d26f0ea8832/rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d42a0ef2bdf6bc81e1cc2d49d12460f63c6ae1423c4f4851b828e454ccf6f1", size = 382575, upload-time = "2025-08-07T08:25:46.524Z" }, - { url = "https://files.pythonhosted.org/packages/51/e1/5f5296a21d1189f0f116a938af2e346d83172bf814d373695e54004a936f/rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e39169ac6aae06dd79c07c8a69d9da867cef6a6d7883a0186b46bb46ccfb0c3", size = 397435, upload-time = "2025-08-07T08:25:48.204Z" }, - { url = "https://files.pythonhosted.org/packages/97/79/3af99b7852b2b55cad8a08863725cbe9dc14781bcf7dc6ecead0c3e1dc54/rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:935afcdea4751b0ac918047a2df3f720212892347767aea28f5b3bf7be4f27c0", size = 514861, upload-time = "2025-08-07T08:25:49.814Z" }, - { url = "https://files.pythonhosted.org/packages/df/3e/11fd6033708ed3ae0e6947bb94f762f56bb46bf59a1b16eef6944e8a62ee/rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8de567dec6d451649a781633d36f5c7501711adee329d76c095be2178855b042", size = 402776, upload-time = "2025-08-07T08:25:51.135Z" }, - { url = "https://files.pythonhosted.org/packages/b7/89/f9375ceaa996116de9cbc949874804c7874d42fb258c384c037a46d730b8/rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:555ed147cbe8c8f76e72a4c6cd3b7b761cbf9987891b9448808148204aed74a5", size = 384665, upload-time = "2025-08-07T08:25:52.82Z" }, - { url = "https://files.pythonhosted.org/packages/48/bf/0061e55c6f1f573a63c0f82306b8984ed3b394adafc66854a936d5db3522/rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:d2cc2b34f9e1d31ce255174da82902ad75bd7c0d88a33df54a77a22f2ef421ee", size = 402518, upload-time = "2025-08-07T08:25:54.073Z" }, - { url = "https://files.pythonhosted.org/packages/ae/dc/8d506676bfe87b3b683332ec8e6ab2b0be118a3d3595ed021e3274a63191/rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cb0702c12983be3b2fab98ead349ac63a98216d28dda6f518f52da5498a27a1b", size = 416247, upload-time = "2025-08-07T08:25:55.433Z" }, - { url = "https://files.pythonhosted.org/packages/2e/02/9a89eea1b75c69e81632de7963076e455b1e00e1cfb46dfdabb055fa03e3/rpds_py-0.27.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ba783541be46f27c8faea5a6645e193943c17ea2f0ffe593639d906a327a9bcc", size = 559456, upload-time = "2025-08-07T08:25:56.866Z" }, - { url = "https://files.pythonhosted.org/packages/38/4a/0f3ac4351957847c0d322be6ec72f916e43804a2c1d04e9672ea4a67c315/rpds_py-0.27.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:2406d034635d1497c596c40c85f86ecf2bf9611c1df73d14078af8444fe48031", size = 587778, upload-time = "2025-08-07T08:25:58.202Z" }, - { url = "https://files.pythonhosted.org/packages/c2/8e/39d0d7401095bed5a5ad5ef304fae96383f9bef40ca3f3a0807ff5b68d9d/rpds_py-0.27.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dea0808153f1fbbad772669d906cddd92100277533a03845de6893cadeffc8be", size = 555247, upload-time = "2025-08-07T08:25:59.707Z" }, - { url = "https://files.pythonhosted.org/packages/e0/04/6b8311e811e620b9eaca67cd80a118ff9159558a719201052a7b2abb88bf/rpds_py-0.27.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d2a81bdcfde4245468f7030a75a37d50400ac2455c3a4819d9d550c937f90ab5", size = 230256, upload-time = "2025-08-07T08:26:01.07Z" }, - { url = "https://files.pythonhosted.org/packages/59/64/72ab5b911fdcc48058359b0e786e5363e3fde885156116026f1a2ba9a5b5/rpds_py-0.27.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e6491658dd2569f05860bad645569145c8626ac231877b0fb2d5f9bcb7054089", size = 371658, upload-time = "2025-08-07T08:26:02.369Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4b/90ff04b4da055db53d8fea57640d8d5d55456343a1ec9a866c0ecfe10fd1/rpds_py-0.27.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec77545d188f8bdd29d42bccb9191682a46fb2e655e3d1fb446d47c55ac3b8d", size = 355529, upload-time = "2025-08-07T08:26:03.83Z" }, - { url = "https://files.pythonhosted.org/packages/a4/be/527491fb1afcd86fc5ce5812eb37bc70428ee017d77fee20de18155c3937/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a4aebf8ca02bbb90a9b3e7a463bbf3bee02ab1c446840ca07b1695a68ce424", size = 382822, upload-time = "2025-08-07T08:26:05.52Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a5/dcdb8725ce11e6d0913e6fcf782a13f4b8a517e8acc70946031830b98441/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44524b96481a4c9b8e6c46d6afe43fa1fb485c261e359fbe32b63ff60e3884d8", size = 397233, upload-time = "2025-08-07T08:26:07.179Z" }, - { url = "https://files.pythonhosted.org/packages/33/f9/0947920d1927e9f144660590cc38cadb0795d78fe0d9aae0ef71c1513b7c/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45d04a73c54b6a5fd2bab91a4b5bc8b426949586e61340e212a8484919183859", size = 514892, upload-time = "2025-08-07T08:26:08.622Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ed/d1343398c1417c68f8daa1afce56ef6ce5cc587daaf98e29347b00a80ff2/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:343cf24de9ed6c728abefc5d5c851d5de06497caa7ac37e5e65dd572921ed1b5", size = 402733, upload-time = "2025-08-07T08:26:10.433Z" }, - { url = "https://files.pythonhosted.org/packages/1d/0b/646f55442cd14014fb64d143428f25667a100f82092c90087b9ea7101c74/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aed8118ae20515974650d08eb724150dc2e20c2814bcc307089569995e88a14", size = 384447, upload-time = "2025-08-07T08:26:11.847Z" }, - { url = "https://files.pythonhosted.org/packages/4b/15/0596ef7529828e33a6c81ecf5013d1dd33a511a3e0be0561f83079cda227/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:af9d4fd79ee1cc8e7caf693ee02737daabfc0fcf2773ca0a4735b356c8ad6f7c", size = 402502, upload-time = "2025-08-07T08:26:13.537Z" }, - { url = "https://files.pythonhosted.org/packages/c3/8d/986af3c42f8454a6cafff8729d99fb178ae9b08a9816325ac7a8fa57c0c0/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f0396e894bd1e66c74ecbc08b4f6a03dc331140942c4b1d345dd131b68574a60", size = 416651, upload-time = "2025-08-07T08:26:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9a/b4ec3629b7b447e896eec574469159b5b60b7781d3711c914748bf32de05/rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:59714ab0a5af25d723d8e9816638faf7f4254234decb7d212715c1aa71eee7be", size = 559460, upload-time = "2025-08-07T08:26:16.295Z" }, - { url = "https://files.pythonhosted.org/packages/61/63/d1e127b40c3e4733b3a6f26ae7a063cdf2bc1caa5272c89075425c7d397a/rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:88051c3b7d5325409f433c5a40328fcb0685fc04e5db49ff936e910901d10114", size = 588072, upload-time = "2025-08-07T08:26:17.776Z" }, - { url = "https://files.pythonhosted.org/packages/04/7e/8ffc71a8f6833d9c9fb999f5b0ee736b8b159fd66968e05c7afc2dbcd57e/rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:181bc29e59e5e5e6e9d63b143ff4d5191224d355e246b5a48c88ce6b35c4e466", size = 555083, upload-time = "2025-08-07T08:26:19.301Z" }, +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/dc/95f074d43452b3ef5d06276696ece4b3b5d696e7c9ad7173c54b1390cd70/rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea", size = 27419, upload-time = "2025-10-22T22:24:29.327Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/f8/13bb772dc7cbf2c3c5b816febc34fa0cb2c64a08e0569869585684ce6631/rpds_py-0.28.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7b6013db815417eeb56b2d9d7324e64fcd4fa289caeee6e7a78b2e11fc9b438a", size = 362820, upload-time = "2025-10-22T22:21:15.074Z" }, + { url = "https://files.pythonhosted.org/packages/84/91/6acce964aab32469c3dbe792cb041a752d64739c534e9c493c701ef0c032/rpds_py-0.28.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a4c6b05c685c0c03f80dabaeb73e74218c49deea965ca63f76a752807397207", size = 348499, upload-time = "2025-10-22T22:21:17.658Z" }, + { url = "https://files.pythonhosted.org/packages/f1/93/c05bb1f4f5e0234db7c4917cb8dd5e2e0a9a7b26dc74b1b7bee3c9cfd477/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4794c6c3fbe8f9ac87699b131a1f26e7b4abcf6d828da46a3a52648c7930eba", size = 379356, upload-time = "2025-10-22T22:21:19.847Z" }, + { url = "https://files.pythonhosted.org/packages/5c/37/e292da436f0773e319753c567263427cdf6c645d30b44f09463ff8216cda/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e8456b6ee5527112ff2354dd9087b030e3429e43a74f480d4a5ca79d269fd85", size = 390151, upload-time = "2025-10-22T22:21:21.569Z" }, + { url = "https://files.pythonhosted.org/packages/76/87/a4e3267131616e8faf10486dc00eaedf09bd61c87f01e5ef98e782ee06c9/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:beb880a9ca0a117415f241f66d56025c02037f7c4efc6fe59b5b8454f1eaa50d", size = 524831, upload-time = "2025-10-22T22:21:23.394Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c8/4a4ca76f0befae9515da3fad11038f0fce44f6bb60b21fe9d9364dd51fb0/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6897bebb118c44b38c9cb62a178e09f1593c949391b9a1a6fe777ccab5934ee7", size = 404687, upload-time = "2025-10-22T22:21:25.201Z" }, + { url = "https://files.pythonhosted.org/packages/6a/65/118afe854424456beafbbebc6b34dcf6d72eae3a08b4632bc4220f8240d9/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b553dd06e875249fd43efd727785efb57a53180e0fde321468222eabbeaafa", size = 382683, upload-time = "2025-10-22T22:21:26.536Z" }, + { url = "https://files.pythonhosted.org/packages/f7/bc/0625064041fb3a0c77ecc8878c0e8341b0ae27ad0f00cf8f2b57337a1e63/rpds_py-0.28.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:f0b2044fdddeea5b05df832e50d2a06fe61023acb44d76978e1b060206a8a476", size = 398927, upload-time = "2025-10-22T22:21:27.864Z" }, + { url = "https://files.pythonhosted.org/packages/5d/1a/fed7cf2f1ee8a5e4778f2054153f2cfcf517748875e2f5b21cf8907cd77d/rpds_py-0.28.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05cf1e74900e8da73fa08cc76c74a03345e5a3e37691d07cfe2092d7d8e27b04", size = 411590, upload-time = "2025-10-22T22:21:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/a8e0f67fa374a6c472dbb0afdaf1ef744724f165abb6899f20e2f1563137/rpds_py-0.28.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:efd489fec7c311dae25e94fe7eeda4b3d06be71c68f2cf2e8ef990ffcd2cd7e8", size = 559843, upload-time = "2025-10-22T22:21:30.917Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ea/e10353f6d7c105be09b8135b72787a65919971ae0330ad97d87e4e199880/rpds_py-0.28.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ada7754a10faacd4f26067e62de52d6af93b6d9542f0df73c57b9771eb3ba9c4", size = 584188, upload-time = "2025-10-22T22:21:32.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/b0/a19743e0763caf0c89f6fc6ba6fbd9a353b24ffb4256a492420c5517da5a/rpds_py-0.28.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c2a34fd26588949e1e7977cfcbb17a9a42c948c100cab890c6d8d823f0586457", size = 550052, upload-time = "2025-10-22T22:21:34.702Z" }, + { url = "https://files.pythonhosted.org/packages/de/bc/ec2c004f6c7d6ab1e25dae875cdb1aee087c3ebed5b73712ed3000e3851a/rpds_py-0.28.0-cp310-cp310-win32.whl", hash = "sha256:f9174471d6920cbc5e82a7822de8dfd4dcea86eb828b04fc8c6519a77b0ee51e", size = 215110, upload-time = "2025-10-22T22:21:36.645Z" }, + { url = "https://files.pythonhosted.org/packages/6c/de/4ce8abf59674e17187023933547d2018363e8fc76ada4f1d4d22871ccb6e/rpds_py-0.28.0-cp310-cp310-win_amd64.whl", hash = "sha256:6e32dd207e2c4f8475257a3540ab8a93eff997abfa0a3fdb287cae0d6cd874b8", size = 223850, upload-time = "2025-10-22T22:21:38.006Z" }, + { url = "https://files.pythonhosted.org/packages/a6/34/058d0db5471c6be7bef82487ad5021ff8d1d1d27794be8730aad938649cf/rpds_py-0.28.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:03065002fd2e287725d95fbc69688e0c6daf6c6314ba38bdbaa3895418e09296", size = 362344, upload-time = "2025-10-22T22:21:39.713Z" }, + { url = "https://files.pythonhosted.org/packages/5d/67/9503f0ec8c055a0782880f300c50a2b8e5e72eb1f94dfc2053da527444dd/rpds_py-0.28.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28ea02215f262b6d078daec0b45344c89e161eab9526b0d898221d96fdda5f27", size = 348440, upload-time = "2025-10-22T22:21:41.056Z" }, + { url = "https://files.pythonhosted.org/packages/68/2e/94223ee9b32332a41d75b6f94b37b4ce3e93878a556fc5f152cbd856a81f/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25dbade8fbf30bcc551cb352376c0ad64b067e4fc56f90e22ba70c3ce205988c", size = 379068, upload-time = "2025-10-22T22:21:42.593Z" }, + { url = "https://files.pythonhosted.org/packages/b4/25/54fd48f9f680cfc44e6a7f39a5fadf1d4a4a1fd0848076af4a43e79f998c/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c03002f54cc855860bfdc3442928ffdca9081e73b5b382ed0b9e8efe6e5e205", size = 390518, upload-time = "2025-10-22T22:21:43.998Z" }, + { url = "https://files.pythonhosted.org/packages/1b/85/ac258c9c27f2ccb1bd5d0697e53a82ebcf8088e3186d5d2bf8498ee7ed44/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9699fa7990368b22032baf2b2dce1f634388e4ffc03dfefaaac79f4695edc95", size = 525319, upload-time = "2025-10-22T22:21:45.645Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/c6734774789566d46775f193964b76627cd5f42ecf246d257ce84d1912ed/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9b06fe1a75e05e0713f06ea0c89ecb6452210fd60e2f1b6ddc1067b990e08d9", size = 404896, upload-time = "2025-10-22T22:21:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/14e37ce83202c632c89b0691185dca9532288ff9d390eacae3d2ff771bae/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9f83e7b326a3f9ec3ef84cda98fb0a74c7159f33e692032233046e7fd15da2", size = 382862, upload-time = "2025-10-22T22:21:49.176Z" }, + { url = "https://files.pythonhosted.org/packages/6a/83/f3642483ca971a54d60caa4449f9d6d4dbb56a53e0072d0deff51b38af74/rpds_py-0.28.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:0d3259ea9ad8743a75a43eb7819324cdab393263c91be86e2d1901ee65c314e0", size = 398848, upload-time = "2025-10-22T22:21:51.024Z" }, + { url = "https://files.pythonhosted.org/packages/44/09/2d9c8b2f88e399b4cfe86efdf2935feaf0394e4f14ab30c6c5945d60af7d/rpds_py-0.28.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a7548b345f66f6695943b4ef6afe33ccd3f1b638bd9afd0f730dd255c249c9e", size = 412030, upload-time = "2025-10-22T22:21:52.665Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f5/e1cec473d4bde6df1fd3738be8e82d64dd0600868e76e92dfeaebbc2d18f/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9a40040aa388b037eb39416710fbcce9443498d2eaab0b9b45ae988b53f5c67", size = 559700, upload-time = "2025-10-22T22:21:54.123Z" }, + { url = "https://files.pythonhosted.org/packages/8d/be/73bb241c1649edbf14e98e9e78899c2c5e52bbe47cb64811f44d2cc11808/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f60c7ea34e78c199acd0d3cda37a99be2c861dd2b8cf67399784f70c9f8e57d", size = 584581, upload-time = "2025-10-22T22:21:56.102Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9c/ffc6e9218cd1eb5c2c7dbd276c87cd10e8c2232c456b554169eb363381df/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1571ae4292649100d743b26d5f9c63503bb1fedf538a8f29a98dce2d5ba6b4e6", size = 549981, upload-time = "2025-10-22T22:21:58.253Z" }, + { url = "https://files.pythonhosted.org/packages/5f/50/da8b6d33803a94df0149345ee33e5d91ed4d25fc6517de6a25587eae4133/rpds_py-0.28.0-cp311-cp311-win32.whl", hash = "sha256:5cfa9af45e7c1140af7321fa0bef25b386ee9faa8928c80dc3a5360971a29e8c", size = 214729, upload-time = "2025-10-22T22:21:59.625Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/b0f48c4c320ee24c8c20df8b44acffb7353991ddf688af01eef5f93d7018/rpds_py-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd8d86b5d29d1b74100982424ba53e56033dc47720a6de9ba0259cf81d7cecaa", size = 223977, upload-time = "2025-10-22T22:22:01.092Z" }, + { url = "https://files.pythonhosted.org/packages/b4/21/c8e77a2ac66e2ec4e21f18a04b4e9a0417ecf8e61b5eaeaa9360a91713b4/rpds_py-0.28.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e27d3a5709cc2b3e013bf93679a849213c79ae0573f9b894b284b55e729e120", size = 217326, upload-time = "2025-10-22T22:22:02.944Z" }, + { url = "https://files.pythonhosted.org/packages/b8/5c/6c3936495003875fe7b14f90ea812841a08fca50ab26bd840e924097d9c8/rpds_py-0.28.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b4f28583a4f247ff60cd7bdda83db8c3f5b05a7a82ff20dd4b078571747708f", size = 366439, upload-time = "2025-10-22T22:22:04.525Z" }, + { url = "https://files.pythonhosted.org/packages/56/f9/a0f1ca194c50aa29895b442771f036a25b6c41a35e4f35b1a0ea713bedae/rpds_py-0.28.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d678e91b610c29c4b3d52a2c148b641df2b4676ffe47c59f6388d58b99cdc424", size = 348170, upload-time = "2025-10-22T22:22:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/18/ea/42d243d3a586beb72c77fa5def0487daf827210069a95f36328e869599ea/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e819e0e37a44a78e1383bf1970076e2ccc4dc8c2bbaa2f9bd1dc987e9afff628", size = 378838, upload-time = "2025-10-22T22:22:07.932Z" }, + { url = "https://files.pythonhosted.org/packages/e7/78/3de32e18a94791af8f33601402d9d4f39613136398658412a4e0b3047327/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5ee514e0f0523db5d3fb171f397c54875dbbd69760a414dccf9d4d7ad628b5bd", size = 393299, upload-time = "2025-10-22T22:22:09.435Z" }, + { url = "https://files.pythonhosted.org/packages/13/7e/4bdb435afb18acea2eb8a25ad56b956f28de7c59f8a1d32827effa0d4514/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3fa06d27fdcee47f07a39e02862da0100cb4982508f5ead53ec533cd5fe55e", size = 518000, upload-time = "2025-10-22T22:22:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/31/d0/5f52a656875cdc60498ab035a7a0ac8f399890cc1ee73ebd567bac4e39ae/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46959ef2e64f9e4a41fc89aa20dbca2b85531f9a72c21099a3360f35d10b0d5a", size = 408746, upload-time = "2025-10-22T22:22:13.143Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/49ce51767b879cde77e7ad9fae164ea15dce3616fe591d9ea1df51152706/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8455933b4bcd6e83fde3fefc987a023389c4b13f9a58c8d23e4b3f6d13f78c84", size = 386379, upload-time = "2025-10-22T22:22:14.602Z" }, + { url = "https://files.pythonhosted.org/packages/6a/99/e4e1e1ee93a98f72fc450e36c0e4d99c35370220e815288e3ecd2ec36a2a/rpds_py-0.28.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ad50614a02c8c2962feebe6012b52f9802deec4263946cddea37aaf28dd25a66", size = 401280, upload-time = "2025-10-22T22:22:16.063Z" }, + { url = "https://files.pythonhosted.org/packages/61/35/e0c6a57488392a8b319d2200d03dad2b29c0db9996f5662c3b02d0b86c02/rpds_py-0.28.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e5deca01b271492553fdb6c7fd974659dce736a15bae5dad7ab8b93555bceb28", size = 412365, upload-time = "2025-10-22T22:22:17.504Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6a/841337980ea253ec797eb084665436007a1aad0faac1ba097fb906c5f69c/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:735f8495a13159ce6a0d533f01e8674cec0c57038c920495f87dcb20b3ddb48a", size = 559573, upload-time = "2025-10-22T22:22:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/e7/5e/64826ec58afd4c489731f8b00729c5f6afdb86f1df1df60bfede55d650bb/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:961ca621ff10d198bbe6ba4957decca61aa2a0c56695384c1d6b79bf61436df5", size = 583973, upload-time = "2025-10-22T22:22:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ee/44d024b4843f8386a4eeaa4c171b3d31d55f7177c415545fd1a24c249b5d/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2374e16cc9131022e7d9a8f8d65d261d9ba55048c78f3b6e017971a4f5e6353c", size = 553800, upload-time = "2025-10-22T22:22:22.25Z" }, + { url = "https://files.pythonhosted.org/packages/7d/89/33e675dccff11a06d4d85dbb4d1865f878d5020cbb69b2c1e7b2d3f82562/rpds_py-0.28.0-cp312-cp312-win32.whl", hash = "sha256:d15431e334fba488b081d47f30f091e5d03c18527c325386091f31718952fe08", size = 216954, upload-time = "2025-10-22T22:22:24.105Z" }, + { url = "https://files.pythonhosted.org/packages/af/36/45f6ebb3210887e8ee6dbf1bc710ae8400bb417ce165aaf3024b8360d999/rpds_py-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:a410542d61fc54710f750d3764380b53bf09e8c4edbf2f9141a82aa774a04f7c", size = 227844, upload-time = "2025-10-22T22:22:25.551Z" }, + { url = "https://files.pythonhosted.org/packages/57/91/f3fb250d7e73de71080f9a221d19bd6a1c1eb0d12a1ea26513f6c1052ad6/rpds_py-0.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:1f0cfd1c69e2d14f8c892b893997fa9a60d890a0c8a603e88dca4955f26d1edd", size = 217624, upload-time = "2025-10-22T22:22:26.914Z" }, + { url = "https://files.pythonhosted.org/packages/ae/bc/b43f2ea505f28119bd551ae75f70be0c803d2dbcd37c1b3734909e40620b/rpds_py-0.28.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f5e7101145427087e493b9c9b959da68d357c28c562792300dd21a095118ed16", size = 363913, upload-time = "2025-10-22T22:24:07.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/f2/db318195d324c89a2c57dc5195058cbadd71b20d220685c5bd1da79ee7fe/rpds_py-0.28.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:31eb671150b9c62409a888850aaa8e6533635704fe2b78335f9aaf7ff81eec4d", size = 350452, upload-time = "2025-10-22T22:24:08.754Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/1391c819b8573a4898cedd6b6c5ec5bc370ce59e5d6bdcebe3c9c1db4588/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b55c1f64482f7d8bd39942f376bfdf2f6aec637ee8c805b5041e14eeb771db", size = 380957, upload-time = "2025-10-22T22:24:10.826Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5c/e5de68ee7eb7248fce93269833d1b329a196d736aefb1a7481d1e99d1222/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24743a7b372e9a76171f6b69c01aedf927e8ac3e16c474d9fe20d552a8cb45c7", size = 391919, upload-time = "2025-10-22T22:24:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/fb/4f/2376336112cbfeb122fd435d608ad8d5041b3aed176f85a3cb32c262eb80/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:389c29045ee8bbb1627ea190b4976a310a295559eaf9f1464a1a6f2bf84dde78", size = 528541, upload-time = "2025-10-22T22:24:14.197Z" }, + { url = "https://files.pythonhosted.org/packages/68/53/5ae232e795853dd20da7225c5dd13a09c0a905b1a655e92bdf8d78a99fd9/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23690b5827e643150cf7b49569679ec13fe9a610a15949ed48b85eb7f98f34ec", size = 405629, upload-time = "2025-10-22T22:24:16.001Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2d/351a3b852b683ca9b6b8b38ed9efb2347596973849ba6c3a0e99877c10aa/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f0c9266c26580e7243ad0d72fc3e01d6b33866cfab5084a6da7576bcf1c4f72", size = 384123, upload-time = "2025-10-22T22:24:17.585Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/870804daa00202728cc91cb8e2385fa9f1f4eb49857c49cfce89e304eae6/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4c6c4db5d73d179746951486df97fd25e92396be07fc29ee8ff9a8f5afbdfb27", size = 400923, upload-time = "2025-10-22T22:24:19.512Z" }, + { url = "https://files.pythonhosted.org/packages/53/25/3706b83c125fa2a0bccceac951de3f76631f6bd0ee4d02a0ed780712ef1b/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3b695a8fa799dd2cfdb4804b37096c5f6dba1ac7f48a7fbf6d0485bcd060316", size = 413767, upload-time = "2025-10-22T22:24:21.316Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f9/ce43dbe62767432273ed2584cef71fef8411bddfb64125d4c19128015018/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:6aa1bfce3f83baf00d9c5fcdbba93a3ab79958b4c7d7d1f55e7fe68c20e63912", size = 561530, upload-time = "2025-10-22T22:24:22.958Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/ffe77999ed8f81e30713dd38fd9ecaa161f28ec48bb80fa1cd9118399c27/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:7b0f9dceb221792b3ee6acb5438eb1f02b0cb2c247796a72b016dcc92c6de829", size = 585453, upload-time = "2025-10-22T22:24:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d2/4a73b18821fd4669762c855fd1f4e80ceb66fb72d71162d14da58444a763/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5d0145edba8abd3db0ab22b5300c99dc152f5c9021fab861be0f0544dc3cbc5f", size = 552199, upload-time = "2025-10-22T22:24:26.54Z" }, ] [[package]] @@ -3450,57 +3819,58 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" }, - { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" }, - { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" }, - { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" }, - { url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" }, - { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" }, - { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" }, - { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" }, - { url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" }, - { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" }, - { url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload-time = "2025-08-07T19:05:38.468Z" }, - { url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload-time = "2025-08-07T19:05:40.391Z" }, - { url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" }, +version = "0.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/62/50b7727004dfe361104dfbf898c45a9a2fdfad8c72c04ae62900224d6ecf/ruff-0.14.3.tar.gz", hash = "sha256:4ff876d2ab2b161b6de0aa1f5bd714e8e9b4033dc122ee006925fbacc4f62153", size = 5558687, upload-time = "2025-10-31T00:26:26.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8e/0c10ff1ea5d4360ab8bfca4cb2c9d979101a391f3e79d2616c9bf348cd26/ruff-0.14.3-py3-none-linux_armv6l.whl", hash = "sha256:876b21e6c824f519446715c1342b8e60f97f93264012de9d8d10314f8a79c371", size = 12535613, upload-time = "2025-10-31T00:25:44.302Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c8/6724f4634c1daf52409fbf13fefda64aa9c8f81e44727a378b7b73dc590b/ruff-0.14.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6fd8c79b457bedd2abf2702b9b472147cd860ed7855c73a5247fa55c9117654", size = 12855812, upload-time = "2025-10-31T00:25:47.793Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/db1bce591d55fd5f8a08bb02517fa0b5097b2ccabd4ea1ee29aa72b67d96/ruff-0.14.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71ff6edca490c308f083156938c0c1a66907151263c4abdcb588602c6e696a14", size = 11944026, upload-time = "2025-10-31T00:25:49.657Z" }, + { url = "https://files.pythonhosted.org/packages/0b/75/4f8dbd48e03272715d12c87dc4fcaaf21b913f0affa5f12a4e9c6f8a0582/ruff-0.14.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:786ee3ce6139772ff9272aaf43296d975c0217ee1b97538a98171bf0d21f87ed", size = 12356818, upload-time = "2025-10-31T00:25:51.949Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9b/506ec5b140c11d44a9a4f284ea7c14ebf6f8b01e6e8917734a3325bff787/ruff-0.14.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd6291d0061811c52b8e392f946889916757610d45d004e41140d81fb6cd5ddc", size = 12336745, upload-time = "2025-10-31T00:25:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e1/c560d254048c147f35e7f8131d30bc1f63a008ac61595cf3078a3e93533d/ruff-0.14.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a497ec0c3d2c88561b6d90f9c29f5ae68221ac00d471f306fa21fa4264ce5fcd", size = 13101684, upload-time = "2025-10-31T00:25:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/a5/32/e310133f8af5cd11f8cc30f52522a3ebccc5ea5bff4b492f94faceaca7a8/ruff-0.14.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e231e1be58fc568950a04fbe6887c8e4b85310e7889727e2b81db205c45059eb", size = 14535000, upload-time = "2025-10-31T00:25:58.397Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a1/7b0470a22158c6d8501eabc5e9b6043c99bede40fa1994cadf6b5c2a61c7/ruff-0.14.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:469e35872a09c0e45fecf48dd960bfbce056b5db2d5e6b50eca329b4f853ae20", size = 14156450, upload-time = "2025-10-31T00:26:00.889Z" }, + { url = "https://files.pythonhosted.org/packages/0a/96/24bfd9d1a7f532b560dcee1a87096332e461354d3882124219bcaff65c09/ruff-0.14.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d6bc90307c469cb9d28b7cfad90aaa600b10d67c6e22026869f585e1e8a2db0", size = 13568414, upload-time = "2025-10-31T00:26:03.291Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e7/138b883f0dfe4ad5b76b58bf4ae675f4d2176ac2b24bdd81b4d966b28c61/ruff-0.14.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2f8a0bbcffcfd895df39c9a4ecd59bb80dca03dc43f7fb63e647ed176b741e", size = 13315293, upload-time = "2025-10-31T00:26:05.708Z" }, + { url = "https://files.pythonhosted.org/packages/33/f4/c09bb898be97b2eb18476b7c950df8815ef14cf956074177e9fbd40b7719/ruff-0.14.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:678fdd7c7d2d94851597c23ee6336d25f9930b460b55f8598e011b57c74fd8c5", size = 13539444, upload-time = "2025-10-31T00:26:08.09Z" }, + { url = "https://files.pythonhosted.org/packages/9c/aa/b30a1db25fc6128b1dd6ff0741fa4abf969ded161599d07ca7edd0739cc0/ruff-0.14.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1ec1ac071e7e37e0221d2f2dbaf90897a988c531a8592a6a5959f0603a1ecf5e", size = 12252581, upload-time = "2025-10-31T00:26:10.297Z" }, + { url = "https://files.pythonhosted.org/packages/da/13/21096308f384d796ffe3f2960b17054110a9c3828d223ca540c2b7cc670b/ruff-0.14.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afcdc4b5335ef440d19e7df9e8ae2ad9f749352190e96d481dc501b753f0733e", size = 12307503, upload-time = "2025-10-31T00:26:12.646Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cc/a350bac23f03b7dbcde3c81b154706e80c6f16b06ff1ce28ed07dc7b07b0/ruff-0.14.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7bfc42f81862749a7136267a343990f865e71fe2f99cf8d2958f684d23ce3dfa", size = 12675457, upload-time = "2025-10-31T00:26:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/cb/76/46346029fa2f2078826bc88ef7167e8c198e58fe3126636e52f77488cbba/ruff-0.14.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a65e448cfd7e9c59fae8cf37f9221585d3354febaad9a07f29158af1528e165f", size = 13403980, upload-time = "2025-10-31T00:26:17.81Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a4/35f1ef68c4e7b236d4a5204e3669efdeefaef21f0ff6a456792b3d8be438/ruff-0.14.3-py3-none-win32.whl", hash = "sha256:f3d91857d023ba93e14ed2d462ab62c3428f9bbf2b4fbac50a03ca66d31991f7", size = 12500045, upload-time = "2025-10-31T00:26:20.503Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/51960ae340823c9859fb60c63301d977308735403e2134e17d1d2858c7fb/ruff-0.14.3-py3-none-win_amd64.whl", hash = "sha256:d7b7006ac0756306db212fd37116cce2bd307e1e109375e1c6c106002df0ae5f", size = 13594005, upload-time = "2025-10-31T00:26:22.533Z" }, + { url = "https://files.pythonhosted.org/packages/b7/73/4de6579bac8e979fca0a77e54dec1f1e011a0d268165eb8a9bc0982a6564/ruff-0.14.3-py3-none-win_arm64.whl", hash = "sha256:26eb477ede6d399d898791d01961e16b86f02bc2486d0d1a7a9bb2379d055dc1", size = 12590017, upload-time = "2025-10-31T00:26:24.52Z" }, ] [[package]] name = "scikit-learn" -version = "1.7.1" +version = "1.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "joblib" }, { name = "numpy" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "threadpoolctl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/84/5f4af978fff619706b8961accac84780a6d298d82a8873446f72edb4ead0/scikit_learn-1.7.1.tar.gz", hash = "sha256:24b3f1e976a4665aa74ee0fcaac2b8fccc6ae77c8e07ab25da3ba6d3292b9802", size = 7190445, upload-time = "2025-07-18T08:01:54.5Z" } +sdist = { url = "https://files.pythonhosted.org/packages/37/59/44985a2bdc95c74e34fef3d10cb5d93ce13b0e2a7baefffe1b53853b502d/scikit_learn-1.5.2.tar.gz", hash = "sha256:b4237ed7b3fdd0a4882792e68ef2545d5baa50aca3bb45aa7df468138ad8f94d", size = 7001680, upload-time = "2024-09-11T15:50:10.957Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/88/0dd5be14ef19f2d80a77780be35a33aa94e8a3b3223d80bee8892a7832b4/scikit_learn-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:406204dd4004f0517f0b23cf4b28c6245cbd51ab1b6b78153bc784def214946d", size = 9338868, upload-time = "2025-07-18T08:01:00.25Z" }, - { url = "https://files.pythonhosted.org/packages/fd/52/3056b6adb1ac58a0bc335fc2ed2fcf599974d908855e8cb0ca55f797593c/scikit_learn-1.7.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:16af2e44164f05d04337fd1fc3ae7c4ea61fd9b0d527e22665346336920fe0e1", size = 8655943, upload-time = "2025-07-18T08:01:02.974Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a4/e488acdece6d413f370a9589a7193dac79cd486b2e418d3276d6ea0b9305/scikit_learn-1.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2f2e78e56a40c7587dea9a28dc4a49500fa2ead366869418c66f0fd75b80885c", size = 9652056, upload-time = "2025-07-18T08:01:04.978Z" }, - { url = "https://files.pythonhosted.org/packages/18/41/bceacec1285b94eb9e4659b24db46c23346d7e22cf258d63419eb5dec6f7/scikit_learn-1.7.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b62b76ad408a821475b43b7bb90a9b1c9a4d8d125d505c2df0539f06d6e631b1", size = 9473691, upload-time = "2025-07-18T08:01:07.006Z" }, - { url = "https://files.pythonhosted.org/packages/12/7b/e1ae4b7e1dd85c4ca2694ff9cc4a9690970fd6150d81b975e6c5c6f8ee7c/scikit_learn-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:9963b065677a4ce295e8ccdee80a1dd62b37249e667095039adcd5bce6e90deb", size = 8900873, upload-time = "2025-07-18T08:01:09.332Z" }, - { url = "https://files.pythonhosted.org/packages/b4/bd/a23177930abd81b96daffa30ef9c54ddbf544d3226b8788ce4c3ef1067b4/scikit_learn-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90c8494ea23e24c0fb371afc474618c1019dc152ce4a10e4607e62196113851b", size = 9334838, upload-time = "2025-07-18T08:01:11.239Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a1/d3a7628630a711e2ac0d1a482910da174b629f44e7dd8cfcd6924a4ef81a/scikit_learn-1.7.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:bb870c0daf3bf3be145ec51df8ac84720d9972170786601039f024bf6d61a518", size = 8651241, upload-time = "2025-07-18T08:01:13.234Z" }, - { url = "https://files.pythonhosted.org/packages/26/92/85ec172418f39474c1cd0221d611345d4f433fc4ee2fc68e01f524ccc4e4/scikit_learn-1.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40daccd1b5623f39e8943ab39735cadf0bdce80e67cdca2adcb5426e987320a8", size = 9718677, upload-time = "2025-07-18T08:01:15.649Z" }, - { url = "https://files.pythonhosted.org/packages/df/ce/abdb1dcbb1d2b66168ec43b23ee0cee356b4cc4100ddee3943934ebf1480/scikit_learn-1.7.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30d1f413cfc0aa5a99132a554f1d80517563c34a9d3e7c118fde2d273c6fe0f7", size = 9511189, upload-time = "2025-07-18T08:01:18.013Z" }, - { url = "https://files.pythonhosted.org/packages/b2/3b/47b5eaee01ef2b5a80ba3f7f6ecf79587cb458690857d4777bfd77371c6f/scikit_learn-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:c711d652829a1805a95d7fe96654604a8f16eab5a9e9ad87b3e60173415cb650", size = 8914794, upload-time = "2025-07-18T08:01:20.357Z" }, - { url = "https://files.pythonhosted.org/packages/cb/16/57f176585b35ed865f51b04117947fe20f130f78940c6477b6d66279c9c2/scikit_learn-1.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3cee419b49b5bbae8796ecd690f97aa412ef1674410c23fc3257c6b8b85b8087", size = 9260431, upload-time = "2025-07-18T08:01:22.77Z" }, - { url = "https://files.pythonhosted.org/packages/67/4e/899317092f5efcab0e9bc929e3391341cec8fb0e816c4789686770024580/scikit_learn-1.7.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2fd8b8d35817b0d9ebf0b576f7d5ffbbabdb55536b0655a8aaae629d7ffd2e1f", size = 8637191, upload-time = "2025-07-18T08:01:24.731Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1b/998312db6d361ded1dd56b457ada371a8d8d77ca2195a7d18fd8a1736f21/scikit_learn-1.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:588410fa19a96a69763202f1d6b7b91d5d7a5d73be36e189bc6396bfb355bd87", size = 9486346, upload-time = "2025-07-18T08:01:26.713Z" }, - { url = "https://files.pythonhosted.org/packages/ad/09/a2aa0b4e644e5c4ede7006748f24e72863ba2ae71897fecfd832afea01b4/scikit_learn-1.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3142f0abe1ad1d1c31a2ae987621e41f6b578144a911ff4ac94781a583adad7", size = 9290988, upload-time = "2025-07-18T08:01:28.938Z" }, - { url = "https://files.pythonhosted.org/packages/15/fa/c61a787e35f05f17fc10523f567677ec4eeee5f95aa4798dbbbcd9625617/scikit_learn-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3ddd9092c1bd469acab337d87930067c87eac6bd544f8d5027430983f1e1ae88", size = 8735568, upload-time = "2025-07-18T08:01:30.936Z" }, + { url = "https://files.pythonhosted.org/packages/98/89/be41419b4bec629a4691183a5eb1796f91252a13a5ffa243fd958cad7e91/scikit_learn-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:299406827fb9a4f862626d0fe6c122f5f87f8910b86fe5daa4c32dcd742139b6", size = 12106070, upload-time = "2024-09-11T15:49:19.633Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e0/3b6d777d375f3b685f433c93384cdb724fb078e1dc8f8ff0950467e56c30/scikit_learn-1.5.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:2d4cad1119c77930b235579ad0dc25e65c917e756fe80cab96aa3b9428bd3fb0", size = 10971758, upload-time = "2024-09-11T15:49:22.484Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/eb7dd56c371640753953277de11356c46a3149bfeebb3d7dcd90b993715a/scikit_learn-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c412ccc2ad9bf3755915e3908e677b367ebc8d010acbb3f182814524f2e5540", size = 12500080, upload-time = "2024-09-11T15:49:24.975Z" }, + { url = "https://files.pythonhosted.org/packages/4c/1e/a7c7357e704459c7d56a18df4a0bf08669442d1f8878cc0864beccd6306a/scikit_learn-1.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a686885a4b3818d9e62904d91b57fa757fc2bed3e465c8b177be652f4dd37c8", size = 13347241, upload-time = "2024-09-11T15:49:27.891Z" }, + { url = "https://files.pythonhosted.org/packages/48/76/154ebda6794faf0b0f3ccb1b5cd9a19f0a63cb9e1f3d2c61b6114002677b/scikit_learn-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:c15b1ca23d7c5f33cc2cb0a0d6aaacf893792271cddff0edbd6a40e8319bc113", size = 11000477, upload-time = "2024-09-11T15:49:30.693Z" }, + { url = "https://files.pythonhosted.org/packages/ff/91/609961972f694cb9520c4c3d201e377a26583e1eb83bc5a334c893729214/scikit_learn-1.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03b6158efa3faaf1feea3faa884c840ebd61b6484167c711548fce208ea09445", size = 12088580, upload-time = "2024-09-11T15:49:33.55Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7a/19fe32c810c5ceddafcfda16276d98df299c8649e24e84d4f00df4a91e01/scikit_learn-1.5.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1ff45e26928d3b4eb767a8f14a9a6efbf1cbff7c05d1fb0f95f211a89fd4f5de", size = 10975994, upload-time = "2024-09-11T15:49:35.728Z" }, + { url = "https://files.pythonhosted.org/packages/4c/75/62e49f8a62bf3c60b0e64d0fce540578ee4f0e752765beb2e1dc7c6d6098/scikit_learn-1.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f763897fe92d0e903aa4847b0aec0e68cadfff77e8a0687cabd946c89d17e675", size = 12465782, upload-time = "2024-09-11T15:49:38.596Z" }, + { url = "https://files.pythonhosted.org/packages/49/21/3723de321531c9745e40f1badafd821e029d346155b6c79704e0b7197552/scikit_learn-1.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8b0ccd4a902836493e026c03256e8b206656f91fbcc4fde28c57a5b752561f1", size = 13322034, upload-time = "2024-09-11T15:49:41.452Z" }, + { url = "https://files.pythonhosted.org/packages/17/1c/ccdd103cfcc9435a18819856fbbe0c20b8fa60bfc3343580de4be13f0668/scikit_learn-1.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:6c16d84a0d45e4894832b3c4d0bf73050939e21b99b01b6fd59cbb0cf39163b6", size = 11015224, upload-time = "2024-09-11T15:49:43.692Z" }, + { url = "https://files.pythonhosted.org/packages/a4/db/b485c1ac54ff3bd9e7e6b39d3cc6609c4c76a65f52ab0a7b22b6c3ab0e9d/scikit_learn-1.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f932a02c3f4956dfb981391ab24bda1dbd90fe3d628e4b42caef3e041c67707a", size = 12110344, upload-time = "2024-09-11T15:49:46.253Z" }, + { url = "https://files.pythonhosted.org/packages/54/1a/7deb52fa23aebb855431ad659b3c6a2e1709ece582cb3a63d66905e735fe/scikit_learn-1.5.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3b923d119d65b7bd555c73be5423bf06c0105678ce7e1f558cb4b40b0a5502b1", size = 11033502, upload-time = "2024-09-11T15:49:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/4a7a205b14c11225609b75b28402c196e4396ac754dab6a81971b811781c/scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f60021ec1574e56632be2a36b946f8143bf4e5e6af4a06d85281adc22938e0dd", size = 12085794, upload-time = "2024-09-11T15:49:51.388Z" }, + { url = "https://files.pythonhosted.org/packages/c6/29/044048c5e911373827c0e1d3051321b9183b2a4f8d4e2f11c08fcff83f13/scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:394397841449853c2290a32050382edaec3da89e35b3e03d6cc966aebc6a8ae6", size = 12945797, upload-time = "2024-09-11T15:49:53.579Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ce/c0b912f2f31aeb1b756a6ba56bcd84dd1f8a148470526a48515a3f4d48cd/scikit_learn-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:57cc1786cfd6bd118220a92ede80270132aa353647684efa385a74244a41e3b1", size = 10985467, upload-time = "2024-09-11T15:49:56.446Z" }, ] [[package]] @@ -3547,7 +3917,7 @@ wheels = [ [[package]] name = "scipy" -version = "1.16.1" +version = "1.16.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12' and sys_platform != 'win32'", @@ -3558,26 +3928,42 @@ resolution-markers = [ dependencies = [ { name = "numpy", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/4a/b927028464795439faec8eaf0b03b011005c487bb2d07409f28bf30879c4/scipy-1.16.1.tar.gz", hash = "sha256:44c76f9e8b6e8e488a586190ab38016e4ed2f8a038af7cd3defa903c0a2238b3", size = 30580861, upload-time = "2025-07-27T16:33:30.834Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/91/812adc6f74409b461e3a5fa97f4f74c769016919203138a3bf6fc24ba4c5/scipy-1.16.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:c033fa32bab91dc98ca59d0cf23bb876454e2bb02cbe592d5023138778f70030", size = 36552519, upload-time = "2025-07-27T16:26:29.658Z" }, - { url = "https://files.pythonhosted.org/packages/47/18/8e355edcf3b71418d9e9f9acd2708cc3a6c27e8f98fde0ac34b8a0b45407/scipy-1.16.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6e5c2f74e5df33479b5cd4e97a9104c511518fbd979aa9b8f6aec18b2e9ecae7", size = 28638010, upload-time = "2025-07-27T16:26:38.196Z" }, - { url = "https://files.pythonhosted.org/packages/d9/eb/e931853058607bdfbc11b86df19ae7a08686121c203483f62f1ecae5989c/scipy-1.16.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0a55ffe0ba0f59666e90951971a884d1ff6f4ec3275a48f472cfb64175570f77", size = 20909790, upload-time = "2025-07-27T16:26:43.93Z" }, - { url = "https://files.pythonhosted.org/packages/45/0c/be83a271d6e96750cd0be2e000f35ff18880a46f05ce8b5d3465dc0f7a2a/scipy-1.16.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f8a5d6cd147acecc2603fbd382fed6c46f474cccfcf69ea32582e033fb54dcfe", size = 23513352, upload-time = "2025-07-27T16:26:50.017Z" }, - { url = "https://files.pythonhosted.org/packages/7c/bf/fe6eb47e74f762f933cca962db7f2c7183acfdc4483bd1c3813cfe83e538/scipy-1.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb18899127278058bcc09e7b9966d41a5a43740b5bb8dcba401bd983f82e885b", size = 33534643, upload-time = "2025-07-27T16:26:57.503Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ba/63f402e74875486b87ec6506a4f93f6d8a0d94d10467280f3d9d7837ce3a/scipy-1.16.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adccd93a2fa937a27aae826d33e3bfa5edf9aa672376a4852d23a7cd67a2e5b7", size = 35376776, upload-time = "2025-07-27T16:27:06.639Z" }, - { url = "https://files.pythonhosted.org/packages/c3/b4/04eb9d39ec26a1b939689102da23d505ea16cdae3dbb18ffc53d1f831044/scipy-1.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:18aca1646a29ee9a0625a1be5637fa798d4d81fdf426481f06d69af828f16958", size = 35698906, upload-time = "2025-07-27T16:27:14.943Z" }, - { url = "https://files.pythonhosted.org/packages/04/d6/bb5468da53321baeb001f6e4e0d9049eadd175a4a497709939128556e3ec/scipy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d85495cef541729a70cdddbbf3e6b903421bc1af3e8e3a9a72a06751f33b7c39", size = 38129275, upload-time = "2025-07-27T16:27:23.873Z" }, - { url = "https://files.pythonhosted.org/packages/c4/94/994369978509f227cba7dfb9e623254d0d5559506fe994aef4bea3ed469c/scipy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:226652fca853008119c03a8ce71ffe1b3f6d2844cc1686e8f9806edafae68596", size = 38644572, upload-time = "2025-07-27T16:27:32.637Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d9/ec4864f5896232133f51382b54a08de91a9d1af7a76dfa372894026dfee2/scipy-1.16.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81b433bbeaf35728dad619afc002db9b189e45eebe2cd676effe1fb93fef2b9c", size = 36575194, upload-time = "2025-07-27T16:27:41.321Z" }, - { url = "https://files.pythonhosted.org/packages/5c/6d/40e81ecfb688e9d25d34a847dca361982a6addf8e31f0957b1a54fbfa994/scipy-1.16.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:886cc81fdb4c6903a3bb0464047c25a6d1016fef77bb97949817d0c0d79f9e04", size = 28594590, upload-time = "2025-07-27T16:27:49.204Z" }, - { url = "https://files.pythonhosted.org/packages/0e/37/9f65178edfcc629377ce9a64fc09baebea18c80a9e57ae09a52edf84880b/scipy-1.16.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:15240c3aac087a522b4eaedb09f0ad061753c5eebf1ea430859e5bf8640d5919", size = 20866458, upload-time = "2025-07-27T16:27:54.98Z" }, - { url = "https://files.pythonhosted.org/packages/2c/7b/749a66766871ea4cb1d1ea10f27004db63023074c22abed51f22f09770e0/scipy-1.16.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f81a25805f3659b48126b5053d9e823d3215e4a63730b5e1671852a1705921", size = 23539318, upload-time = "2025-07-27T16:28:01.604Z" }, - { url = "https://files.pythonhosted.org/packages/c4/db/8d4afec60eb833a666434d4541a3151eedbf2494ea6d4d468cbe877f00cd/scipy-1.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6c62eea7f607f122069b9bad3f99489ddca1a5173bef8a0c75555d7488b6f725", size = 33292899, upload-time = "2025-07-27T16:28:09.147Z" }, - { url = "https://files.pythonhosted.org/packages/51/1e/79023ca3bbb13a015d7d2757ecca3b81293c663694c35d6541b4dca53e98/scipy-1.16.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f965bbf3235b01c776115ab18f092a95aa74c271a52577bcb0563e85738fd618", size = 35162637, upload-time = "2025-07-27T16:28:17.535Z" }, - { url = "https://files.pythonhosted.org/packages/b6/49/0648665f9c29fdaca4c679182eb972935b3b4f5ace41d323c32352f29816/scipy-1.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f006e323874ffd0b0b816d8c6a8e7f9a73d55ab3b8c3f72b752b226d0e3ac83d", size = 35490507, upload-time = "2025-07-27T16:28:25.705Z" }, - { url = "https://files.pythonhosted.org/packages/62/8f/66cbb9d6bbb18d8c658f774904f42a92078707a7c71e5347e8bf2f52bb89/scipy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8fd15fc5085ab4cca74cb91fe0a4263b1f32e4420761ddae531ad60934c2119", size = 37923998, upload-time = "2025-07-27T16:28:34.339Z" }, - { url = "https://files.pythonhosted.org/packages/14/c3/61f273ae550fbf1667675701112e380881905e28448c080b23b5a181df7c/scipy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7b8013c6c066609577d910d1a2a077021727af07b6fab0ee22c2f901f22352a", size = 38508060, upload-time = "2025-07-27T16:28:43.242Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" }, + { url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" }, + { url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" }, + { url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" }, + { url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" }, + { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" }, + { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, + { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" }, + { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" }, +] + +[[package]] +name = "seaborn" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, ] [[package]] @@ -3591,15 +3977,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.34.1" +version = "2.43.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/38/10d6bfe23df1bfc65ac2262ed10b45823f47f810b0057d3feeea1ca5c7ed/sentry_sdk-2.34.1.tar.gz", hash = "sha256:69274eb8c5c38562a544c3e9f68b5be0a43be4b697f5fd385bf98e4fbe672687", size = 336969, upload-time = "2025-07-30T11:13:37.93Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/18/09875b4323b03ca9025bae7e6539797b27e4fc032998a466b4b9c3d24653/sentry_sdk-2.43.0.tar.gz", hash = "sha256:52ed6e251c5d2c084224d73efee56b007ef5c2d408a4a071270e82131d336e20", size = 368953, upload-time = "2025-10-29T11:26:08.156Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/3e/bb34de65a5787f76848a533afbb6610e01fbcdd59e76d8679c254e02255c/sentry_sdk-2.34.1-py2.py3-none-any.whl", hash = "sha256:b7a072e1cdc5abc48101d5146e1ae680fa81fe886d8d95aaa25a0b450c818d32", size = 357743, upload-time = "2025-07-30T11:13:36.145Z" }, + { url = "https://files.pythonhosted.org/packages/69/31/8228fa962f7fd8814d634e4ebece8780e2cdcfbdf0cd2e14d4a6861a7cd5/sentry_sdk-2.43.0-py2.py3-none-any.whl", hash = "sha256:4aacafcf1756ef066d359ae35030881917160ba7f6fc3ae11e0e58b09edc2d5d", size = 400997, upload-time = "2025-10-29T11:26:05.77Z" }, ] [[package]] @@ -3611,6 +3997,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, ] +[[package]] +name = "shap" +version = "0.46.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "numba" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "scikit-learn" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "slicer" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/46/1b497452be642e19af56044814dfe32ee795805b443378821136729017a0/shap-0.46.0.tar.gz", hash = "sha256:bdaa5b098be5a958348015e940f6fd264339b5db1e651f9898a3117be95b05a0", size = 1214102, upload-time = "2024-06-27T10:17:22.263Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a8/97442ec8e7aaad01d860768232b3b7051adb0560a9c79e52ce5e1222cbf1/shap-0.46.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:905b2d7a0262ef820785a7c0e3c7f24c9d281e6f934edb65cbe811fe0e971187", size = 459332, upload-time = "2024-06-27T10:16:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/00/b3/2795a586a4446c8cbf04b6e8f15c19b4a6fb867e5c6cf9fcbca97d56a20b/shap-0.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bccbb30ffbf8b9ed53e476d0c1319fdfcbeac455fe9df277fb0d570d92790e80", size = 455839, upload-time = "2024-06-27T10:16:37.654Z" }, + { url = "https://files.pythonhosted.org/packages/13/a6/b75760a52664dd82d530f9e232918bb74d1d6c39abcf34523c4f75cd4264/shap-0.46.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9633d3d7174acc01455538169ca6e6344f570530384548631aeadcf7bfdaaaea", size = 540067, upload-time = "2024-06-27T10:16:39.713Z" }, + { url = "https://files.pythonhosted.org/packages/35/13/70e07364855b05d8aa628ec5aec4f038444ede0e26eee2be00c38077ee72/shap-0.46.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6097eb2ab7e8c194254bac3e462266490fbdd43bfe35a1014e9ee21c4ef10ee", size = 537808, upload-time = "2024-06-27T10:16:41.955Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fc/dd28e6838630cd436914116aa07a019753a40b956a05831b71bd3f7ce914/shap-0.46.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0cf7c6e3f056cf3bfd16bcfd5744d0cc25b851555b1e750a3ab889b3077d2d05", size = 1538235, upload-time = "2024-06-27T10:16:43.681Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fe/f9e4d5e002bb58047c81edb6448579c179925c3807c98589ee70953587ab/shap-0.46.0-cp310-cp310-win_amd64.whl", hash = "sha256:949bd7fa40371c3f1885a30ae0611dd481bf4ac90066ff726c73cb5bb393032b", size = 456103, upload-time = "2024-06-27T10:16:46.764Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a1/43bd69f32ddf381a09de18ea94d4b215d5ced3a24ff1a7b7d1a9401b5b85/shap-0.46.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f18217c98f39fd485d541f6aab0b860b3be74b69b21d4faf11959e3fcba765c5", size = 459333, upload-time = "2024-06-27T10:16:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9e/dce41d5ec9e79add65faf4381d8d4492247b29daaa6cc7d7fd0298abc1e2/shap-0.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5bbdae4489577c6fce1cfe2d9d8f3d5b96d69284d29645fe651f78f6e965aeb4", size = 455835, upload-time = "2024-06-27T10:16:51.074Z" }, + { url = "https://files.pythonhosted.org/packages/06/6a/09e3cb9864118337c0f3c2a0dc5add6b642e9f672665062e186d67ba992d/shap-0.46.0-cp311-cp311-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13d36dc58d1e8c010feb4e7da71c77d23626a52d12d16b02869e793b11be4695", size = 540163, upload-time = "2024-06-27T10:16:53.179Z" }, + { url = "https://files.pythonhosted.org/packages/c3/74/440eacbdf21c1b2e0a5b6962b79d4435e56a88588043d144a16c7785a596/shap-0.46.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70e06fdfdf53d5fb932c82f4529397552b262e0ccce734f5226fb1e1eab2bc3e", size = 537765, upload-time = "2024-06-27T10:16:54.763Z" }, + { url = "https://files.pythonhosted.org/packages/08/e6/027ca36efcc8871eda4084bde5e4658a90e84006086186e39588fd03b396/shap-0.46.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:943f0806fa00b4fafb174f172a73d88de2d8600e6d69c2e2bff833f00e6c4c21", size = 1538290, upload-time = "2024-06-27T10:16:56.819Z" }, + { url = "https://files.pythonhosted.org/packages/82/29/923869e92c74bf07ec2b9a52ad5ac67d4184c873ba33ada7d4584356463a/shap-0.46.0-cp311-cp311-win_amd64.whl", hash = "sha256:c972a2efdc9fc00d543efaa55805eca947b8c418d065962d967824c2d5d295d0", size = 456103, upload-time = "2024-06-27T10:16:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/05/c5/3c4fe600dd71fd2785d21f86a3e7f1f13de60c9b434052e05ba17598f81e/shap-0.46.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a9cc9be191562bea1a782baff912854d267c6f4831bbf454d8d7bb7df7ddb214", size = 459316, upload-time = "2024-06-27T10:17:00.313Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1a/c00a1e7a68a4af29f2b40c8a8740dd241cba6cc58cd6ac266956a954a41d/shap-0.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab1fecfb43604605be17e26ae12bde4406c451c46b54b980d9570cec03fbc239", size = 455333, upload-time = "2024-06-27T10:17:02.719Z" }, + { url = "https://files.pythonhosted.org/packages/7f/0a/e3ab0dcddf4db1158fbf0d6c96348ba5f3031275f59088e0e3b7630cdcde/shap-0.46.0-cp312-cp312-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b216adf2a17b0e0694f17965ac29354ca8c4f27ac3c66f68bf6fc4cb2aa28207", size = 543894, upload-time = "2024-06-27T10:17:04.941Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8f/ca077689b76161b51b420031b88948ef92ade55730e85490215222734729/shap-0.46.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6e5dc5257b747a784f7a9b3acb64216a9011f01734f3c96b27fe5e15ae5f99f", size = 540735, upload-time = "2024-06-27T10:17:06.61Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b6/169de0d8971c91decd3dacfd63edeeedfc1bba30bfc6abf8480142aafd48/shap-0.46.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1230bf973463041dfa15734f290fbf3ab9c6e4e8222339c76f68fc355b940d80", size = 1537953, upload-time = "2024-06-27T10:17:08.225Z" }, + { url = "https://files.pythonhosted.org/packages/04/58/b2ea558ec8d9ed3728e83dfacb1b920c54a1a1f6feee2632c04676c3c1e9/shap-0.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:0cbbf996537b2a42d3bc7f2a13492988822ee1bfd7220700989408dfb9e1c5ad", size = 456226, upload-time = "2024-06-27T10:17:10.589Z" }, +] + [[package]] name = "shellingham" version = "1.5.4" @@ -3629,6 +4053,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053, upload-time = "2021-05-05T14:18:17.237Z" }, ] +[[package]] +name = "slicer" +version = "0.0.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/f9/b4bce2825b39b57760b361e6131a3dacee3d8951c58cb97ad120abb90317/slicer-0.0.8.tar.gz", hash = "sha256:2e7553af73f0c0c2d355f4afcc3ecf97c6f2156fcf4593955c3f56cf6c4d6eb7", size = 14894, upload-time = "2024-03-09T23:35:26.826Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/81/9ef641ff4e12cbcca30e54e72fb0951a2ba195d0cda0ba4100e532d929db/slicer-0.0.8-py3-none-any.whl", hash = "sha256:6c206258543aecd010d497dc2eca9d2805860a0b3758673903456b7df7934dc3", size = 15251, upload-time = "2024-03-09T07:03:07.708Z" }, +] + [[package]] name = "smmap" version = "5.0.2" @@ -3649,48 +4082,48 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.7" +version = "2.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, ] [[package]] name = "sqlalchemy" -version = "2.0.43" +version = "2.0.44" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d7/bc/d59b5d97d27229b0e009bd9098cd81af71c2fa5549c580a0a67b9bed0496/sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", size = 9762949, upload-time = "2025-08-11T14:24:58.438Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/4e/985f7da36f09592c5ade99321c72c15101d23c0bb7eecfd1daaca5714422/sqlalchemy-2.0.43-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70322986c0c699dca241418fcf18e637a4369e0ec50540a2b907b184c8bca069", size = 2133162, upload-time = "2025-08-11T15:52:17.854Z" }, - { url = "https://files.pythonhosted.org/packages/37/34/798af8db3cae069461e3bc0898a1610dc469386a97048471d364dc8aae1c/sqlalchemy-2.0.43-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87accdbba88f33efa7b592dc2e8b2a9c2cdbca73db2f9d5c510790428c09c154", size = 2123082, upload-time = "2025-08-11T15:52:19.181Z" }, - { url = "https://files.pythonhosted.org/packages/fb/0f/79cf4d9dad42f61ec5af1e022c92f66c2d110b93bb1dc9b033892971abfa/sqlalchemy-2.0.43-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c00e7845d2f692ebfc7d5e4ec1a3fd87698e4337d09e58d6749a16aedfdf8612", size = 3208871, upload-time = "2025-08-11T15:50:30.656Z" }, - { url = "https://files.pythonhosted.org/packages/56/b3/59befa58fb0e1a9802c87df02344548e6d007e77e87e6084e2131c29e033/sqlalchemy-2.0.43-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:022e436a1cb39b13756cf93b48ecce7aa95382b9cfacceb80a7d263129dfd019", size = 3209583, upload-time = "2025-08-11T15:57:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/29/d2/124b50c0eb8146e8f0fe16d01026c1a073844f0b454436d8544fe9b33bd7/sqlalchemy-2.0.43-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c5e73ba0d76eefc82ec0219d2301cb33bfe5205ed7a2602523111e2e56ccbd20", size = 3148177, upload-time = "2025-08-11T15:50:32.078Z" }, - { url = "https://files.pythonhosted.org/packages/83/f5/e369cd46aa84278107624617034a5825fedfc5c958b2836310ced4d2eadf/sqlalchemy-2.0.43-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c2e02f06c68092b875d5cbe4824238ab93a7fa35d9c38052c033f7ca45daa18", size = 3172276, upload-time = "2025-08-11T15:57:49.477Z" }, - { url = "https://files.pythonhosted.org/packages/de/2b/4602bf4c3477fa4c837c9774e6dd22e0389fc52310c4c4dfb7e7ba05e90d/sqlalchemy-2.0.43-cp310-cp310-win32.whl", hash = "sha256:e7a903b5b45b0d9fa03ac6a331e1c1d6b7e0ab41c63b6217b3d10357b83c8b00", size = 2101491, upload-time = "2025-08-11T15:54:59.191Z" }, - { url = "https://files.pythonhosted.org/packages/38/2d/bfc6b6143adef553a08295490ddc52607ee435b9c751c714620c1b3dd44d/sqlalchemy-2.0.43-cp310-cp310-win_amd64.whl", hash = "sha256:4bf0edb24c128b7be0c61cd17eef432e4bef507013292415f3fb7023f02b7d4b", size = 2125148, upload-time = "2025-08-11T15:55:00.593Z" }, - { url = "https://files.pythonhosted.org/packages/9d/77/fa7189fe44114658002566c6fe443d3ed0ec1fa782feb72af6ef7fbe98e7/sqlalchemy-2.0.43-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:52d9b73b8fb3e9da34c2b31e6d99d60f5f99fd8c1225c9dad24aeb74a91e1d29", size = 2136472, upload-time = "2025-08-11T15:52:21.789Z" }, - { url = "https://files.pythonhosted.org/packages/99/ea/92ac27f2fbc2e6c1766bb807084ca455265707e041ba027c09c17d697867/sqlalchemy-2.0.43-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f42f23e152e4545157fa367b2435a1ace7571cab016ca26038867eb7df2c3631", size = 2126535, upload-time = "2025-08-11T15:52:23.109Z" }, - { url = "https://files.pythonhosted.org/packages/94/12/536ede80163e295dc57fff69724caf68f91bb40578b6ac6583a293534849/sqlalchemy-2.0.43-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fb1a8c5438e0c5ea51afe9c6564f951525795cf432bed0c028c1cb081276685", size = 3297521, upload-time = "2025-08-11T15:50:33.536Z" }, - { url = "https://files.pythonhosted.org/packages/03/b5/cacf432e6f1fc9d156eca0560ac61d4355d2181e751ba8c0cd9cb232c8c1/sqlalchemy-2.0.43-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db691fa174e8f7036afefe3061bc40ac2b770718be2862bfb03aabae09051aca", size = 3297343, upload-time = "2025-08-11T15:57:51.186Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ba/d4c9b526f18457667de4c024ffbc3a0920c34237b9e9dd298e44c7c00ee5/sqlalchemy-2.0.43-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe2b3b4927d0bc03d02ad883f402d5de201dbc8894ac87d2e981e7d87430e60d", size = 3232113, upload-time = "2025-08-11T15:50:34.949Z" }, - { url = "https://files.pythonhosted.org/packages/aa/79/c0121b12b1b114e2c8a10ea297a8a6d5367bc59081b2be896815154b1163/sqlalchemy-2.0.43-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d3d9b904ad4a6b175a2de0738248822f5ac410f52c2fd389ada0b5262d6a1e3", size = 3258240, upload-time = "2025-08-11T15:57:52.983Z" }, - { url = "https://files.pythonhosted.org/packages/79/99/a2f9be96fb382f3ba027ad42f00dbe30fdb6ba28cda5f11412eee346bec5/sqlalchemy-2.0.43-cp311-cp311-win32.whl", hash = "sha256:5cda6b51faff2639296e276591808c1726c4a77929cfaa0f514f30a5f6156921", size = 2101248, upload-time = "2025-08-11T15:55:01.855Z" }, - { url = "https://files.pythonhosted.org/packages/ee/13/744a32ebe3b4a7a9c7ea4e57babae7aa22070d47acf330d8e5a1359607f1/sqlalchemy-2.0.43-cp311-cp311-win_amd64.whl", hash = "sha256:c5d1730b25d9a07727d20ad74bc1039bbbb0a6ca24e6769861c1aa5bf2c4c4a8", size = 2126109, upload-time = "2025-08-11T15:55:04.092Z" }, - { url = "https://files.pythonhosted.org/packages/61/db/20c78f1081446095450bdc6ee6cc10045fce67a8e003a5876b6eaafc5cc4/sqlalchemy-2.0.43-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:20d81fc2736509d7a2bd33292e489b056cbae543661bb7de7ce9f1c0cd6e7f24", size = 2134891, upload-time = "2025-08-11T15:51:13.019Z" }, - { url = "https://files.pythonhosted.org/packages/45/0a/3d89034ae62b200b4396f0f95319f7d86e9945ee64d2343dcad857150fa2/sqlalchemy-2.0.43-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b9fc27650ff5a2c9d490c13c14906b918b0de1f8fcbb4c992712d8caf40e83", size = 2123061, upload-time = "2025-08-11T15:51:14.319Z" }, - { url = "https://files.pythonhosted.org/packages/cb/10/2711f7ff1805919221ad5bee205971254845c069ee2e7036847103ca1e4c/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6772e3ca8a43a65a37c88e2f3e2adfd511b0b1da37ef11ed78dea16aeae85bd9", size = 3320384, upload-time = "2025-08-11T15:52:35.088Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0e/3d155e264d2ed2778484006ef04647bc63f55b3e2d12e6a4f787747b5900/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a113da919c25f7f641ffbd07fbc9077abd4b3b75097c888ab818f962707eb48", size = 3329648, upload-time = "2025-08-11T15:56:34.153Z" }, - { url = "https://files.pythonhosted.org/packages/5b/81/635100fb19725c931622c673900da5efb1595c96ff5b441e07e3dd61f2be/sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4286a1139f14b7d70141c67a8ae1582fc2b69105f1b09d9573494eb4bb4b2687", size = 3258030, upload-time = "2025-08-11T15:52:36.933Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ed/a99302716d62b4965fded12520c1cbb189f99b17a6d8cf77611d21442e47/sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:529064085be2f4d8a6e5fab12d36ad44f1909a18848fcfbdb59cc6d4bbe48efe", size = 3294469, upload-time = "2025-08-11T15:56:35.553Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a2/3a11b06715149bf3310b55a98b5c1e84a42cfb949a7b800bc75cb4e33abc/sqlalchemy-2.0.43-cp312-cp312-win32.whl", hash = "sha256:b535d35dea8bbb8195e7e2b40059e2253acb2b7579b73c1b432a35363694641d", size = 2098906, upload-time = "2025-08-11T15:55:00.645Z" }, - { url = "https://files.pythonhosted.org/packages/bc/09/405c915a974814b90aa591280623adc6ad6b322f61fd5cff80aeaef216c9/sqlalchemy-2.0.43-cp312-cp312-win_amd64.whl", hash = "sha256:1c6d85327ca688dbae7e2b06d7d84cfe4f3fffa5b5f9e21bb6ce9d0e1a0e0e0a", size = 2126260, upload-time = "2025-08-11T15:55:02.965Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d9/13bdde6521f322861fab67473cec4b1cc8999f3871953531cf61945fad92/sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", size = 1924759, upload-time = "2025-08-11T15:39:53.024Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/a7/e9ccfa7eecaf34c6f57d8cb0bb7cbdeeff27017cc0f5d0ca90fdde7a7c0d/sqlalchemy-2.0.44-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce", size = 2137282, upload-time = "2025-10-10T15:36:10.965Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/50bc121885bdf10833a4f65ecbe9fe229a3215f4d65a58da8a181734cae3/sqlalchemy-2.0.44-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985", size = 2127322, upload-time = "2025-10-10T15:36:12.428Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/a8573b7230a3ce5ee4b961a2d510d71b43872513647398e595b744344664/sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0", size = 3214772, upload-time = "2025-10-10T15:34:15.09Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/c63d8adb6a7edaf8dcb6f75a2b1e9f8577960a1e489606859c4d73e7d32b/sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e", size = 3214434, upload-time = "2025-10-10T15:47:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a6/243d277a4b54fae74d4797957a7320a5c210c293487f931cbe036debb697/sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749", size = 3155365, upload-time = "2025-10-10T15:34:17.932Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f8/6a39516ddd75429fd4ee5a0d72e4c80639fab329b2467c75f363c2ed9751/sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2", size = 3178910, upload-time = "2025-10-10T15:47:02.346Z" }, + { url = "https://files.pythonhosted.org/packages/43/f0/118355d4ad3c39d9a2f5ee4c7304a9665b3571482777357fa9920cd7a6b4/sqlalchemy-2.0.44-cp310-cp310-win32.whl", hash = "sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165", size = 2105624, upload-time = "2025-10-10T15:38:15.552Z" }, + { url = "https://files.pythonhosted.org/packages/61/83/6ae5f9466f8aa5d0dcebfff8c9c33b98b27ce23292df3b990454b3d434fd/sqlalchemy-2.0.44-cp310-cp310-win_amd64.whl", hash = "sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5", size = 2129240, upload-time = "2025-10-10T15:38:17.175Z" }, + { url = "https://files.pythonhosted.org/packages/e3/81/15d7c161c9ddf0900b076b55345872ed04ff1ed6a0666e5e94ab44b0163c/sqlalchemy-2.0.44-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd", size = 2140517, upload-time = "2025-10-10T15:36:15.64Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d5/4abd13b245c7d91bdf131d4916fd9e96a584dac74215f8b5bc945206a974/sqlalchemy-2.0.44-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa", size = 2130738, upload-time = "2025-10-10T15:36:16.91Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3c/8418969879c26522019c1025171cefbb2a8586b6789ea13254ac602986c0/sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e", size = 3304145, upload-time = "2025-10-10T15:34:19.569Z" }, + { url = "https://files.pythonhosted.org/packages/94/2d/fdb9246d9d32518bda5d90f4b65030b9bf403a935cfe4c36a474846517cb/sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e", size = 3304511, upload-time = "2025-10-10T15:47:05.088Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/40f2ad1da97d5c83f6c1269664678293d3fe28e90ad17a1093b735420549/sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399", size = 3235161, upload-time = "2025-10-10T15:34:21.193Z" }, + { url = "https://files.pythonhosted.org/packages/95/cb/7cf4078b46752dca917d18cf31910d4eff6076e5b513c2d66100c4293d83/sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b", size = 3261426, upload-time = "2025-10-10T15:47:07.196Z" }, + { url = "https://files.pythonhosted.org/packages/f8/3b/55c09b285cb2d55bdfa711e778bdffdd0dc3ffa052b0af41f1c5d6e582fa/sqlalchemy-2.0.44-cp311-cp311-win32.whl", hash = "sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3", size = 2105392, upload-time = "2025-10-10T15:38:20.051Z" }, + { url = "https://files.pythonhosted.org/packages/c7/23/907193c2f4d680aedbfbdf7bf24c13925e3c7c292e813326c1b84a0b878e/sqlalchemy-2.0.44-cp311-cp311-win_amd64.whl", hash = "sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5", size = 2130293, upload-time = "2025-10-10T15:38:21.601Z" }, + { url = "https://files.pythonhosted.org/packages/62/c4/59c7c9b068e6813c898b771204aad36683c96318ed12d4233e1b18762164/sqlalchemy-2.0.44-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250", size = 2139675, upload-time = "2025-10-10T16:03:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ae/eeb0920537a6f9c5a3708e4a5fc55af25900216bdb4847ec29cfddf3bf3a/sqlalchemy-2.0.44-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29", size = 2127726, upload-time = "2025-10-10T16:03:35.934Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d5/2ebbabe0379418eda8041c06b0b551f213576bfe4c2f09d77c06c07c8cc5/sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44", size = 3327603, upload-time = "2025-10-10T15:35:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/5aa65852dadc24b7d8ae75b7efb8d19303ed6ac93482e60c44a585930ea5/sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1", size = 3337842, upload-time = "2025-10-10T15:43:45.431Z" }, + { url = "https://files.pythonhosted.org/packages/41/92/648f1afd3f20b71e880ca797a960f638d39d243e233a7082c93093c22378/sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7", size = 3264558, upload-time = "2025-10-10T15:35:29.93Z" }, + { url = "https://files.pythonhosted.org/packages/40/cf/e27d7ee61a10f74b17740918e23cbc5bc62011b48282170dc4c66da8ec0f/sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d", size = 3301570, upload-time = "2025-10-10T15:43:48.407Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3d/3116a9a7b63e780fb402799b6da227435be878b6846b192f076d2f838654/sqlalchemy-2.0.44-cp312-cp312-win32.whl", hash = "sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4", size = 2103447, upload-time = "2025-10-10T15:03:21.678Z" }, + { url = "https://files.pythonhosted.org/packages/25/83/24690e9dfc241e6ab062df82cc0df7f4231c79ba98b273fa496fb3dd78ed/sqlalchemy-2.0.44-cp312-cp312-win_amd64.whl", hash = "sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e", size = 2130912, upload-time = "2025-10-10T15:03:24.656Z" }, + { url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" }, ] [[package]] @@ -3708,9 +4141,11 @@ version = "0.2.0.dev0" source = { editable = "." } dependencies = [ { name = "bcrypt" }, + { name = "cachetools" }, { name = "cloud-sql-python-connector", extra = ["pymysql"] }, { name = "databricks-sdk" }, - { name = "databricks-sql-connector" }, + { name = "databricks-sql-connector", extra = ["pyarrow"] }, + { name = "edvise" }, { name = "fastapi", extra = ["standard"] }, { name = "google-cloud-storage" }, { name = "jsonpickle" }, @@ -3729,6 +4164,7 @@ dependencies = [ { name = "strenum" }, { name = "thefuzz" }, { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "types-cachetools" }, { name = "types-paramiko" }, { name = "types-requests" }, ] @@ -3748,13 +4184,15 @@ dev = [ [package.metadata] requires-dist = [ { name = "bcrypt", specifier = "~=4.2.0" }, + { name = "cachetools" }, { name = "cloud-sql-python-connector", extras = ["pymysql"], specifier = "~=1.14.0" }, { name = "databricks-sdk", specifier = "~=0.38.0" }, - { name = "databricks-sql-connector", specifier = "~=3.5.0" }, + { name = "databricks-sql-connector", extras = ["pyarrow"], specifier = "~=4.2.0" }, + { name = "edvise", git = "https://github.com/datakind/edvise.git?rev=develop" }, { name = "fastapi", extras = ["standard"], specifier = "~=0.115.4" }, - { name = "google-cloud-storage", specifier = "~=2.18.2" }, + { name = "google-cloud-storage", specifier = "==2.19.0" }, { name = "jsonpickle", specifier = "~=4.0.1" }, - { name = "mlflow", specifier = "~=2.15.0" }, + { name = "mlflow", specifier = "~=2.22" }, { name = "pandas", specifier = "~=2.0" }, { name = "pandera", specifier = "~=0.13" }, { name = "paramiko", specifier = "~=3.5.0" }, @@ -3769,6 +4207,7 @@ requires-dist = [ { name = "strenum", specifier = "~=0.4.15" }, { name = "thefuzz", extras = ["speedup"], specifier = "~=0.22.1" }, { name = "tomli", marker = "python_full_version < '3.11'", specifier = "~=2.0" }, + { name = "types-cachetools" }, { name = "types-paramiko", specifier = "~=3.5.0.0" }, { name = "types-requests", specifier = "~=2.32.0.0" }, ] @@ -3811,6 +4250,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, ] +[[package]] +name = "statsmodels" +version = "0.14.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "patsy" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/81/e8d74b34f85285f7335d30c5e3c2d7c0346997af9f3debf9a0a9a63de184/statsmodels-0.14.6.tar.gz", hash = "sha256:4d17873d3e607d398b85126cd4ed7aad89e4e9d89fc744cdab1af3189a996c2a", size = 20689085, upload-time = "2025-12-05T23:08:39.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/6d/9ec309a175956f88eb8420ac564297f37cf9b1f73f89db74da861052dc29/statsmodels-0.14.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4ff0649a2df674c7ffb6fa1a06bffdb82a6adf09a48e90e000a15a6aaa734b0", size = 10142419, upload-time = "2025-12-05T19:27:35.625Z" }, + { url = "https://files.pythonhosted.org/packages/86/8f/338c5568315ec5bf3ac7cd4b71e34b98cb3b0f834919c0c04a0762f878a1/statsmodels-0.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:109012088b3e370080846ab053c76d125268631410142daad2f8c10770e8e8d9", size = 10022819, upload-time = "2025-12-05T19:27:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/b0/77/5fc4cbc2d608f9b483b0675f82704a8bcd672962c379fe4d82100d388dbf/statsmodels-0.14.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e93bd5d220f3cb6fc5fc1bffd5b094966cab8ee99f6c57c02e95710513d6ac3f", size = 10118927, upload-time = "2025-12-05T23:07:51.256Z" }, + { url = "https://files.pythonhosted.org/packages/94/55/b86c861c32186403fe121d9ab27bc16d05839b170d92a978beb33abb995e/statsmodels-0.14.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06eec42d682fdb09fe5d70a05930857efb141754ec5a5056a03304c1b5e32fd9", size = 10413015, upload-time = "2025-12-05T23:08:53.95Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/daf0dba729ccdc4176605f4a0fd5cfe71cdda671749dca10e74a732b8b1c/statsmodels-0.14.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0444e88557df735eda7db330806fe09d51c9f888bb1f5906cb3a61fb1a3ed4a8", size = 10441248, upload-time = "2025-12-05T23:09:09.353Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1c/2e10b7c7cc44fa418272996bf0427b8016718fd62f995d9c1f7ab37adf35/statsmodels-0.14.6-cp310-cp310-win_amd64.whl", hash = "sha256:e83a9abe653835da3b37fb6ae04b45480c1de11b3134bd40b09717192a1456ea", size = 9583410, upload-time = "2025-12-05T19:28:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4d/df4dd089b406accfc3bb5ee53ba29bb3bdf5ae61643f86f8f604baa57656/statsmodels-0.14.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ad5c2810fc6c684254a7792bf1cbaf1606cdee2a253f8bd259c43135d87cfb4", size = 10121514, upload-time = "2025-12-05T19:28:16.521Z" }, + { url = "https://files.pythonhosted.org/packages/82/af/ec48daa7f861f993b91a0dcc791d66e1cf56510a235c5cbd2ab991a31d5c/statsmodels-0.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:341fa68a7403e10a95c7b6e41134b0da3a7b835ecff1eb266294408535a06eb6", size = 10003346, upload-time = "2025-12-05T19:28:29.568Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2c/c8f7aa24cd729970728f3f98822fb45149adc216f445a9301e441f7ac760/statsmodels-0.14.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdf1dfe2a3ca56f5529118baf33a13efed2783c528f4a36409b46bbd2d9d48eb", size = 10129872, upload-time = "2025-12-05T23:09:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/40/c6/9ae8e9b0721e9b6eb5f340c3a0ce8cd7cce4f66e03dd81f80d60f111987f/statsmodels-0.14.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3764ba8195c9baf0925a96da0743ff218067a269f01d155ca3558deed2658ca", size = 10381964, upload-time = "2025-12-05T23:09:41.326Z" }, + { url = "https://files.pythonhosted.org/packages/28/8c/cf3d30c8c2da78e2ad1f50ade8b7fabec3ff4cdfc56fbc02e097c4577f90/statsmodels-0.14.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e8d2e519852adb1b420e018f5ac6e6684b2b877478adf7fda2cfdb58f5acb5d", size = 10409611, upload-time = "2025-12-05T23:09:57.131Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cc/018f14ecb58c6cb89de9d52695740b7d1f5a982aa9ea312483ea3c3d5f77/statsmodels-0.14.6-cp311-cp311-win_amd64.whl", hash = "sha256:2738a00fca51196f5a7d44b06970ace6b8b30289839e4808d656f8a98e35faa7", size = 9580385, upload-time = "2025-12-05T19:28:42.778Z" }, + { url = "https://files.pythonhosted.org/packages/25/ce/308e5e5da57515dd7cab3ec37ea2d5b8ff50bef1fcc8e6d31456f9fae08e/statsmodels-0.14.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe76140ae7adc5ff0e60a3f0d56f4fffef484efa803c3efebf2fcd734d72ecb5", size = 10091932, upload-time = "2025-12-05T19:28:55.446Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/affbabf3c27fb501ec7b5808230c619d4d1a4525c07301074eb4bda92fa9/statsmodels-0.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26d4f0ed3b31f3c86f83a92f5c1f5cbe63fc992cd8915daf28ca49be14463a1c", size = 9997345, upload-time = "2025-12-05T19:29:10.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/f5/3a73b51e6450c31652c53a8e12e24eac64e3824be816c0c2316e7dbdcb7d/statsmodels-0.14.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8c00a42863e4f4733ac9d078bbfad816249c01451740e6f5053ecc7db6d6368", size = 10058649, upload-time = "2025-12-05T23:10:12.775Z" }, + { url = "https://files.pythonhosted.org/packages/81/68/dddd76117df2ef14c943c6bbb6618be5c9401280046f4ddfc9fb4596a1b8/statsmodels-0.14.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b58cf7474aa9e7e3b0771a66537148b2df9b5884fbf156096c0e6c1ff0469d", size = 10339446, upload-time = "2025-12-05T23:10:28.503Z" }, + { url = "https://files.pythonhosted.org/packages/56/4a/dce451c74c4050535fac1ec0c14b80706d8fc134c9da22db3c8a0ec62c33/statsmodels-0.14.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81e7dcc5e9587f2567e52deaff5220b175bf2f648951549eae5fc9383b62bc37", size = 10368705, upload-time = "2025-12-05T23:10:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/60/15/3daba2df40be8b8a9a027d7f54c8dedf24f0d81b96e54b52293f5f7e3418/statsmodels-0.14.6-cp312-cp312-win_amd64.whl", hash = "sha256:b5eb07acd115aa6208b4058211138393a7e6c2cf12b6f213ede10f658f6a714f", size = 9543991, upload-time = "2025-12-05T23:10:58.536Z" }, +] + [[package]] name = "strenum" version = "0.4.15" @@ -3820,6 +4293,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/69/297302c5f5f59c862faa31e6cb9a4cd74721cd1e052b38e464c5b402df8b/StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659", size = 8851, upload-time = "2023-06-29T22:02:56.947Z" }, ] +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + [[package]] name = "terminado" version = "0.18.1" @@ -3866,14 +4348,26 @@ sdist = { url = "https://files.pythonhosted.org/packages/3c/2d/8946864f716ac82dc [[package]] name = "tinycss2" -version = "1.4.0" +version = "1.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "webencodings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, + { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, +] + +[[package]] +name = "tinyhtml5" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/1f/cfe2f6b30557c92b3f31d41707e09cef5c1efbd87392bc6c0430c46b0e4d/tinyhtml5-2.1.0.tar.gz", hash = "sha256:60a50ec3d938a37e491efa01af895853060943dcebb5627de5b10d188b338a67", size = 179242, upload-time = "2026-03-05T17:06:30.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/48/01695a036b695f83fea7aef6955d735db0f517b1c8e25ddb399ac0bdbcbf/tinyhtml5-2.1.0-py3-none-any.whl", hash = "sha256:6e11cfff38515834268daf89d5f85bbde0b6dd02e8d9e212d1385c2289b89f0a", size = 39686, upload-time = "2026-03-05T17:06:28.498Z" }, ] [[package]] @@ -3933,6 +4427,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" }, ] +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + [[package]] name = "traitlets" version = "5.14.3" @@ -3956,7 +4462,7 @@ wheels = [ [[package]] name = "typer" -version = "0.16.0" +version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -3964,9 +4470,27 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/28/7c85c8032b91dbe79725b6f17d2fffc595dff06a35c7a30a37bef73a1ab4/typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37", size = 106492, upload-time = "2025-10-20T17:03:49.445Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" }, + { url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028, upload-time = "2025-10-20T17:03:47.617Z" }, +] + +[[package]] +name = "types-cachetools" +version = "6.2.0.20251022" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/a8/f9bcc7f1be63af43ef0170a773e2d88817bcc7c9d8769f2228c802826efe/types_cachetools-6.2.0.20251022.tar.gz", hash = "sha256:f1d3c736f0f741e89ec10f0e1b0138625023e21eb33603a930c149e0318c0cef", size = 9608, upload-time = "2025-10-22T03:03:58.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/2d/8d821ed80f6c2c5b427f650bf4dc25b80676ed63d03388e4b637d2557107/types_cachetools-6.2.0.20251022-py3-none-any.whl", hash = "sha256:698eb17b8f16b661b90624708b6915f33dbac2d185db499ed57e4997e7962cad", size = 9341, upload-time = "2025-10-22T03:03:57.036Z" }, +] + +[[package]] +name = "types-markdown" +version = "3.10.0.20251106" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/e4/060f0dadd9b551cae77d6407f2bc84b168f918d90650454aff219c1b3ed2/types_markdown-3.10.0.20251106.tar.gz", hash = "sha256:12836f7fcbd7221db8baeb0d3a2f820b95050d0824bfa9665c67b4d144a1afa1", size = 19486, upload-time = "2025-11-06T03:06:44.317Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/58/f666ca9391f2a8bd33bb0b0797cde6ac3e764866708d5f8aec6fab215320/types_markdown-3.10.0.20251106-py3-none-any.whl", hash = "sha256:2c39512a573899b59efae07e247ba088a75b70e3415e81277692718f430afd7e", size = 25862, upload-time = "2025-11-06T03:06:43.082Z" }, ] [[package]] @@ -3982,12 +4506,12 @@ wheels = [ ] [[package]] -name = "types-python-dateutil" -version = "2.9.0.20250809" +name = "types-pyyaml" +version = "6.0.12.20250915" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/53/07dac71db45fb6b3c71c2fd29a87cada2239eac7ecfb318e6ebc7da00a3b/types_python_dateutil-2.9.0.20250809.tar.gz", hash = "sha256:69cbf8d15ef7a75c3801d65d63466e46ac25a0baa678d89d0a137fc31a608cc1", size = 15820, upload-time = "2025-08-09T03:14:14.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/5e/67312e679f612218d07fcdbd14017e6d571ce240a5ba1ad734f15a8523cc/types_python_dateutil-2.9.0.20250809-py3-none-any.whl", hash = "sha256:768890cac4f2d7fd9e0feb6f3217fce2abbfdfc0cadd38d11fba325a815e4b9f", size = 17707, upload-time = "2025-08-09T03:14:13.314Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, ] [[package]] @@ -4004,11 +4528,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.14.1" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] @@ -4026,14 +4550,14 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.1" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] @@ -4065,16 +4589,16 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.35.0" +version = "0.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, + { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, ] [package.optional-dependencies] @@ -4090,28 +4614,28 @@ standard = [ [[package]] name = "uvloop" -version = "0.21.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/76/44a55515e8c9505aa1420aebacf4dd82552e5e15691654894e90d0bd051a/uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", size = 1442019, upload-time = "2024-10-14T23:37:20.068Z" }, - { url = "https://files.pythonhosted.org/packages/35/5a/62d5800358a78cc25c8a6c72ef8b10851bdb8cca22e14d9c74167b7f86da/uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", size = 801898, upload-time = "2024-10-14T23:37:22.663Z" }, - { url = "https://files.pythonhosted.org/packages/f3/96/63695e0ebd7da6c741ccd4489b5947394435e198a1382349c17b1146bb97/uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", size = 3827735, upload-time = "2024-10-14T23:37:25.129Z" }, - { url = "https://files.pythonhosted.org/packages/61/e0/f0f8ec84979068ffae132c58c79af1de9cceeb664076beea86d941af1a30/uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", size = 3825126, upload-time = "2024-10-14T23:37:27.59Z" }, - { url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", size = 3705789, upload-time = "2024-10-14T23:37:29.385Z" }, - { url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", size = 3800523, upload-time = "2024-10-14T23:37:32.048Z" }, - { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410, upload-time = "2024-10-14T23:37:33.612Z" }, - { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476, upload-time = "2024-10-14T23:37:36.11Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855, upload-time = "2024-10-14T23:37:37.683Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185, upload-time = "2024-10-14T23:37:40.226Z" }, - { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256, upload-time = "2024-10-14T23:37:42.839Z" }, - { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323, upload-time = "2024-10-14T23:37:45.337Z" }, - { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284, upload-time = "2024-10-14T23:37:47.833Z" }, - { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349, upload-time = "2024-10-14T23:37:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089, upload-time = "2024-10-14T23:37:51.703Z" }, - { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770, upload-time = "2024-10-14T23:37:54.122Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321, upload-time = "2024-10-14T23:37:55.766Z" }, - { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022, upload-time = "2024-10-14T23:37:58.195Z" }, +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, ] [[package]] @@ -4125,77 +4649,96 @@ wheels = [ [[package]] name = "watchfiles" -version = "1.1.0" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406, upload-time = "2025-06-15T19:06:59.42Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/dd/579d1dc57f0f895426a1211c4ef3b0cb37eb9e642bb04bdcd962b5df206a/watchfiles-1.1.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc", size = 405757, upload-time = "2025-06-15T19:04:51.058Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a0/7a0318cd874393344d48c34d53b3dd419466adf59a29ba5b51c88dd18b86/watchfiles-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df", size = 397511, upload-time = "2025-06-15T19:04:52.79Z" }, - { url = "https://files.pythonhosted.org/packages/06/be/503514656d0555ec2195f60d810eca29b938772e9bfb112d5cd5ad6f6a9e/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68", size = 450739, upload-time = "2025-06-15T19:04:54.203Z" }, - { url = "https://files.pythonhosted.org/packages/4e/0d/a05dd9e5f136cdc29751816d0890d084ab99f8c17b86f25697288ca09bc7/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc", size = 458106, upload-time = "2025-06-15T19:04:55.607Z" }, - { url = "https://files.pythonhosted.org/packages/f1/fa/9cd16e4dfdb831072b7ac39e7bea986e52128526251038eb481effe9f48e/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97", size = 484264, upload-time = "2025-06-15T19:04:57.009Z" }, - { url = "https://files.pythonhosted.org/packages/32/04/1da8a637c7e2b70e750a0308e9c8e662ada0cca46211fa9ef24a23937e0b/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c", size = 597612, upload-time = "2025-06-15T19:04:58.409Z" }, - { url = "https://files.pythonhosted.org/packages/30/01/109f2762e968d3e58c95731a206e5d7d2a7abaed4299dd8a94597250153c/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5", size = 477242, upload-time = "2025-06-15T19:04:59.786Z" }, - { url = "https://files.pythonhosted.org/packages/b5/b8/46f58cf4969d3b7bc3ca35a98e739fa4085b0657a1540ccc29a1a0bc016f/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9", size = 453148, upload-time = "2025-06-15T19:05:01.103Z" }, - { url = "https://files.pythonhosted.org/packages/a5/cd/8267594263b1770f1eb76914940d7b2d03ee55eca212302329608208e061/watchfiles-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72", size = 626574, upload-time = "2025-06-15T19:05:02.582Z" }, - { url = "https://files.pythonhosted.org/packages/a1/2f/7f2722e85899bed337cba715723e19185e288ef361360718973f891805be/watchfiles-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc", size = 624378, upload-time = "2025-06-15T19:05:03.719Z" }, - { url = "https://files.pythonhosted.org/packages/bf/20/64c88ec43d90a568234d021ab4b2a6f42a5230d772b987c3f9c00cc27b8b/watchfiles-1.1.0-cp310-cp310-win32.whl", hash = "sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587", size = 279829, upload-time = "2025-06-15T19:05:04.822Z" }, - { url = "https://files.pythonhosted.org/packages/39/5c/a9c1ed33de7af80935e4eac09570de679c6e21c07070aa99f74b4431f4d6/watchfiles-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82", size = 292192, upload-time = "2025-06-15T19:05:06.348Z" }, - { url = "https://files.pythonhosted.org/packages/8b/78/7401154b78ab484ccaaeef970dc2af0cb88b5ba8a1b415383da444cdd8d3/watchfiles-1.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2", size = 405751, upload-time = "2025-06-15T19:05:07.679Z" }, - { url = "https://files.pythonhosted.org/packages/76/63/e6c3dbc1f78d001589b75e56a288c47723de28c580ad715eb116639152b5/watchfiles-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c", size = 397313, upload-time = "2025-06-15T19:05:08.764Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a2/8afa359ff52e99af1632f90cbf359da46184207e893a5f179301b0c8d6df/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d", size = 450792, upload-time = "2025-06-15T19:05:09.869Z" }, - { url = "https://files.pythonhosted.org/packages/1d/bf/7446b401667f5c64972a57a0233be1104157fc3abf72c4ef2666c1bd09b2/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7", size = 458196, upload-time = "2025-06-15T19:05:11.91Z" }, - { url = "https://files.pythonhosted.org/packages/58/2f/501ddbdfa3fa874ea5597c77eeea3d413579c29af26c1091b08d0c792280/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c", size = 484788, upload-time = "2025-06-15T19:05:13.373Z" }, - { url = "https://files.pythonhosted.org/packages/61/1e/9c18eb2eb5c953c96bc0e5f626f0e53cfef4bd19bd50d71d1a049c63a575/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575", size = 597879, upload-time = "2025-06-15T19:05:14.725Z" }, - { url = "https://files.pythonhosted.org/packages/8b/6c/1467402e5185d89388b4486745af1e0325007af0017c3384cc786fff0542/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8", size = 477447, upload-time = "2025-06-15T19:05:15.775Z" }, - { url = "https://files.pythonhosted.org/packages/2b/a1/ec0a606bde4853d6c4a578f9391eeb3684a9aea736a8eb217e3e00aa89a1/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f", size = 453145, upload-time = "2025-06-15T19:05:17.17Z" }, - { url = "https://files.pythonhosted.org/packages/90/b9/ef6f0c247a6a35d689fc970dc7f6734f9257451aefb30def5d100d6246a5/watchfiles-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4", size = 626539, upload-time = "2025-06-15T19:05:18.557Z" }, - { url = "https://files.pythonhosted.org/packages/34/44/6ffda5537085106ff5aaa762b0d130ac6c75a08015dd1621376f708c94de/watchfiles-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d", size = 624472, upload-time = "2025-06-15T19:05:19.588Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e3/71170985c48028fa3f0a50946916a14055e741db11c2e7bc2f3b61f4d0e3/watchfiles-1.1.0-cp311-cp311-win32.whl", hash = "sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2", size = 279348, upload-time = "2025-06-15T19:05:20.856Z" }, - { url = "https://files.pythonhosted.org/packages/89/1b/3e39c68b68a7a171070f81fc2561d23ce8d6859659406842a0e4bebf3bba/watchfiles-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12", size = 292607, upload-time = "2025-06-15T19:05:21.937Z" }, - { url = "https://files.pythonhosted.org/packages/61/9f/2973b7539f2bdb6ea86d2c87f70f615a71a1fc2dba2911795cea25968aea/watchfiles-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a", size = 285056, upload-time = "2025-06-15T19:05:23.12Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b8/858957045a38a4079203a33aaa7d23ea9269ca7761c8a074af3524fbb240/watchfiles-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179", size = 402339, upload-time = "2025-06-15T19:05:24.516Z" }, - { url = "https://files.pythonhosted.org/packages/80/28/98b222cca751ba68e88521fabd79a4fab64005fc5976ea49b53fa205d1fa/watchfiles-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5", size = 394409, upload-time = "2025-06-15T19:05:25.469Z" }, - { url = "https://files.pythonhosted.org/packages/86/50/dee79968566c03190677c26f7f47960aff738d32087087bdf63a5473e7df/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297", size = 450939, upload-time = "2025-06-15T19:05:26.494Z" }, - { url = "https://files.pythonhosted.org/packages/40/45/a7b56fb129700f3cfe2594a01aa38d033b92a33dddce86c8dfdfc1247b72/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0", size = 457270, upload-time = "2025-06-15T19:05:27.466Z" }, - { url = "https://files.pythonhosted.org/packages/b5/c8/fa5ef9476b1d02dc6b5e258f515fcaaecf559037edf8b6feffcbc097c4b8/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e", size = 483370, upload-time = "2025-06-15T19:05:28.548Z" }, - { url = "https://files.pythonhosted.org/packages/98/68/42cfcdd6533ec94f0a7aab83f759ec11280f70b11bfba0b0f885e298f9bd/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee", size = 598654, upload-time = "2025-06-15T19:05:29.997Z" }, - { url = "https://files.pythonhosted.org/packages/d3/74/b2a1544224118cc28df7e59008a929e711f9c68ce7d554e171b2dc531352/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd", size = 478667, upload-time = "2025-06-15T19:05:31.172Z" }, - { url = "https://files.pythonhosted.org/packages/8c/77/e3362fe308358dc9f8588102481e599c83e1b91c2ae843780a7ded939a35/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f", size = 452213, upload-time = "2025-06-15T19:05:32.299Z" }, - { url = "https://files.pythonhosted.org/packages/6e/17/c8f1a36540c9a1558d4faf08e909399e8133599fa359bf52ec8fcee5be6f/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4", size = 626718, upload-time = "2025-06-15T19:05:33.415Z" }, - { url = "https://files.pythonhosted.org/packages/26/45/fb599be38b4bd38032643783d7496a26a6f9ae05dea1a42e58229a20ac13/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f", size = 623098, upload-time = "2025-06-15T19:05:34.534Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e7/fdf40e038475498e160cd167333c946e45d8563ae4dd65caf757e9ffe6b4/watchfiles-1.1.0-cp312-cp312-win32.whl", hash = "sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd", size = 279209, upload-time = "2025-06-15T19:05:35.577Z" }, - { url = "https://files.pythonhosted.org/packages/3f/d3/3ae9d5124ec75143bdf088d436cba39812122edc47709cd2caafeac3266f/watchfiles-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47", size = 292786, upload-time = "2025-06-15T19:05:36.559Z" }, - { url = "https://files.pythonhosted.org/packages/26/2f/7dd4fc8b5f2b34b545e19629b4a018bfb1de23b3a496766a2c1165ca890d/watchfiles-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6", size = 284343, upload-time = "2025-06-15T19:05:37.5Z" }, - { url = "https://files.pythonhosted.org/packages/be/7c/a3d7c55cfa377c2f62c4ae3c6502b997186bc5e38156bafcb9b653de9a6d/watchfiles-1.1.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5", size = 406748, upload-time = "2025-06-15T19:06:44.2Z" }, - { url = "https://files.pythonhosted.org/packages/38/d0/c46f1b2c0ca47f3667b144de6f0515f6d1c670d72f2ca29861cac78abaa1/watchfiles-1.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d", size = 398801, upload-time = "2025-06-15T19:06:45.774Z" }, - { url = "https://files.pythonhosted.org/packages/70/9c/9a6a42e97f92eeed77c3485a43ea96723900aefa3ac739a8c73f4bff2cd7/watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea", size = 451528, upload-time = "2025-06-15T19:06:46.791Z" }, - { url = "https://files.pythonhosted.org/packages/51/7b/98c7f4f7ce7ff03023cf971cd84a3ee3b790021ae7584ffffa0eb2554b96/watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6", size = 454095, upload-time = "2025-06-15T19:06:48.211Z" }, - { url = "https://files.pythonhosted.org/packages/8c/6b/686dcf5d3525ad17b384fd94708e95193529b460a1b7bf40851f1328ec6e/watchfiles-1.1.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3", size = 406910, upload-time = "2025-06-15T19:06:49.335Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d3/71c2dcf81dc1edcf8af9f4d8d63b1316fb0a2dd90cbfd427e8d9dd584a90/watchfiles-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c", size = 398816, upload-time = "2025-06-15T19:06:50.433Z" }, - { url = "https://files.pythonhosted.org/packages/b8/fa/12269467b2fc006f8fce4cd6c3acfa77491dd0777d2a747415f28ccc8c60/watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432", size = 451584, upload-time = "2025-06-15T19:06:51.834Z" }, - { url = "https://files.pythonhosted.org/packages/bd/d3/254cea30f918f489db09d6a8435a7de7047f8cb68584477a515f160541d6/watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792", size = 454009, upload-time = "2025-06-15T19:06:52.896Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { 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/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { 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/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { 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/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { 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/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, ] [[package]] name = "wcwidth" -version = "0.2.13" +version = "0.2.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] + +[[package]] +name = "weasyprint" +version = "68.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "cssselect2" }, + { name = "fonttools", extra = ["woff"] }, + { name = "pillow" }, + { name = "pydyf" }, + { name = "pyphen" }, + { name = "tinycss2" }, + { name = "tinyhtml5" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/3e/65c0f176e6fb5c2b0a1ac13185b366f727d9723541babfa7fa4309998169/weasyprint-68.1.tar.gz", hash = "sha256:d3b752049b453a5c95edb27ce78d69e9319af5a34f257fa0f4c738c701b4184e", size = 1542379, upload-time = "2026-02-06T15:04:11.203Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/dd/14eb73cea481ad8162d3b18a4850d4a84d6e804a22840cca207648532265/weasyprint-68.1-py3-none-any.whl", hash = "sha256:4dc3ba63c68bbbce3e9617cb2226251c372f5ee90a8a484503b1c099da9cf5be", size = 319789, upload-time = "2026-02-06T15:04:09.189Z" }, ] [[package]] name = "webcolors" -version = "24.11.1" +version = "25.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/29/061ec845fb58521848f3739e466efd8250b4b7b98c1b6c5bf4d40b419b7e/webcolors-24.11.1.tar.gz", hash = "sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6", size = 45064, upload-time = "2024-11-11T07:43:24.224Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/e8/c0e05e4684d13459f93d312077a9a2efbe04d59c393bc2b8802248c908d4/webcolors-24.11.1-py3-none-any.whl", hash = "sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9", size = 14934, upload-time = "2024-11-11T07:43:22.529Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, ] [[package]] @@ -4209,11 +4752,11 @@ wheels = [ [[package]] name = "websocket-client" -version = "1.8.0" +version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, ] [[package]] @@ -4276,69 +4819,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" }, ] +[[package]] +name = "widgetsnbextension" +version = "4.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/f4/c67440c7fb409a71b7404b7aefcd7569a9c0d6bd071299bf4198ae7a5d95/widgetsnbextension-4.0.15.tar.gz", hash = "sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9", size = 1097402, upload-time = "2025-11-01T21:15:55.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" }, +] + [[package]] name = "yarl" -version = "1.20.1" +version = "1.22.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/65/7fed0d774abf47487c64be14e9223749468922817b5e8792b8a64792a1bb/yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4", size = 132910, upload-time = "2025-06-10T00:42:31.108Z" }, - { url = "https://files.pythonhosted.org/packages/8a/7b/988f55a52da99df9e56dc733b8e4e5a6ae2090081dc2754fc8fd34e60aa0/yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a", size = 90644, upload-time = "2025-06-10T00:42:33.851Z" }, - { url = "https://files.pythonhosted.org/packages/f7/de/30d98f03e95d30c7e3cc093759982d038c8833ec2451001d45ef4854edc1/yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed", size = 89322, upload-time = "2025-06-10T00:42:35.688Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7a/f2f314f5ebfe9200724b0b748de2186b927acb334cf964fd312eb86fc286/yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e", size = 323786, upload-time = "2025-06-10T00:42:37.817Z" }, - { url = "https://files.pythonhosted.org/packages/15/3f/718d26f189db96d993d14b984ce91de52e76309d0fd1d4296f34039856aa/yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73", size = 319627, upload-time = "2025-06-10T00:42:39.937Z" }, - { url = "https://files.pythonhosted.org/packages/a5/76/8fcfbf5fa2369157b9898962a4a7d96764b287b085b5b3d9ffae69cdefd1/yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e", size = 339149, upload-time = "2025-06-10T00:42:42.627Z" }, - { url = "https://files.pythonhosted.org/packages/3c/95/d7fc301cc4661785967acc04f54a4a42d5124905e27db27bb578aac49b5c/yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8", size = 333327, upload-time = "2025-06-10T00:42:44.842Z" }, - { url = "https://files.pythonhosted.org/packages/65/94/e21269718349582eee81efc5c1c08ee71c816bfc1585b77d0ec3f58089eb/yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23", size = 326054, upload-time = "2025-06-10T00:42:47.149Z" }, - { url = "https://files.pythonhosted.org/packages/32/ae/8616d1f07853704523519f6131d21f092e567c5af93de7e3e94b38d7f065/yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70", size = 315035, upload-time = "2025-06-10T00:42:48.852Z" }, - { url = "https://files.pythonhosted.org/packages/48/aa/0ace06280861ef055855333707db5e49c6e3a08840a7ce62682259d0a6c0/yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb", size = 338962, upload-time = "2025-06-10T00:42:51.024Z" }, - { url = "https://files.pythonhosted.org/packages/20/52/1e9d0e6916f45a8fb50e6844f01cb34692455f1acd548606cbda8134cd1e/yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2", size = 335399, upload-time = "2025-06-10T00:42:53.007Z" }, - { url = "https://files.pythonhosted.org/packages/f2/65/60452df742952c630e82f394cd409de10610481d9043aa14c61bf846b7b1/yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30", size = 338649, upload-time = "2025-06-10T00:42:54.964Z" }, - { url = "https://files.pythonhosted.org/packages/7b/f5/6cd4ff38dcde57a70f23719a838665ee17079640c77087404c3d34da6727/yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309", size = 358563, upload-time = "2025-06-10T00:42:57.28Z" }, - { url = "https://files.pythonhosted.org/packages/d1/90/c42eefd79d0d8222cb3227bdd51b640c0c1d0aa33fe4cc86c36eccba77d3/yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24", size = 357609, upload-time = "2025-06-10T00:42:59.055Z" }, - { url = "https://files.pythonhosted.org/packages/03/c8/cea6b232cb4617514232e0f8a718153a95b5d82b5290711b201545825532/yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13", size = 350224, upload-time = "2025-06-10T00:43:01.248Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a3/eaa0ab9712f1f3d01faf43cf6f1f7210ce4ea4a7e9b28b489a2261ca8db9/yarl-1.20.1-cp310-cp310-win32.whl", hash = "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8", size = 81753, upload-time = "2025-06-10T00:43:03.486Z" }, - { url = "https://files.pythonhosted.org/packages/8f/34/e4abde70a9256465fe31c88ed02c3f8502b7b5dead693a4f350a06413f28/yarl-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16", size = 86817, upload-time = "2025-06-10T00:43:05.231Z" }, - { url = "https://files.pythonhosted.org/packages/b1/18/893b50efc2350e47a874c5c2d67e55a0ea5df91186b2a6f5ac52eff887cd/yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e", size = 133833, upload-time = "2025-06-10T00:43:07.393Z" }, - { url = "https://files.pythonhosted.org/packages/89/ed/b8773448030e6fc47fa797f099ab9eab151a43a25717f9ac043844ad5ea3/yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b", size = 91070, upload-time = "2025-06-10T00:43:09.538Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e3/409bd17b1e42619bf69f60e4f031ce1ccb29bd7380117a55529e76933464/yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b", size = 89818, upload-time = "2025-06-10T00:43:11.575Z" }, - { url = "https://files.pythonhosted.org/packages/f8/77/64d8431a4d77c856eb2d82aa3de2ad6741365245a29b3a9543cd598ed8c5/yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4", size = 347003, upload-time = "2025-06-10T00:43:14.088Z" }, - { url = "https://files.pythonhosted.org/packages/8d/d2/0c7e4def093dcef0bd9fa22d4d24b023788b0a33b8d0088b51aa51e21e99/yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1", size = 336537, upload-time = "2025-06-10T00:43:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f3/fc514f4b2cf02cb59d10cbfe228691d25929ce8f72a38db07d3febc3f706/yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833", size = 362358, upload-time = "2025-06-10T00:43:18.704Z" }, - { url = "https://files.pythonhosted.org/packages/ea/6d/a313ac8d8391381ff9006ac05f1d4331cee3b1efaa833a53d12253733255/yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d", size = 357362, upload-time = "2025-06-10T00:43:20.888Z" }, - { url = "https://files.pythonhosted.org/packages/00/70/8f78a95d6935a70263d46caa3dd18e1f223cf2f2ff2037baa01a22bc5b22/yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8", size = 348979, upload-time = "2025-06-10T00:43:23.169Z" }, - { url = "https://files.pythonhosted.org/packages/cb/05/42773027968968f4f15143553970ee36ead27038d627f457cc44bbbeecf3/yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf", size = 337274, upload-time = "2025-06-10T00:43:27.111Z" }, - { url = "https://files.pythonhosted.org/packages/05/be/665634aa196954156741ea591d2f946f1b78ceee8bb8f28488bf28c0dd62/yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e", size = 363294, upload-time = "2025-06-10T00:43:28.96Z" }, - { url = "https://files.pythonhosted.org/packages/eb/90/73448401d36fa4e210ece5579895731f190d5119c4b66b43b52182e88cd5/yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389", size = 358169, upload-time = "2025-06-10T00:43:30.701Z" }, - { url = "https://files.pythonhosted.org/packages/c3/b0/fce922d46dc1eb43c811f1889f7daa6001b27a4005587e94878570300881/yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f", size = 362776, upload-time = "2025-06-10T00:43:32.51Z" }, - { url = "https://files.pythonhosted.org/packages/f1/0d/b172628fce039dae8977fd22caeff3eeebffd52e86060413f5673767c427/yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845", size = 381341, upload-time = "2025-06-10T00:43:34.543Z" }, - { url = "https://files.pythonhosted.org/packages/6b/9b/5b886d7671f4580209e855974fe1cecec409aa4a89ea58b8f0560dc529b1/yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1", size = 379988, upload-time = "2025-06-10T00:43:36.489Z" }, - { url = "https://files.pythonhosted.org/packages/73/be/75ef5fd0fcd8f083a5d13f78fd3f009528132a1f2a1d7c925c39fa20aa79/yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e", size = 371113, upload-time = "2025-06-10T00:43:38.592Z" }, - { url = "https://files.pythonhosted.org/packages/50/4f/62faab3b479dfdcb741fe9e3f0323e2a7d5cd1ab2edc73221d57ad4834b2/yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773", size = 81485, upload-time = "2025-06-10T00:43:41.038Z" }, - { url = "https://files.pythonhosted.org/packages/f0/09/d9c7942f8f05c32ec72cd5c8e041c8b29b5807328b68b4801ff2511d4d5e/yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e", size = 86686, upload-time = "2025-06-10T00:43:42.692Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload-time = "2025-06-10T00:43:44.369Z" }, - { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload-time = "2025-06-10T00:43:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload-time = "2025-06-10T00:43:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload-time = "2025-06-10T00:43:49.924Z" }, - { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload-time = "2025-06-10T00:43:51.7Z" }, - { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload-time = "2025-06-10T00:43:53.494Z" }, - { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload-time = "2025-06-10T00:43:55.766Z" }, - { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload-time = "2025-06-10T00:43:58.056Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload-time = "2025-06-10T00:43:59.773Z" }, - { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload-time = "2025-06-10T00:44:02.051Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload-time = "2025-06-10T00:44:04.196Z" }, - { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload-time = "2025-06-10T00:44:06.527Z" }, - { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload-time = "2025-06-10T00:44:08.379Z" }, - { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload-time = "2025-06-10T00:44:10.51Z" }, - { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload-time = "2025-06-10T00:44:12.834Z" }, - { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload-time = "2025-06-10T00:44:14.731Z" }, - { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload-time = "2025-06-10T00:44:16.716Z" }, - { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517, upload-time = "2025-10-06T14:08:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495, upload-time = "2025-10-06T14:08:46.2Z" }, + { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400, upload-time = "2025-10-06T14:08:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545, upload-time = "2025-10-06T14:08:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598, upload-time = "2025-10-06T14:08:51.215Z" }, + { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893, upload-time = "2025-10-06T14:08:53.144Z" }, + { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240, upload-time = "2025-10-06T14:08:55.036Z" }, + { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965, upload-time = "2025-10-06T14:08:56.722Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026, upload-time = "2025-10-06T14:08:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637, upload-time = "2025-10-06T14:09:00.506Z" }, + { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082, upload-time = "2025-10-06T14:09:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811, upload-time = "2025-10-06T14:09:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223, upload-time = "2025-10-06T14:09:05.401Z" }, + { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118, upload-time = "2025-10-06T14:09:11.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852, upload-time = "2025-10-06T14:09:12.958Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012, upload-time = "2025-10-06T14:09:14.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, ] [[package]] @@ -4349,3 +4898,22 @@ sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50e wheels = [ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ] + +[[package]] +name = "zopfli" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/4c/efa0760686d4cc69e68a8f284d3c6c5884722c50f810af0e277fb7d61621/zopfli-0.4.0.tar.gz", hash = "sha256:a8ee992b2549e090cd3f0178bf606dd41a29e0613a04cdf5054224662c72dce6", size = 176720, upload-time = "2025-11-07T17:00:59.507Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/62/ec5cb67ee379c6a4f296f1277b971ff8c26460bf8775f027f82c519a0a72/zopfli-0.4.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d1b98ad47c434ef213444a03ef2f826eeec100144d64f6a57504b9893d3931ce", size = 287433, upload-time = "2025-11-07T17:00:45.662Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9e/8f81e69bd771014a488c4c64476b6e6faab91b2c913d0f81eca7e06401eb/zopfli-0.4.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:18b5f1570f64d4988482e4466f10ef5f2a30f687c19ad62a64560f2152dc89eb", size = 847135, upload-time = "2025-11-07T17:00:47.483Z" }, + { url = "https://files.pythonhosted.org/packages/24/84/6e60eeaaa1c1eae7b4805f1c528f3e8ae62cef323ec1e52347a11031e3ba/zopfli-0.4.0-cp310-abi3-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b72a010d205d00b2855acc2302772067362f9ab5a012e3550662aec60d28e6b3", size = 831606, upload-time = "2025-11-07T17:00:48.576Z" }, + { url = "https://files.pythonhosted.org/packages/6d/aa/a4d5de7ed8e809953cb5e8992bddc40f38461ec5a44abfb010953875adfc/zopfli-0.4.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c3ba02a9a6ca90481d2b2f68bab038b310d63a1e3b5ae305e95a6599787ed941", size = 1789376, upload-time = "2025-11-07T17:00:49.63Z" }, + { url = "https://files.pythonhosted.org/packages/39/95/4d1e943fbc44157f58b623625686d0b970f2fda269e721fbf9546b93f6cc/zopfli-0.4.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7d66337be6d5613dec55213e9ac28f378c41e2cc04fbad4a10748e4df774ca85", size = 1879013, upload-time = "2025-11-07T17:00:50.751Z" }, + { url = "https://files.pythonhosted.org/packages/95/db/4f2eebf73c0e2df293a366a1d176cd315a74ce0b00f83826a7ba9ddd1ab3/zopfli-0.4.0-cp310-abi3-win32.whl", hash = "sha256:03181d48e719fcb6cf8340189c61e8f9883d8bbbdf76bf5212a74457f7d083c1", size = 83655, upload-time = "2025-11-07T17:00:51.797Z" }, + { url = "https://files.pythonhosted.org/packages/24/f6/bd80c5278b1185dc41155c77bc61bfe1d817254a7f2115f66aa69a270b89/zopfli-0.4.0-cp310-abi3-win_amd64.whl", hash = "sha256:f94e4dd7d76b4fe9f5d9229372be20d7f786164eea5152d1af1c34298c3d5975", size = 100824, upload-time = "2025-11-07T17:00:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/76/30/92a53a60f20b639c9ce67d0e99cdfc3c4adfc6bc3530a60b724c4765f7e7/zopfli-0.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b8bdb41fbfdc4738b7bdc09ed7c1e951579fae192391a5e694d59bb186cdbec7", size = 156095, upload-time = "2025-11-07T17:00:53.476Z" }, + { url = "https://files.pythonhosted.org/packages/6f/62/480d6b8d12cc6ef1a1da54fe62f30602c7941256a50c563f37e18168bab1/zopfli-0.4.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9097e8e1dfdb7f5aea5464e469946857e80502b6d29ba1b232450916bd4a74d1", size = 126543, upload-time = "2025-11-07T17:00:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d6/1e182231c836c13c5438d13f7425e51fcc7d2dc96a03b1665d6100b7713c/zopfli-0.4.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f67d04280065e24cb9a4174cb6b3d1f763687f8cb2963aa135ad8f57c6995f5a", size = 124992, upload-time = "2025-11-07T17:00:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/4e/52/4e67fa948c213368540a807a96da822035c71ffcc7a5ada8ee90da5b9614/zopfli-0.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:25e4863b8dc30e5d5309f87c106b0b7d3da4ed0e340b8a52b36d4471e797589f", size = 100851, upload-time = "2025-11-07T17:00:58.331Z" }, +]