diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 6cb06bf..323a740 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -8,6 +8,7 @@ on: jobs: build: name: Build distribution + if: ${{ github.event_name == 'workflow_dispatch' || (!contains(github.event.release.tag_name, '-alpha') && !contains(github.event.release.tag_name, '-beta') && !contains(github.event.release.tag_name, '-rc')) }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -31,6 +32,7 @@ jobs: publish-pypi: name: Publish to PyPI + if: ${{ github.event_name == 'workflow_dispatch' || (!contains(github.event.release.tag_name, '-alpha') && !contains(github.event.release.tag_name, '-beta') && !contains(github.event.release.tag_name, '-rc')) }} needs: build runs-on: ubuntu-latest environment: diff --git a/.github/workflows/publish-testpypi.yml b/.github/workflows/publish-testpypi.yml index 1758649..c4de5bd 100644 --- a/.github/workflows/publish-testpypi.yml +++ b/.github/workflows/publish-testpypi.yml @@ -2,7 +2,10 @@ name: Publish to TestPyPI on: push: - branches: [main] + tags: + - "v*-alpha*" + - "v*-beta*" + - "v*-rc*" workflow_dispatch: jobs: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index db4e0a2..071d36c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,36 +12,39 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 - + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - + - name: Install dependencies run: | pip install -e ".[dev]" - + - name: Run tests run: | - python -m pytest tests/ -v --tb=short + pytest tests/ -v --tb=short -m "not integration" lint: name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - + - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.10" - + - name: Install dependencies - run: pip install ruff - - - name: Run ruff + run: pip install ruff==0.6.4 + + - name: Run ruff check run: ruff check src/ + + - name: Run ruff format check + run: ruff format --check src/ diff --git a/Makefile b/Makefile index a432e31..60a08da 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,17 @@ -lock: - @cd integrations/destination-databricks-py && poetry lock +.PHONY: install test lint format clean install: - @cd integrations/destination-databricks-py && poetry install \ No newline at end of file + uv pip install -e ".[dev]" + +test: + uv run pytest tests/ -v -m "not integration" + +lint: + uv run ruff check src/ + +format: + uv run ruff format src/ + +clean: + rm -rf build/ dist/ *.egg-info src/*.egg-info .pytest_cache + find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true diff --git a/README.md b/README.md index cb00bb1..2a6123a 100644 --- a/README.md +++ b/README.md @@ -1,168 +1,221 @@ -# Brickbyte 🧱 +# brickbyte -**Sync data from 600+ source connectors to Databricks with streaming performance.** +Sync data from 600+ source connectors to Databricks Unity Catalog with streaming performance. -Brickbyte wraps [PyAirbyte](https://github.com/airbytehq/airbyte) to extract data from any source and streams it directly to Databricks Unity Catalog. +brickbyte wraps [PyAirbyte](https://github.com/airbytehq/PyAirbyte) to extract data from 600+ sources into Databricks. For sources that Lakeflow Connect already covers, use Lakeflow Connect. brickbyte fills the gap for everything else. Schedule with Lakeflow Jobs, transform downstream with Declarative Pipelines. ## Features -- **600+ Sources** - All Airbyte connectors work out of the box -- **Streaming Architecture** - Bypasses local disk, no OOM issues -- **High Performance** - Uses Unity Catalog Volumes and `COPY INTO` -- **Flexible Output** - Raw JSON or flattened columns -- **AI Enrichment** - Auto-generate table descriptions and detect PII via Foundation Models -- **Preview** - See what schema changes will occur before syncing -- **Simple API** - One-line sync +- **600+ Sources** - Any Airbyte connector works out of the box +- **Streaming Architecture** - Bounded memory, no local disk needed +- **Raw + Flattened Output** - JSON blob or spread columns +- **Safe Overwrite** - Atomic staged replace preserves metadata +- **Incremental Sync** - State-managed delta processing for connectors that support state APIs +- **Deduplication** - MERGE-based dedup with validated user-defined keys +- **Concurrent Streams** - Parallel writes with isolated per-thread writers +- **Progress Reporting** - Callback events every 5000 records plus per-stream completion +- **Timeout Control** - Cooperative timeout for long-running syncs +- **Preview** - Sample-based schema comparison before committing ## Quick Start ```python -%pip install airbyte databricks-sdk databricks-sql-connector virtualenv -%pip install git+https://github.com/park-peter/brickbyte.git -dbutils.library.restartPython() -``` +import brickbyte -```python -from brickbyte import Brickbyte +bb = brickbyte.client() -bb = Brickbyte() -bb.sync( +result = bb.sync( source="source-faker", - source_config={"count": 100}, + source_config={"count": 1000}, catalog="main", schema="bronze", ) + +print(f"Synced {result.records_written} records") ``` -## Output Formats +## Output Schema -### Raw Mode (Default) -Stores data as JSON for schema flexibility: +### Raw Mode (default) -| id | extracted_at | data | -|----|--------------|------| -| abc-123 | 2026-01-13 10:00:00 | {"displayName": "John", "email": "john@..."} | +| Column | Type | Description | +|--------|------|-------------| +| `record_id` | STRING | Unique UUID per record | +| `extracted_at` | TIMESTAMP | UTC extraction time | +| `data` | STRING | JSON blob of source record | +| `run_id` | STRING | UUID identifying the sync run | -Query with JSON syntax: -```sql -SELECT data:displayName::STRING as name FROM my_table +### Flatten Mode (`flatten=True`) + +Source fields become top-level columns, plus metadata: + +| Column | Type | Description | +|--------|------|-------------| +| `_record_id` | STRING | Unique UUID per record | +| `_extracted_at` | TIMESTAMP | UTC extraction time | +| `_run_id` | STRING | UUID identifying the sync run | +| *(source columns)* | *(inferred)* | Source data fields | + +## Architecture + +``` + ┌──────────────────┐ + │ PyAirbyte │ + │ 600+ Connectors │ + └────────┬─────────┘ + │ + ┌────────▼─────────┐ + │ brickbyte │ + │ Streaming Core │ + └───┬─────────┬────┘ + │ │ + ┌────────────▼──┐ ┌──▼────────────┐ + │ Spark Writer │ │ SQL Writer │ + │ (in-notebook) │ │ (COPY INTO) │ + │ No Volume │ │ Volume needed │ + └───────┬───────┘ └───────┬────────┘ + │ │ + └─────────┬────────┘ + ┌────────▼─────────┐ + │ Delta Lake │ + │ Unity Catalog │ + └──────────────────┘ ``` -### Flattened Mode -Expands all fields into columns: +## Examples + +### Flattened Output ```python -bb.sync(..., flatten=True) +result = bb.sync( + source="source-faker", + source_config={"count": 100}, + catalog="main", + schema="bronze", + flatten=True, +) ``` -| displayName | email | _id | _extracted_at | -|-------------|-------|-----|---------------| -| John | john@... | abc-123 | 2026-01-13 10:00:00 | +### Incremental Sync -## Examples +```python +result = bb.sync( + source="source-github", + source_config={"repository": "owner/repo"}, + catalog="main", + schema="bronze", + incremental=True, +) +``` + +Incremental mode requires connector state APIs (`set_stream_state`, `set_state_for_stream`, or `set_state`) to apply saved state before reading. If saved state exists but the connector does not support state injection, the sync fails fast. -### Simple Sync (Overwrite) +### Deduplication ```python -bb.sync( - source="source-github", - source_config={ - "credentials": {"personal_access_token": "ghp_..."}, - "repositories": ["owner/repo"], - }, +result = bb.sync( + source="source-faker", + source_config={"count": 100}, catalog="main", schema="bronze", - staging_volume="main.staging.brickbyte_volume", + deduplicate=True, + dedup_keys=["email"], # or per-stream: {"users": ["email"], "orders": ["order_id"]} ) ``` -### Flattened Output +`dedup_keys` is applied only when `deduplicate=True`; otherwise it is ignored. Dedup key names must be valid identifier strings (unsafe characters like backticks/semicolons are rejected). + +### Concurrent Streams ```python -bb.sync( - source="source-salesforce", - source_config={...}, +result = bb.sync( + source="source-faker", + source_config={"count": 100}, catalog="main", schema="bronze", - flatten=True, # All fields as top-level columns + max_parallel_streams=4, ) ``` -### With AI Metadata Enrichment +### Progress Reporting ```python +def on_progress(event): + print(f"{event.stream_name}: {event.records_processed} records") + result = bb.sync( - source="source-salesforce", - source_config={...}, + source="source-faker", + source_config={"count": 100}, catalog="main", schema="bronze", - enrich_metadata=True, + progress_callback=on_progress, ) -# Tables get: -# - AI-generated table description (COMMENT ON TABLE) -# - Field descriptions stored in TBLPROPERTIES -# - PII detection stored as table TAGS ``` -### Preview Before Sync +Progress callbacks fire every 5000 processed records per stream and once when each stream completes. + +### Timeout ```python -preview = bb.preview( +result = bb.sync( source="source-github", - source_config={...}, + source_config={"repository": "owner/repo"}, catalog="main", schema="bronze", + timeout_seconds=300, ) -print(preview) ``` -## Architecture +### Preview + +```python +preview = bb.preview( + source="source-faker", + source_config={"count": 100}, + catalog="main", + schema="bronze", +) +print(preview) +``` -### Hybrid Mode -Brickbyte automatically selects the best write strategy: +Preview reports sampled source records, current target counts, and inferred schema changes. -1. **Native Spark** (Default in Databricks Notebooks/Jobs) - - Uses `createDataFrame` + micro-batch writes to Delta - - **Fastest performance**. No Volume required. +## Credential Management -2. **SQL Streaming** (Remote / Local) - - Writes to Volume → `COPY INTO` via SQL Warehouse - - Robust remote execution. Requires `staging_volume`. +brickbyte auto-discovers credentials from Databricks Secrets: +```bash +# Store credentials (one-time setup) +databricks secrets put-secret brickbyte source-s3/aws_access_key_id +databricks secrets put-secret brickbyte source-s3/aws_secret_access_key ``` -[In Notebook] ──▶ Spark createDataFrame ──▶ Delta Table (No Volume) -[Remote] ──▶ SQL Streaming ──▶ Volume ──▶ COPY INTO ──▶ Delta Table +```python +# Credentials auto-discovered - just provide non-sensitive config +result = bb.sync( + source="source-s3", + source_config={"bucket": "my-bucket"}, + catalog="main", + schema="bronze", +) ``` +Supports dotted keys for nested config (`source-x/credentials.client_id` maps to `{"credentials": {"client_id": "..."}}`), custom scopes, and YAML profiles for credential reuse. + ## Requirements -- Python 3.10+ -- Databricks workspace with Unity Catalog -- SQL Warehouse -- Unity Catalog Volume for staging (Required only for Remote/SQL mode) - -## Dependencies - -```toml -[project] -dependencies = [ - "virtualenv", - "databricks-sdk>=0.74.0", - "databricks-sql-connector>=4.2.2", - "airbyte>=0.34.0", - "pyarrow>=14.0.0", -] - -[project.optional-dependencies] -local-spark = ["delta-spark>=3.0.0", "pyspark>=3.5.0"] -``` +- Python 3.10 - 3.12 +- Databricks Unity Catalog +- For SQL mode: SQL Warehouse + Unity Catalog Volume + +## Development -For local Spark + Delta development: ```bash -pip install brickbyte[local-spark] +uv sync --extra dev +uv run pytest tests/ -v -m "not integration" +uv run ruff check src/ ``` ## License -Apache-2.0 License +Apache 2.0 diff --git a/notebooks/_setup.py b/notebooks/_setup.py index 65910c5..980663f 100644 --- a/notebooks/_setup.py +++ b/notebooks/_setup.py @@ -1,5 +1,5 @@ # Databricks notebook source -# MAGIC %pip install airbyte databricks-sdk databricks-sql-connector virtualenv pyarrow +# MAGIC %pip install airbyte==0.38.0 databricks-sdk==0.95.0 databricks-sql-connector==4.2.5 virtualenv==20.29.3 pyarrow==21.0.0 pyyaml==6.0.3 # MAGIC %pip install git+https://github.com/park-peter/brickbyte.git --force-reinstall --no-deps # COMMAND ---------- diff --git a/notebooks/brickbyte-azure-blob.py b/notebooks/brickbyte-azure-blob.py index f363b32..8afe9a7 100644 --- a/notebooks/brickbyte-azure-blob.py +++ b/notebooks/brickbyte-azure-blob.py @@ -4,7 +4,7 @@ # COMMAND ---------- # MAGIC %md -# MAGIC # Azure Blob Storage to Databricks with BrickByte +# MAGIC # Azure Blob Storage to Databricks with brickbyte # MAGIC # MAGIC This notebook syncs files from Azure Blob Storage to Delta Lake tables in Unity Catalog. # MAGIC @@ -22,9 +22,9 @@ # COMMAND ---------- -from brickbyte import Brickbyte +import brickbyte -bb = Brickbyte() +bb = brickbyte.client() # COMMAND ---------- @@ -219,8 +219,4 @@ # }, # catalog="", # TODO: Set your Unity Catalog name # schema="", # TODO: Set your target schema -# enrich_metadata=True, -# enrich_model="databricks-meta-llama-3-3-70b-instruct", # ) - -# print(f"Enriched tables: {result.enriched_tables}") diff --git a/notebooks/brickbyte-confluence.py b/notebooks/brickbyte-confluence.py index 02e7624..c50dc78 100644 --- a/notebooks/brickbyte-confluence.py +++ b/notebooks/brickbyte-confluence.py @@ -1,6 +1,6 @@ # Databricks notebook source # MAGIC %md -# MAGIC # BrickByte - Confluence Example +# MAGIC # brickbyte - Confluence Example # MAGIC # MAGIC Sync data from Atlassian Confluence to Databricks. # MAGIC @@ -14,9 +14,9 @@ # COMMAND ---------- -from brickbyte import Brickbyte +import brickbyte -bb = Brickbyte() +bb = brickbyte.client() # COMMAND ---------- diff --git a/notebooks/brickbyte-datadog.py b/notebooks/brickbyte-datadog.py index 61b004e..e3a3ca7 100644 --- a/notebooks/brickbyte-datadog.py +++ b/notebooks/brickbyte-datadog.py @@ -1,6 +1,6 @@ # Databricks notebook source # MAGIC %md -# MAGIC # BrickByte - DataDog Example +# MAGIC # brickbyte - DataDog Example # MAGIC # MAGIC Sync monitoring data from DataDog to Databricks. # MAGIC @@ -14,9 +14,9 @@ # COMMAND ---------- -from brickbyte import Brickbyte +import brickbyte -bb = Brickbyte() +bb = brickbyte.client() # COMMAND ---------- diff --git a/notebooks/brickbyte-example.py b/notebooks/brickbyte-example.py index 8ea1c4d..be67ef1 100644 --- a/notebooks/brickbyte-example.py +++ b/notebooks/brickbyte-example.py @@ -4,31 +4,31 @@ # COMMAND ---------- # MAGIC %md -# MAGIC # BrickByte Quick Start -# MAGIC -# MAGIC BrickByte bridges Airbyte's 600+ connectors directly into Databricks. -# MAGIC +# MAGIC # brickbyte Quick Start +# MAGIC +# MAGIC brickbyte bridges Airbyte's 600+ connectors directly into Databricks. +# MAGIC # MAGIC ## Credential Management -# MAGIC -# MAGIC BrickByte automatically discovers credentials from **Databricks Secrets**: -# MAGIC +# MAGIC +# MAGIC brickbyte automatically discovers credentials from **Databricks Secrets**: +# MAGIC # MAGIC | Scope | Key Pattern | Example | # MAGIC |-------|-------------|---------| # MAGIC | `brickbyte` | `{source-name}/{field}` | `source-s3/aws_access_key_id` | -# MAGIC +# MAGIC # MAGIC **Setup your secrets once:** # MAGIC ``` # MAGIC databricks secrets put-secret brickbyte source-s3/aws_access_key_id # MAGIC databricks secrets put-secret brickbyte source-s3/aws_secret_access_key # MAGIC ``` -# MAGIC +# MAGIC # MAGIC Then just sync - credentials are discovered automatically! # COMMAND ---------- -from brickbyte import Brickbyte +import brickbyte -bb = Brickbyte() +bb = brickbyte.client() # COMMAND ---------- @@ -51,7 +51,7 @@ # MAGIC %md # MAGIC ## Sync with Auto-Discovered Credentials -# MAGIC +# MAGIC # MAGIC If you've set up secrets in scope `brickbyte`, credentials are merged automatically: # COMMAND ---------- @@ -85,9 +85,9 @@ # Validate specific source if bb.validate_credentials("source-s3"): - print("✓ S3 credentials found") + print("S3 credentials found") else: - print("✗ S3 credentials not configured") + print("S3 credentials not configured") # COMMAND ---------- @@ -97,15 +97,15 @@ # COMMAND ---------- # Use a different secrets scope -bb_custom = BrickByte(secrets_scope="my-team-secrets") +bb_custom = brickbyte.client(secrets_scope="my-team-secrets") # COMMAND ---------- # MAGIC %md # MAGIC ## YAML Profiles (Advanced) -# MAGIC +# MAGIC # MAGIC For credential reuse across multiple sources, use a YAML profiles file: -# MAGIC +# MAGIC # MAGIC ```yaml # MAGIC # /Workspace/Shared/brickbyte/profiles.yml # MAGIC profiles: @@ -113,7 +113,7 @@ # MAGIC tenant_id: "{{ secret('azure/tenant_id') }}" # MAGIC client_id: "{{ secret('azure/client_id') }}" # MAGIC client_secret: "{{ secret('azure/client_secret') }}" -# MAGIC +# MAGIC # MAGIC mappings: # MAGIC source-microsoft-teams: azure-shared # MAGIC source-azure-blob-storage: azure-shared @@ -122,7 +122,7 @@ # COMMAND ---------- # Load with YAML profiles -# bb_profiles = BrickByte(profiles="/Workspace/Shared/brickbyte/profiles.yml") +# bb_profiles = brickbyte.client(profiles="/Workspace/Shared/brickbyte/profiles.yml") # COMMAND ---------- @@ -153,7 +153,4 @@ # source_config={"count": 100}, # catalog="main", # schema="bronze", -# enrich_metadata=True, -# enrich_model="databricks-meta-llama-3-3-70b-instruct", # ) -# print(f"Enriched tables: {result.enriched_tables}") diff --git a/notebooks/brickbyte-gcs.py b/notebooks/brickbyte-gcs.py index 8977a47..7a66b61 100644 --- a/notebooks/brickbyte-gcs.py +++ b/notebooks/brickbyte-gcs.py @@ -4,7 +4,7 @@ # COMMAND ---------- # MAGIC %md -# MAGIC # Google Cloud Storage (GCS) to Databricks with BrickByte +# MAGIC # Google Cloud Storage (GCS) to Databricks with brickbyte # MAGIC # MAGIC This notebook syncs files from Google Cloud Storage to Delta Lake tables in Unity Catalog. # MAGIC @@ -17,9 +17,9 @@ # COMMAND ---------- -from brickbyte import Brickbyte +import brickbyte -bb = Brickbyte() +bb = brickbyte.client() # COMMAND ---------- @@ -245,8 +245,4 @@ # }, # catalog="", # TODO: Set your Unity Catalog name # schema="", # TODO: Set your target schema -# enrich_metadata=True, -# enrich_model="databricks-meta-llama-3-3-70b-instruct", # ) - -# print(f"Enriched tables: {result.enriched_tables}") diff --git a/notebooks/brickbyte-github.py b/notebooks/brickbyte-github.py index 420d0f8..d7ba9db 100644 --- a/notebooks/brickbyte-github.py +++ b/notebooks/brickbyte-github.py @@ -1,6 +1,6 @@ # Databricks notebook source # MAGIC %md -# MAGIC # BrickByte - GitHub Example +# MAGIC # brickbyte - GitHub Example # MAGIC # MAGIC Sync data from GitHub to Databricks. # MAGIC @@ -13,9 +13,9 @@ # COMMAND ---------- -from brickbyte import Brickbyte +import brickbyte -bb = Brickbyte() +bb = brickbyte.client() # COMMAND ---------- diff --git a/notebooks/brickbyte-google-drive.py b/notebooks/brickbyte-google-drive.py index 4dd2665..e3c3864 100644 --- a/notebooks/brickbyte-google-drive.py +++ b/notebooks/brickbyte-google-drive.py @@ -4,7 +4,7 @@ # COMMAND ---------- # MAGIC %md -# MAGIC # Google Drive to Databricks with BrickByte +# MAGIC # Google Drive to Databricks with brickbyte # MAGIC # MAGIC This notebook syncs files from Google Drive to Delta Lake tables in Unity Catalog. # MAGIC @@ -16,9 +16,9 @@ # COMMAND ---------- -from brickbyte import Brickbyte +import brickbyte -bb = Brickbyte() +bb = brickbyte.client() # COMMAND ---------- @@ -199,8 +199,4 @@ # }, # catalog="", # TODO: Set your Unity Catalog name # schema="", # TODO: Set your target schema -# enrich_metadata=True, -# enrich_model="databricks-meta-llama-3-3-70b-instruct", # ) - -# print(f"Enriched tables: {result.enriched_tables}") diff --git a/notebooks/brickbyte-microsoft-teams.py b/notebooks/brickbyte-microsoft-teams.py index baca7fc..7881c29 100644 --- a/notebooks/brickbyte-microsoft-teams.py +++ b/notebooks/brickbyte-microsoft-teams.py @@ -4,7 +4,7 @@ # COMMAND ---------- # MAGIC %md -# MAGIC # Microsoft Teams to Databricks with BrickByte +# MAGIC # Microsoft Teams to Databricks with brickbyte # MAGIC # MAGIC This notebook syncs data from Microsoft Teams to Delta Lake tables in Unity Catalog. # MAGIC @@ -30,9 +30,9 @@ # COMMAND ---------- -from brickbyte import Brickbyte +import brickbyte -bb = Brickbyte() +bb = brickbyte.client() # COMMAND ---------- @@ -40,7 +40,7 @@ # MAGIC ## Azure AD App Setup # MAGIC # MAGIC 1. Go to [Azure Portal](https://portal.azure.com/) → Azure Active Directory → App registrations -# MAGIC 2. Click **New registration**, name it (e.g., "BrickByte Teams Connector") +# MAGIC 2. Click **New registration**, name it (e.g., "brickbyte Teams Connector") # MAGIC 3. Under **API permissions**, add Microsoft Graph **Application permissions**: # MAGIC - `Group.Read.All` # MAGIC - `Channel.Read.All` @@ -265,11 +265,8 @@ # streams=["users", "teams", "channels"], # catalog="", # TODO: Set your Unity Catalog name # schema="", # TODO: Set your target schema -# enrich_metadata=True, -# enrich_model="databricks-meta-llama-3-3-70b-instruct", # ) -# print(f"Enriched tables: {result.enriched_tables}") # COMMAND ---------- diff --git a/notebooks/brickbyte-s3.py b/notebooks/brickbyte-s3.py index 58c726d..13bb551 100644 --- a/notebooks/brickbyte-s3.py +++ b/notebooks/brickbyte-s3.py @@ -4,7 +4,7 @@ # COMMAND ---------- # MAGIC %md -# MAGIC # Amazon S3 to Databricks with BrickByte +# MAGIC # Amazon S3 to Databricks with brickbyte # MAGIC # MAGIC This notebook syncs files from Amazon S3 to Delta Lake tables in Unity Catalog. # MAGIC @@ -21,9 +21,9 @@ # COMMAND ---------- -from brickbyte import Brickbyte +import brickbyte -bb = Brickbyte() +bb = brickbyte.client() # COMMAND ---------- @@ -357,8 +357,4 @@ # }, # catalog="", # TODO: Set your Unity Catalog name # schema="", # TODO: Set your target schema -# enrich_metadata=True, -# enrich_model="databricks-meta-llama-3-3-70b-instruct", # ) - -# print(f"Enriched tables: {result.enriched_tables}") diff --git a/pyproject.toml b/pyproject.toml index b66b907..c9bb78b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta" [project] name = "brickbyte" -version = "0.1.0" +version = "0.1.0rc1" description = "Sync data from Airbyte sources to Databricks Unity Catalog with streaming architecture" authors = [ { name="Sri Tikkireddy", email="sri.tikkireddy@databricks.com" }, { name="Peter Park", email="peter.park@databricks.com" }, ] readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.10,<3.13" keywords = ["databricks", "airbyte", "etl", "data-engineering", "unity-catalog", "delta-lake"] classifiers = [ "Development Status :: 4 - Beta", @@ -26,11 +26,12 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ - "virtualenv", - "databricks-sdk>=0.74.0", - "databricks-sql-connector>=4.2.2", - "airbyte>=0.34.0", - "pyarrow>=14.0.0", + "virtualenv==20.29.3", + "databricks-sdk==0.95.0", + "databricks-sql-connector==4.2.5", + "airbyte==0.38.0", + "pyarrow==21.0.0", + "pyyaml==6.0.3", ] [project.urls] @@ -46,6 +47,9 @@ dev = [ "pytest", "ruff==0.6.4", ] +progress = [ + "tqdm==4.67.3", +] local-spark = [ "delta-spark>=3.2.0", "pyspark>=3.5.0", @@ -84,4 +88,6 @@ docstring-code-format = true docstring-code-line-length = 88 [tool.pytest.ini_options] -pythonpath = ["src"] \ No newline at end of file +pythonpath = ["src"] +markers = ["integration: requires configured Databricks workspace"] +addopts = "--strict-markers" diff --git a/src/brickbyte/__init__.py b/src/brickbyte/__init__.py index 74c5772..27ec6f7 100644 --- a/src/brickbyte/__init__.py +++ b/src/brickbyte/__init__.py @@ -1,40 +1,14 @@ """ -Brickbyte - Sync data from 600+ sources directly into Databricks. +brickbyte - Sync data from 600+ sources directly into Databricks. """ + import logging -import os -import shutil -import subprocess from dataclasses import dataclass, field -from pathlib import Path -from typing import Dict, List, Optional +from typing import List from brickbyte.types import Source -# Configure logging -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" -) -# Suppress noisy third-party DEBUG/INFO logs -logging.getLogger().setLevel(logging.WARNING) - -_noisy_loggers = [ - "py4j", - "pyspark", - "pyspark.sql.connect", - "pyspark.sql.connect.client", - "databricks", - "databricks.sdk", - "urllib3", - "grpc", - "airbyte", -] -for _logger_name in _noisy_loggers: - logging.getLogger(_logger_name).setLevel(logging.WARNING) - -logger = logging.getLogger("brickbyte") -logger.setLevel(logging.INFO) +logging.getLogger("brickbyte").addHandler(logging.NullHandler()) @dataclass @@ -44,341 +18,34 @@ class SyncResult: records_written: int streams_synced: List[str] failed_streams: List[str] = field(default_factory=list) - enriched_tables: List[str] = field(default_factory=list) - - -class VirtualEnvManager: - """Manages isolated Python virtual environments for source connectors.""" - - def __init__(self, env_dir: str): - self.env_dir = env_dir - def create_virtualenv(self): - import virtualenv - virtualenv.cli_run([self.env_dir]) - def install_source( - self, source: str, override_install: Optional[str] = None - ): - library = override_install or f"airbyte-{source}" - subprocess.check_call( - [os.path.join(self.env_dir, "bin", "pip"), "install", library], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - - def delete_virtualenv(self): - if os.path.exists(self.env_dir): - shutil.rmtree(self.env_dir) - - @property - def bin_path(self): - return os.path.join(self.env_dir, "bin") - - -class Brickbyte: +def client( + base_venv_directory: str | None = None, + secrets_scope: str = "brickbyte", + profiles: str | None = None, +): """ - Brickbyte - Sync data from any source connector to Databricks. - - Uses a streaming architecture to bypass local disk storage and - write directly to Unity Catalog. - - Supports automatic credential discovery from Databricks Secrets: - - Default scope: "brickbyte" - - Key convention: "{source-name}/{field}" (e.g., "source-s3/aws_access_key_id") - - Optional YAML profiles for credential reuse across sources + Create a brickbyte Client. + + Args: + base_venv_directory: Directory to store virtual environments. + Defaults to user's home directory. + secrets_scope: Databricks Secrets scope for credential discovery + (default: "brickbyte") + profiles: Optional path to YAML profiles file for advanced + credential configuration (e.g., credential reuse) + + Returns: + Client instance """ + from brickbyte._client import Client - def __init__( - self, - base_venv_directory: Optional[str] = None, - secrets_scope: str = "brickbyte", - profiles: Optional[str] = None, - ): - """ - Initialize Brickbyte. - - Args: - base_venv_directory: Directory to store virtual environments. - Defaults to user's home directory. - secrets_scope: Databricks Secrets scope for credential discovery - (default: "brickbyte") - profiles: Optional path to YAML profiles file for advanced - credential configuration (e.g., credential reuse) - """ - self._base_venv_directory = base_venv_directory or str(Path.home()) - self._source_env_managers: Dict[str, VirtualEnvManager] = {} - - # Initialize credential resolver - from brickbyte.credentials import CredentialResolver - self._credential_resolver = CredentialResolver( - secrets_scope=secrets_scope, - profiles_path=profiles, - ) - - def _setup_source(self, source: str, source_install: Optional[str] = None): - """Install source connector in isolated venv.""" - if source in self._source_env_managers: - return - - path = os.path.join(self._base_venv_directory, f"brickbyte-{source}") - manager = VirtualEnvManager(path) - manager.create_virtualenv() - manager.install_source(source, source_install) - self._source_env_managers[source] = manager - - def _get_source_exec_path(self, source: str) -> str: - """Get path to source connector executable.""" - return os.path.join(self._source_env_managers[source].bin_path, source) - - def _validate_sync_params( - self, - mode: str, - staging_volume: str, - ): - """Validate sync parameters.""" - valid_modes = ("append", "overwrite") - if mode not in valid_modes: - if mode == "merge": - raise NotImplementedError("Merge mode is not yet supported.") - raise ValueError( - f"Invalid mode '{mode}'. Must be one of: {', '.join(valid_modes)}" - ) - - def preview( - self, - source: str, - source_config: dict, - catalog: str, - schema: str, - streams: Optional[List[str]] = None, - source_install: Optional[str] = None, - sample_size: int = 5, - ): - """ - Preview a sync operation. - - Args: - source: Source connector name - source_config: Configuration dictionary for the source - catalog: Unity Catalog name - schema: Target schema name - streams: List of streams to preview (None = all streams) - source_install: Override source installation - sample_size: Number of sample records per stream - - Returns: - PreviewResult with detailed comparison - """ - import airbyte as ab - - from brickbyte.preview import PreviewEngine - - merged_config = self._credential_resolver.merge_credentials(source, source_config) - - try: - logger.info(f"Setting up {source}...") - self._setup_source(source, source_install) - - ab_source = ab.get_source( - source, - config=merged_config, - local_executable=self._get_source_exec_path(source), - ) - ab_source.check() - - if streams: - ab_source.select_streams(streams) - else: - ab_source.select_all_streams() - - selected = list(ab_source.get_selected_streams()) - - logger.info("Generating preview (streaming)...") - engine = PreviewEngine(catalog=catalog, schema=schema) - result = engine.preview( - ab_source=ab_source, - streams=selected, - sample_size=sample_size, - ) - - return result - - finally: - self.cleanup() - - def sync( - self, - source: str, - source_config: dict, - catalog: str, - schema: str, - staging_volume: Optional[str] = None, - streams: Optional[List[str]] = None, - mode: str = "overwrite", - flatten: bool = False, - enrich_metadata: bool = False, - enrich_model: Optional[str] = None, - warehouse_id: Optional[str] = None, - source_install: Optional[str] = None, - cleanup: bool = True, - buffer_size_records: int = 50000, - buffer_size_mb: int = 100, - continue_on_error: bool = False, - ) -> SyncResult: - """ - Sync data from a source connector to Databricks (Streaming). - - Args: - source: Source connector name (e.g., "source-github") - source_config: Configuration dictionary for the source connector - catalog: Unity Catalog name (e.g., "main") - schema: Target schema name (e.g., "bronze") - staging_volume: Unity Catalog Volume path (REQUIRED for remote) - streams: List of streams to sync. None = all streams (default) - mode: Write mode ("overwrite" or "append") - flatten: If True, flatten record fields into columns. - If False (default), store as JSON in 'data' column. - enrich_metadata: If True, use AI to generate column descriptions - enrich_model: Foundation Model endpoint for enrichment - warehouse_id: SQL warehouse ID (optional, auto-discovered) - source_install: Override source installation (e.g., custom git URL) - cleanup: Whether to cleanup venvs after sync (default: True) - buffer_size_records: Records per micro-batch (default: 50k) - buffer_size_mb: Max batch size in MB (default: 100MB) - continue_on_error: If True, continue syncing other streams if one fails - - Returns: - SyncResult with records_written, streams_synced, failed_streams, enriched_tables - """ - import airbyte as ab - - from brickbyte.writers import create_streaming_writer - - self._validate_sync_params(mode, staging_volume) - merged_config = self._credential_resolver.merge_credentials(source, source_config) - - try: - logger.info(f"Setting up {source}...") - self._setup_source(source, source_install) - - logger.info(f"Configuring {source}...") - ab_source = ab.get_source( - source, - config=merged_config, - local_executable=self._get_source_exec_path(source), - ) - - logger.info("Validating source connection...") - ab_source.check() - - if streams: - ab_source.select_streams(streams) - else: - ab_source.select_all_streams() - - selected = list(ab_source.get_selected_streams()) - - via_msg = f" via {staging_volume}" if staging_volume else " (Native Spark)" - logger.info(f"Streaming {len(selected)} streams to {catalog}.{schema}{via_msg}...") - - writer = create_streaming_writer( - catalog=catalog, - schema=schema, - staging_volume=staging_volume, - warehouse_id=warehouse_id, - buffer_size_records=buffer_size_records, - buffer_size_mb=buffer_size_mb, - flatten=flatten, - ) - - total_records = 0 - failed_streams: List[str] = [] - - for stream_name in selected: - logger.info(f" Streaming: {stream_name}") - - if mode == "overwrite": - writer.drop_table(stream_name) - - try: - records_generator = ab_source.get_records(stream_name) - count = 0 - for record in records_generator: - writer.write_record(stream_name, record) - count += 1 - if count % 10000 == 0: - logger.info(f" ...streamed {count} records") - - writer.flush_stream(stream_name) - logger.info(f" ✓ {count} records streamed") - total_records += count - except Exception as e: - error_name = type(e).__name__ - logger.error(f" ✗ Failed to stream {stream_name}: {e}") - failed_streams.append(stream_name) - - is_fatal = "ConnectorFailed" in error_name - if is_fatal and not continue_on_error: - raise - if not continue_on_error: - raise - - if failed_streams: - if continue_on_error: - logger.warning( - f"Completed with {len(failed_streams)} failed streams: {failed_streams}" - ) - else: - raise RuntimeError(f"Sync failed. Failed streams: {failed_streams}") - - writer.close() - - successful_streams = [s for s in selected if s not in failed_streams] - - enriched_tables = [] - if enrich_metadata and successful_streams: - logger.info("Enriching metadata with AI...") - from brickbyte.enrichment import enrich_table - - model = enrich_model or "databricks-meta-llama-3-3-70b-instruct" - for stream_name in successful_streams: - try: - enrich_table( - catalog=catalog, - schema=schema, - table=stream_name, - apply_to_catalog=True, - model_name=model, - ) - enriched_tables.append(stream_name) - except Exception as e: - logger.warning(f" Warning: Could not enrich {stream_name}: {e}") - - return SyncResult( - records_written=total_records, - streams_synced=successful_streams, - failed_streams=failed_streams, - enriched_tables=enriched_tables, - ) - - finally: - if cleanup: - self.cleanup() - - def cleanup(self): - """Remove virtual environments.""" - for manager in self._source_env_managers.values(): - manager.delete_virtualenv() - self._source_env_managers.clear() - - def list_configured_sources(self) -> List[str]: - """List all sources that have credentials configured.""" - return self._credential_resolver.list_available_sources() - - def validate_credentials(self, source: str) -> bool: - """Check if credentials are configured for a source.""" - return self._credential_resolver.validate(source) + return Client( + base_venv_directory=base_venv_directory, + secrets_scope=secrets_scope, + profiles=profiles, + ) -__all__ = ["Brickbyte", "SyncResult", "Source"] +__all__ = ["client", "SyncResult", "Source"] diff --git a/src/brickbyte/_client.py b/src/brickbyte/_client.py new file mode 100644 index 0000000..43b8f7c --- /dev/null +++ b/src/brickbyte/_client.py @@ -0,0 +1,766 @@ +""" +Internal Client class for brickbyte. +""" + +import logging +import os +import shutil +import subprocess +import threading +import uuid +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional, Union + +from brickbyte import SyncResult + +logger = logging.getLogger("brickbyte") + + +class VirtualEnvManager: + """Manages isolated Python virtual environments for source connectors.""" + + def __init__(self, env_dir: str): + self.env_dir = env_dir + + def create_virtualenv(self): + import virtualenv + + virtualenv.cli_run([self.env_dir]) + + def install_source(self, source: str, override_install: Optional[str] = None): + library = override_install or f"airbyte-{source}" + subprocess.check_call( + [os.path.join(self.env_dir, "bin", "pip"), "install", library], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + def delete_virtualenv(self): + if os.path.exists(self.env_dir): + shutil.rmtree(self.env_dir) + + @property + def bin_path(self): + return os.path.join(self.env_dir, "bin") + + +class Client: + """ + brickbyte Client - Sync data from any source connector to Databricks. + + Uses a streaming architecture to bypass local disk storage and + write directly to Unity Catalog. + + Supports automatic credential discovery from Databricks Secrets: + - Default scope: "brickbyte" + - Key convention: "{source-name}/{field}" (e.g., "source-s3/aws_access_key_id") + - Optional YAML profiles for credential reuse across sources + """ + + def __init__( + self, + base_venv_directory: Optional[str] = None, + secrets_scope: str = "brickbyte", + profiles: Optional[str] = None, + ): + self._base_venv_directory = base_venv_directory or str(Path.home()) + self._source_env_managers: Dict[str, VirtualEnvManager] = {} + + from brickbyte.credentials import CredentialResolver + + self._credential_resolver = CredentialResolver( + secrets_scope=secrets_scope, + profiles_path=profiles, + ) + + def _setup_source(self, source: str, source_install: Optional[str] = None): + """Install source connector in isolated venv.""" + if source in self._source_env_managers: + return + + path = os.path.join(self._base_venv_directory, f"brickbyte-{source}") + manager = VirtualEnvManager(path) + manager.create_virtualenv() + manager.install_source(source, source_install) + self._source_env_managers[source] = manager + + def _get_source_exec_path(self, source: str) -> str: + """Get path to source connector executable.""" + return os.path.join(self._source_env_managers[source].bin_path, source) + + def _validate_sync_params(self, mode: str): + """Validate sync parameters.""" + valid_modes = ("append", "overwrite") + if mode not in valid_modes: + if mode == "merge": + raise NotImplementedError("Merge mode is not yet supported.") + raise ValueError(f"Invalid mode '{mode}'. Must be one of: {', '.join(valid_modes)}") + + def _create_source_instance( + self, + ab_module: Any, + source: str, + source_config: dict, + ) -> Any: + """Create a configured Airbyte source instance.""" + return ab_module.get_source( + source, + config=source_config, + local_executable=self._get_source_exec_path(source), + ) + + @staticmethod + def _select_streams(ab_source: Any, streams: Optional[List[str]]) -> None: + """Select requested streams on a source instance.""" + if streams: + ab_source.select_streams(streams) + else: + ab_source.select_all_streams() + + def preview( + self, + source: str, + source_config: dict, + catalog: str, + schema: str, + streams: Optional[List[str]] = None, + source_install: Optional[str] = None, + sample_size: int = 5, + ): + """ + Preview a sync operation. + + Args: + source: Source connector name + source_config: Configuration dictionary for the source + catalog: Unity Catalog name + schema: Target schema name + streams: List of streams to preview (None = all streams) + source_install: Override source installation + sample_size: Number of sample records per stream + + Returns: + PreviewResult with sampled source records, target counts, and schema changes + """ + import airbyte as ab + + from brickbyte.preview import PreviewEngine + + merged_config = self._credential_resolver.merge_credentials(source, source_config) + + try: + logger.info(f"Setting up {source}...") + self._setup_source(source, source_install) + + ab_source = self._create_source_instance(ab, source, merged_config) + ab_source.check() + + self._select_streams(ab_source, streams) + + selected = list(ab_source.get_selected_streams()) + + logger.info("Generating preview (streaming)...") + engine = PreviewEngine(catalog=catalog, schema=schema) + result = engine.preview( + ab_source=ab_source, + streams=selected, + sample_size=sample_size, + ) + + return result + + finally: + self.cleanup() + + def sync( + self, + source: str, + source_config: dict, + catalog: str, + schema: str, + staging_volume: Optional[str] = None, + streams: Optional[List[str]] = None, + mode: str = "overwrite", + flatten: bool = False, + warehouse_id: Optional[str] = None, + source_install: Optional[str] = None, + cleanup: bool = False, + buffer_size_records: int = 50000, + buffer_size_mb: int = 100, + continue_on_error: bool = False, + timeout_seconds: Optional[int] = None, + incremental: bool = False, + deduplicate: bool = False, + dedup_keys: Optional[Union[List[str], Dict[str, List[str]]]] = None, + max_parallel_streams: int = 1, + progress_callback: Optional[Callable] = None, + ) -> SyncResult: + """ + Sync data from a source connector to Databricks (Streaming). + + Args: + source: Source connector name (e.g., "source-github") + source_config: Configuration dictionary for the source connector + catalog: Unity Catalog name (e.g., "main") + schema: Target schema name (e.g., "bronze") + staging_volume: Unity Catalog Volume path (REQUIRED for remote) + streams: List of streams to sync. None = all streams (default) + mode: Write mode ("overwrite" or "append") + flatten: If True, flatten record fields into columns. + If False (default), store as JSON in 'data' column. + warehouse_id: SQL warehouse ID (optional, auto-discovered) + source_install: Override source installation (e.g., custom git URL) + cleanup: Whether to cleanup venvs after sync (default: False) + buffer_size_records: Records per micro-batch (default: 50k) + buffer_size_mb: Max batch size in MB (default: 100MB) + continue_on_error: If True, continue syncing other streams if one fails + timeout_seconds: Optional timeout in seconds for the sync operation + incremental: If True, use incremental sync with state management + deduplicate: If True, deduplicate records after sync + dedup_keys: Column(s) to use as dedup keys (required when deduplicate=True; + ignored when deduplicate=False) + max_parallel_streams: Max number of streams to write in parallel (default: 1) + progress_callback: Optional callback for progress reporting + + Returns: + SyncResult with records_written, streams_synced, failed_streams + """ + import airbyte as ab + + from brickbyte._sanitize import sanitize_stream_name + from brickbyte.writers import create_streaming_writer + + self._validate_sync_params(mode) + merged_config = self._credential_resolver.merge_credentials(source, source_config) + + run_id = str(uuid.uuid4()) + + # Normalize dedup_keys if deduplicate is enabled + normalized_dedup_keys = None + if deduplicate: + normalized_dedup_keys = self._normalize_dedup_keys(dedup_keys, streams) + elif dedup_keys is not None: + logger.info("dedup_keys provided but deduplicate=False; ignoring dedup_keys") + + # Set up timeout + cancel_event = None + timer = None + if timeout_seconds is not None: + cancel_event = threading.Event() + timer = threading.Timer(timeout_seconds, cancel_event.set) + timer.daemon = True + timer.start() + + writer = None + progress_reporter = None + try: + logger.info(f"Setting up {source}...") + self._setup_source(source, source_install) + + logger.info(f"Configuring {source}...") + ab_source = self._create_source_instance(ab, source, merged_config) + + logger.info("Validating source connection...") + ab_source.check() + + self._select_streams(ab_source, streams) + + selected = list(ab_source.get_selected_streams()) + + # Sanitize stream names upfront and check for collisions + sanitized_map = {} + for stream in selected: + sanitized = sanitize_stream_name(stream) + if sanitized in sanitized_map and sanitized_map[sanitized] != stream: + raise ValueError( + f"Stream name collision after sanitization: " + f"'{sanitized_map[sanitized]}' and '{stream}' both map to '{sanitized}'" + ) + sanitized_map[sanitized] = stream + + if normalized_dedup_keys is not None and "__all__" in normalized_dedup_keys: + all_keys = normalized_dedup_keys["__all__"] + normalized_dedup_keys = {s: all_keys for s in selected} + + if deduplicate and isinstance(normalized_dedup_keys, dict): + for dk_stream in normalized_dedup_keys: + if dk_stream not in selected: + if dk_stream in sanitized_map: + orig = sanitized_map[dk_stream] + raise ValueError( + f"dedup_keys key '{dk_stream}' is a sanitized name. " + f"Use the original Airbyte stream name '{orig}' instead." + ) + raise ValueError( + f"dedup_keys key '{dk_stream}' does not match any selected stream. " + f"Selected streams: {selected}" + ) + + state_manager = None + stream_states: Dict[str, dict] = {} + if incremental: + from brickbyte._state import StateManager + + state_manager = StateManager( + catalog=catalog, + schema=schema, + staging_volume=staging_volume, + warehouse_id=warehouse_id, + ) + for stream_name in selected: + saved = state_manager.get_state(source, stream_name) + if saved is not None: + stream_states[stream_name] = saved + logger.info(f" Loaded incremental state for {stream_name}") + if max_parallel_streams == 1: + self._apply_incremental_state(ab_source, stream_states) + + via_msg = f" via {staging_volume}" if staging_volume else " (Native Spark)" + logger.info(f"Streaming {len(selected)} streams to {catalog}.{schema}{via_msg}...") + + if progress_callback is not None: + from brickbyte._progress import ProgressReporter + + progress_reporter = ProgressReporter( + total_streams=len(selected), + callback=progress_callback, + ) + + total_records = 0 + failed_streams: List[str] = [] + successful_streams: List[str] = [] + + # Common writer-creation kwargs used by both paths + _writer_kwargs = dict( + catalog=catalog, + schema=schema, + staging_volume=staging_volume, + warehouse_id=warehouse_id, + buffer_size_records=buffer_size_records, + buffer_size_mb=buffer_size_mb, + flatten=flatten, + run_id=run_id, + dedup_keys=normalized_dedup_keys, + ) + + if max_parallel_streams > 1: + import concurrent.futures + + def _sync_stream_parallel(stream_name: str): + """Sync a single stream in an isolated worker.""" + logger.info(f" Streaming: {stream_name}") + stream_source = self._create_source_instance(ab, source, merged_config) + self._select_streams(stream_source, [stream_name]) + if incremental and stream_name in stream_states: + self._apply_incremental_state( + stream_source, + {stream_name: stream_states[stream_name]}, + ) + + thread_writer = create_streaming_writer(**_writer_kwargs) + try: + if mode == "overwrite": + thread_writer.safe_overwrite_begin(stream_name, run_id) + + count = 0 + for record in stream_source.get_records(stream_name): + if cancel_event and cancel_event.is_set(): + raise TimeoutError( + f"Sync timed out after {timeout_seconds} seconds" + ) + + thread_writer.write_record(stream_name, record) + count += 1 + if progress_reporter and count % 5000 == 0: + progress_reporter.record_processed(stream_name, count) + + thread_writer.flush_stream(stream_name) + + if mode == "overwrite": + thread_writer.safe_overwrite_finish(stream_name, run_id) + + self._run_dedup_for_stream( + stream_name, + deduplicate, + normalized_dedup_keys, + flatten, + catalog, + schema, + thread_writer, + ) + + state = None + if incremental: + state = self._extract_incremental_state( + ab_source=stream_source, + stream_name=stream_name, + run_id=run_id, + records_written=count, + ) + + return stream_name, count, state + except Exception as e: + logger.error(f" Failed to stream {stream_name}: {e}") + raise + finally: + thread_writer.close() + + with concurrent.futures.ThreadPoolExecutor( + max_workers=max_parallel_streams + ) as executor: + future_to_stream = { + executor.submit(_sync_stream_parallel, stream_name): stream_name + for stream_name in selected + } + + for future in concurrent.futures.as_completed(future_to_stream): + stream_name = future_to_stream[future] + try: + _sname, count, state = future.result() + total_records += count + successful_streams.append(_sname) + + if progress_reporter: + progress_reporter.stream_completed(_sname, count) + + if incremental and state_manager is not None: + state_manager.save_state( + source=source, + stream_name=_sname, + state=state, + run_id=run_id, + ) + + logger.info(f" {count} records streamed") + except Exception as e: + error_name = type(e).__name__ + failed_streams.append(stream_name) + + is_fatal = "ConnectorFailed" in error_name + if is_fatal or not continue_on_error: + for pending in future_to_stream: + if pending is not future: + pending.cancel() + raise + + else: + # Sequential processing (default) + writer = create_streaming_writer(**_writer_kwargs) + + for stream_name in selected: + logger.info(f" Streaming: {stream_name}") + + if mode == "overwrite": + writer.safe_overwrite_begin(stream_name, run_id) + + try: + records_generator = ab_source.get_records(stream_name) + count = 0 + for record in records_generator: + if cancel_event and cancel_event.is_set(): + raise TimeoutError( + f"Sync timed out after {timeout_seconds} seconds" + ) + + writer.write_record(stream_name, record) + count += 1 + + if progress_reporter and count % 5000 == 0: + progress_reporter.record_processed(stream_name, count) + + if count % 10000 == 0: + logger.info(f" ...streamed {count} records") + + if cancel_event and count % 1000 == 0 and cancel_event.is_set(): + raise TimeoutError( + f"Sync timed out after {timeout_seconds} seconds" + ) + + writer.flush_stream(stream_name) + + if mode == "overwrite": + writer.safe_overwrite_finish(stream_name, run_id) + + self._run_dedup_for_stream( + stream_name, + deduplicate, + normalized_dedup_keys, + flatten, + catalog, + schema, + writer, + ) + + logger.info(f" {count} records streamed") + total_records += count + successful_streams.append(stream_name) + + if progress_reporter: + progress_reporter.stream_completed(stream_name, count) + + self._save_incremental_state( + state_manager=state_manager, + incremental=incremental, + ab_source=ab_source, + source=source, + stream_name=stream_name, + run_id=run_id, + records_written=count, + ) + + except Exception as e: + error_name = type(e).__name__ + logger.error(f" Failed to stream {stream_name}: {e}") + failed_streams.append(stream_name) + + is_fatal = "ConnectorFailed" in error_name + if is_fatal: + raise + if not continue_on_error: + raise + + if failed_streams: + if continue_on_error: + logger.warning( + f"Completed with {len(failed_streams)} failed streams: " f"{failed_streams}" + ) + else: + raise RuntimeError(f"Sync failed. Failed streams: {failed_streams}") + + return SyncResult( + records_written=total_records, + streams_synced=successful_streams, + failed_streams=failed_streams, + ) + + finally: + if timer is not None: + timer.cancel() + if progress_reporter is not None: + try: + progress_reporter.close() + except Exception as e: + logger.debug(f"Failed to close progress reporter: {e}") + if writer is not None: + writer.close() + if cleanup: + self.cleanup() + + def _run_dedup_for_stream( + self, + stream_name: str, + deduplicate: bool, + normalized_dedup_keys: Optional[Dict[str, List[str]]], + flatten: bool, + catalog: str, + schema: str, + executor_writer, + ): + """Run dedup for a single stream using the provided writer as executor.""" + if not deduplicate or not normalized_dedup_keys: + return + + stream_keys = normalized_dedup_keys.get(stream_name) + if stream_keys is None: + return + + from brickbyte._dedup import deduplicate_stream + from brickbyte._sanitize import sanitize_stream_name + + sanitized = sanitize_stream_name(stream_name) + table_name = f"`{catalog}`.`{schema}`.`{sanitized}`" + dk_cols = [f"_dk_{i}" for i in range(len(stream_keys))] + + if flatten: + deduplicate_stream( + executor=executor_writer, + table_name=table_name, + key_columns=dk_cols, + run_id_col="_run_id", + extracted_at_col="_extracted_at", + record_id_col="_record_id", + flatten=True, + ) + else: + deduplicate_stream( + executor=executor_writer, + table_name=table_name, + key_columns=dk_cols, + run_id_col="run_id", + extracted_at_col="extracted_at", + record_id_col="record_id", + flatten=False, + ) + + def _normalize_dedup_keys( + self, + dedup_keys: Optional[Union[List[str], Dict[str, List[str]]]], + streams: Optional[List[str]], + ) -> Dict[str, List[str]]: + """Normalize dedup_keys to Dict[str, List[str]].""" + if dedup_keys is None: + raise ValueError( + "dedup_keys is required when deduplicate=True. " + "Provide a list of column names or a dict mapping stream names to column lists." + ) + + if isinstance(dedup_keys, list): + if len(dedup_keys) == 0: + raise ValueError("dedup_keys must be non-empty") + self._validate_dedup_key_list(dedup_keys, context="dedup_keys") + return {"__all__": dedup_keys} + + if isinstance(dedup_keys, dict): + for stream_name, keys in dedup_keys.items(): + if not isinstance(keys, list) or len(keys) == 0: + raise ValueError(f"dedup_keys for stream '{stream_name}' must be non-empty") + self._validate_dedup_key_list( + keys, + context=f"dedup_keys for stream '{stream_name}'", + ) + return dedup_keys + + raise ValueError("dedup_keys must be a list or dict") + + def _validate_dedup_key_list(self, keys: List[str], context: str) -> None: + """Validate dedup key identifier safety.""" + from brickbyte._sanitize import validate_identifier + + for key in keys: + if not isinstance(key, str) or not key: + raise ValueError(f"{context} must contain non-empty string keys") + try: + validate_identifier(key) + except ValueError as e: + raise ValueError(f"{context} contains invalid key '{key}': {e}") from e + + def _apply_incremental_state( + self, + ab_source: Any, + stream_states: Dict[str, dict], + ) -> None: + """Apply previously saved stream states to the source before reading.""" + if not stream_states: + return + + for method_name in ("set_stream_state", "set_state_for_stream"): + method = getattr(ab_source, method_name, None) + if not callable(method): + continue + try: + for stream_name, state in stream_states.items(): + method(stream_name, state) + logger.info(f"Applied incremental state for {len(stream_states)} stream(s)") + return + except TypeError: + continue + + set_state = getattr(ab_source, "set_state", None) + if callable(set_state): + state_payload = { + "streams": [ + { + "stream": {"name": stream_name}, + "stream_state": state, + } + for stream_name, state in stream_states.items() + ] + } + for payload in (state_payload, stream_states): + try: + set_state(payload) + logger.info(f"Applied incremental state for {len(stream_states)} stream(s)") + return + except TypeError: + continue + + raise NotImplementedError( + "incremental=True requires source state injection support " + "(set_stream_state/set_state_for_stream/set_state)." + ) + + def _extract_incremental_state( + self, + ab_source: Any, + stream_name: str, + run_id: str, + records_written: int, + ) -> dict: + """Extract connector-emitted stream state when available.""" + fallback_state = {"run_id": run_id, "records": records_written} + + for method_name in ("get_stream_state", "stream_state"): + method = getattr(ab_source, method_name, None) + if not callable(method): + continue + try: + state = method(stream_name) + if state is not None: + return state + except TypeError: + continue + except Exception as e: + logger.debug(f"Could not read stream state via {method_name}: {e}") + + get_state = getattr(ab_source, "get_state", None) + if callable(get_state): + for args in ((stream_name,), tuple()): + try: + state = get_state(*args) + except TypeError: + continue + except Exception as e: + logger.debug(f"Could not read state via get_state: {e}") + break + if state is None: + continue + if isinstance(state, dict): + if stream_name in state: + return state[stream_name] + streams_state = state.get("streams") + if isinstance(streams_state, dict) and stream_name in streams_state: + return streams_state[stream_name] + return state + + return fallback_state + + def _save_incremental_state( + self, + state_manager, + incremental: bool, + ab_source: Any, + source: str, + stream_name: str, + run_id: str, + records_written: int, + ) -> None: + """Persist state for a successfully synced stream.""" + if not incremental or state_manager is None: + return + + state = self._extract_incremental_state( + ab_source=ab_source, + stream_name=stream_name, + run_id=run_id, + records_written=records_written, + ) + state_manager.save_state( + source=source, + stream_name=stream_name, + state=state, + run_id=run_id, + ) + + def cleanup(self): + """Remove virtual environments.""" + for manager in self._source_env_managers.values(): + manager.delete_virtualenv() + self._source_env_managers.clear() + + def list_configured_sources(self) -> List[str]: + """List all sources that have credentials configured.""" + return self._credential_resolver.list_available_sources() + + def validate_credentials(self, source: str) -> bool: + """Check if credentials are configured for a source.""" + return self._credential_resolver.validate(source) diff --git a/src/brickbyte/_dedup.py b/src/brickbyte/_dedup.py new file mode 100644 index 0000000..1baa154 --- /dev/null +++ b/src/brickbyte/_dedup.py @@ -0,0 +1,92 @@ +""" +Deduplication logic for brickbyte. + +Uses MERGE to remove duplicate records based on user-specified keys. +""" + +import logging +from typing import List + +logger = logging.getLogger("brickbyte") + + +def deduplicate_stream( + executor, + table_name: str, + key_columns: List[str], + run_id_col: str, + extracted_at_col: str, + record_id_col: str, + flatten: bool = True, + dk_missing_col: str = "_dk_missing", +): + """ + Deduplicate a stream's table using MERGE. + + Keeps the row with the latest extracted_at per unique key combo. + On timestamp ties, breaks by record_id (lexicographic max). + Records with _dk_missing=true are excluded from dedup. + + Args: + executor: Writer instance with _execute (SQL) or spark (Spark) attribute + table_name: Fully qualified table name + key_columns: Columns to use as dedup keys + run_id_col: Name of the run_id column + extracted_at_col: Name of the extracted_at column + record_id_col: Name of the record_id column + flatten: Whether the table is in flatten mode + dk_missing_col: Name of the dk_missing indicator column + """ + if not key_columns: + return + + from brickbyte._sanitize import validate_identifier + + validated_keys = [validate_identifier(col) for col in key_columns] + validate_identifier(run_id_col) + validated_extracted_at_col = validate_identifier(extracted_at_col) + validated_record_id_col = validate_identifier(record_id_col) + validated_dk_missing_col = validate_identifier(dk_missing_col) + + key_match = " AND ".join(f"t.`{col}` <=> s.`{col}`" for col in validated_keys) + + # Build the dedup MERGE statement + # This keeps only the latest record per key combo + merge_sql = f""" + MERGE INTO {table_name} t + USING ( + SELECT *, ROW_NUMBER() OVER ( + PARTITION BY {', '.join(f'`{c}`' for c in validated_keys)} + ORDER BY `{validated_extracted_at_col}` DESC, `{validated_record_id_col}` DESC + ) AS _rn + FROM {table_name} + WHERE `{validated_dk_missing_col}` = false + ) s + ON {key_match} + AND t.`{validated_record_id_col}` = s.`{validated_record_id_col}` + WHEN MATCHED AND s._rn > 1 THEN DELETE + """ + + _execute_sql(executor, merge_sql) + + +def _execute_sql(executor, sql: str): + """Execute SQL via the provided executor. + + The executor should be a writer instance that has either a ``spark`` + attribute (SparkStreamingWriter) or an ``_execute`` method + (SQLStreamingWriter). Passing ``None`` is a programming error — + callers must always supply the writer that owns the table. + """ + if executor is None: + raise RuntimeError( + "No executor provided for dedup SQL. " + "This is a bug — the writer that wrote the table must be passed." + ) + + if hasattr(executor, "spark"): + executor.spark.sql(sql) + elif hasattr(executor, "_execute"): + executor._execute(sql) + else: + raise RuntimeError(f"Unknown executor type: {type(executor)}") diff --git a/src/brickbyte/_progress.py b/src/brickbyte/_progress.py new file mode 100644 index 0000000..92c605f --- /dev/null +++ b/src/brickbyte/_progress.py @@ -0,0 +1,105 @@ +""" +Progress reporting for brickbyte sync operations. +""" + +import threading +import time +from dataclasses import dataclass +from typing import Callable, Optional + + +@dataclass +class ProgressEvent: + """Progress event emitted during sync.""" + + stream_name: str + records_processed: int + total_streams: int + streams_completed: int + elapsed_seconds: float + + +class ProgressReporter: + """ + Reports sync progress via callback and optional tqdm bar. + """ + + def __init__( + self, + total_streams: int, + callback: Optional[Callable[[ProgressEvent], None]] = None, + use_tqdm: bool = False, + ): + self.total_streams = total_streams + self.callback = callback + self.streams_completed = 0 + self._start_time = time.monotonic() + self._tqdm_bar = None + self._records_by_stream: dict = {} + self._lock = threading.Lock() + + if use_tqdm or self._is_notebook(): + try: + from tqdm.auto import tqdm + + self._tqdm_bar = tqdm( + total=total_streams, + desc="brickbyte sync", + unit="stream", + ) + except ImportError: + pass + + def _is_notebook(self) -> bool: + """Detect if running in a notebook environment.""" + try: + from IPython import get_ipython + + shell = get_ipython() + if shell is None: + return False + return "ZMQInteractiveShell" in type(shell).__name__ + except (ImportError, NameError): + return False + + def record_processed(self, stream_name: str, count: int): + """Called periodically during record processing.""" + with self._lock: + self._records_by_stream[stream_name] = count + streams_completed = self.streams_completed + + if count % 5000 == 0 and self.callback: + event = ProgressEvent( + stream_name=stream_name, + records_processed=count, + total_streams=self.total_streams, + streams_completed=streams_completed, + elapsed_seconds=time.monotonic() - self._start_time, + ) + self.callback(event) + + def stream_completed(self, stream_name: str, records: int): + """Called when a stream finishes.""" + with self._lock: + self.streams_completed += 1 + self._records_by_stream[stream_name] = records + streams_completed = self.streams_completed + + if self._tqdm_bar: + self._tqdm_bar.update(1) + self._tqdm_bar.set_postfix(stream=stream_name, records=records) + + if self.callback: + event = ProgressEvent( + stream_name=stream_name, + records_processed=records, + total_streams=self.total_streams, + streams_completed=streams_completed, + elapsed_seconds=time.monotonic() - self._start_time, + ) + self.callback(event) + + def close(self): + """Close tqdm bar if present.""" + if self._tqdm_bar: + self._tqdm_bar.close() diff --git a/src/brickbyte/_sanitize.py b/src/brickbyte/_sanitize.py new file mode 100644 index 0000000..8df82d1 --- /dev/null +++ b/src/brickbyte/_sanitize.py @@ -0,0 +1,61 @@ +""" +Stream name sanitization and SQL identifier validation for brickbyte. +""" + +import re + + +def sanitize_stream_name(name: str) -> str: + """ + Sanitize a stream name for use as a table name. + + - Lowercase + - Replace hyphens, dots, and whitespace with underscores + - Strip characters that are invalid even in backtick-quoted identifiers + - Prefix with underscore if starts with digit + """ + result = name.lower() + result = re.sub(r"[-.\s]+", "_", result) + # Remove null bytes, backticks, semicolons + result = re.sub(r"[\x00`;]+", "", result) + # Strip leading/trailing underscores from the substitution + result = result.strip("_") or "_" + # Prefix with underscore if starts with digit + if result[0].isdigit(): + result = f"_{result}" + return result + + +def validate_identifier(name: str) -> str: + """ + Validate that a name is safe for use inside backtick-quoted identifiers. + + Only rejects characters that are unsafe even inside backtick-quoted + identifiers: null bytes, backticks, semicolons. Does NOT reject hyphens, + dots, or unicode — Databricks allows these inside backtick-quoted identifiers. + + Returns the validated name. + Raises ValueError if the name contains dangerous characters. + """ + if not name: + raise ValueError("Identifier cannot be empty") + + dangerous = re.search(r"[\x00`;]", name) + if dangerous: + raise ValueError( + f"Identifier '{name}' contains unsafe character: " f"{repr(dangerous.group())}" + ) + + return name + + +def quoted_table_name(catalog: str, schema: str, table: str) -> str: + """ + Build a fully-qualified, backtick-quoted table name. + + Validates all parts and returns `catalog`.`schema`.`table`. + """ + validate_identifier(catalog) + validate_identifier(schema) + validate_identifier(table) + return f"`{catalog}`.`{schema}`.`{table}`" diff --git a/src/brickbyte/_schema.py b/src/brickbyte/_schema.py new file mode 100644 index 0000000..525c1f8 --- /dev/null +++ b/src/brickbyte/_schema.py @@ -0,0 +1,31 @@ +""" +Canonical schema constants and DDL for brickbyte tables. +""" + +# Raw mode columns (no underscore prefix - all columns are brickbyte-owned) +RAW_RECORD_ID = "record_id" +RAW_EXTRACTED_AT = "extracted_at" +RAW_DATA = "data" +RAW_RUN_ID = "run_id" + +RAW_COLUMNS = [RAW_RECORD_ID, RAW_EXTRACTED_AT, RAW_DATA, RAW_RUN_ID] + +# Flatten mode metadata columns (underscore prefix to avoid collision with source fields) +FLATTEN_RECORD_ID = "_record_id" +FLATTEN_EXTRACTED_AT = "_extracted_at" +FLATTEN_RUN_ID = "_run_id" + +FLATTEN_META_COLUMNS = [FLATTEN_RECORD_ID, FLATTEN_EXTRACTED_AT, FLATTEN_RUN_ID] + +# SQL DDL for raw table creation +RAW_TABLE_DDL = """ +CREATE TABLE IF NOT EXISTS {table_name} ( + record_id STRING, + extracted_at TIMESTAMP, + data STRING, + run_id STRING +) +""" + +# Dedup key missing indicator column +DK_MISSING = "_dk_missing" diff --git a/src/brickbyte/_state.py b/src/brickbyte/_state.py new file mode 100644 index 0000000..9caa0ed --- /dev/null +++ b/src/brickbyte/_state.py @@ -0,0 +1,212 @@ +""" +Incremental sync state management for brickbyte. + +Manages the `__brickbyte_state` table to track sync state per (source, stream) pair. +""" + +import json +import logging +from typing import Optional + +logger = logging.getLogger("brickbyte") + +STATE_TABLE_SUFFIX = "__brickbyte_state" + +STATE_TABLE_DDL = """ +CREATE TABLE IF NOT EXISTS {table_name} ( + source STRING, + stream_name STRING, + state STRING, + run_id STRING, + updated_at TIMESTAMP +) +""" + +UPSERT_STATE_SPARK_SQL = """ +MERGE INTO {table_name} t +USING (SELECT :source AS source, :stream_name AS stream_name, + :state AS state, :run_id AS run_id, + current_timestamp() AS updated_at) s +ON t.source = s.source AND t.stream_name = s.stream_name +WHEN MATCHED THEN UPDATE SET + t.state = s.state, t.run_id = s.run_id, t.updated_at = s.updated_at +WHEN NOT MATCHED THEN INSERT (source, stream_name, state, run_id, updated_at) + VALUES (s.source, s.stream_name, s.state, s.run_id, s.updated_at) +""" + +# SQL connector uses named params with :name syntax +UPSERT_STATE_SQL = """ +MERGE INTO {table_name} t +USING (SELECT :source AS source, :stream_name AS stream_name, + :state AS state, :run_id AS run_id, + current_timestamp() AS updated_at) s +ON t.source = s.source AND t.stream_name = s.stream_name +WHEN MATCHED THEN UPDATE SET + t.state = s.state, t.run_id = s.run_id, t.updated_at = s.updated_at +WHEN NOT MATCHED THEN INSERT (source, stream_name, state, run_id, updated_at) + VALUES (s.source, s.stream_name, s.state, s.run_id, s.updated_at) +""" + + +class StateManager: + """Manages incremental sync state in a Delta table. + + Works with both Spark (when active) and the SQL connector (when + staging_volume / warehouse_id are provided, i.e. remote mode). + """ + + def __init__( + self, + catalog: str, + schema: str, + staging_volume: Optional[str] = None, + warehouse_id: Optional[str] = None, + ): + self.catalog = catalog + self.schema = schema + self._state_table = f"`{catalog}`.`{schema}`.`{STATE_TABLE_SUFFIX}`" + self._spark = None + self._connection = None + self._initialized = False + self._staging_volume = staging_volume + self._warehouse_id = warehouse_id + + def _ensure_table(self): + """Create the state table if it doesn't exist.""" + if self._initialized: + return + + ddl = STATE_TABLE_DDL.format(table_name=self._state_table) + spark = self._get_spark() + if spark: + spark.sql(ddl) + else: + self._sql_execute(ddl) + self._initialized = True + + def _get_spark(self): + """Get Spark session if available.""" + if self._spark is None: + try: + from pyspark.sql import SparkSession + + self._spark = SparkSession.getActiveSession() + except ImportError: + pass + return self._spark + + def _get_connection(self): + """Get or create a SQL connector connection for remote mode.""" + if self._connection is not None: + return self._connection + + from databricks.sdk import WorkspaceClient + + w = WorkspaceClient() + server_hostname = w.config.host.replace("https://", "").rstrip("/") + access_token = w.config.token + + wh_id = self._warehouse_id + if not wh_id: + warehouses = list(w.warehouses.list()) + running = [wh for wh in warehouses if wh.state and wh.state.value == "RUNNING"] + if running: + wh_id = running[0].id + else: + raise RuntimeError( + "No running SQL warehouse found for state management. " + "Provide warehouse_id or start a warehouse." + ) + + from databricks import sql + + self._connection = sql.connect( + server_hostname=server_hostname, + http_path=f"/sql/1.0/warehouses/{wh_id}", + access_token=access_token, + catalog=self.catalog, + schema=self.schema, + ) + return self._connection + + def _sql_execute(self, query: str, params: Optional[dict] = None): + """Execute a query via SQL connector.""" + conn = self._get_connection() + cursor = conn.cursor() + try: + if params: + cursor.execute(query, params) + else: + cursor.execute(query) + return cursor.fetchall() if cursor.description else [] + finally: + cursor.close() + + def save_state(self, source: str, stream_name: str, state: dict, run_id: str): + """Save state for a (source, stream) pair via MERGE upsert.""" + self._ensure_table() + state_json = json.dumps(state, default=str) + + params = { + "source": source, + "stream_name": stream_name, + "state": state_json, + "run_id": run_id, + } + + spark = self._get_spark() + if spark: + spark.sql( + UPSERT_STATE_SPARK_SQL.format(table_name=self._state_table), + args=params, + ) + else: + self._sql_execute( + UPSERT_STATE_SQL.format(table_name=self._state_table), + params, + ) + + def get_state(self, source: str, stream_name: str) -> Optional[dict]: + """Load state for a (source, stream) pair. Returns None if no state exists.""" + self._ensure_table() + + spark = self._get_spark() + if spark: + from pyspark.sql.functions import col + + df = ( + spark.table(self._state_table) + .filter((col("source") == source) & (col("stream_name") == stream_name)) + .select("state") + .limit(1) + ) + rows = df.collect() + if rows: + return json.loads(rows[0]["state"]) + return None + + rows = self._sql_execute( + f"SELECT state FROM {self._state_table} " + f"WHERE source = :source AND stream_name = :stream_name LIMIT 1", + {"source": source, "stream_name": stream_name}, + ) + if rows: + return json.loads(rows[0][0]) + return None + + def clear_state(self, source: str, stream_name: str): + """Delete state for a (source, stream) pair.""" + self._ensure_table() + + spark = self._get_spark() + if spark: + spark.sql( + f"DELETE FROM {self._state_table} " + f"WHERE source = '{source}' AND stream_name = '{stream_name}'" + ) + else: + self._sql_execute( + f"DELETE FROM {self._state_table} " + f"WHERE source = :source AND stream_name = :stream_name", + {"source": source, "stream_name": stream_name}, + ) diff --git a/src/brickbyte/credentials.py b/src/brickbyte/credentials.py index b7b5369..4302e7e 100644 --- a/src/brickbyte/credentials.py +++ b/src/brickbyte/credentials.py @@ -1,9 +1,10 @@ """ -Credential management for BrickByte. +Credential management for brickbyte. Provides automatic credential resolution from Databricks Secrets with optional YAML profiles for advanced use cases. """ + import logging import re from typing import Any, Dict, List, Optional @@ -14,29 +15,17 @@ class CredentialResolver: """ Resolves credentials from Databricks Secrets with convention-based discovery. - + Default convention: Scope: "brickbyte" (configurable) Keys: "{source-name}/{field}" (e.g., "source-s3/aws_access_key_id") - - Usage: - resolver = CredentialResolver() - creds = resolver.get_credentials("source-s3") - # Returns: {"aws_access_key_id": "...", "aws_secret_access_key": "..."} """ - + def __init__( self, secrets_scope: str = "brickbyte", profiles_path: Optional[str] = None, ): - """ - Initialize the credential resolver. - - Args: - secrets_scope: Databricks Secrets scope name (default: "brickbyte") - profiles_path: Optional path to YAML profiles file for advanced config - """ self.secrets_scope = secrets_scope self.profiles_path = profiles_path self._cache: Dict[str, Dict[str, Any]] = {} @@ -44,71 +33,97 @@ def __init__( self._mappings: Dict[str, str] = {} self._dbutils = None self._available_keys: Optional[List[str]] = None - + # Load profiles if provided if profiles_path: self._load_profiles(profiles_path) - + def _get_dbutils(self): """Get dbutils instance (lazy loading).""" if self._dbutils is None: try: from pyspark.sql import SparkSession + spark = SparkSession.getActiveSession() if spark: from pyspark.dbutils import DBUtils + self._dbutils = DBUtils(spark) except Exception: pass - + if self._dbutils is None: try: import IPython + self._dbutils = IPython.get_ipython().user_ns.get("dbutils") except Exception: pass - + return self._dbutils - + def _list_secrets_for_source(self, source: str) -> List[str]: """List all secret keys available for a source.""" dbutils = self._get_dbutils() if not dbutils: return [] - + try: # Cache the full list of keys if self._available_keys is None: secrets = dbutils.secrets.list(self.secrets_scope) self._available_keys = [s.key for s in secrets] - + # Filter keys that start with the source name prefix = f"{source}/" - return [ - key[len(prefix):] for key in self._available_keys - if key.startswith(prefix) - ] + return [key[len(prefix) :] for key in self._available_keys if key.startswith(prefix)] except Exception as e: logger.debug(f"Could not list secrets: {e}") return [] - + def _get_secret(self, key: str) -> Optional[str]: - """Get a single secret value.""" + """Get a single secret value from the default scope.""" dbutils = self._get_dbutils() if not dbutils: return None - + try: return dbutils.secrets.get(scope=self.secrets_scope, key=key) except Exception as e: logger.debug(f"Could not get secret {key}: {e}") return None - + + def _get_secret_with_scope(self, scope: str, key: str) -> Optional[str]: + """Get a single secret value with explicit scope.""" + dbutils = self._get_dbutils() + if not dbutils: + return None + + try: + return dbutils.secrets.get(scope=scope, key=key) + except Exception as e: + logger.debug(f"Could not get secret {scope}/{key}: {e}") + return None + + def _set_nested(self, d: dict, dotted_key: str, value: Any): + """Set a value in a nested dict using dotted key notation. + + Example: _set_nested(d, "credentials.client_id", "val") + produces d = {"credentials": {"client_id": "val"}} + """ + keys = dotted_key.split(".") + current = d + for k in keys[:-1]: + if k not in current or not isinstance(current[k], dict): + current[k] = {} + current = current[k] + current[keys[-1]] = value + def _load_profiles(self, path: str): """Load YAML profiles from file.""" try: import yaml - + # Handle workspace paths if path.startswith("/Workspace"): dbutils = self._get_dbutils() @@ -120,7 +135,7 @@ def _load_profiles(self, path: str): else: with open(path, "r") as f: content = f.read() - + data = yaml.safe_load(content) self._profiles = data.get("profiles", {}) self._mappings = data.get("mappings", {}) @@ -129,64 +144,64 @@ def _load_profiles(self, path: str): logger.warning(f"Could not load profiles from {path}: {e}") self._profiles = {} self._mappings = {} - + def _resolve_profile(self, profile_name: str) -> Dict[str, Any]: """Resolve a named profile to credentials.""" if not self._profiles or profile_name not in self._profiles: return {} - + profile = self._profiles[profile_name] resolved = {} - + for key, value in profile.items(): if isinstance(value, str): # Check for secret reference: {{ secret('scope/key') }} or {{ secret('key') }} match = re.match(r"\{\{\s*secret\(['\"]([^'\"]+)['\"]\)\s*\}\}", value) if match: secret_ref = match.group(1) - # If no scope specified, use the default scope if "/" in secret_ref: - scope_key = secret_ref + # Explicit scope/key + scope, skey = secret_ref.split("/", 1) + secret_value = self._get_secret_with_scope(scope, skey) else: - scope_key = secret_ref - - secret_value = self._get_secret(scope_key) + # No scope specified, use default scope with source prefix + secret_value = self._get_secret(secret_ref) + if secret_value: resolved[key] = secret_value + else: + logger.warning( + f"Could not resolve secret reference '{secret_ref}' " + f"in profile '{profile_name}'" + ) else: resolved[key] = value else: resolved[key] = value - + return resolved - + def get_credentials(self, source: str) -> Dict[str, Any]: """ Get credentials for a source. - + Resolution order: 1. Check if source is mapped to a profile (from YAML) 2. Fall back to convention-based discovery from secrets - - Args: - source: Source connector name (e.g., "source-s3") - - Returns: - Dictionary of credentials for the source """ # Return cached if available if source in self._cache: return self._cache[source] - + credentials = {} - + # Check for profile mapping first if source in self._mappings: profile_name = self._mappings[source] credentials = self._resolve_profile(profile_name) if credentials: logger.debug(f"Resolved credentials for {source} from profile '{profile_name}'") - + # Fall back to convention-based discovery if not credentials: keys = self._list_secrets_for_source(source) @@ -194,15 +209,16 @@ def get_credentials(self, source: str) -> Dict[str, Any]: full_key = f"{source}/{key}" value = self._get_secret(full_key) if value: - credentials[key] = value - + # Support dotted-key nested mapping + self._set_nested(credentials, key, value) + if credentials: logger.debug(f"Discovered {len(credentials)} credentials for {source} from secrets") - + # Cache the result self._cache[source] = credentials return credentials - + def merge_credentials( self, source: str, @@ -210,24 +226,17 @@ def merge_credentials( ) -> Dict[str, Any]: """ Merge discovered credentials into source_config. - + Explicit values in source_config take precedence over discovered credentials. - - Args: - source: Source connector name - source_config: User-provided configuration - - Returns: - Merged configuration with credentials """ discovered = self.get_credentials(source) if not discovered: return source_config - + # Deep merge - discovered credentials as base, source_config overrides merged = self._deep_merge(discovered, source_config) return merged - + def _deep_merge(self, base: Dict, override: Dict) -> Dict: """Deep merge two dictionaries, with override taking precedence.""" result = base.copy() @@ -237,45 +246,37 @@ def _deep_merge(self, base: Dict, override: Dict) -> Dict: else: result[key] = value return result - + def validate(self, source: str) -> bool: - """ - Validate that credentials exist for a source. - - Args: - source: Source connector name - - Returns: - True if credentials were found - """ + """Validate that credentials exist for a source.""" creds = self.get_credentials(source) return len(creds) > 0 - + def list_available_sources(self) -> List[str]: """List all sources that have credentials configured.""" dbutils = self._get_dbutils() if not dbutils: return list(self._mappings.keys()) - + try: if self._available_keys is None: secrets = dbutils.secrets.list(self.secrets_scope) self._available_keys = [s.key for s in secrets] - + # Extract unique source names from keys sources = set() for key in self._available_keys: if "/" in key: source = key.split("/")[0] sources.add(source) - + # Add mapped sources sources.update(self._mappings.keys()) - + return sorted(sources) except Exception: return list(self._mappings.keys()) - + def clear_cache(self): """Clear the credential cache.""" self._cache.clear() @@ -286,16 +287,7 @@ def create_credential_resolver( secrets_scope: str = "brickbyte", profiles_path: Optional[str] = None, ) -> CredentialResolver: - """ - Create a credential resolver. - - Args: - secrets_scope: Databricks Secrets scope (default: "brickbyte") - profiles_path: Optional path to YAML profiles file - - Returns: - CredentialResolver instance - """ + """Create a credential resolver.""" return CredentialResolver( secrets_scope=secrets_scope, profiles_path=profiles_path, diff --git a/src/brickbyte/enrichment/__init__.py b/src/brickbyte/enrichment/__init__.py deleted file mode 100644 index 2200174..0000000 --- a/src/brickbyte/enrichment/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -BrickByte Enrichment Module. - -Provides AI-powered metadata enrichment for tables: -- Column descriptions via Foundation Models -- PII detection -- Data classification -""" -from brickbyte.enrichment.semantic import SemanticEnricher, enrich_table - -__all__ = ["SemanticEnricher", "enrich_table"] - diff --git a/src/brickbyte/enrichment/semantic.py b/src/brickbyte/enrichment/semantic.py deleted file mode 100644 index 0b96fc6..0000000 --- a/src/brickbyte/enrichment/semantic.py +++ /dev/null @@ -1,351 +0,0 @@ -""" -AI-powered semantic enrichment for BrickByte. -Uses Databricks Foundation Models to generate metadata. -""" -import json -import logging -import re -from dataclasses import dataclass, field -from typing import Dict, List, Optional - -logger = logging.getLogger(__name__) - - -@dataclass -class ColumnEnrichment: - """Enrichment results for a single column.""" - - column_name: str - description: Optional[str] = None - is_pii: bool = False - pii_type: Optional[str] = None # e.g., "email", "phone", "ssn", "name" - data_classification: Optional[str] = None # e.g., "public", "internal", "confidential" - - def __str__(self) -> str: - parts = [f"{self.column_name}:"] - if self.description: - parts.append(f' "{self.description}"') - if self.is_pii: - parts.append(f" ⚠️ PII detected: {self.pii_type}") - if self.data_classification: - parts.append(f" Classification: {self.data_classification}") - return "\n".join(parts) - - -@dataclass -class TableEnrichment: - """Enrichment results for a table.""" - - table_name: str - columns: List[ColumnEnrichment] = field(default_factory=list) - table_description: Optional[str] = None - - def __str__(self) -> str: - lines = [f"Table: {self.table_name}"] - if self.table_description: - lines.append(f"Description: {self.table_description}") - lines.append("") - for col in self.columns: - lines.append(str(col)) - return "\n".join(lines) - - -# Prompt template for Foundation Model -ENRICHMENT_PROMPT = """Analyze this database table and provide metadata enrichment. - -Table: {table_name} -Columns and sample data: -{column_samples} - -For each column, provide: -1. A brief description (1-2 sentences) -2. Whether it contains PII (personally identifiable information) -3. If PII, what type (email, phone, ssn, name, address, etc.) -4. Data classification (public, internal, confidential, restricted) - -Also provide a brief description of the table's purpose. - -Respond in JSON format: -{{ - "table_description": "Brief description of the table", - "columns": [ - {{ - "name": "column_name", - "description": "Description of the column", - "is_pii": true/false, - "pii_type": "type or null", - "classification": "public/internal/confidential/restricted" - }} - ] -}} -""" - - -class SemanticEnricher: - """ - AI-powered semantic enrichment using Databricks Foundation Models. - - Generates: - - Column descriptions from data samples - - PII detection - - Data classification suggestions - """ - - def __init__( - self, - model_name: str = "databricks-meta-llama-3-3-70b-instruct", - sample_rows: int = 50, - ): - """ - Initialize the enricher. - - Args: - model_name: Foundation Model endpoint name - sample_rows: Number of sample rows to analyze - """ - self.model_name = model_name - self.sample_rows = sample_rows - self._spark = None - self._client = None - - @property - def spark(self): - """Get or create Spark session.""" - if self._spark is None: - from pyspark.sql import SparkSession - self._spark = SparkSession.builder.getOrCreate() - return self._spark - - @property - def client(self): - """Get or create Databricks SDK client.""" - if self._client is None: - from databricks.sdk import WorkspaceClient - self._client = WorkspaceClient() - return self._client - - def _get_column_samples(self, table_name: str) -> Dict[str, List[str]]: - """Get sample values for each column by parsing data JSON column.""" - # Try new column name first, fall back to legacy name - schema = self.spark.table(table_name).schema - col_names = [f.name for f in schema.fields] - - if "data" in col_names: - data_col = "data" - elif "_airbyte_data" in col_names: - data_col = "_airbyte_data" - else: - # Flattened mode - sample all columns directly - df = self.spark.sql( - f"SELECT * FROM {table_name} LIMIT {self.sample_rows}" - ).toPandas() - samples = {} - for col in df.columns: - if not col.startswith("_"): - vals = df[col].dropna().astype(str).head(5).tolist() - samples[col] = [v[:100] for v in vals] - return samples - - df = self.spark.sql( - f"SELECT {data_col} FROM {table_name} LIMIT {self.sample_rows}" - ).toPandas() - - samples = {} - for _, row in df.iterrows(): - try: - record = json.loads(row[data_col]) - for col, value in record.items(): - if col not in samples: - samples[col] = [] - if value is not None and len(samples[col]) < 5: - samples[col].append(str(value)[:100]) - except (json.JSONDecodeError, KeyError, TypeError): - continue - - return samples - - def _format_samples_for_prompt(self, samples: Dict[str, List[str]]) -> str: - """Format column samples for the prompt.""" - lines = [] - for col, values in samples.items(): - values_str = ", ".join(f'"{v}"' for v in values[:3]) - lines.append(f"- {col}: {values_str}") - return "\n".join(lines) - - def _call_foundation_model(self, prompt: str) -> str: - """Call the Foundation Model API.""" - try: - from databricks.sdk.service.serving import ( - ChatMessage, - ChatMessageRole, - ) - - response = self.client.serving_endpoints.query( - name=self.model_name, - messages=[ChatMessage(role=ChatMessageRole.USER, content=prompt)], - ) - return response.choices[0].message.content - except Exception as e: - logger.warning(f"Warning: Foundation Model call failed: {e}") - return "{}" - - def _parse_enrichment_response( - self, - response: str, - table_name: str, - ) -> TableEnrichment: - """Parse the Foundation Model response into structured enrichment.""" - enrichment = TableEnrichment(table_name=table_name) - - # Try to extract JSON from response - try: - # Find JSON in response (may have surrounding text) - json_match = re.search(r'\{[\s\S]*\}', response) - if json_match: - data = json.loads(json_match.group()) - else: - data = {} - except json.JSONDecodeError: - data = {} - - enrichment.table_description = data.get("table_description") - - for col_data in data.get("columns", []): - col = ColumnEnrichment( - column_name=col_data.get("name", ""), - description=col_data.get("description"), - is_pii=col_data.get("is_pii", False), - pii_type=col_data.get("pii_type"), - data_classification=col_data.get("classification"), - ) - if col.column_name: - enrichment.columns.append(col) - - return enrichment - - def enrich(self, table_name: str) -> TableEnrichment: - """ - Generate semantic enrichment for a table. - - Args: - table_name: Fully qualified table name (catalog.schema.table) - - Returns: - TableEnrichment with AI-generated metadata - """ - logger.info(f" Analyzing table: {table_name}") - - # Get column samples - samples = self._get_column_samples(table_name) - - if not samples: - logger.info(" No data columns found to analyze") - return TableEnrichment(table_name=table_name) - - # Build prompt - samples_str = self._format_samples_for_prompt(samples) - prompt = ENRICHMENT_PROMPT.format( - table_name=table_name, - column_samples=samples_str, - ) - - # Call Foundation Model - logger.info(" Calling Foundation Model...") - response = self._call_foundation_model(prompt) - - # Parse response - enrichment = self._parse_enrichment_response(response, table_name) - - logger.info(f" ✓ Generated descriptions for {len(enrichment.columns)} columns") - - return enrichment - - def apply_to_catalog(self, enrichment: TableEnrichment): - """ - Apply enrichment metadata to Unity Catalog. - - Since data may be stored as JSON in data column, we store field-level - metadata as table tags and set the table description. - """ - logger.info(f" Applying metadata to {enrichment.table_name}") - - # Set table comment - if enrichment.table_description: - try: - escaped_desc = enrichment.table_description.replace("'", "''") - self.spark.sql( - f"COMMENT ON TABLE {enrichment.table_name} IS '{escaped_desc}'" - ) - logger.info(" ✓ Set table description") - except Exception as e: - logger.warning(f" Warning: Could not set table comment: {e}") - - # Store field metadata as table tags (fields are inside JSON) - pii_fields = [] - for col in enrichment.columns: - if col.is_pii: - pii_fields.append(f"{col.column_name}:{col.pii_type or 'pii'}") - - if pii_fields: - try: - pii_value = ",".join(pii_fields) - self.spark.sql( - f"ALTER TABLE {enrichment.table_name} " - f"SET TAGS ('pii_fields' = '{pii_value}')" - ) - logger.info(f" ✓ Tagged PII fields: {pii_fields}") - except Exception as e: - logger.warning(f" Warning: Could not set PII tags: {e}") - - # Store column descriptions as table property for reference - if enrichment.columns: - try: - # Build a summary of field descriptions - desc_summary = "; ".join( - f"{c.column_name}: {c.description}" - for c in enrichment.columns[:10] # Limit to avoid huge properties - if c.description - ) - if desc_summary: - escaped = desc_summary.replace("'", "''")[:1000] - self.spark.sql( - f"ALTER TABLE {enrichment.table_name} " - f"SET TBLPROPERTIES ('brickbyte.field_descriptions' = '{escaped}')" - ) - logger.info(" ✓ Stored field descriptions in table properties") - except Exception as e: - logger.warning(f" Warning: Could not set field descriptions: {e}") - - logger.info(" ✓ Applied metadata to catalog") - - -def enrich_table( - catalog: str, - schema: str, - table: str, - apply_to_catalog: bool = True, - model_name: str = "databricks-meta-llama-3-3-70b-instruct", -) -> TableEnrichment: - """ - Convenience function to enrich a single table. - - Args: - catalog: Unity Catalog name - schema: Schema name - table: Table name - apply_to_catalog: Whether to apply metadata to Unity Catalog - model_name: Foundation Model to use - - Returns: - TableEnrichment with AI-generated metadata - """ - table_name = f"{catalog}.{schema}.{table}" - - enricher = SemanticEnricher(model_name=model_name) - enrichment = enricher.enrich(table_name) - - if apply_to_catalog: - enricher.apply_to_catalog(enrichment) - - return enrichment - diff --git a/src/brickbyte/preview.py b/src/brickbyte/preview.py index 5c7725e..0446741 100644 --- a/src/brickbyte/preview.py +++ b/src/brickbyte/preview.py @@ -1,20 +1,39 @@ """ -Preview engine for BrickByte. -Provides diff calculation and schema comparison before syncing. +Preview engine for brickbyte. +Provides sample-based schema comparison before syncing. """ + +import logging from dataclasses import dataclass, field from typing import Any, Dict, List, Optional +logger = logging.getLogger("brickbyte") + +# Mapping from Python types to approximate Spark types for comparison +_PYTHON_TO_SPARK = { + "int": "LongType", + "float": "DoubleType", + "str": "StringType", + "bool": "BooleanType", + "NoneType": "NullType", + "list": "ArrayType", + "dict": "MapType", + "datetime": "TimestampType", + "date": "DateType", + "bytes": "BinaryType", + "Decimal": "DecimalType", +} + @dataclass class SchemaChange: """Represents a schema change between source and target.""" - + column: str change_type: str # "added", "removed", "type_changed" source_type: Optional[str] = None target_type: Optional[str] = None - + def __str__(self) -> str: if self.change_type == "added": return f" + {self.column} ({self.source_type}) - NEW" @@ -27,184 +46,186 @@ def __str__(self) -> str: @dataclass class StreamPreview: """Preview information for a single stream.""" - + stream_name: str - source_count: int + sampled_records: int target_count: int - new_records: int = 0 - modified_records: int = 0 - deleted_records: int = 0 schema_changes: List[SchemaChange] = field(default_factory=list) sample_records: List[dict] = field(default_factory=list) - + def __str__(self) -> str: - parts = [] - - # Record counts - if self.new_records > 0: - parts.append(f"+{self.new_records} new") - if self.modified_records > 0: - parts.append(f"~{self.modified_records} modified") - if self.deleted_records > 0: - parts.append(f"-{self.deleted_records} deleted") - - if not parts: - if self.source_count >= 0: - parts.append(f"{self.source_count} records") - else: - parts.append("Unknown records (Streaming)") - - line = f"{self.stream_name}: {' | '.join(parts)}" - - # Schema changes + line = ( + f"{self.stream_name}: sampled {self.sampled_records} records" + f" | target has {self.target_count} records" + ) + if self.schema_changes: line += "\n Schema changes:" for change in self.schema_changes: line += f"\n {change}" - + return line @dataclass class PreviewResult: """Complete preview result for all streams.""" - + streams: List[StreamPreview] = field(default_factory=list) - total_source_records: int = 0 - total_new_records: int = 0 - total_modified_records: int = 0 - total_deleted_records: int = 0 + total_sampled_records: int = 0 + total_target_records: int = 0 has_schema_changes: bool = False - + def __str__(self) -> str: lines = ["=" * 60, "Sync Preview", "=" * 60, ""] - + for stream in self.streams: lines.append(str(stream)) - + lines.append("") lines.append("-" * 60) lines.append( - f"Total: {self.total_source_records} records " - f"(+{self.total_new_records} new, " - f"~{self.total_modified_records} modified, " - f"-{self.total_deleted_records} deleted)" + f"Total: sampled {self.total_sampled_records} records " + f"across {len(self.streams)} streams " + f"| target has {self.total_target_records} records" ) - + if self.has_schema_changes: - lines.append("⚠️ Schema changes detected") - + lines.append("Schema changes detected") + lines.append("=" * 60) - + return "\n".join(lines) class PreviewEngine: """ Generates previews of sync operations. - - Compares source data (sampled) with existing target tables to show: + + Samples source data and compares it with existing target tables to show: - Target record counts - - Schema changes (inferred from samples) + - Schema changes inferred from samples - Sample records """ - + def __init__(self, catalog: str, schema: str): - """ - Initialize the preview engine. - - Args: - catalog: Unity Catalog name - schema: Target schema name - """ self.catalog = catalog self.schema = schema self._spark = None - + @property def spark(self): """Get or create Spark session.""" if self._spark is None: try: from pyspark.sql import SparkSession + self._spark = SparkSession.builder.getOrCreate() except ImportError: return None return self._spark - + def get_table_name(self, stream_name: str) -> str: - """Get fully qualified table name.""" - return f"{self.catalog}.{self.schema}.{stream_name}" - + """Get fully qualified table name using sanitized stream name.""" + from brickbyte._sanitize import sanitize_stream_name + + sanitized = sanitize_stream_name(stream_name) + return f"`{self.catalog}`.`{self.schema}`.`{sanitized}`" + def table_exists(self, stream_name: str) -> bool: """Check if target table exists.""" if not self.spark: return False - + table_name = self.get_table_name(stream_name) return self.spark.catalog.tableExists(table_name) - + def get_target_count(self, stream_name: str) -> int: """Get record count from target table.""" if not self.table_exists(stream_name): return 0 - + table_name = self.get_table_name(stream_name) return self.spark.table(table_name).count() - + def get_target_schema(self, stream_name: str) -> Dict[str, str]: - """Get schema of target table.""" + """Get schema of target table. + + Returns type names stripped of trailing ``()`` so that + ``StringType()`` becomes ``StringType``, matching the values + produced by ``_PYTHON_TO_SPARK``. + """ if not self.table_exists(stream_name): return {} - + table_name = self.get_table_name(stream_name) df = self.spark.table(table_name) - return {f.name: str(f.dataType) for f in df.schema.fields} - + schema = {} + for f in df.schema.fields: + type_str = str(f.dataType) + # Spark str() on simple types yields e.g. "StringType()" — + # strip the trailing "()" for consistent comparison. + if type_str.endswith("()"): + type_str = type_str[:-2] + schema[f.name] = type_str + return schema + def get_source_schema(self, sample_records: List[dict]) -> Dict[str, str]: """Infer schema from sample records.""" if not sample_records: return {} - - # Simple inference from first record - # In a real scenario, we might want to check Airbyte catalog + record = sample_records[0] return {k: type(v).__name__ for k, v in record.items()} - + def compare_schemas( self, source_schema: Dict[str, str], target_schema: Dict[str, str], ) -> List[SchemaChange]: - """Compare source and target schemas.""" + """Compare source and target schemas, including type changes.""" changes = [] - + source_cols = set(source_schema.keys()) target_cols = set(target_schema.keys()) - + # New columns for col in source_cols - target_cols: - changes.append(SchemaChange( - column=col, - change_type="added", - source_type=source_schema[col], - )) - + changes.append( + SchemaChange( + column=col, + change_type="added", + source_type=source_schema[col], + ) + ) + # Removed columns for col in target_cols - source_cols: - changes.append(SchemaChange( - column=col, - change_type="removed", - target_type=target_schema[col], - )) - - # Type changes - simplified - # Note: inferred source types (python types) vs target types (spark types) - # mismatch is expected, so we largely skip strict type comparison here - # unless we map them properly. For now, we omit type_changed to avoid noise. - + changes.append( + SchemaChange( + column=col, + change_type="removed", + target_type=target_schema[col], + ) + ) + + # Type changes (map Python types to Spark types for comparison) + for col in source_cols & target_cols: + source_type = source_schema[col] + target_type = target_schema[col] + mapped_source = _PYTHON_TO_SPARK.get(source_type, source_type) + if mapped_source != target_type and source_type != target_type: + changes.append( + SchemaChange( + column=col, + change_type="type_changed", + source_type=source_type, + target_type=target_type, + ) + ) + return changes - + def preview_stream( self, ab_source: Any, @@ -213,63 +234,46 @@ def preview_stream( ) -> StreamPreview: """Generate preview for a single stream.""" target_count = self.get_target_count(stream_name) - - # Get samples from stream + sample_records = [] try: - # We only peek at the first N records records_gen = ab_source.get_records(stream_name) for i, record in enumerate(records_gen): if i >= sample_size: break sample_records.append(record) - except Exception: - pass - + except Exception as e: + logger.warning(f"Could not sample records for {stream_name}: {e}") + # Compare schemas source_schema = self.get_source_schema(sample_records) target_schema = self.get_target_schema(stream_name) schema_changes = self.compare_schemas(source_schema, target_schema) - + return StreamPreview( stream_name=stream_name, - source_count=-1, # Unknown in streaming + sampled_records=len(sample_records), target_count=target_count, - new_records=-1, # Unknown - modified_records=-1, # Unknown - deleted_records=-1, # Unknown schema_changes=schema_changes, sample_records=sample_records, ) - + def preview( self, ab_source: Any, streams: List[str], sample_size: int = 5, ) -> PreviewResult: - """ - Generate preview for all streams. - - Args: - ab_source: Initialized Airbyte source - streams: List of stream names - sample_size: Number of sample records per stream - - Returns: - PreviewResult with all stream previews - """ + """Generate preview for all streams.""" result = PreviewResult() - + for stream_name in streams: - stream_preview = self.preview_stream( - ab_source, stream_name, sample_size - ) + stream_preview = self.preview_stream(ab_source, stream_name, sample_size) result.streams.append(stream_preview) - - # Totals are less relevant with unknown counts, but we act best effort + result.total_sampled_records += stream_preview.sampled_records + result.total_target_records += stream_preview.target_count + if stream_preview.schema_changes: result.has_schema_changes = True - - return result + return result diff --git a/src/brickbyte/types.py b/src/brickbyte/types.py index b5ce350..eb17295 100644 --- a/src/brickbyte/types.py +++ b/src/brickbyte/types.py @@ -103,5 +103,5 @@ "source-youtube-analytics", "source-zendesk-chat", "source-zendesk-support", - "source-zenloop" -] \ No newline at end of file + "source-zenloop", +] diff --git a/src/brickbyte/writers/__init__.py b/src/brickbyte/writers/__init__.py index 9a0a4ed..ef38aa5 100644 --- a/src/brickbyte/writers/__init__.py +++ b/src/brickbyte/writers/__init__.py @@ -1,15 +1,15 @@ """ -Brickbyte Writers Module. +brickbyte Writers Module. """ + import logging -from typing import Optional, Union +from typing import Dict, List, Optional from brickbyte.writers.base import BaseWriter -from brickbyte.writers.spark_streaming_writer import SparkStreamingWriter -from brickbyte.writers.sql_streaming_writer import SQLStreamingWriter logger = logging.getLogger(__name__) + def create_streaming_writer( catalog: str, schema: str, @@ -19,26 +19,31 @@ def create_streaming_writer( buffer_size_records: int = 50000, buffer_size_mb: int = 100, flatten: bool = False, -) -> Union[SparkStreamingWriter, SQLStreamingWriter]: + run_id: str = "", + dedup_keys: Optional[Dict[str, List[str]]] = None, +) -> BaseWriter: """ Create a streaming writer based on environment. - + Logic: 1. If Spark is active (and not force_sql=True) -> SparkStreamingWriter (No Volume needed). 2. Else -> SQLStreamingWriter (Volume REQUIRED). """ - + # 1. Attempt to detect Spark spark_active = False if not force_sql: try: from pyspark.sql import SparkSession + if SparkSession.getActiveSession(): spark_active = True except ImportError: pass - + if spark_active: + from brickbyte.writers.spark_streaming_writer import SparkStreamingWriter + logger.info("Spark detected. Using Native Spark Streaming Writer.") return SparkStreamingWriter( catalog=catalog, @@ -46,40 +51,42 @@ def create_streaming_writer( buffer_size_records=buffer_size_records, buffer_size_mb=buffer_size_mb, flatten=flatten, + run_id=run_id, + dedup_keys=dedup_keys, ) - + # 2. Fallback to SQL Writer logger.info("Spark not active (or forced off). Using SQL Streaming Writer.") - + if not staging_volume: raise ValueError( - "staging_volume is REQUIRED when running outside of Databricks (or when forcing SQL mode). " - "Because we cannot access local disk from the warehouse, we must stage files in a Volume." + "staging_volume is REQUIRED when running outside of Databricks " + "(or when forcing SQL mode). " + "Because we cannot access local disk from the warehouse, " + "we must stage files in a Volume." ) from databricks.sdk import WorkspaceClient - + + from brickbyte.writers.sql_streaming_writer import SQLStreamingWriter + w = WorkspaceClient() server_hostname = w.config.host.replace("https://", "").rstrip("/") access_token = w.config.token - + # Auto-discover warehouse if not provided if not warehouse_id: warehouses = list(w.warehouses.list()) - running = [ - wh for wh in warehouses - if wh.state and wh.state.value == "RUNNING" - ] + running = [wh for wh in warehouses if wh.state and wh.state.value == "RUNNING"] if running: warehouse_id = running[0].id else: raise ValueError( - "No running SQL warehouse found. " - "Specify warehouse_id or start a warehouse." + "No running SQL warehouse found. " "Specify warehouse_id or start a warehouse." ) - + http_path = f"/sql/1.0/warehouses/{warehouse_id}" - + return SQLStreamingWriter( catalog=catalog, schema=schema, @@ -90,12 +97,12 @@ def create_streaming_writer( buffer_size_records=buffer_size_records, buffer_size_mb=buffer_size_mb, flatten=flatten, + run_id=run_id, + dedup_keys=dedup_keys, ) __all__ = [ "BaseWriter", - "SparkStreamingWriter", - "SQLStreamingWriter", "create_streaming_writer", ] diff --git a/src/brickbyte/writers/base.py b/src/brickbyte/writers/base.py index 25c9d29..c19c5b5 100644 --- a/src/brickbyte/writers/base.py +++ b/src/brickbyte/writers/base.py @@ -1,65 +1,92 @@ """ -Abstract base writer for BrickByte. +Abstract base writer for brickbyte. Defines the interface all writers must implement. """ + from abc import ABC, abstractmethod -from typing import Dict, Optional +from typing import Dict, List, Optional class BaseWriter(ABC): """ - Abstract base class for all BrickByte writers. - + Abstract base class for all brickbyte writers. + Writers handle writing data from PyAirbyte cache to Databricks. """ - - def __init__(self, catalog: str, schema: str): - """ - Initialize the writer. - - Args: - catalog: Unity Catalog name - schema: Target schema name - """ + + def __init__( + self, + catalog: str, + schema: str, + run_id: str = "", + dedup_keys: Optional[Dict[str, List[str]]] = None, + ): self.catalog = catalog self.schema = schema - + self.run_id = run_id + self.dedup_keys = dedup_keys + def get_table_name(self, stream_name: str) -> str: - """Get fully qualified table name for a stream.""" - return f"{self.catalog}.{self.schema}.{stream_name}" - + """Get fully qualified, backtick-quoted table name for a stream.""" + from brickbyte._sanitize import quoted_table_name, sanitize_stream_name + + sanitized = sanitize_stream_name(stream_name) + return quoted_table_name(self.catalog, self.schema, sanitized) + + def get_staging_table_name(self, stream_name: str, run_id: str) -> str: + """Get staging table name for safe overwrite.""" + from brickbyte._sanitize import quoted_table_name, sanitize_stream_name + + sanitized = sanitize_stream_name(stream_name) + run_id_short = run_id[:8] + staging_name = f"{sanitized}__stg__{run_id_short}" + return quoted_table_name(self.catalog, self.schema, staging_name) + + def _get_dedup_keys_for_stream(self, stream_name: str) -> Optional[List[str]]: + """Get dedup keys for a specific stream.""" + if self.dedup_keys is None: + return None + # Check for __all__ (list was expanded to all streams) + if "__all__" in self.dedup_keys: + return self.dedup_keys["__all__"] + return self.dedup_keys.get(stream_name) + @abstractmethod def table_exists(self, stream_name: str) -> bool: """Check if a table exists.""" pass - + @abstractmethod def get_table_schema(self, stream_name: str) -> Optional[Dict[str, str]]: - """ - Get schema of an existing table. - - Returns: - Dict mapping column names to types, or None if table doesn't exist - """ + """Get schema of an existing table.""" pass - + @abstractmethod def drop_table(self, stream_name: str): """Drop a table if it exists.""" pass - + @abstractmethod def write_record(self, stream_name: str, record: dict): """Buffer a single record for writing.""" pass - + @abstractmethod def flush_stream(self, stream_name: str): """Flush buffered records for a specific stream.""" pass - + @abstractmethod def close(self): """Flush all buffers and clean up resources.""" pass + @abstractmethod + def safe_overwrite_begin(self, stream_name: str, run_id: str): + """Begin safe overwrite — redirect writes to staging table.""" + pass + + @abstractmethod + def safe_overwrite_finish(self, stream_name: str, run_id: str): + """Finish safe overwrite — atomic swap from staging to target.""" + pass diff --git a/src/brickbyte/writers/spark_streaming_writer.py b/src/brickbyte/writers/spark_streaming_writer.py index e93e746..ceb5741 100644 --- a/src/brickbyte/writers/spark_streaming_writer.py +++ b/src/brickbyte/writers/spark_streaming_writer.py @@ -1,18 +1,19 @@ """ -Spark Streaming writer for Brickbyte using native Databricks/Spark execution. +Spark Streaming writer for brickbyte using native Databricks/Spark execution. Uses micro-batch streaming for: - Bounded memory usage (flushes at configurable thresholds) - Fault tolerance (each flush = implicit checkpoint) - Databricks auto-optimize handles small file compaction """ + import json import logging -import sys -from datetime import datetime +from datetime import datetime, timezone from typing import Dict, List, Optional from uuid import uuid4 +from brickbyte._schema import DK_MISSING from brickbyte.writers.base import BaseWriter logger = logging.getLogger(__name__) @@ -21,13 +22,20 @@ class SparkStreamingWriter(BaseWriter): """ Writes data to Databricks using micro-batch streaming. - - Each flush writes to Delta immediately, providing: - - Implicit checkpointing (resume from last successful batch on failure) - - Bounded memory (configurable batch size) - - Databricks auto-optimize handles small file compaction """ + _SAFE_WIDENINGS = { + ("IntegerType", "LongType"), + ("IntegerType", "DoubleType"), + ("LongType", "DoubleType"), + ("FloatType", "DoubleType"), + ("ShortType", "IntegerType"), + ("ShortType", "LongType"), + ("ByteType", "ShortType"), + ("ByteType", "IntegerType"), + ("ByteType", "LongType"), + } + def __init__( self, catalog: str, @@ -35,18 +43,10 @@ def __init__( buffer_size_records: int = 50000, buffer_size_mb: int = 100, flatten: bool = False, + run_id: str = "", + dedup_keys: Optional[Dict[str, List[str]]] = None, ): - """ - Initialize Spark Streaming Writer. - - Args: - catalog: Unity Catalog name - schema: Target schema name - buffer_size_records: Records per micro-batch (default: 50k) - buffer_size_mb: Max batch size in MB (default: 100MB) - flatten: If True, flatten record fields into columns (default: False) - """ - super().__init__(catalog, schema) + super().__init__(catalog, schema, run_id=run_id, dedup_keys=dedup_keys) self.buffer_size_records = buffer_size_records self.buffer_size_bytes = buffer_size_mb * 1024 * 1024 self.flatten = flatten @@ -55,95 +55,132 @@ def __init__( self._buffers: Dict[str, List[dict]] = {} self._buffer_counts: Dict[str, int] = {} self._buffer_sizes: Dict[str, int] = {} + self._overwrite_streams: Dict[str, str] = {} # stream_name -> staging_table_name @property def spark(self): """Get or create Spark session.""" if self._spark is None: from pyspark.sql import SparkSession + self._spark = SparkSession.builder.getOrCreate() return self._spark def table_exists(self, stream_name: str) -> bool: - """Check if a table exists.""" table_name = self.get_table_name(stream_name) return self.spark.catalog.tableExists(table_name) def get_table_schema(self, stream_name: str) -> Optional[Dict[str, str]]: - """Get schema of an existing table.""" if not self.table_exists(stream_name): return None - + table_name = self.get_table_name(stream_name) df = self.spark.table(table_name) return {f.name: str(f.dataType) for f in df.schema.fields} def drop_table(self, stream_name: str): - """Drop a table if it exists.""" table_name = self.get_table_name(stream_name) self.spark.sql(f"DROP TABLE IF EXISTS {table_name}") - def _transform_record(self, record: dict) -> dict: + def _transform_record(self, stream_name: str, record: dict) -> dict: """Transform record based on flatten mode.""" if self.flatten: - # Flattened: all fields as top-level columns + metadata transformed = dict(record) - transformed["_id"] = str(uuid4()) - transformed["_extracted_at"] = datetime.now() + transformed["_record_id"] = str(uuid4()) + transformed["_extracted_at"] = datetime.now(timezone.utc) + transformed["_run_id"] = self.run_id + + # Add dedup key columns if configured + dk_keys = self._get_dedup_keys_for_stream(stream_name) + if dk_keys is not None: + dk_missing = False + for i, key in enumerate(dk_keys): + if key in record: + transformed[f"_dk_{i}"] = record[key] + else: + transformed[f"_dk_{i}"] = None + dk_missing = True + transformed[DK_MISSING] = dk_missing + return transformed else: - # Raw: 3 columns with JSON blob - return { - "id": str(uuid4()), - "extracted_at": datetime.now(), - "data": json.dumps(record, default=str) + transformed = { + "record_id": str(uuid4()), + "extracted_at": datetime.now(timezone.utc), + "data": json.dumps(record, default=str), + "run_id": self.run_id, } + # Add dedup key columns if configured + dk_keys = self._get_dedup_keys_for_stream(stream_name) + if dk_keys is not None: + dk_missing = False + for i, key in enumerate(dk_keys): + if key in record: + transformed[f"_dk_{i}"] = record[key] + else: + transformed[f"_dk_{i}"] = None + dk_missing = True + transformed[DK_MISSING] = dk_missing + + return transformed + def write_record(self, stream_name: str, record: dict): """Buffer a single record.""" if stream_name not in self._buffers: self._buffers[stream_name] = [] self._buffer_counts[stream_name] = 0 self._buffer_sizes[stream_name] = 0 - - transformed = self._transform_record(record) + + transformed = self._transform_record(stream_name, record) self._buffers[stream_name].append(transformed) self._buffer_counts[stream_name] += 1 - - # Estimate size based on data field or full record + + # Estimate size if self.flatten: - self._buffer_sizes[stream_name] += sys.getsizeof(str(transformed)) + self._buffer_sizes[stream_name] += sum( + len(str(v).encode("utf-8")) for v in transformed.values() + ) else: - self._buffer_sizes[stream_name] += sys.getsizeof(transformed.get("data", "")) - + self._buffer_sizes[stream_name] += len(transformed["data"].encode("utf-8")) + # Flush micro-batch when thresholds hit - if (self._buffer_counts[stream_name] >= self.buffer_size_records or - self._buffer_sizes[stream_name] >= self.buffer_size_bytes): + if ( + self._buffer_counts[stream_name] >= self.buffer_size_records + or self._buffer_sizes[stream_name] >= self.buffer_size_bytes + ): self._write_micro_batch(stream_name) + def _get_write_table_name(self, stream_name: str) -> str: + """Get the table name to write to (staging during overwrite, target otherwise).""" + if stream_name in self._overwrite_streams: + return self._overwrite_streams[stream_name] + return self.get_table_name(stream_name) + def _write_micro_batch(self, stream_name: str): - """Write a micro-batch to Delta (each call = implicit checkpoint).""" + """Write a micro-batch to Delta.""" if stream_name not in self._buffers or not self._buffers[stream_name]: return records = self._buffers[stream_name] batch_count = len(records) - table_name = self.get_table_name(stream_name) - + table_name = self._get_write_table_name(stream_name) + try: df = self.spark.createDataFrame(records) - (df.write - .format("delta") - .mode("append") - .option("mergeSchema", "true") - .saveAsTable(table_name)) - + ( + df.write.format("delta") + .mode("append") + .option("mergeSchema", "true") + .saveAsTable(table_name) + ) + logger.debug("Wrote %d records to %s", batch_count, table_name) - + except Exception as e: logger.error("Error writing batch for %s: %s", stream_name, e) raise - + # Reset buffer self._buffers[stream_name] = [] self._buffer_counts[stream_name] = 0 @@ -155,5 +192,131 @@ def flush_stream(self, stream_name: str): def close(self): """Flush all remaining buffers.""" - for stream_name in self._buffers: + for stream_name in list(self._buffers.keys()): self.flush_stream(stream_name) + + def safe_overwrite_begin(self, stream_name: str, run_id: str): + """Begin safe overwrite — redirect writes to staging table.""" + staging_name = self.get_staging_table_name(stream_name, run_id) + self._overwrite_streams[stream_name] = staging_name + # Drop any leftover staging table + self.spark.sql(f"DROP TABLE IF EXISTS {staging_name}") + + def safe_overwrite_finish(self, stream_name: str, run_id: str): + """Finish safe overwrite — atomic swap from staging to target.""" + staging_name = self.get_staging_table_name(stream_name, run_id) + target_name = self.get_table_name(stream_name) + + try: + target_exists = self.spark.catalog.tableExists(target_name) + + if target_exists: + self._atomic_overwrite(target_name, staging_name) + self.spark.sql(f"DROP TABLE IF EXISTS {staging_name}") + else: + self.spark.sql(f"ALTER TABLE {staging_name} RENAME TO {target_name}") + except Exception: + # On failure, drop staging table, target untouched + self.spark.sql(f"DROP TABLE IF EXISTS {staging_name}") + raise + finally: + self._overwrite_streams.pop(stream_name, None) + + def _atomic_overwrite(self, target_name: str, staging_name: str): + """Perform atomic INSERT OVERWRITE with schema alignment.""" + target_df = self.spark.table(target_name) + staging_df = self.spark.table(staging_name) + + target_schema = {f.name: f.dataType for f in target_df.schema.fields} + staging_schema = {f.name: f.dataType for f in staging_df.schema.fields} + + target_cols = set(target_schema.keys()) + staging_cols = set(staging_schema.keys()) + + for col in target_cols & staging_cols: + t_type = self._type_name(target_schema[col]) + s_type = self._type_name(staging_schema[col]) + if t_type != s_type: + if (s_type, t_type) not in self._SAFE_WIDENINGS and ( + t_type, + s_type, + ) not in self._SAFE_WIDENINGS: + if t_type != "StringType" and s_type != "StringType": + raise ValueError( + f"Incompatible type change for column '{col}': " + f"{t_type} -> {s_type}. " + f"Drop the table manually to reset schema." + ) + + # Add new columns from staging to target + new_cols = staging_cols - target_cols + for col in new_cols: + col_type = self._sql_type(staging_schema[col]) + self.spark.sql(f"ALTER TABLE {target_name} ADD COLUMNS (`{col}` {col_type})") + + all_cols = target_cols | staging_cols + select_parts = [] + for col in sorted(all_cols): + if col in staging_cols and col in target_cols: + s_type = self._type_name(staging_schema[col]) + t_type = self._type_name(target_schema[col]) + if s_type != t_type and (s_type, t_type) in self._SAFE_WIDENINGS: + target_sql_type = self._sql_type(target_schema[col]) + select_parts.append(f"CAST(`{col}` AS {target_sql_type}) AS `{col}`") + elif s_type != t_type and (t_type, s_type) in self._SAFE_WIDENINGS: + staging_sql_type = self._sql_type(staging_schema[col]) + self.spark.sql( + f"ALTER TABLE {target_name} " + f"ALTER COLUMN `{col}` TYPE {staging_sql_type}" + ) + select_parts.append(f"`{col}`") + else: + select_parts.append(f"`{col}`") + elif col in staging_cols: + select_parts.append(f"`{col}`") + else: + select_parts.append(f"NULL AS `{col}`") + + col_list = ", ".join(f"`{c}`" for c in sorted(all_cols)) + select_expr = ", ".join(select_parts) + + self.spark.sql( + f"INSERT OVERWRITE {target_name} ({col_list}) " + f"SELECT {select_expr} FROM {staging_name}" + ) + + @staticmethod + def _type_name(data_type) -> str: + """Get normalized Spark class-based type name (e.g., IntegerType).""" + if isinstance(data_type, str): + if data_type.endswith("()"): + return data_type[:-2] + return data_type + return type(data_type).__name__ + + @staticmethod + def _sql_type(data_type) -> str: + """Get SQL type string for DDL/CAST clauses.""" + if hasattr(data_type, "simpleString"): + return data_type.simpleString() + + as_str = str(data_type) + if as_str.endswith("()"): + as_str = as_str[:-2] + if as_str.startswith("DecimalType(") and as_str.endswith(")"): + precision_scale = as_str[len("DecimalType(") : -1] + return f"DECIMAL({precision_scale})" + mapping = { + "ByteType": "TINYINT", + "ShortType": "SMALLINT", + "IntegerType": "INT", + "LongType": "BIGINT", + "FloatType": "FLOAT", + "DoubleType": "DOUBLE", + "StringType": "STRING", + "BooleanType": "BOOLEAN", + "TimestampType": "TIMESTAMP", + "DateType": "DATE", + "BinaryType": "BINARY", + } + return mapping.get(as_str, as_str) diff --git a/src/brickbyte/writers/sql_streaming_writer.py b/src/brickbyte/writers/sql_streaming_writer.py index 2b499d9..041174e 100644 --- a/src/brickbyte/writers/sql_streaming_writer.py +++ b/src/brickbyte/writers/sql_streaming_writer.py @@ -1,22 +1,25 @@ """ -SQL Streaming writer for Brickbyte using PyArrow buffering and COPY INTO. +SQL Streaming writer for brickbyte using PyArrow buffering and COPY INTO. Uses micro-batch streaming for: - Bounded memory usage (flushes at configurable thresholds) - Fault tolerance (each flush = implicit checkpoint) - Databricks auto-optimize handles small file compaction """ + import json import logging import os -import sys -from datetime import datetime +import shutil +import tempfile +from datetime import datetime, timezone from typing import Dict, List, Optional from uuid import uuid4 import pyarrow as pa import pyarrow.parquet as pq +from brickbyte._schema import DK_MISSING, RAW_TABLE_DDL from brickbyte.writers.base import BaseWriter logger = logging.getLogger(__name__) @@ -25,11 +28,6 @@ class SQLStreamingWriter(BaseWriter): """ Writes data to Databricks using micro-batch streaming via SQL Connector. - - Each flush writes to Delta immediately via COPY INTO, providing: - - Implicit checkpointing (resume from last successful batch on failure) - - Bounded memory (configurable batch size) - - Databricks auto-optimize handles small file compaction """ def __init__( @@ -43,28 +41,16 @@ def __init__( buffer_size_records: int = 50000, buffer_size_mb: int = 100, flatten: bool = False, + run_id: str = "", + dedup_keys: Optional[Dict[str, List[str]]] = None, ): - """ - Initialize SQL Streaming Writer. - - Args: - catalog: Unity Catalog name - schema: Target schema name - staging_volume: Unity Catalog Volume path for staging parquet files - server_hostname: Databricks server hostname - http_path: SQL Warehouse HTTP path - access_token: Databricks access token - buffer_size_records: Records per micro-batch (default: 50k) - buffer_size_mb: Max batch size in MB (default: 100MB) - flatten: If True, flatten record fields into columns (default: False) - """ - super().__init__(catalog, schema) + super().__init__(catalog, schema, run_id=run_id, dedup_keys=dedup_keys) self.staging_volume = staging_volume self.server_hostname = server_hostname self.http_path = http_path self._access_token = access_token self.flatten = flatten - + self.buffer_size_records = buffer_size_records self.buffer_size_bytes = buffer_size_mb * 1024 * 1024 @@ -72,7 +58,10 @@ def __init__( self._buffers: Dict[str, List[dict]] = {} self._buffer_counts: Dict[str, int] = {} self._buffer_sizes: Dict[str, int] = {} - + self._batch_index: int = 0 + self._overwrite_streams: Dict[str, str] = {} + self._local_staging_root = tempfile.mkdtemp(prefix="brickbyte-sql-") + parts = self.staging_volume.split(".") if len(parts) != 3: raise ValueError( @@ -85,12 +74,14 @@ def _get_connection(self): """Get or create database connection.""" if self._connection is None: from databricks import sql + self._connection = sql.connect( server_hostname=self.server_hostname, http_path=self.http_path, access_token=self._access_token, catalog=self.catalog, schema=self.schema, + staging_allowed_local_path=self._local_staging_root, ) return self._connection @@ -104,14 +95,22 @@ def _execute(self, query: str): cursor.close() def _get_staging_dir(self, stream_name: str) -> str: - """Get staging directory path in Volume.""" - base_path = f"/Volumes/{self._vol_subpath}" - stream_dir = os.path.join(base_path, "brickbyte_streaming", stream_name) + """Get local staging directory path for parquet generation.""" + from brickbyte._sanitize import sanitize_stream_name + + sanitized = sanitize_stream_name(stream_name) + stream_dir = os.path.join(self._local_staging_root, sanitized) os.makedirs(stream_dir, exist_ok=True) return stream_dir + def _get_volume_dir(self, stream_name: str) -> str: + """Get destination directory path inside the Unity Catalog Volume.""" + from brickbyte._sanitize import sanitize_stream_name + + sanitized = sanitize_stream_name(stream_name) + return f"/Volumes/{self._vol_subpath}/brickbyte_streaming/{self.run_id}/{sanitized}" + def table_exists(self, stream_name: str) -> bool: - """Check if a table exists.""" table_name = self.get_table_name(stream_name) try: self._execute(f"DESCRIBE TABLE {table_name}") @@ -120,111 +119,180 @@ def table_exists(self, stream_name: str) -> bool: return False def get_table_schema(self, stream_name: str) -> Optional[Dict[str, str]]: - """Get schema of an existing table.""" if not self.table_exists(stream_name): return None - + table_name = self.get_table_name(stream_name) conn = self._get_connection() cursor = conn.cursor() - cursor.execute(f"DESCRIBE TABLE {table_name}") - results = cursor.fetchall() - cursor.close() - return {row[0]: row[1] for row in results} + try: + cursor.execute(f"DESCRIBE TABLE {table_name}") + results = cursor.fetchall() + return {row[0]: row[1] for row in results} + finally: + cursor.close() def drop_table(self, stream_name: str): - """Drop a table if it exists.""" table_name = self.get_table_name(stream_name) self._execute(f"DROP TABLE IF EXISTS {table_name}") - def _transform_record(self, record: dict) -> dict: + def _transform_record(self, stream_name: str, record: dict) -> dict: """Transform record based on flatten mode.""" if self.flatten: - # Flattened: all fields as top-level columns + metadata transformed = dict(record) - transformed["_id"] = str(uuid4()) - transformed["_extracted_at"] = datetime.now() + transformed["_record_id"] = str(uuid4()) + transformed["_extracted_at"] = datetime.now(timezone.utc) + transformed["_run_id"] = self.run_id + + dk_keys = self._get_dedup_keys_for_stream(stream_name) + if dk_keys is not None: + dk_missing = False + for i, key in enumerate(dk_keys): + if key in record: + transformed[f"_dk_{i}"] = record[key] + else: + transformed[f"_dk_{i}"] = None + dk_missing = True + transformed[DK_MISSING] = dk_missing + return transformed else: - # Raw: 3 columns with JSON blob - return { - "id": str(uuid4()), - "extracted_at": datetime.now(), - "data": json.dumps(record, default=str) + transformed = { + "record_id": str(uuid4()), + "extracted_at": datetime.now(timezone.utc), + "data": json.dumps(record, default=str), + "run_id": self.run_id, } + dk_keys = self._get_dedup_keys_for_stream(stream_name) + if dk_keys is not None: + dk_missing = False + for i, key in enumerate(dk_keys): + if key in record: + transformed[f"_dk_{i}"] = record[key] + else: + transformed[f"_dk_{i}"] = None + dk_missing = True + transformed[DK_MISSING] = dk_missing + + return transformed + def write_record(self, stream_name: str, record: dict): """Buffer a single record.""" if stream_name not in self._buffers: self._buffers[stream_name] = [] self._buffer_counts[stream_name] = 0 self._buffer_sizes[stream_name] = 0 - - transformed = self._transform_record(record) + + transformed = self._transform_record(stream_name, record) self._buffers[stream_name].append(transformed) self._buffer_counts[stream_name] += 1 - - # Estimate size based on data field or full record + + # Estimate size if self.flatten: - self._buffer_sizes[stream_name] += sys.getsizeof(str(transformed)) + self._buffer_sizes[stream_name] += sum( + len(str(v).encode("utf-8")) for v in transformed.values() + ) else: - self._buffer_sizes[stream_name] += sys.getsizeof(transformed.get("data", "")) - + self._buffer_sizes[stream_name] += len(transformed["data"].encode("utf-8")) + # Check both thresholds - if (self._buffer_counts[stream_name] >= self.buffer_size_records or - self._buffer_sizes[stream_name] >= self.buffer_size_bytes): + if ( + self._buffer_counts[stream_name] >= self.buffer_size_records + or self._buffer_sizes[stream_name] >= self.buffer_size_bytes + ): self.flush_stream(stream_name) + def _get_write_table_name(self, stream_name: str) -> str: + """Get the table name to write to.""" + if stream_name in self._overwrite_streams: + return self._overwrite_streams[stream_name] + return self.get_table_name(stream_name) + def flush_stream(self, stream_name: str): """Flush buffer for a specific stream.""" if stream_name not in self._buffers or not self._buffers[stream_name]: return records = self._buffers[stream_name] - + table_name = self._get_write_table_name(stream_name) + + local_staging_dir = self._get_staging_dir(stream_name) + volume_staging_dir = self._get_volume_dir(stream_name) + filename = f"{self.run_id}_{self._batch_index:06d}.parquet" + file_path = os.path.join(local_staging_dir, filename) + volume_file_path = f"{volume_staging_dir}/{filename}" + self._batch_index += 1 + try: table = pa.Table.from_pylist(records) - - staging_dir = self._get_staging_dir(stream_name) - filename = f"batch_{datetime.now().strftime('%Y%m%d%H%M%S%f')}.parquet" - file_path = os.path.join(staging_dir, filename) - - pq.write_table(table, file_path, compression='zstd') - - table_name = self.get_table_name(stream_name) - - # For raw mode, create table with known schema - # For flatten mode, rely on COPY INTO with mergeSchema + pq.write_table(table, file_path, compression="zstd") + if not self.flatten: - create_query = f""" - CREATE TABLE IF NOT EXISTS {table_name} ( - id STRING, - extracted_at TIMESTAMP, - data STRING - ) - """ + create_query = RAW_TABLE_DDL.format(table_name=table_name) self._execute(create_query) - + else: + # Flatten first-write: infer DDL from PyArrow schema + if not self._table_exists_by_name(table_name): + ddl = self._infer_ddl_from_arrow(table.schema, table_name) + self._execute(ddl) + + self._execute(f"PUT '{file_path}' INTO '{volume_file_path}' OVERWRITE") copy_query = f""" COPY INTO {table_name} - FROM '{file_path}' + FROM '{volume_file_path}' FILEFORMAT = PARQUET FORMAT_OPTIONS ('mergeSchema' = 'true') - COPY_OPTIONS ('force' = 'true') """ self._execute(copy_query) - - os.remove(file_path) - + except Exception as e: logger.error(f"Error flushing stream {stream_name}: {e}") raise - + finally: + try: + self._execute(f"REMOVE '{volume_file_path}'") + except Exception: + pass + # Always clean up the parquet file + if os.path.exists(file_path): + os.remove(file_path) + # Reset buffer self._buffers[stream_name] = [] self._buffer_counts[stream_name] = 0 self._buffer_sizes[stream_name] = 0 + def _table_exists_by_name(self, table_name: str) -> bool: + """Check if a table exists by its full quoted name.""" + try: + self._execute(f"DESCRIBE TABLE {table_name}") + return True + except Exception: + return False + + def _infer_ddl_from_arrow(self, arrow_schema: pa.Schema, table_name: str) -> str: + """Generate CREATE TABLE DDL from a PyArrow schema.""" + _TYPE_MAP = { + pa.string(): "STRING", + pa.int64(): "BIGINT", + pa.int32(): "INT", + pa.float64(): "DOUBLE", + pa.float32(): "FLOAT", + pa.bool_(): "BOOLEAN", + pa.date32(): "DATE", + } + + columns = [] + for field in arrow_schema: + sql_type = _TYPE_MAP.get(field.type, "STRING") + if pa.types.is_timestamp(field.type): + sql_type = "TIMESTAMP" + columns.append(f" `{field.name}` {sql_type}") + + cols_ddl = ",\n".join(columns) + return f"CREATE TABLE IF NOT EXISTS {table_name} (\n{cols_ddl}\n)" + def close(self): """Flush all remaining buffers and close connection.""" for stream_name in list(self._buffers.keys()): @@ -233,10 +301,127 @@ def close(self): try: staging_dir = self._get_staging_dir(stream_name) if os.path.exists(staging_dir): - os.rmdir(staging_dir) + shutil.rmtree(staging_dir) except Exception: pass - + if self._connection: self._connection.close() self._connection = None + + if os.path.exists(self._local_staging_root): + shutil.rmtree(self._local_staging_root, ignore_errors=True) + + def safe_overwrite_begin(self, stream_name: str, run_id: str): + """Begin safe overwrite — redirect writes to staging table.""" + staging_name = self.get_staging_table_name(stream_name, run_id) + self._overwrite_streams[stream_name] = staging_name + try: + self._execute(f"DROP TABLE IF EXISTS {staging_name}") + except Exception: + pass + + def safe_overwrite_finish(self, stream_name: str, run_id: str): + """Finish safe overwrite — atomic swap from staging to target.""" + staging_name = self.get_staging_table_name(stream_name, run_id) + target_name = self.get_table_name(stream_name) + + try: + target_exists = self._table_exists_by_name(target_name) + + if target_exists: + self._atomic_overwrite_sql(target_name, staging_name) + self._execute(f"DROP TABLE IF EXISTS {staging_name}") + else: + self._execute(f"ALTER TABLE {staging_name} RENAME TO {target_name}") + except Exception: + self._execute(f"DROP TABLE IF EXISTS {staging_name}") + raise + finally: + self._overwrite_streams.pop(stream_name, None) + + # Safe widening pairs: (narrower, wider) — SQL type names (lowercase) + _SAFE_WIDENINGS_SQL = { + ("int", "bigint"), + ("int", "double"), + ("bigint", "double"), + ("float", "double"), + ("smallint", "int"), + ("smallint", "bigint"), + ("tinyint", "smallint"), + ("tinyint", "int"), + ("tinyint", "bigint"), + } + + def _atomic_overwrite_sql(self, target_name: str, staging_name: str): + """Perform atomic INSERT OVERWRITE via SQL with schema alignment and type checks.""" + conn = self._get_connection() + cursor = conn.cursor() + try: + cursor.execute(f"DESCRIBE TABLE {target_name}") + target_schema = {row[0]: row[1] for row in cursor.fetchall()} + + cursor.execute(f"DESCRIBE TABLE {staging_name}") + staging_schema = {row[0]: row[1] for row in cursor.fetchall()} + finally: + cursor.close() + + target_cols = set(target_schema.keys()) + staging_cols = set(staging_schema.keys()) + + # Check for incompatible type changes + for col in target_cols & staging_cols: + t_type = target_schema[col].lower() + s_type = staging_schema[col].lower() + if t_type != s_type: + pair = (s_type, t_type) + reverse = (t_type, s_type) + is_safe = pair in self._SAFE_WIDENINGS_SQL + is_reverse_safe = reverse in self._SAFE_WIDENINGS_SQL + if not is_safe and not is_reverse_safe: + if t_type != "string" and s_type != "string": + raise ValueError( + f"Incompatible type change for column '{col}': " + f"{target_schema[col]} -> {staging_schema[col]}. " + f"Drop the table manually to reset schema." + ) + + # Add new columns from staging to target + new_cols = staging_cols - target_cols + for col in new_cols: + col_type = staging_schema[col] + self._execute(f"ALTER TABLE {target_name} ADD COLUMNS (`{col}` {col_type})") + + all_cols = target_cols | staging_cols + select_parts = [] + for col in sorted(all_cols): + if col in staging_cols and col in target_cols: + s_type = staging_schema[col].lower() + t_type = target_schema[col].lower() + if s_type != t_type: + # Always widen to the wider type + if (s_type, t_type) in self._SAFE_WIDENINGS_SQL: + select_parts.append(f"CAST(`{col}` AS {target_schema[col]}) AS `{col}`") + elif (t_type, s_type) in self._SAFE_WIDENINGS_SQL: + # Staging is wider — widen target to match + self._execute( + f"ALTER TABLE {target_name} " + f"ALTER COLUMN `{col}` TYPE {staging_schema[col]}" + ) + select_parts.append(f"`{col}`") + else: + select_parts.append(f"`{col}`") + else: + select_parts.append(f"`{col}`") + elif col in staging_cols: + select_parts.append(f"`{col}`") + else: + select_parts.append(f"NULL AS `{col}`") + + col_list = ", ".join(f"`{c}`" for c in sorted(all_cols)) + select_expr = ", ".join(select_parts) + + self._execute( + f"INSERT OVERWRITE {target_name} ({col_list}) " + f"SELECT {select_expr} FROM {staging_name}" + ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..bb97ea4 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,55 @@ +""" +Shared pytest fixtures for fast and isolated test runs. +""" +import sys +from unittest.mock import MagicMock, patch + +import pytest + + +def pytest_addoption(parser): + parser.addoption( + "--run-integration", + action="store_true", + default=False, + help="Run tests marked as integration.", + ) + + +def pytest_collection_modifyitems(config, items): + if config.getoption("--run-integration"): + return + + skip_integration = pytest.mark.skip( + reason="integration test: pass --run-integration to run" + ) + for item in items: + if "integration" in item.keywords: + item.add_marker(skip_integration) + + +@pytest.fixture(autouse=True) +def isolate_connector_setup(request, monkeypatch): + """ + Keep unit tests hermetic by skipping connector venv creation/install work. + """ + if request.node.get_closest_marker("integration"): + return + + from brickbyte._client import Client + + def _noop_setup(self, source, source_install=None): + return None + + def _fake_exec_path(self, source): + return f"/tmp/brickbyte-{source}" + + monkeypatch.setattr(Client, "_setup_source", _noop_setup) + monkeypatch.setattr(Client, "_get_source_exec_path", _fake_exec_path) + + +@pytest.fixture +def mock_airbyte(): + mock_ab = MagicMock() + with patch.dict(sys.modules, {"airbyte": mock_ab}): + yield mock_ab diff --git a/tests/test_base_writer.py b/tests/test_base_writer.py index fa8b646..8627cce 100644 --- a/tests/test_base_writer.py +++ b/tests/test_base_writer.py @@ -2,7 +2,6 @@ Tests for BaseWriter abstract class and common functionality. """ import json -from datetime import datetime from unittest.mock import MagicMock, patch import pytest @@ -16,40 +15,24 @@ class TestBaseWriter: """Test BaseWriter abstract class.""" def test_cannot_instantiate_directly(self): - """Test that BaseWriter cannot be instantiated directly.""" with pytest.raises(TypeError, match="Can't instantiate abstract class"): BaseWriter(catalog="main", schema="test") - def test_get_table_name(self): - """Test get_table_name via concrete implementation.""" - with patch("databricks.sql.connect"): - writer = SQLStreamingWriter( - catalog="main", - schema="bronze", - staging_volume="a.b.c", - server_hostname="h", - http_path="p", - access_token="t", - ) - - assert writer.get_table_name("users") == "main.bronze.users" - assert writer.get_table_name("my_table") == "main.bronze.my_table" - class TestTransformRecord: """Test _transform_record across implementations.""" @pytest.fixture - def spark_writer(self, tmp_path): - import os - with patch.dict(os.environ, {"SPARK_LOCAL_DIRS": str(tmp_path)}): - writer = SparkStreamingWriter(catalog="main", schema="test") - writer._spark = MagicMock() - return writer + def spark_writer(self): + writer = SparkStreamingWriter( + catalog="main", schema="test", run_id="test-run-id" + ) + writer._spark = MagicMock() + return writer @pytest.fixture def sql_writer(self): - with patch("databricks.sql.connect"): + with patch("os.path.exists", return_value=True): return SQLStreamingWriter( catalog="main", schema="test", @@ -57,116 +40,84 @@ def sql_writer(self): server_hostname="h", http_path="p", access_token="t", + run_id="test-run-id", ) - def test_spark_transform_adds_metadata(self, spark_writer): - """Test SparkStreamingWriter adds Airbyte metadata.""" + def test_sql_raw_transform_adds_metadata(self, sql_writer): record = {"id": 1, "email": "test@example.com"} - transformed = spark_writer._transform_record(record) - - assert "_airbyte_raw_id" in transformed - assert "_airbyte_extracted_at" in transformed - assert "_airbyte_data" in transformed - - def test_sql_transform_adds_metadata(self, sql_writer): - """Test SQLStreamingWriter adds Airbyte metadata.""" - record = {"id": 1, "email": "test@example.com"} - transformed = sql_writer._transform_record(record) - - assert "_airbyte_raw_id" in transformed - assert "_airbyte_extracted_at" in transformed - assert "_airbyte_data" in transformed - - def test_raw_id_is_uuid(self, spark_writer): - """Test that _airbyte_raw_id is a valid UUID.""" - record = {"id": 1} - transformed = spark_writer._transform_record(record) - - raw_id = transformed["_airbyte_raw_id"] - # UUID format: 8-4-4-4-12 hex digits - assert len(raw_id) == 36 - assert raw_id.count("-") == 4 - - def test_extracted_at_is_datetime(self, spark_writer): - """Test that _airbyte_extracted_at is a datetime.""" - record = {"id": 1} - transformed = spark_writer._transform_record(record) - - assert isinstance(transformed["_airbyte_extracted_at"], datetime) + transformed = sql_writer._transform_record("stream1", record) + + assert "record_id" in transformed + assert "extracted_at" in transformed + assert "data" in transformed + assert "run_id" in transformed def test_data_is_json_string(self, spark_writer): - """Test that _airbyte_data is a valid JSON string.""" record = {"id": 1, "nested": {"key": "value"}, "list": [1, 2, 3]} - transformed = spark_writer._transform_record(record) - - data_str = transformed["_airbyte_data"] + transformed = spark_writer._transform_record("stream1", record) + + data_str = transformed["data"] assert isinstance(data_str, str) - - # Should be valid JSON + parsed = json.loads(data_str) assert parsed["id"] == 1 assert parsed["nested"]["key"] == "value" assert parsed["list"] == [1, 2, 3] - def test_unique_raw_ids(self, spark_writer): - """Test that each transform generates unique raw_id.""" + def test_unique_record_ids(self, spark_writer): record = {"id": 1} - + ids = set() for _ in range(100): - transformed = spark_writer._transform_record(record) - ids.add(transformed["_airbyte_raw_id"]) - - assert len(ids) == 100 # All unique + transformed = spark_writer._transform_record("stream1", record) + ids.add(transformed["record_id"]) + + assert len(ids) == 100 def test_transform_handles_special_characters(self, spark_writer): - """Test that transform handles special characters in data.""" record = { "text": 'Hello "world"', "unicode": "日本語", "newlines": "line1\nline2", } - transformed = spark_writer._transform_record(record) - - parsed = json.loads(transformed["_airbyte_data"]) + transformed = spark_writer._transform_record("stream1", record) + + parsed = json.loads(transformed["data"]) assert parsed["text"] == 'Hello "world"' assert parsed["unicode"] == "日本語" assert parsed["newlines"] == "line1\nline2" def test_transform_handles_none_values(self, spark_writer): - """Test that transform handles None values.""" record = {"id": 1, "optional": None} - transformed = spark_writer._transform_record(record) - - parsed = json.loads(transformed["_airbyte_data"]) + transformed = spark_writer._transform_record("stream1", record) + + parsed = json.loads(transformed["data"]) assert parsed["optional"] is None def test_transform_handles_empty_record(self, spark_writer): - """Test that transform handles empty records.""" record = {} - transformed = spark_writer._transform_record(record) - - assert transformed["_airbyte_data"] == "{}" + transformed = spark_writer._transform_record("stream1", record) + + assert transformed["data"] == "{}" class TestWriterConsistency: """Test that both writers behave consistently.""" @pytest.fixture - def spark_writer(self, tmp_path): - import os - with patch.dict(os.environ, {"SPARK_LOCAL_DIRS": str(tmp_path)}): - writer = SparkStreamingWriter( - catalog="main", - schema="test", - buffer_size_records=100, - ) - writer._spark = MagicMock() - return writer + def spark_writer(self): + writer = SparkStreamingWriter( + catalog="main", + schema="test", + buffer_size_records=100, + run_id="test-run", + ) + writer._spark = MagicMock() + return writer @pytest.fixture def sql_writer(self): - with patch("databricks.sql.connect"): + with patch("os.path.exists", return_value=True): return SQLStreamingWriter( catalog="main", schema="test", @@ -175,25 +126,20 @@ def sql_writer(self): http_path="p", access_token="t", buffer_size_records=100, + run_id="test-run", ) def test_same_table_name_format(self, spark_writer, sql_writer): - """Test both writers generate same table names.""" assert spark_writer.get_table_name("users") == sql_writer.get_table_name("users") - assert spark_writer.get_table_name("test") == sql_writer.get_table_name("test") - def test_same_transform_schema(self, spark_writer, sql_writer): - """Test both writers produce same transformed schema.""" + def test_same_raw_transform_schema(self, spark_writer, sql_writer): record = {"id": 1, "name": "test"} - - spark_result = spark_writer._transform_record(record) - sql_result = sql_writer._transform_record(record) - - # Same keys - assert set(spark_result.keys()) == set(sql_result.keys()) - - # Same types - assert type(spark_result["_airbyte_raw_id"]) == type(sql_result["_airbyte_raw_id"]) - assert type(spark_result["_airbyte_extracted_at"]) == type(sql_result["_airbyte_extracted_at"]) - assert type(spark_result["_airbyte_data"]) == type(sql_result["_airbyte_data"]) + spark_result = spark_writer._transform_record("stream1", record) + sql_result = sql_writer._transform_record("stream1", record) + + assert set(spark_result.keys()) == set(sql_result.keys()) + assert type(spark_result["record_id"]) == type(sql_result["record_id"]) + assert type(spark_result["extracted_at"]) == type(sql_result["extracted_at"]) + assert type(spark_result["data"]) == type(sql_result["data"]) + assert type(spark_result["run_id"]) == type(sql_result["run_id"]) diff --git a/tests/test_buffer_thresholds.py b/tests/test_buffer_thresholds.py index 9c1de22..57bd255 100644 --- a/tests/test_buffer_thresholds.py +++ b/tests/test_buffer_thresholds.py @@ -1,7 +1,6 @@ """ Tests for buffer size thresholds (records AND bytes). """ -import os from unittest.mock import MagicMock, patch import pytest @@ -14,23 +13,21 @@ class TestBufferSizeBytes: """Test byte-based buffer thresholds.""" @pytest.fixture - def spark_writer(self, tmp_path): - """SparkStreamingWriter with low byte threshold.""" - with patch.dict(os.environ, {"SPARK_LOCAL_DIRS": str(tmp_path)}): - writer = SparkStreamingWriter( - catalog="main", - schema="test", - buffer_size_records=1000, # High record limit - buffer_size_mb=1, # 1MB byte limit (will hit first) - ) - writer._spark = MagicMock() - writer._write_micro_batch = MagicMock() - return writer + def spark_writer(self): + writer = SparkStreamingWriter( + catalog="main", + schema="test", + buffer_size_records=1000, + buffer_size_mb=1, + run_id="test-run", + ) + writer._spark = MagicMock() + writer._write_micro_batch = MagicMock() + return writer @pytest.fixture def sql_writer(self): - """SQLStreamingWriter with low byte threshold.""" - with patch("databricks.sql.connect"): + with patch("os.path.exists", return_value=True): writer = SQLStreamingWriter( catalog="main", schema="test", @@ -38,99 +35,90 @@ def sql_writer(self): server_hostname="host", http_path="/sql", access_token="token", - buffer_size_records=1000, # High record limit - buffer_size_mb=1, # 1MB byte limit + buffer_size_records=1000, + buffer_size_mb=1, + run_id="test-run", ) writer.flush_stream = MagicMock() return writer def test_spark_flushes_on_byte_threshold(self, spark_writer): - """Test SparkStreamingWriter flushes when byte threshold is hit.""" - # sys.getsizeof returns ~50 bytes overhead per string - # So we need many medium-sized records to exceed 1MB - # 1MB = 1,048,576 bytes / ~100 bytes per record = ~10k records - # Use 20k char string to make each record ~20KB large_data = "x" * 20_000 - - # Write records until we exceed threshold - for i in range(40): # 40 * ~25KB = ~1MB + + for i in range(40): spark_writer.write_record("stream1", {"data": large_data, "i": i}) - + assert spark_writer._write_micro_batch.call_count == 0 - - # One more should trigger flush + for i in range(20): spark_writer.write_record("stream1", {"data": large_data, "i": i}) - + assert spark_writer._write_micro_batch.call_count >= 1 def test_sql_flushes_on_byte_threshold(self, sql_writer): - """Test SQLStreamingWriter flushes when byte threshold is hit.""" large_data = "x" * 20_000 - + for i in range(40): sql_writer.write_record("stream1", {"data": large_data, "i": i}) - + assert sql_writer.flush_stream.call_count == 0 - + for i in range(20): sql_writer.write_record("stream1", {"data": large_data, "i": i}) - + assert sql_writer.flush_stream.call_count >= 1 - def test_record_threshold_still_works(self, tmp_path): - """Test that record count threshold works independently.""" - with patch.dict(os.environ, {"SPARK_LOCAL_DIRS": str(tmp_path)}): - writer = SparkStreamingWriter( - catalog="main", - schema="test", - buffer_size_records=2, # Low record limit - buffer_size_mb=1000, # High byte limit (won't hit) - ) - writer._spark = MagicMock() - writer._write_micro_batch = MagicMock() - - # Small records - byte threshold won't be hit - writer.write_record("stream1", {"id": 1}) - assert writer._write_micro_batch.call_count == 0 - - writer.write_record("stream1", {"id": 2}) - assert writer._write_micro_batch.call_count == 1 + def test_record_threshold_still_works(self): + writer = SparkStreamingWriter( + catalog="main", + schema="test", + buffer_size_records=2, + buffer_size_mb=1000, + run_id="test-run", + ) + writer._spark = MagicMock() + writer._write_micro_batch = MagicMock() + + writer.write_record("stream1", {"id": 1}) + assert writer._write_micro_batch.call_count == 0 + + writer.write_record("stream1", {"id": 2}) + assert writer._write_micro_batch.call_count == 1 def test_buffer_size_tracking(self, spark_writer): - """Test that buffer sizes are tracked correctly.""" - spark_writer._write_micro_batch = MagicMock() # Prevent actual flush - + spark_writer._write_micro_batch = MagicMock() + spark_writer.write_record("stream1", {"data": "small"}) - + assert spark_writer._buffer_sizes["stream1"] > 0 initial_size = spark_writer._buffer_sizes["stream1"] - + spark_writer.write_record("stream1", {"data": "another"}) - + assert spark_writer._buffer_sizes["stream1"] > initial_size - def test_buffer_reset_after_flush(self, tmp_path): - """Test that all buffer tracking is reset after flush.""" - with patch.dict(os.environ, {"SPARK_LOCAL_DIRS": str(tmp_path)}): - writer = SparkStreamingWriter( - catalog="main", - schema="test", - buffer_size_records=2, - buffer_size_mb=100, - ) - writer._spark = MagicMock() - - # Mock the write operations - with patch("pyarrow.parquet.write_table"), patch("os.remove"): - mock_df = MagicMock() - writer._spark.read.parquet.return_value = mock_df - - writer.write_record("stream1", {"id": 1}) - writer.write_record("stream1", {"id": 2}) # Triggers flush - - # All tracking should be reset - assert writer._buffers["stream1"] == [] - assert writer._buffer_counts["stream1"] == 0 - assert writer._buffer_sizes["stream1"] == 0 + def test_buffer_reset_after_flush(self): + writer = SparkStreamingWriter( + catalog="main", + schema="test", + buffer_size_records=2, + buffer_size_mb=100, + run_id="test-run", + ) + writer._spark = MagicMock() + + # Mock the Spark write chain + mock_df = MagicMock() + mock_write = MagicMock() + mock_df.write = mock_write + mock_write.format.return_value = mock_write + mock_write.mode.return_value = mock_write + mock_write.option.return_value = mock_write + writer._spark.createDataFrame.return_value = mock_df + + writer.write_record("stream1", {"id": 1}) + writer.write_record("stream1", {"id": 2}) + assert writer._buffers["stream1"] == [] + assert writer._buffer_counts["stream1"] == 0 + assert writer._buffer_sizes["stream1"] == 0 diff --git a/tests/test_concurrent.py b/tests/test_concurrent.py new file mode 100644 index 0000000..fe9c57b --- /dev/null +++ b/tests/test_concurrent.py @@ -0,0 +1,214 @@ +""" +Tests for concurrent stream processing. +""" +import threading +from unittest.mock import MagicMock, patch + +import pytest + +import brickbyte + + +class TestConcurrentStreams: + @pytest.fixture + def bb(self, tmp_path): + return brickbyte.client(base_venv_directory=str(tmp_path)) + + def _set_up_mock_sources(self, mock_airbyte, stream_records, stream_states=None): + stream_names = list(stream_records) + + def make_source(): + mock_source = MagicMock() + selected = {"streams": list(stream_names)} + + def select_all_streams(): + selected["streams"] = list(stream_names) + + def select_streams(streams): + selected["streams"] = list(streams) + + def get_selected_streams(): + return list(selected["streams"]) + + def get_records(stream_name): + behavior = stream_records[stream_name] + if isinstance(behavior, Exception): + raise behavior + return iter(behavior) + + mock_source.select_all_streams.side_effect = select_all_streams + mock_source.select_streams.side_effect = select_streams + mock_source.get_selected_streams.side_effect = get_selected_streams + mock_source.get_records.side_effect = get_records + mock_source.check.return_value = None + + if stream_states is not None: + mock_source.get_stream_state.side_effect = ( + lambda stream_name: stream_states[stream_name] + ) + + return mock_source + + mock_airbyte.get_source.side_effect = lambda *args, **kwargs: make_source() + + def test_parallel_streams_each_get_own_writer(self, bb, mock_airbyte): + self._set_up_mock_sources( + mock_airbyte, + { + "stream1": [{"id": 1}], + "stream2": [{"id": 2}], + "stream3": [{"id": 3}], + }, + ) + + writers_created = [] + + def mock_create_writer(**kwargs): + w = MagicMock() + writers_created.append(w) + return w + + with patch( + "brickbyte.writers.create_streaming_writer", side_effect=mock_create_writer + ): + result = bb.sync( + source="source-faker", + source_config={}, + catalog="main", + schema="test", + staging_volume="main.staging.vol", + mode="append", + max_parallel_streams=3, + ) + + assert result.records_written == 3 + # Each stream gets its own writer (in thread pool) + no sequential writer + assert len(writers_created) == 3 + + def test_parallel_mode_completes_when_streams_exceed_workers(self, bb, mock_airbyte): + self._set_up_mock_sources( + mock_airbyte, + { + "stream1": [{"id": 1}], + "stream2": [{"id": 2}], + "stream3": [{"id": 3}], + }, + ) + + result_holder = {} + error_holder = {} + + with patch("brickbyte.writers.create_streaming_writer") as mock_factory: + mock_factory.return_value = MagicMock() + + def run_sync(): + try: + result_holder["result"] = bb.sync( + source="source-faker", + source_config={}, + catalog="main", + schema="test", + staging_volume="main.staging.vol", + mode="append", + max_parallel_streams=2, + ) + except Exception as e: # pragma: no cover - assertion below + error_holder["error"] = e + + thread = threading.Thread(target=run_sync, daemon=True) + thread.start() + thread.join(1) + + assert thread.is_alive() is False + assert "error" not in error_holder + assert result_holder["result"].records_written == 3 + + def test_error_propagation_with_continue_on_error_false(self, bb, mock_airbyte): + self._set_up_mock_sources( + mock_airbyte, + { + "stream1": RuntimeError("connection failed"), + "stream2": [{"id": 1}], + }, + ) + + with patch("brickbyte.writers.create_streaming_writer") as mock_factory: + mock_writer = MagicMock() + mock_factory.return_value = mock_writer + + with pytest.raises(RuntimeError, match="connection failed"): + bb.sync( + source="source-faker", + source_config={}, + catalog="main", + schema="test", + staging_volume="main.staging.vol", + mode="append", + max_parallel_streams=2, + continue_on_error=False, + ) + + def test_sequential_mode_uses_single_writer(self, bb, mock_airbyte): + self._set_up_mock_sources( + mock_airbyte, + { + "stream1": [{"id": 1}], + "stream2": [{"id": 2}], + }, + ) + + with patch( + "brickbyte.writers.create_streaming_writer" + ) as mock_factory: + mock_writer = MagicMock() + mock_factory.return_value = mock_writer + + result = bb.sync( + source="source-faker", + source_config={}, + catalog="main", + schema="test", + staging_volume="main.staging.vol", + mode="append", + max_parallel_streams=1, + ) + + assert result.records_written == 2 + # Single writer for sequential mode + mock_factory.assert_called_once() + + def test_parallel_incremental_saves_state_per_stream(self, bb, mock_airbyte): + self._set_up_mock_sources( + mock_airbyte, + { + "users": [{"id": 1}], + "orders": [{"id": 2}], + }, + stream_states={ + "users": {"cursor": "users"}, + "orders": {"cursor": "orders"}, + }, + ) + + with patch("brickbyte._state.StateManager") as mock_state_manager_cls: + mock_state_manager = MagicMock() + mock_state_manager.get_state.return_value = None + mock_state_manager_cls.return_value = mock_state_manager + + with patch("brickbyte.writers.create_streaming_writer") as mock_factory: + mock_writer = MagicMock() + mock_factory.return_value = mock_writer + + result = bb.sync( + source="source-faker", + source_config={}, + catalog="main", + schema="test", + staging_volume="main.staging.vol", + mode="append", + max_parallel_streams=2, + incremental=True, + ) + + assert result.records_written == 2 + assert mock_state_manager.save_state.call_count == 2 diff --git a/tests/test_credentials.py b/tests/test_credentials.py index 3efcdb5..ead2f0e 100644 --- a/tests/test_credentials.py +++ b/tests/test_credentials.py @@ -1,132 +1,87 @@ """Tests for credential resolution.""" -import pytest -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock -# Import credentials module directly to avoid virtualenv import from brickbyte.credentials import CredentialResolver class TestCredentialResolver: - """Tests for CredentialResolver class.""" - def test_init_default_scope(self): - """Test default secrets scope is 'brickbyte'.""" resolver = CredentialResolver() assert resolver.secrets_scope == "brickbyte" def test_init_custom_scope(self): - """Test custom secrets scope.""" resolver = CredentialResolver(secrets_scope="custom-scope") assert resolver.secrets_scope == "custom-scope" def test_merge_credentials_no_discovered(self): - """Test merge when no credentials discovered.""" resolver = CredentialResolver() - source_config = {"bucket": "my-bucket", "region": "us-east-1"} result = resolver.merge_credentials("source-s3", source_config) - - # Should return original config unchanged assert result == source_config def test_merge_credentials_with_discovered(self): - """Test merge with discovered credentials.""" resolver = CredentialResolver() - # Manually inject cached credentials resolver._cache["source-s3"] = { "aws_access_key_id": "discovered_key", "aws_secret_access_key": "discovered_secret", } - source_config = {"bucket": "my-bucket"} result = resolver.merge_credentials("source-s3", source_config) - - # Should merge discovered credentials assert result["bucket"] == "my-bucket" assert result["aws_access_key_id"] == "discovered_key" assert result["aws_secret_access_key"] == "discovered_secret" def test_merge_credentials_explicit_override(self): - """Test that explicit config overrides discovered credentials.""" resolver = CredentialResolver() - # Manually inject cached credentials resolver._cache["source-s3"] = { "aws_access_key_id": "discovered_key", "aws_secret_access_key": "discovered_secret", "region_name": "us-west-2", } - source_config = { "bucket": "my-bucket", - "aws_access_key_id": "explicit_key", # Override + "aws_access_key_id": "explicit_key", } result = resolver.merge_credentials("source-s3", source_config) - - # Explicit value should override discovered assert result["aws_access_key_id"] == "explicit_key" - # Non-overridden discovered value should remain assert result["aws_secret_access_key"] == "discovered_secret" assert result["region_name"] == "us-west-2" assert result["bucket"] == "my-bucket" def test_deep_merge_nested_dicts(self): - """Test deep merge with nested dictionaries.""" resolver = CredentialResolver() - base = { - "credentials": { - "client_id": "base_id", - "client_secret": "base_secret", - }, + "credentials": {"client_id": "base_id", "client_secret": "base_secret"}, "other": "value", } - override = { - "credentials": { - "client_id": "override_id", - }, - "bucket": "my-bucket", - } - + override = {"credentials": {"client_id": "override_id"}, "bucket": "my-bucket"} result = resolver._deep_merge(base, override) - assert result["credentials"]["client_id"] == "override_id" assert result["credentials"]["client_secret"] == "base_secret" assert result["other"] == "value" assert result["bucket"] == "my-bucket" def test_validate_with_credentials(self): - """Test validate returns True when credentials exist.""" resolver = CredentialResolver() resolver._cache["source-s3"] = {"aws_access_key_id": "key"} - assert resolver.validate("source-s3") is True def test_validate_without_credentials(self): - """Test validate returns False when no credentials exist.""" resolver = CredentialResolver() - assert resolver.validate("source-nonexistent") is False def test_clear_cache(self): - """Test cache clearing.""" resolver = CredentialResolver() resolver._cache["source-s3"] = {"key": "value"} resolver._available_keys = ["source-s3/key"] - resolver.clear_cache() - assert resolver._cache == {} assert resolver._available_keys is None class TestCredentialResolverWithMockedDbutils: - """Tests with mocked dbutils.""" - def test_list_secrets_for_source(self): - """Test listing secrets for a specific source.""" resolver = CredentialResolver() - - # Mock dbutils mock_dbutils = MagicMock() mock_secret1 = MagicMock() mock_secret1.key = "source-s3/aws_access_key_id" @@ -134,162 +89,183 @@ def test_list_secrets_for_source(self): mock_secret2.key = "source-s3/aws_secret_access_key" mock_secret3 = MagicMock() mock_secret3.key = "source-gcs/service_account" - mock_dbutils.secrets.list.return_value = [mock_secret1, mock_secret2, mock_secret3] resolver._dbutils = mock_dbutils - keys = resolver._list_secrets_for_source("source-s3") - assert "aws_access_key_id" in keys assert "aws_secret_access_key" in keys assert "service_account" not in keys def test_get_secret(self): - """Test getting a single secret.""" resolver = CredentialResolver() - mock_dbutils = MagicMock() mock_dbutils.secrets.get.return_value = "secret_value" resolver._dbutils = mock_dbutils - value = resolver._get_secret("source-s3/aws_access_key_id") - assert value == "secret_value" mock_dbutils.secrets.get.assert_called_once_with( - scope="brickbyte", - key="source-s3/aws_access_key_id" + scope="brickbyte", key="source-s3/aws_access_key_id" + ) + + def test_get_secret_with_scope(self): + resolver = CredentialResolver() + mock_dbutils = MagicMock() + mock_dbutils.secrets.get.return_value = "scoped_value" + resolver._dbutils = mock_dbutils + value = resolver._get_secret_with_scope("custom-scope", "my_key") + assert value == "scoped_value" + mock_dbutils.secrets.get.assert_called_once_with( + scope="custom-scope", key="my_key" ) def test_list_available_sources(self): - """Test listing all available sources.""" resolver = CredentialResolver() - mock_dbutils = MagicMock() mock_secrets = [] - for key in ["source-s3/key1", "source-s3/key2", "source-gcs/key1", "source-teams/key1"]: + for key in [ + "source-s3/key1", + "source-s3/key2", + "source-gcs/key1", + "source-teams/key1", + ]: mock_secret = MagicMock() mock_secret.key = key mock_secrets.append(mock_secret) - mock_dbutils.secrets.list.return_value = mock_secrets resolver._dbutils = mock_dbutils - sources = resolver.list_available_sources() - assert "source-s3" in sources assert "source-gcs" in sources assert "source-teams" in sources def test_get_credentials_convention_based(self): - """Test getting credentials via convention-based discovery.""" resolver = CredentialResolver() - mock_dbutils = MagicMock() mock_secret1 = MagicMock() mock_secret1.key = "source-s3/aws_access_key_id" mock_secret2 = MagicMock() mock_secret2.key = "source-s3/aws_secret_access_key" - mock_dbutils.secrets.list.return_value = [mock_secret1, mock_secret2] mock_dbutils.secrets.get.side_effect = lambda scope, key: { "source-s3/aws_access_key_id": "key123", "source-s3/aws_secret_access_key": "secret456", }.get(key) - resolver._dbutils = mock_dbutils - creds = resolver.get_credentials("source-s3") - assert creds["aws_access_key_id"] == "key123" assert creds["aws_secret_access_key"] == "secret456" + def test_dotted_key_nested_mapping(self): + resolver = CredentialResolver() + mock_dbutils = MagicMock() + mock_secret = MagicMock() + mock_secret.key = "source-x/credentials.client_id" + mock_dbutils.secrets.list.return_value = [mock_secret] + mock_dbutils.secrets.get.side_effect = lambda scope, key: { + "source-x/credentials.client_id": "my_client_id", + }.get(key) + resolver._dbutils = mock_dbutils + creds = resolver.get_credentials("source-x") + assert creds["credentials"]["client_id"] == "my_client_id" -class TestYamlProfiles: - """Tests for YAML profile loading.""" +class TestYamlProfiles: def test_resolve_profile_simple(self): - """Test resolving a simple profile without secret references.""" resolver = CredentialResolver() resolver._profiles = { - "test-profile": { - "region": "us-east-1", - "bucket": "my-bucket", - } + "test-profile": {"region": "us-east-1", "bucket": "my-bucket"} } - result = resolver._resolve_profile("test-profile") - assert result["region"] == "us-east-1" assert result["bucket"] == "my-bucket" def test_resolve_profile_nonexistent(self): - """Test resolving a nonexistent profile returns empty dict.""" resolver = CredentialResolver() resolver._profiles = {} - result = resolver._resolve_profile("nonexistent") - assert result == {} def test_mappings_take_precedence(self): - """Test that profile mappings are used when available.""" resolver = CredentialResolver() resolver._profiles = { - "azure-shared": { - "tenant_id": "tenant123", - "client_id": "client456", - } - } - resolver._mappings = { - "source-microsoft-teams": "azure-shared", + "azure-shared": {"tenant_id": "tenant123", "client_id": "client456"} } - + resolver._mappings = {"source-microsoft-teams": "azure-shared"} creds = resolver.get_credentials("source-microsoft-teams") - assert creds["tenant_id"] == "tenant123" assert creds["client_id"] == "client456" + def test_resolve_profile_with_explicit_scope_secret(self): + resolver = CredentialResolver() + mock_dbutils = MagicMock() + mock_dbutils.secrets.get.return_value = "resolved_secret" + resolver._dbutils = mock_dbutils -def _has_virtualenv(): - """Check if virtualenv is available.""" - try: - import virtualenv - return True - except ImportError: - return False + resolver._profiles = { + "test-profile": {"api_key": "{{ secret('custom-scope/my_key') }}"} + } + result = resolver._resolve_profile("test-profile") + assert result["api_key"] == "resolved_secret" + mock_dbutils.secrets.get.assert_called_with(scope="custom-scope", key="my_key") + + def test_unresolved_secret_logs_warning(self, caplog): + import logging + + resolver = CredentialResolver() + resolver._dbutils = MagicMock() + resolver._dbutils.secrets.get.return_value = None + + resolver._profiles = { + "test-profile": {"api_key": "{{ secret('missing_key') }}"} + } + + with caplog.at_level(logging.WARNING, logger="brickbyte.credentials"): + result = resolver._resolve_profile("test-profile") + + assert "api_key" not in result + assert "Could not resolve secret" in caplog.text + + +class TestSetNested: + def test_simple_key(self): + resolver = CredentialResolver() + d = {} + resolver._set_nested(d, "key", "value") + assert d == {"key": "value"} + + def test_dotted_key(self): + resolver = CredentialResolver() + d = {} + resolver._set_nested(d, "credentials.client_id", "my_id") + assert d == {"credentials": {"client_id": "my_id"}} + + def test_deep_dotted_key(self): + resolver = CredentialResolver() + d = {} + resolver._set_nested(d, "a.b.c", "deep") + assert d == {"a": {"b": {"c": "deep"}}} -@pytest.mark.skipif(not _has_virtualenv(), reason="virtualenv not installed") -class TestBrickbyteCredentialIntegration: - """Tests for Brickbyte credential integration.""" +class TestClientCredentialIntegration: + def test_client_init_with_default_scope(self): + import brickbyte - def test_brickbyte_init_with_default_scope(self): - """Test Brickbyte initializes credential resolver with default scope.""" - from brickbyte import Brickbyte - - bb = Brickbyte() - + bb = brickbyte.client() assert bb._credential_resolver.secrets_scope == "brickbyte" - def test_brickbyte_init_with_custom_scope(self): - """Test Brickbyte with custom secrets scope.""" - from brickbyte import Brickbyte - - bb = Brickbyte(secrets_scope="my-custom-scope") - + def test_client_init_with_custom_scope(self): + import brickbyte + + bb = brickbyte.client(secrets_scope="my-custom-scope") assert bb._credential_resolver.secrets_scope == "my-custom-scope" def test_list_configured_sources(self): - """Test listing configured sources.""" - from brickbyte import Brickbyte - - bb = Brickbyte() - bb._credential_resolver._cache = { - "source-s3": {"key": "value"}, - } + import brickbyte + + bb = brickbyte.client() + bb._credential_resolver._cache = {"source-s3": {"key": "value"}} bb._credential_resolver._available_keys = ["source-s3/key", "source-gcs/key"] - - # Mock dbutils to return the cached keys + mock_dbutils = MagicMock() mock_s3 = MagicMock() mock_s3.key = "source-s3/key" @@ -297,18 +273,15 @@ def test_list_configured_sources(self): mock_gcs.key = "source-gcs/key" mock_dbutils.secrets.list.return_value = [mock_s3, mock_gcs] bb._credential_resolver._dbutils = mock_dbutils - + sources = bb.list_configured_sources() - assert "source-s3" in sources assert "source-gcs" in sources def test_validate_credentials(self): - """Test credential validation.""" - from brickbyte import Brickbyte - - bb = Brickbyte() + import brickbyte + + bb = brickbyte.client() bb._credential_resolver._cache["source-s3"] = {"key": "value"} - assert bb.validate_credentials("source-s3") is True assert bb.validate_credentials("source-nonexistent") is False diff --git a/tests/test_dedup.py b/tests/test_dedup.py new file mode 100644 index 0000000..6e726c3 --- /dev/null +++ b/tests/test_dedup.py @@ -0,0 +1,386 @@ +""" +Tests for deduplication logic. +""" +from unittest.mock import MagicMock, patch + +import pytest + +import brickbyte +from brickbyte._dedup import deduplicate_stream +from brickbyte._schema import DK_MISSING +from brickbyte.writers.spark_streaming_writer import SparkStreamingWriter + + +class TestDedupKeyNormalization: + @pytest.fixture + def bb(self, tmp_path): + return brickbyte.client(base_venv_directory=str(tmp_path)) + + def test_deduplicate_true_without_keys_raises(self, bb): + with pytest.raises(ValueError, match="dedup_keys is required"): + bb._normalize_dedup_keys(None, None) + + def test_deduplicate_with_empty_list_raises(self, bb): + with pytest.raises(ValueError, match="non-empty"): + bb._normalize_dedup_keys([], None) + + def test_deduplicate_with_per_stream_empty_raises(self, bb): + with pytest.raises(ValueError, match="non-empty"): + bb._normalize_dedup_keys({"stream": []}, None) + + def test_list_keys_normalized_to_dict(self, bb): + result = bb._normalize_dedup_keys(["email"], None) + assert result == {"__all__": ["email"]} + + def test_dict_keys_pass_through(self, bb): + result = bb._normalize_dedup_keys({"users": ["email"]}, None) + assert result == {"users": ["email"]} + + def test_invalid_identifier_in_list_raises(self, bb): + with pytest.raises(ValueError, match="invalid key"): + bb._normalize_dedup_keys(["bad`key"], None) + + def test_invalid_identifier_in_dict_raises(self, bb): + with pytest.raises(ValueError, match="invalid key"): + bb._normalize_dedup_keys({"users": ["bad;key"]}, None) + + +class TestDedupTransformRecord: + def test_flatten_mode_dedup_keys_added(self): + writer = SparkStreamingWriter( + catalog="main", + schema="test", + flatten=True, + run_id="test-run", + dedup_keys={"stream1": ["email"]}, + ) + writer._spark = MagicMock() + + record = {"id": 1, "email": "test@example.com"} + transformed = writer._transform_record("stream1", record) + + assert "_dk_0" in transformed + assert transformed["_dk_0"] == "test@example.com" + assert transformed[DK_MISSING] is False + + def test_raw_mode_dedup_keys_extracted_from_source(self): + writer = SparkStreamingWriter( + catalog="main", + schema="test", + flatten=False, + run_id="test-run", + dedup_keys={"stream1": ["user-id"]}, + ) + writer._spark = MagicMock() + + record = {"user-id": "abc123", "name": "test"} + transformed = writer._transform_record("stream1", record) + + assert "_dk_0" in transformed + assert transformed["_dk_0"] == "abc123" + assert transformed[DK_MISSING] is False + + def test_missing_key_sets_dk_missing(self): + writer = SparkStreamingWriter( + catalog="main", + schema="test", + flatten=True, + run_id="test-run", + dedup_keys={"stream1": ["email"]}, + ) + writer._spark = MagicMock() + + record = {"id": 1} # No email field + transformed = writer._transform_record("stream1", record) + + assert transformed["_dk_0"] is None + assert transformed[DK_MISSING] is True + + def test_null_key_value_dk_missing_stays_false(self): + writer = SparkStreamingWriter( + catalog="main", + schema="test", + flatten=True, + run_id="test-run", + dedup_keys={"stream1": ["email"]}, + ) + writer._spark = MagicMock() + + record = {"id": 1, "email": None} + transformed = writer._transform_record("stream1", record) + + assert transformed["_dk_0"] is None + assert transformed[DK_MISSING] is False + + def test_no_dedup_keys_no_dk_columns(self): + writer = SparkStreamingWriter( + catalog="main", + schema="test", + flatten=True, + run_id="test-run", + ) + writer._spark = MagicMock() + + record = {"id": 1, "email": "test@example.com"} + transformed = writer._transform_record("stream1", record) + + assert "_dk_0" not in transformed + assert DK_MISSING not in transformed + + def test_stream_not_in_dedup_dict_no_dk_columns(self): + writer = SparkStreamingWriter( + catalog="main", + schema="test", + flatten=True, + run_id="test-run", + dedup_keys={"other_stream": ["email"]}, + ) + writer._spark = MagicMock() + + record = {"id": 1, "email": "test@example.com"} + transformed = writer._transform_record("stream1", record) + + assert "_dk_0" not in transformed + assert DK_MISSING not in transformed + + def test_multiple_dedup_keys(self): + writer = SparkStreamingWriter( + catalog="main", + schema="test", + flatten=True, + run_id="test-run", + dedup_keys={"stream1": ["email", "phone"]}, + ) + writer._spark = MagicMock() + + record = {"id": 1, "email": "test@example.com", "phone": "555-1234"} + transformed = writer._transform_record("stream1", record) + + assert transformed["_dk_0"] == "test@example.com" + assert transformed["_dk_1"] == "555-1234" + assert transformed[DK_MISSING] is False + + +class TestDedupKeyValidation: + @pytest.fixture + def bb(self, tmp_path): + return brickbyte.client(base_venv_directory=str(tmp_path)) + + def test_dict_key_using_sanitized_name_raises(self, bb, mock_airbyte): + mock_source = MagicMock() + mock_airbyte.get_source.return_value = mock_source + mock_source.get_selected_streams.return_value = ["my-stream"] + mock_source.get_records.return_value = [] + + with patch("brickbyte.writers.create_streaming_writer") as mock_factory: + mock_writer = MagicMock() + mock_factory.return_value = mock_writer + + with pytest.raises(ValueError, match="sanitized name"): + bb.sync( + source="source-faker", + source_config={}, + catalog="main", + schema="test", + staging_volume="main.staging.vol", + mode="append", + deduplicate=True, + dedup_keys={"my_stream": ["email"]}, + ) + + def test_unmatched_dict_key_raises(self, bb, mock_airbyte): + mock_source = MagicMock() + mock_airbyte.get_source.return_value = mock_source + mock_source.get_selected_streams.return_value = ["users"] + mock_source.get_records.return_value = [] + + with patch("brickbyte.writers.create_streaming_writer") as mock_factory: + mock_writer = MagicMock() + mock_factory.return_value = mock_writer + + with pytest.raises(ValueError, match="does not match"): + bb.sync( + source="source-faker", + source_config={}, + catalog="main", + schema="test", + staging_volume="main.staging.vol", + mode="append", + deduplicate=True, + dedup_keys={"nonexistent": ["email"]}, + ) + + def test_dedup_keys_ignored_when_deduplicate_false(self, bb, mock_airbyte): + mock_source = MagicMock() + mock_airbyte.get_source.return_value = mock_source + mock_source.get_selected_streams.return_value = ["users"] + mock_source.get_records.return_value = [{"id": 1}] + + with patch("brickbyte.writers.create_streaming_writer") as mock_factory: + mock_writer = MagicMock() + mock_factory.return_value = mock_writer + + result = bb.sync( + source="source-faker", + source_config={}, + catalog="main", + schema="test", + staging_volume="main.staging.vol", + mode="append", + deduplicate=False, + dedup_keys=["email"], + ) + + assert result.records_written == 1 + + +class TestDedupListKeysExpansion: + """Test that List[str] dedup_keys are expanded to per-stream dict.""" + + @pytest.fixture + def bb(self, tmp_path): + return brickbyte.client(base_venv_directory=str(tmp_path)) + + def test_list_keys_applied_to_all_streams(self, bb, mock_airbyte): + """List[str] dedup_keys should apply to every selected stream.""" + mock_source = MagicMock() + mock_airbyte.get_source.return_value = mock_source + mock_source.get_selected_streams.return_value = ["users", "orders"] + mock_source.get_records.side_effect = [ + [{"id": 1, "email": "a@b.com"}], + [{"id": 2, "email": "c@d.com"}], + ] + + with patch("brickbyte.writers.create_streaming_writer") as mock_factory: + mock_writer = MagicMock() + mock_factory.return_value = mock_writer + + result = bb.sync( + source="source-faker", + source_config={}, + catalog="main", + schema="test", + staging_volume="main.staging.vol", + mode="append", + deduplicate=True, + dedup_keys=["email"], + ) + + assert result.records_written == 2 + # The writer's dedup_keys kwarg should be a per-stream dict, + # NOT {"__all__": ["email"]} + call_kwargs = mock_factory.call_args[1] + dk = call_kwargs["dedup_keys"] + assert "users" in dk + assert "orders" in dk + assert "__all__" not in dk + assert dk["users"] == ["email"] + assert dk["orders"] == ["email"] + + def test_dedup_runs_with_executor_in_parallel_mode(self, bb, mock_airbyte): + """In parallel mode, dedup must not fail with executor=None.""" + mock_source = MagicMock() + mock_airbyte.get_source.return_value = mock_source + mock_source.get_selected_streams.return_value = ["users"] + mock_source.get_records.return_value = [{"id": 1, "email": "a@b.com"}] + + writers_created = [] + + def mock_create_writer(**kwargs): + w = MagicMock() + w.spark = MagicMock() # Has spark attr so _execute_sql works + writers_created.append(w) + return w + + with patch( + "brickbyte.writers.create_streaming_writer", + side_effect=mock_create_writer, + ): + result = bb.sync( + source="source-faker", + source_config={}, + catalog="main", + schema="test", + staging_volume="main.staging.vol", + mode="append", + deduplicate=True, + dedup_keys=["email"], + max_parallel_streams=2, + ) + + assert result.records_written == 1 + # Dedup MERGE should have been called on the writer + assert any( + w.spark.sql.called for w in writers_created + ), "dedup MERGE should have been invoked on a writer" + + +class TestRunDedupRouting: + @pytest.fixture + def bb(self, tmp_path): + return brickbyte.client(base_venv_directory=str(tmp_path)) + + def test_flatten_mode_uses_internal_dk_columns(self, bb): + with patch("brickbyte._dedup.deduplicate_stream") as mock_dedup: + bb._run_dedup_for_stream( + stream_name="users", + deduplicate=True, + normalized_dedup_keys={"users": ["email", "phone"]}, + flatten=True, + catalog="main", + schema="test", + executor_writer=MagicMock(), + ) + + kwargs = mock_dedup.call_args.kwargs + assert kwargs["key_columns"] == ["_dk_0", "_dk_1"] + assert kwargs["run_id_col"] == "_run_id" + assert kwargs["extracted_at_col"] == "_extracted_at" + assert kwargs["record_id_col"] == "_record_id" + + +class TestDeduplicateStream: + def test_deduplicate_executes_merge(self): + mock_executor = MagicMock() + mock_executor.spark = MagicMock() + + deduplicate_stream( + executor=mock_executor, + table_name="`main`.`test`.`users`", + key_columns=["_dk_0"], + run_id_col="run_id", + extracted_at_col="extracted_at", + record_id_col="record_id", + flatten=False, + ) + + mock_executor.spark.sql.assert_called_once() + call_args = str(mock_executor.spark.sql.call_args) + assert "MERGE INTO" in call_args + assert "_dk_0" in call_args + + def test_deduplicate_empty_keys_noop(self): + mock_executor = MagicMock() + deduplicate_stream( + executor=mock_executor, + table_name="`main`.`test`.`users`", + key_columns=[], + run_id_col="run_id", + extracted_at_col="extracted_at", + record_id_col="record_id", + ) + mock_executor.spark.sql.assert_not_called() + + def test_deduplicate_invalid_key_identifier_raises(self): + mock_executor = MagicMock() + mock_executor.spark = MagicMock() + + with pytest.raises(ValueError, match="unsafe character"): + deduplicate_stream( + executor=mock_executor, + table_name="`main`.`test`.`users`", + key_columns=["bad`col"], + run_id_col="run_id", + extracted_at_col="extracted_at", + record_id_col="record_id", + ) diff --git a/tests/test_functional.py b/tests/test_functional.py index 08ce982..d01360e 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -1,52 +1,144 @@ """ -Verification tests for Brickbyte functionalities (Streaming Only). +Verification tests for brickbyte functionalities (Streaming Only). """ from unittest.mock import MagicMock, patch import pytest -from brickbyte import Brickbyte +import brickbyte class TestBrickbyteFunctional: - @pytest.fixture - def mock_airbyte(self): - import sys - mock_ab = MagicMock() - with patch.dict(sys.modules, {"airbyte": mock_ab}): - yield mock_ab + def bb(self, tmp_path): + return brickbyte.client(base_venv_directory=str(tmp_path)) - @pytest.fixture - def brickbyte(self, tmp_path): - return Brickbyte(base_venv_directory=str(tmp_path)) - - def test_sync_streaming_default(self, brickbyte, mock_airbyte): - """Test the sync method with default streaming behavior.""" - # Mock dependencies + def test_sync_streaming_default(self, bb, mock_airbyte): mock_source = MagicMock() mock_airbyte.get_source.return_value = mock_source mock_source.get_selected_streams.return_value = ["test_stream"] - - # Mock records generator - mock_source.get_records.return_value = [{"id": 1, "val": "a"}, {"id": 2, "val": "b"}] - - # Mock writer factory + mock_source.get_records.return_value = [ + {"id": 1, "val": "a"}, + {"id": 2, "val": "b"}, + ] + with patch("brickbyte.writers.create_streaming_writer") as mock_create_writer: mock_writer = MagicMock() mock_create_writer.return_value = mock_writer - - result = brickbyte.sync( + + result = bb.sync( source="source-faker", source_config={}, catalog="main", schema="test", - staging_volume="main.staging.vol" + staging_volume="main.staging.vol", + mode="append", ) - - # Verifications + assert result.records_written == 2 mock_create_writer.assert_called_once() assert mock_writer.write_record.call_count == 2 mock_writer.flush_stream.assert_called_with("test_stream") - mock_writer.close.assert_called_once() + + def test_incremental_applies_saved_state(self, bb, mock_airbyte): + mock_source = MagicMock() + mock_airbyte.get_source.return_value = mock_source + mock_source.get_selected_streams.return_value = ["users"] + mock_source.get_records.return_value = [{"id": 1}] + mock_source.set_stream_state = MagicMock() + mock_source.get_stream_state.return_value = {"cursor": "2024-01-02"} + + with patch("brickbyte._state.StateManager") as mock_state_manager_cls: + mock_state_manager = MagicMock() + mock_state_manager.get_state.return_value = {"cursor": "2024-01-01"} + mock_state_manager_cls.return_value = mock_state_manager + + with patch("brickbyte.writers.create_streaming_writer") as mock_create_writer: + mock_writer = MagicMock() + mock_create_writer.return_value = mock_writer + + result = bb.sync( + source="source-faker", + source_config={}, + catalog="main", + schema="test", + staging_volume="main.staging.vol", + mode="append", + incremental=True, + ) + + assert result.records_written == 1 + mock_source.set_stream_state.assert_called_once_with( + "users", + {"cursor": "2024-01-01"}, + ) + mock_state_manager.save_state.assert_called_once() + + def test_incremental_with_saved_state_without_state_api_raises(self, bb, mock_airbyte): + mock_source = MagicMock( + spec=[ + "check", + "select_all_streams", + "get_selected_streams", + "get_records", + ] + ) + mock_airbyte.get_source.return_value = mock_source + mock_source.get_selected_streams.return_value = ["users"] + mock_source.get_records.return_value = [] + + with patch("brickbyte._state.StateManager") as mock_state_manager_cls: + mock_state_manager = MagicMock() + mock_state_manager.get_state.return_value = {"cursor": "2024-01-01"} + mock_state_manager_cls.return_value = mock_state_manager + + with pytest.raises(NotImplementedError, match="state injection support"): + bb.sync( + source="source-faker", + source_config={}, + catalog="main", + schema="test", + staging_volume="main.staging.vol", + mode="append", + incremental=True, + ) + + def test_progress_reporter_closed_on_error(self, bb, mock_airbyte): + mock_source = MagicMock() + mock_airbyte.get_source.return_value = mock_source + mock_source.get_selected_streams.return_value = ["users"] + mock_source.get_records.side_effect = RuntimeError("boom") + + with patch("brickbyte.writers.create_streaming_writer") as mock_create_writer: + mock_writer = MagicMock() + mock_create_writer.return_value = mock_writer + + with patch("brickbyte._progress.ProgressReporter") as mock_reporter_cls: + reporter = MagicMock() + mock_reporter_cls.return_value = reporter + + with pytest.raises(RuntimeError, match="boom"): + bb.sync( + source="source-faker", + source_config={}, + catalog="main", + schema="test", + staging_volume="main.staging.vol", + mode="append", + progress_callback=lambda _evt: None, + ) + + reporter.close.assert_called_once() + + def test_client_factory_returns_client(self): + bb = brickbyte.client() + assert type(bb).__name__ == "Client" + + def test_sync_result_dataclass(self): + result = brickbyte.SyncResult( + records_written=100, + streams_synced=["a", "b"], + failed_streams=["c"], + ) + assert result.records_written == 100 + assert len(result.streams_synced) == 2 diff --git a/tests/test_hybrid.py b/tests/test_hybrid.py index 25a03d4..8e339df 100644 --- a/tests/test_hybrid.py +++ b/tests/test_hybrid.py @@ -6,62 +6,57 @@ import pytest -from brickbyte.writers import SparkStreamingWriter, SQLStreamingWriter, create_streaming_writer +from brickbyte.writers import create_streaming_writer +from brickbyte.writers.spark_streaming_writer import SparkStreamingWriter +from brickbyte.writers.sql_streaming_writer import SQLStreamingWriter class TestHybridFactory: - @patch("os.makedirs") def test_factory_detects_spark(self, mock_makedirs): - """Test that SparkStreamingWriter is created when Spark is active.""" - - # Mock pyspark.sql.SparkSession.getActiveSession with patch.dict(sys.modules, {"pyspark.sql": MagicMock()}): mock_session = MagicMock() - sys.modules["pyspark.sql"].SparkSession.getActiveSession.return_value = mock_session - + sys.modules["pyspark.sql"].SparkSession.getActiveSession.return_value = ( + mock_session + ) + writer = create_streaming_writer( - catalog="main", - schema="test" + catalog="main", schema="test", run_id="test-run" ) - + assert isinstance(writer, SparkStreamingWriter) assert writer.catalog == "main" def test_factory_fallback_to_sql(self): - """Test that SQLStreamingWriter is created when Spark is missing.""" - - # Simulate import error for pyspark with patch.dict(sys.modules, {"pyspark.sql": None}): - # We also need to mock databricks.sdk with patch("databricks.sdk.WorkspaceClient") as mock_ws_client: mock_w = MagicMock() mock_ws_client.return_value = mock_w mock_w.config.host = "https://test-host" mock_w.config.token = "token" - - # Mock warehouse listing + mock_wh = MagicMock() mock_wh.state.value = "RUNNING" mock_wh.id = "wh-123" mock_w.warehouses.list.return_value = [mock_wh] - - writer = create_streaming_writer( - catalog="main", - schema="test", - staging_volume="main.test.vol" - ) - - assert isinstance(writer, SQLStreamingWriter) - assert writer.staging_volume == "main.test.vol" + + with patch("os.path.exists", return_value=True): + writer = create_streaming_writer( + catalog="main", + schema="test", + staging_volume="main.test.vol", + run_id="test-run", + ) + + assert isinstance(writer, SQLStreamingWriter) + assert writer.staging_volume == "main.test.vol" def test_factory_raises_error_no_volume_no_spark(self): - """Test that ValueError is raised if no Spark and no Volume.""" - with patch.dict(sys.modules, {"pyspark.sql": None}): - with pytest.raises(ValueError, match="staging_volume is REQUIRED"): + with pytest.raises(ValueError, match="staging_volume is REQUIRED"): create_streaming_writer( catalog="main", schema="test", - staging_volume=None + staging_volume=None, + run_id="test-run", ) diff --git a/tests/test_incremental.py b/tests/test_incremental.py new file mode 100644 index 0000000..a54282c --- /dev/null +++ b/tests/test_incremental.py @@ -0,0 +1,70 @@ +""" +Tests for incremental sync state management. +""" +from unittest.mock import MagicMock, patch + +import pytest + +from brickbyte._state import StateManager + + +class TestStateManager: + @pytest.fixture + def state_mgr(self): + mgr = StateManager(catalog="main", schema="test") + mgr._spark = MagicMock() + mgr._initialized = True + return mgr + + def test_state_table_name(self, state_mgr): + assert "__brickbyte_state" in state_mgr._state_table + + def test_save_state_calls_merge(self, state_mgr): + state_mgr.save_state( + source="source-faker", + stream_name="users", + state={"cursor": "2024-01-01"}, + run_id="test-run", + ) + state_mgr._spark.sql.assert_called_once() + call_args = str(state_mgr._spark.sql.call_args) + assert "MERGE INTO" in call_args + + @patch("brickbyte._state.col", create=True) + def test_get_state_returns_parsed_json(self, mock_col, state_mgr): + mock_df = MagicMock() + mock_row = MagicMock() + mock_row.__getitem__ = lambda self, key: '{"cursor": "2024-01-01"}' + mock_df.collect.return_value = [mock_row] + + state_mgr._spark.table.return_value.filter.return_value.select.return_value.limit.return_value = mock_df + + with patch.dict("sys.modules", {"pyspark.sql.functions": MagicMock()}): + result = state_mgr.get_state("source-faker", "users") + assert result == {"cursor": "2024-01-01"} + + def test_get_state_returns_none_when_empty(self, state_mgr): + mock_df = MagicMock() + mock_df.collect.return_value = [] + state_mgr._spark.table.return_value.filter.return_value.select.return_value.limit.return_value = mock_df + + with patch.dict("sys.modules", {"pyspark.sql.functions": MagicMock()}): + result = state_mgr.get_state("source-faker", "users") + assert result is None + + def test_ensure_table_creates_ddl(self): + mgr = StateManager(catalog="main", schema="test") + mgr._spark = MagicMock() + mgr._initialized = False + mgr._ensure_table() + + mgr._spark.sql.assert_called_once() + call_args = str(mgr._spark.sql.call_args) + assert "CREATE TABLE IF NOT EXISTS" in call_args + assert mgr._initialized is True + + def test_clear_state(self, state_mgr): + state_mgr.clear_state("source-faker", "users") + state_mgr._spark.sql.assert_called_once() + call_args = str(state_mgr._spark.sql.call_args) + assert "DELETE FROM" in call_args diff --git a/tests/test_mode_validation.py b/tests/test_mode_validation.py index 759b394..cff6f39 100644 --- a/tests/test_mode_validation.py +++ b/tests/test_mode_validation.py @@ -5,58 +5,39 @@ import pytest -from brickbyte import Brickbyte +import brickbyte class TestModeValidation: - """Test _validate_sync_params mode validation.""" - @pytest.fixture - def brickbyte(self, tmp_path): - return Brickbyte(base_venv_directory=str(tmp_path)) + def bb(self, tmp_path): + return brickbyte.client(base_venv_directory=str(tmp_path)) - def test_append_mode_valid(self, brickbyte): - """Test that append mode is valid.""" - # Should not raise - brickbyte._validate_sync_params(mode="append", staging_volume="a.b.c") + def test_append_mode_valid(self, bb): + bb._validate_sync_params(mode="append") - def test_overwrite_mode_valid(self, brickbyte): - """Test that overwrite mode is valid.""" - # Should not raise - brickbyte._validate_sync_params(mode="overwrite", staging_volume="a.b.c") + def test_overwrite_mode_valid(self, bb): + bb._validate_sync_params(mode="overwrite") - def test_merge_mode_not_implemented(self, brickbyte): - """Test that merge mode raises NotImplementedError.""" + def test_merge_mode_not_implemented(self, bb): with pytest.raises(NotImplementedError, match="Merge mode is not yet supported"): - brickbyte._validate_sync_params(mode="merge", staging_volume="a.b.c") + bb._validate_sync_params(mode="merge") - def test_invalid_mode_raises_error(self, brickbyte): - """Test that invalid mode raises ValueError.""" + def test_invalid_mode_raises_error(self, bb): with pytest.raises(ValueError, match="Invalid mode 'invalid'"): - brickbyte._validate_sync_params(mode="invalid", staging_volume="a.b.c") + bb._validate_sync_params(mode="invalid") - def test_unknown_mode_raises_error(self, brickbyte): - """Test that unknown modes raise ValueError.""" + def test_unknown_mode_raises_error(self, bb): with pytest.raises(ValueError, match="Invalid mode"): - brickbyte._validate_sync_params(mode="upsert", staging_volume="a.b.c") + bb._validate_sync_params(mode="upsert") class TestOverwriteMode: - """Test overwrite mode behavior (drop_table before sync).""" - @pytest.fixture - def mock_airbyte(self): - import sys - mock_ab = MagicMock() - with patch.dict(sys.modules, {"airbyte": mock_ab}): - yield mock_ab + def bb(self, tmp_path): + return brickbyte.client(base_venv_directory=str(tmp_path)) - @pytest.fixture - def brickbyte(self, tmp_path): - return Brickbyte(base_venv_directory=str(tmp_path)) - - def test_overwrite_drops_table_before_streaming(self, brickbyte, mock_airbyte): - """Test that overwrite mode calls drop_table before streaming.""" + def test_overwrite_uses_safe_overwrite(self, bb, mock_airbyte): mock_source = MagicMock() mock_airbyte.get_source.return_value = mock_source mock_source.get_selected_streams.return_value = ["users", "orders"] @@ -66,7 +47,7 @@ def test_overwrite_drops_table_before_streaming(self, brickbyte, mock_airbyte): mock_writer = MagicMock() mock_factory.return_value = mock_writer - brickbyte.sync( + bb.sync( source="source-faker", source_config={}, catalog="main", @@ -75,13 +56,12 @@ def test_overwrite_drops_table_before_streaming(self, brickbyte, mock_airbyte): mode="overwrite", ) - # Verify drop_table was called for each stream - assert mock_writer.drop_table.call_count == 2 - mock_writer.drop_table.assert_any_call("users") - mock_writer.drop_table.assert_any_call("orders") + # Should use safe_overwrite_begin/finish instead of drop_table + assert mock_writer.safe_overwrite_begin.call_count == 2 + assert mock_writer.safe_overwrite_finish.call_count == 2 + mock_writer.drop_table.assert_not_called() - def test_append_does_not_drop_table(self, brickbyte, mock_airbyte): - """Test that append mode does NOT call drop_table.""" + def test_append_does_not_drop_table(self, bb, mock_airbyte): mock_source = MagicMock() mock_airbyte.get_source.return_value = mock_source mock_source.get_selected_streams.return_value = ["users"] @@ -91,7 +71,7 @@ def test_append_does_not_drop_table(self, brickbyte, mock_airbyte): mock_writer = MagicMock() mock_factory.return_value = mock_writer - brickbyte.sync( + bb.sync( source="source-faker", source_config={}, catalog="main", @@ -100,26 +80,16 @@ def test_append_does_not_drop_table(self, brickbyte, mock_airbyte): mode="append", ) - # Verify drop_table was NOT called mock_writer.drop_table.assert_not_called() + mock_writer.safe_overwrite_begin.assert_not_called() class TestSyncModeIntegration: - """Integration tests for sync with different modes.""" - - @pytest.fixture - def mock_airbyte(self): - import sys - mock_ab = MagicMock() - with patch.dict(sys.modules, {"airbyte": mock_ab}): - yield mock_ab - @pytest.fixture - def brickbyte(self, tmp_path): - return Brickbyte(base_venv_directory=str(tmp_path)) + def bb(self, tmp_path): + return brickbyte.client(base_venv_directory=str(tmp_path)) - def test_default_mode_is_overwrite(self, brickbyte, mock_airbyte): - """Test that default mode is overwrite.""" + def test_default_mode_is_overwrite(self, bb, mock_airbyte): mock_source = MagicMock() mock_airbyte.get_source.return_value = mock_source mock_source.get_selected_streams.return_value = ["stream1"] @@ -129,8 +99,7 @@ def test_default_mode_is_overwrite(self, brickbyte, mock_airbyte): mock_writer = MagicMock() mock_factory.return_value = mock_writer - # Call without specifying mode - brickbyte.sync( + bb.sync( source="source-faker", source_config={}, catalog="main", @@ -138,6 +107,5 @@ def test_default_mode_is_overwrite(self, brickbyte, mock_airbyte): staging_volume="main.staging.vol", ) - # Default is overwrite, so drop_table should be called - mock_writer.drop_table.assert_called_once_with("stream1") - + # Default is overwrite, so safe_overwrite should be called + mock_writer.safe_overwrite_begin.assert_called_once_with("stream1", mock_factory.call_args[1]["run_id"]) diff --git a/tests/test_preview.py b/tests/test_preview.py index cc0b200..e41798f 100644 --- a/tests/test_preview.py +++ b/tests/test_preview.py @@ -14,33 +14,22 @@ class TestSchemaChange: - """Test SchemaChange dataclass.""" - def test_added_column_str(self): - """Test string representation of added column.""" - change = SchemaChange( - column="new_col", - change_type="added", - source_type="str", - ) + change = SchemaChange(column="new_col", change_type="added", source_type="str") result = str(change) assert "new_col" in result assert "NEW" in result assert "str" in result def test_removed_column_str(self): - """Test string representation of removed column.""" change = SchemaChange( - column="old_col", - change_type="removed", - target_type="StringType", + column="old_col", change_type="removed", target_type="StringType" ) result = str(change) assert "old_col" in result assert "REMOVED" in result def test_type_changed_str(self): - """Test string representation of type change.""" change = SchemaChange( column="col", change_type="type_changed", @@ -54,40 +43,23 @@ def test_type_changed_str(self): class TestStreamPreview: - """Test StreamPreview dataclass.""" - def test_str_with_counts(self): - """Test string representation with record counts.""" - preview = StreamPreview( - stream_name="users", - source_count=100, - target_count=50, - new_records=30, - modified_records=10, - deleted_records=5, - ) + preview = StreamPreview(stream_name="users", sampled_records=5, target_count=50) result = str(preview) assert "users" in result - assert "+30 new" in result - assert "~10 modified" in result - assert "-5 deleted" in result + assert "sampled 5 records" in result + assert "target has 50 records" in result def test_str_streaming_unknown_count(self): - """Test string representation with unknown counts (streaming).""" - preview = StreamPreview( - stream_name="events", - source_count=-1, - target_count=0, - ) + preview = StreamPreview(stream_name="events", sampled_records=0, target_count=0) result = str(preview) assert "events" in result - assert "Unknown" in result or "Streaming" in result + assert "sampled 0 records" in result def test_str_with_schema_changes(self): - """Test string representation with schema changes.""" preview = StreamPreview( stream_name="orders", - source_count=50, + sampled_records=2, target_count=40, schema_changes=[ SchemaChange(column="new_field", change_type="added", source_type="str"), @@ -99,171 +71,132 @@ def test_str_with_schema_changes(self): class TestPreviewResult: - """Test PreviewResult dataclass.""" - def test_str_output(self): - """Test complete preview result string output.""" result = PreviewResult( streams=[ - StreamPreview("users", 100, 50, new_records=30), - StreamPreview("orders", 200, 150, new_records=20), + StreamPreview("users", 5, 50), + StreamPreview("orders", 3, 150), ], - total_source_records=300, - total_new_records=50, + total_sampled_records=8, + total_target_records=200, has_schema_changes=True, ) output = str(result) - assert "Sync Preview" in output assert "users" in output assert "orders" in output + assert "sampled 8 records" in output assert "Schema changes detected" in output def test_str_no_schema_changes(self): - """Test output without schema changes warning.""" result = PreviewResult( - streams=[StreamPreview("data", 10, 10)], - has_schema_changes=False, + streams=[StreamPreview("data", 1, 10)], has_schema_changes=False ) output = str(result) - assert "Schema changes detected" not in output class TestPreviewEngine: - """Test PreviewEngine functionality.""" - @pytest.fixture def engine(self): - """Create a PreviewEngine with mocked Spark.""" engine = PreviewEngine(catalog="main", schema="test") engine._spark = MagicMock() return engine def test_get_table_name(self, engine): - """Test fully qualified table name generation.""" - assert engine.get_table_name("users") == "main.test.users" + assert engine.get_table_name("users") == "`main`.`test`.`users`" def test_table_exists_true(self, engine): - """Test table_exists when table exists.""" engine._spark.catalog.tableExists.return_value = True assert engine.table_exists("users") is True def test_table_exists_false(self, engine): - """Test table_exists when table doesn't exist.""" engine._spark.catalog.tableExists.return_value = False assert engine.table_exists("users") is False def test_table_exists_no_spark(self): - """Test table_exists returns False when no Spark.""" engine = PreviewEngine(catalog="main", schema="test") - # Directly set _spark to None and check behavior - # The spark property returns None when import fails engine._spark = None - - # Mock the spark property to return None - with patch.object(PreviewEngine, 'spark', property(lambda self: None)): + with patch.object(PreviewEngine, "spark", property(lambda self: None)): assert engine.table_exists("users") is False def test_get_target_count(self, engine): - """Test getting target table count.""" engine._spark.catalog.tableExists.return_value = True mock_df = MagicMock() mock_df.count.return_value = 42 engine._spark.table.return_value = mock_df - - count = engine.get_target_count("users") - - assert count == 42 + assert engine.get_target_count("users") == 42 def test_get_target_count_no_table(self, engine): - """Test getting count when table doesn't exist.""" engine._spark.catalog.tableExists.return_value = False - - count = engine.get_target_count("users") - - assert count == 0 + assert engine.get_target_count("users") == 0 def test_get_source_schema(self, engine): - """Test schema inference from sample records.""" - samples = [ - {"id": 1, "name": "test", "active": True, "score": 3.14}, - ] - + samples = [{"id": 1, "name": "test", "active": True, "score": 3.14}] schema = engine.get_source_schema(samples) - assert schema["id"] == "int" assert schema["name"] == "str" assert schema["active"] == "bool" assert schema["score"] == "float" def test_get_source_schema_empty(self, engine): - """Test schema inference with empty samples.""" - schema = engine.get_source_schema([]) - assert schema == {} + assert engine.get_source_schema([]) == {} def test_compare_schemas_added_columns(self, engine): - """Test detecting added columns.""" source = {"id": "int", "name": "str", "new_col": "str"} target = {"id": "LongType", "name": "StringType"} - changes = engine.compare_schemas(source, target) - added = [c for c in changes if c.change_type == "added"] assert len(added) == 1 assert added[0].column == "new_col" def test_compare_schemas_removed_columns(self, engine): - """Test detecting removed columns.""" source = {"id": "int"} target = {"id": "LongType", "old_col": "StringType"} - changes = engine.compare_schemas(source, target) - removed = [c for c in changes if c.change_type == "removed"] assert len(removed) == 1 assert removed[0].column == "old_col" + def test_compare_schemas_type_changes_detected(self, engine): + source = {"id": "int", "name": "str", "score": "int"} + target = {"id": "LongType", "name": "StringType", "score": "StringType"} + changes = engine.compare_schemas(source, target) + type_changed = [c for c in changes if c.change_type == "type_changed"] + assert len(type_changed) == 1 + assert type_changed[0].column == "score" + def test_compare_schemas_no_changes(self, engine): - """Test when schemas match.""" source = {"id": "int", "name": "str"} target = {"id": "LongType", "name": "StringType"} - changes = engine.compare_schemas(source, target) - - # Same columns, different type names (expected) - no changes reported assert len(changes) == 0 def test_preview_stream(self, engine): - """Test previewing a single stream.""" engine._spark.catalog.tableExists.return_value = True mock_df = MagicMock() mock_df.count.return_value = 100 engine._spark.table.return_value = mock_df - - # Mock source mock_source = MagicMock() - mock_source.get_records.return_value = iter([ - {"id": 1, "name": "a"}, - {"id": 2, "name": "b"}, - ]) - + mock_source.get_records.return_value = iter( + [{"id": 1, "name": "a"}, {"id": 2, "name": "b"}] + ) preview = engine.preview_stream(mock_source, "users", sample_size=5) - assert preview.stream_name == "users" assert preview.target_count == 100 + assert preview.sampled_records == 2 assert len(preview.sample_records) == 2 def test_preview_all_streams(self, engine): - """Test previewing multiple streams.""" engine._spark.catalog.tableExists.return_value = False - mock_source = MagicMock() - mock_source.get_records.return_value = iter([{"id": 1}]) - + mock_source.get_records.side_effect = [ + iter([{"id": 1}]), + iter([{"id": 2}]), + ] result = engine.preview(mock_source, ["stream1", "stream2"]) - assert len(result.streams) == 2 + assert result.total_sampled_records == 2 + assert result.total_target_records == 0 assert result.streams[0].stream_name == "stream1" assert result.streams[1].stream_name == "stream2" - diff --git a/tests/test_progress.py b/tests/test_progress.py new file mode 100644 index 0000000..e079775 --- /dev/null +++ b/tests/test_progress.py @@ -0,0 +1,56 @@ +""" +Tests for progress reporting. +""" +from brickbyte._progress import ProgressReporter + + +class TestProgressReporter: + def test_callback_invoked_on_stream_completion(self): + events = [] + + def callback(event): + events.append(event) + + reporter = ProgressReporter(total_streams=3, callback=callback) + reporter.stream_completed("users", 100) + + assert len(events) == 1 + assert events[0].stream_name == "users" + assert events[0].records_processed == 100 + assert events[0].streams_completed == 1 + assert events[0].total_streams == 3 + + def test_callback_invoked_every_5000_records(self): + events = [] + + def callback(event): + events.append(event) + + reporter = ProgressReporter(total_streams=1, callback=callback) + reporter.record_processed("users", 5000) + + assert len(events) == 1 + assert events[0].records_processed == 5000 + + def test_callback_not_invoked_at_non_5000(self): + events = [] + + def callback(event): + events.append(event) + + reporter = ProgressReporter(total_streams=1, callback=callback) + reporter.record_processed("users", 1234) + + assert len(events) == 0 + + def test_streams_completed_counter(self): + reporter = ProgressReporter(total_streams=3) + assert reporter.streams_completed == 0 + reporter.stream_completed("a", 10) + assert reporter.streams_completed == 1 + reporter.stream_completed("b", 20) + assert reporter.streams_completed == 2 + + def test_close_without_tqdm(self): + reporter = ProgressReporter(total_streams=1) + reporter.close() # Should not raise diff --git a/tests/test_safe_overwrite.py b/tests/test_safe_overwrite.py new file mode 100644 index 0000000..3377570 --- /dev/null +++ b/tests/test_safe_overwrite.py @@ -0,0 +1,218 @@ +""" +Tests for safe overwrite (staged replace) behavior. +""" +from unittest.mock import MagicMock + +import pytest + +from brickbyte.writers.spark_streaming_writer import SparkStreamingWriter + + +class TestSafeOverwrite: + @pytest.fixture + def writer(self): + writer = SparkStreamingWriter( + catalog="main", + schema="test", + buffer_size_records=100, + run_id="abcdef12-3456-7890-abcd-ef1234567890", + ) + writer._spark = MagicMock() + return writer + + def test_staging_table_name_format(self, writer): + name = writer.get_staging_table_name("users", "abcdef12-3456-7890-abcd-ef1234567890") + assert "__stg__abcdef12" in name + assert "`main`.`test`." in name + + def test_safe_overwrite_begin_sets_redirect(self, writer): + writer.safe_overwrite_begin("users", "abcdef12") + assert "users" in writer._overwrite_streams + + def test_safe_overwrite_begin_drops_staging(self, writer): + writer.safe_overwrite_begin("users", "abcdef12") + writer._spark.sql.assert_called() + drop_calls = [ + c for c in writer._spark.sql.call_args_list if "DROP TABLE" in str(c) + ] + assert len(drop_calls) >= 1 + + def test_safe_overwrite_finish_rename_when_no_target(self, writer): + writer._overwrite_streams["users"] = "`main`.`test`.`users__stg__abcdef12`" + writer._spark.catalog.tableExists.return_value = False + + writer.safe_overwrite_finish("users", "abcdef12") + + rename_calls = [ + c for c in writer._spark.sql.call_args_list if "ALTER TABLE" in str(c) and "RENAME" in str(c) + ] + assert len(rename_calls) == 1 + + def test_safe_overwrite_finish_atomic_overwrite_when_target_exists(self, writer): + writer._overwrite_streams["users"] = "`main`.`test`.`users__stg__abcdef12`" + writer._spark.catalog.tableExists.return_value = True + + # Mock schemas + mock_target_df = MagicMock() + mock_target_field = MagicMock() + mock_target_field.name = "id" + mock_target_field.dataType = "StringType" + mock_target_df.schema.fields = [mock_target_field] + + mock_staging_df = MagicMock() + mock_staging_field = MagicMock() + mock_staging_field.name = "id" + mock_staging_field.dataType = "StringType" + mock_staging_df.schema.fields = [mock_staging_field] + + writer._spark.table.side_effect = lambda name: { + "`main`.`test`.`users`": mock_target_df, + "`main`.`test`.`users__stg__abcdef12`": mock_staging_df, + }[name] + + writer.safe_overwrite_finish("users", "abcdef12") + + # Should have INSERT OVERWRITE and DROP staging + sql_calls = [str(c) for c in writer._spark.sql.call_args_list] + insert_calls = [c for c in sql_calls if "INSERT OVERWRITE" in c] + drop_calls = [c for c in sql_calls if "DROP TABLE" in c] + assert len(insert_calls) == 1 + assert len(drop_calls) >= 1 + + def test_safe_overwrite_failure_drops_staging(self, writer): + writer._overwrite_streams["users"] = "`main`.`test`.`users__stg__abcdef12`" + writer._spark.catalog.tableExists.side_effect = RuntimeError("test error") + + with pytest.raises(RuntimeError): + writer.safe_overwrite_finish("users", "abcdef12") + + # Staging should be dropped on failure + drop_calls = [ + c for c in writer._spark.sql.call_args_list if "DROP TABLE" in str(c) + ] + assert len(drop_calls) >= 1 + + def test_writes_go_to_staging_during_overwrite(self, writer): + writer.safe_overwrite_begin("stream1", "run123") + + # Write should go to staging table + write_table = writer._get_write_table_name("stream1") + assert "__stg__" in write_table + + def test_writes_go_to_target_normally(self, writer): + write_table = writer._get_write_table_name("stream1") + assert "__stg__" not in write_table + + def test_schema_alignment_new_columns(self, writer): + """Staging has new columns -> target gets them via ALTER TABLE ADD.""" + writer._overwrite_streams["users"] = "`main`.`test`.`users__stg__abcdef12`" + writer._spark.catalog.tableExists.return_value = True + + target_field = MagicMock() + target_field.name = "id" + target_field.dataType = "StringType" + mock_target_df = MagicMock() + mock_target_df.schema.fields = [target_field] + + staging_field_id = MagicMock() + staging_field_id.name = "id" + staging_field_id.dataType = "StringType" + staging_field_new = MagicMock() + staging_field_new.name = "email" + staging_field_new.dataType = "StringType" + mock_staging_df = MagicMock() + mock_staging_df.schema.fields = [staging_field_id, staging_field_new] + + writer._spark.table.side_effect = lambda name: ( + mock_target_df + if "stg" not in name + else mock_staging_df + ) + + writer.safe_overwrite_finish("users", "abcdef12") + + alter_calls = [ + str(c) for c in writer._spark.sql.call_args_list if "ADD COLUMNS" in str(c) + ] + assert len(alter_calls) == 1 + assert "email" in alter_calls[0] + + def test_incompatible_type_change_raises(self, writer): + """Incompatible type changes should raise ValueError.""" + writer._overwrite_streams["users"] = "`main`.`test`.`users__stg__abcdef12`" + writer._spark.catalog.tableExists.return_value = True + + target_field = MagicMock() + target_field.name = "data" + target_field.dataType = "StructType" + mock_target_df = MagicMock() + mock_target_df.schema.fields = [target_field] + + staging_field = MagicMock() + staging_field.name = "data" + staging_field.dataType = "ArrayType" + mock_staging_df = MagicMock() + mock_staging_df.schema.fields = [staging_field] + + writer._spark.table.side_effect = lambda name: ( + mock_target_df + if "stg" not in name + else mock_staging_df + ) + + with pytest.raises(ValueError, match="Incompatible type change"): + writer.safe_overwrite_finish("users", "abcdef12") + + def test_safe_widening_handles_parenthesized_type_strings(self, writer): + writer._overwrite_streams["users"] = "`main`.`test`.`users__stg__abcdef12`" + writer._spark.catalog.tableExists.return_value = True + + target_field = MagicMock() + target_field.name = "id" + target_field.dataType = "LongType()" + mock_target_df = MagicMock() + mock_target_df.schema.fields = [target_field] + + staging_field = MagicMock() + staging_field.name = "id" + staging_field.dataType = "IntegerType()" + mock_staging_df = MagicMock() + mock_staging_df.schema.fields = [staging_field] + + writer._spark.table.side_effect = lambda name: ( + mock_target_df if "stg" not in name else mock_staging_df + ) + + writer.safe_overwrite_finish("users", "abcdef12") + + sql_calls = [str(c) for c in writer._spark.sql.call_args_list] + insert_calls = [c for c in sql_calls if "INSERT OVERWRITE" in c] + assert len(insert_calls) == 1 + assert "CAST(`id` AS BIGINT)" in insert_calls[0] + + def test_reverse_safe_widening_alters_target_to_sql_type(self, writer): + writer._overwrite_streams["users"] = "`main`.`test`.`users__stg__abcdef12`" + writer._spark.catalog.tableExists.return_value = True + + target_field = MagicMock() + target_field.name = "id" + target_field.dataType = "IntegerType()" + mock_target_df = MagicMock() + mock_target_df.schema.fields = [target_field] + + staging_field = MagicMock() + staging_field.name = "id" + staging_field.dataType = "LongType()" + mock_staging_df = MagicMock() + mock_staging_df.schema.fields = [staging_field] + + writer._spark.table.side_effect = lambda name: ( + mock_target_df if "stg" not in name else mock_staging_df + ) + + writer.safe_overwrite_finish("users", "abcdef12") + + sql_calls = [str(c) for c in writer._spark.sql.call_args_list] + alter_calls = [c for c in sql_calls if "ALTER COLUMN `id` TYPE" in c] + assert len(alter_calls) == 1 + assert "BIGINT" in alter_calls[0] diff --git a/tests/test_sanitize.py b/tests/test_sanitize.py new file mode 100644 index 0000000..6a03606 --- /dev/null +++ b/tests/test_sanitize.py @@ -0,0 +1,86 @@ +""" +Tests for stream name sanitization and SQL identifier validation. +""" +import pytest + +from brickbyte._sanitize import quoted_table_name, sanitize_stream_name, validate_identifier + + +class TestSanitizeStreamName: + def test_hyphen_to_underscore(self): + assert sanitize_stream_name("my-stream") == "my_stream" + + def test_dot_to_underscore(self): + assert sanitize_stream_name("my.stream") == "my_stream" + + def test_space_to_underscore(self): + assert sanitize_stream_name("my stream") == "my_stream" + + def test_mixed_separators(self): + assert sanitize_stream_name("my-stream.v2") == "my_stream_v2" + + def test_leading_digit_prefix(self): + assert sanitize_stream_name("123stream") == "_123stream" + + def test_valid_name_unchanged(self): + assert sanitize_stream_name("users") == "users" + + def test_uppercase_lowered(self): + assert sanitize_stream_name("MyStream") == "mystream" + + def test_collision_detection_in_client(self): + """Two streams that collide after sanitization should be caught.""" + # This would be detected in _client.py during sync + name1 = sanitize_stream_name("a-b") + name2 = sanitize_stream_name("a.b") + assert name1 == name2 == "a_b" + + def test_dangerous_chars_removed(self): + assert "`" not in sanitize_stream_name("stream`name") + assert ";" not in sanitize_stream_name("stream;name") + assert "\x00" not in sanitize_stream_name("stream\x00name") + + +class TestValidateIdentifier: + def test_valid_identifier(self): + assert validate_identifier("my_table") == "my_table" + + def test_empty_identifier_rejected(self): + with pytest.raises(ValueError, match="cannot be empty"): + validate_identifier("") + + def test_null_byte_rejected(self): + with pytest.raises(ValueError, match="unsafe character"): + validate_identifier("table\x00name") + + def test_backtick_rejected(self): + with pytest.raises(ValueError, match="unsafe character"): + validate_identifier("table`name") + + def test_semicolon_rejected(self): + with pytest.raises(ValueError, match="unsafe character"): + validate_identifier("table;name") + + def test_hyphen_allowed(self): + assert validate_identifier("my-table") == "my-table" + + def test_dot_allowed(self): + assert validate_identifier("my.table") == "my.table" + + def test_unicode_allowed(self): + assert validate_identifier("日本語テーブル") == "日本語テーブル" + + +class TestQuotedTableName: + def test_basic(self): + assert quoted_table_name("main", "bronze", "users") == "`main`.`bronze`.`users`" + + def test_with_hyphens(self): + assert ( + quoted_table_name("my-catalog", "my-schema", "my-table") + == "`my-catalog`.`my-schema`.`my-table`" + ) + + def test_rejects_dangerous_catalog(self): + with pytest.raises(ValueError): + quoted_table_name("main`", "bronze", "users") diff --git a/tests/test_spark_streaming_writer.py b/tests/test_spark_streaming_writer.py index c69b183..fdfa36d 100644 --- a/tests/test_spark_streaming_writer.py +++ b/tests/test_spark_streaming_writer.py @@ -1,8 +1,8 @@ """ Unit tests for SparkStreamingWriter. """ -from datetime import datetime -from unittest.mock import MagicMock, patch +from datetime import datetime, timezone +from unittest.mock import MagicMock import pytest @@ -10,84 +10,86 @@ class TestSparkStreamingWriter: - @pytest.fixture def writer(self): - """Create a SparkStreamingWriter with mocked Spark.""" writer = SparkStreamingWriter( catalog="main", schema="test", buffer_size_records=3, buffer_size_mb=1, + run_id="test-run-id", ) - # Mock Spark session writer._spark = MagicMock() return writer def test_init_defaults(self): - """Test default initialization values.""" writer = SparkStreamingWriter(catalog="main", schema="bronze") - assert writer.catalog == "main" assert writer.schema == "bronze" assert writer.buffer_size_records == 50000 - assert writer.buffer_size_bytes == 100 * 1024 * 1024 # 100MB + assert writer.buffer_size_bytes == 100 * 1024 * 1024 def test_get_table_name(self, writer): - """Test fully qualified table name generation.""" - assert writer.get_table_name("users") == "main.test.users" - assert writer.get_table_name("orders") == "main.test.orders" + assert writer.get_table_name("users") == "`main`.`test`.`users`" + assert writer.get_table_name("orders") == "`main`.`test`.`orders`" + + def test_transform_record_raw(self, writer): + record = {"id": 1, "name": "test"} + transformed = writer._transform_record("stream1", record) + + assert "record_id" in transformed + assert "extracted_at" in transformed + assert "data" in transformed + assert "run_id" in transformed + assert transformed["run_id"] == "test-run-id" - def test_transform_record(self, writer): - """Test that transform_record adds Airbyte metadata.""" + assert len(transformed["record_id"]) == 36 + assert isinstance(transformed["extracted_at"], datetime) + assert transformed["extracted_at"].tzinfo is not None + assert '"id": 1' in transformed["data"] + assert '"name": "test"' in transformed["data"] + + def test_transform_record_flatten(self): + writer = SparkStreamingWriter( + catalog="main", schema="test", flatten=True, run_id="test-run" + ) + writer._spark = MagicMock() record = {"id": 1, "name": "test"} - transformed = writer._transform_record(record) - - assert "_airbyte_raw_id" in transformed - assert "_airbyte_extracted_at" in transformed - assert "_airbyte_data" in transformed - - # Verify UUID format - assert len(transformed["_airbyte_raw_id"]) == 36 - - # Verify datetime - assert isinstance(transformed["_airbyte_extracted_at"], datetime) - - # Verify JSON serialization - assert '"id": 1' in transformed["_airbyte_data"] - assert '"name": "test"' in transformed["_airbyte_data"] + transformed = writer._transform_record("stream1", record) + + assert "_record_id" in transformed + assert "_extracted_at" in transformed + assert "_run_id" in transformed + assert transformed["id"] == 1 + assert transformed["name"] == "test" + assert "data" not in transformed def test_write_record_buffers(self, writer): - """Test that write_record buffers records correctly.""" writer.write_record("stream1", {"id": 1}) writer.write_record("stream1", {"id": 2}) - assert len(writer._buffers["stream1"]) == 2 assert writer._buffer_counts["stream1"] == 2 def test_write_record_flushes_at_threshold(self, writer): - """Test that write_record flushes when record threshold is hit.""" writer._write_micro_batch = MagicMock() - - # Write up to threshold (3 records) writer.write_record("stream1", {"id": 1}) writer.write_record("stream1", {"id": 2}) assert writer._write_micro_batch.call_count == 0 - - # Third record should trigger flush writer.write_record("stream1", {"id": 3}) assert writer._write_micro_batch.call_count == 1 def test_write_micro_batch(self, writer): - """Test micro-batch write to Delta via createDataFrame.""" - # Setup buffer writer._buffers["stream1"] = [ - {"_airbyte_raw_id": "1", "_airbyte_extracted_at": datetime.now(), "_airbyte_data": "{}"} + { + "record_id": "1", + "extracted_at": datetime.now(timezone.utc), + "data": "{}", + "run_id": "test", + } ] writer._buffer_counts["stream1"] = 1 writer._buffer_sizes["stream1"] = 100 - - # Mock Spark createDataFrame chain + mock_df = MagicMock() mock_write = MagicMock() mock_df.write = mock_write @@ -95,79 +97,58 @@ def test_write_micro_batch(self, writer): mock_write.mode.return_value = mock_write mock_write.option.return_value = mock_write writer._spark.createDataFrame.return_value = mock_df - + writer._write_micro_batch("stream1") - - # Verify createDataFrame was called + writer._spark.createDataFrame.assert_called_once() - - # Verify write chain mock_write.format.assert_called_with("delta") mock_write.mode.assert_called_with("append") - mock_write.saveAsTable.assert_called_with("main.test.stream1") - - # Verify buffer was reset + mock_write.saveAsTable.assert_called_with("`main`.`test`.`stream1`") + assert writer._buffers["stream1"] == [] assert writer._buffer_counts["stream1"] == 0 assert writer._buffer_sizes["stream1"] == 0 def test_flush_stream_calls_write_micro_batch(self, writer): - """Test that flush_stream delegates to _write_micro_batch.""" writer._write_micro_batch = MagicMock() writer._buffers["stream1"] = [{"id": 1}] - writer.flush_stream("stream1") - writer._write_micro_batch.assert_called_once_with("stream1") def test_close_flushes_all_streams(self, writer): - """Test that close flushes all buffered streams.""" writer.flush_stream = MagicMock() writer._buffers["stream1"] = [{"id": 1}] writer._buffers["stream2"] = [{"id": 2}] - writer.close() - assert writer.flush_stream.call_count == 2 def test_drop_table(self, writer): - """Test drop_table executes correct SQL.""" writer.drop_table("users") - - writer._spark.sql.assert_called_with("DROP TABLE IF EXISTS main.test.users") + writer._spark.sql.assert_called_with( + "DROP TABLE IF EXISTS `main`.`test`.`users`" + ) def test_table_exists(self, writer): - """Test table_exists check.""" writer._spark.catalog.tableExists.return_value = True assert writer.table_exists("users") is True - writer._spark.catalog.tableExists.return_value = False assert writer.table_exists("orders") is False def test_get_table_schema(self, writer): - """Test getting table schema.""" - # Mock schema mock_field1 = MagicMock() mock_field1.name = "id" mock_field1.dataType = "LongType" - mock_field2 = MagicMock() mock_field2.name = "name" mock_field2.dataType = "StringType" - mock_df = MagicMock() mock_df.schema.fields = [mock_field1, mock_field2] writer._spark.table.return_value = mock_df writer._spark.catalog.tableExists.return_value = True - schema = writer.get_table_schema("users") - assert schema == {"id": "LongType", "name": "StringType"} def test_transform_record_handles_datetime(self, writer): - """Test that datetime objects in records are serialized.""" record = {"id": 1, "created_at": datetime(2024, 1, 1, 12, 0, 0)} - transformed = writer._transform_record(record) - - # Should not raise, datetime converted to string via default=str - assert "2024-01-01" in transformed["_airbyte_data"] + transformed = writer._transform_record("stream1", record) + assert "2024-01-01" in transformed["data"] diff --git a/tests/test_streaming.py b/tests/test_streaming.py index 73ae4ff..5e18c35 100644 --- a/tests/test_streaming.py +++ b/tests/test_streaming.py @@ -1,18 +1,17 @@ """ -Unit tests for StreamingWriter. +Unit tests for SQLStreamingWriter. """ from unittest.mock import MagicMock, patch import pytest -from brickbyte.writers import SQLStreamingWriter +from brickbyte.writers.sql_streaming_writer import SQLStreamingWriter class TestStreamingWriter: - @pytest.fixture def writer(self): - with patch("databricks.sql.connect") as mock_connect: + with patch("os.path.exists", return_value=True): writer = SQLStreamingWriter( catalog="main", schema="test", @@ -20,56 +19,68 @@ def writer(self): server_hostname="test-host", http_path="/sql", access_token="token", - buffer_size_records=2 + buffer_size_records=2, + run_id="test-run-id", ) - writer._connection = mock_connect.return_value + writer._connection = MagicMock() return writer def test_init_validation(self): - """Test validation of staging volume format.""" with pytest.raises(ValueError): SQLStreamingWriter( catalog="main", schema="test", - staging_volume="invalid_format", - server_hostname="h", - http_path="p", - access_token="t" + staging_volume="invalid_format", + server_hostname="h", + http_path="p", + access_token="t", ) @patch("pyarrow.parquet.write_table") + @patch("os.path.exists", return_value=True) @patch("os.remove") @patch("os.makedirs") - def test_flush_logic(self, mock_makedirs, mock_remove, mock_pq_write, writer): - """Test that data is flushed correctly when threshold is met.""" - # Mock execution + def test_flush_logic(self, mock_makedirs, mock_remove, mock_exists, mock_pq_write, writer): writer._execute = MagicMock() - - # Write records + writer.write_record("stream1", {"id": 1}) assert len(writer._buffers["stream1"]) == 1 - assert writer._execute.call_count == 0 # No flush yet - - # Write second record (hits threshold=2) + assert writer._execute.call_count == 0 + writer.write_record("stream1", {"id": 2}) - - # Should have flushed assert len(writer._buffers["stream1"]) == 0 - assert writer._execute.call_count == 2 # CREATE + COPY INTO + assert writer._execute.call_count == 4 # CREATE + PUT + COPY INTO + REMOVE mock_pq_write.assert_called_once() mock_remove.assert_called_once() - - # Verify COPY INTO query - copy_call = writer._execute.call_args_list[1] + + put_call = writer._execute.call_args_list[1] + assert "PUT" in put_call[0][0] + + copy_call = writer._execute.call_args_list[2] query = copy_call[0][0] assert "COPY INTO" in query - assert "main.test.stream1" in query + assert "force" not in query.lower() + + remove_call = writer._execute.call_args_list[3] + assert "REMOVE" in remove_call[0][0] + + @patch("pyarrow.parquet.write_table") + @patch("os.remove") + @patch("os.makedirs") + def test_deterministic_filenames(self, mock_makedirs, mock_remove, mock_pq_write, writer): + writer._execute = MagicMock() + + writer.write_record("stream1", {"id": 1}) + writer.write_record("stream1", {"id": 2}) + + # Check filename contains run_id and batch index + pq_call_args = mock_pq_write.call_args + file_path = pq_call_args[0][1] + assert "test-run-id" in file_path + assert "000000" in file_path def test_close_flushes_remaining(self, writer): - """Test that close flushes remaining records.""" writer.flush_stream = MagicMock() writer._buffers["s1"] = [{"id": 1}] - writer.close() - writer.flush_stream.assert_called_with("s1") diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..150b497 --- /dev/null +++ b/uv.lock @@ -0,0 +1,3380 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <3.13" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[[package]] +name = "airbyte" +version = "0.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "airbyte-api" }, + { name = "airbyte-cdk" }, + { name = "airbyte-protocol-models-pdv2" }, + { name = "click" }, + { name = "cryptography" }, + { name = "duckdb" }, + { name = "duckdb-engine" }, + { name = "fastmcp" }, + { name = "fastmcp-extensions" }, + { name = "google-auth" }, + { name = "google-cloud-bigquery" }, + { name = "google-cloud-bigquery-storage" }, + { name = "google-cloud-secret-manager" }, + { name = "jsonschema" }, + { name = "orjson" }, + { name = "overrides" }, + { name = "pandas" }, + { name = "psycopg", extra = ["binary", "pool"] }, + { name = "psycopg2-binary" }, + { name = "pyarrow" }, + { name = "pydantic" }, + { name = "pydantic-core" }, + { name = "python-dotenv" }, + { name = "python-ulid" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "rich" }, + { name = "snowflake-connector-python" }, + { name = "snowflake-sqlalchemy" }, + { name = "sqlalchemy" }, + { name = "sqlalchemy-bigquery" }, + { name = "structlog" }, + { name = "typing-extensions" }, + { name = "uuid7" }, + { name = "uv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/53/2f4f4d2af191d2a8a37416c9bdf516dd6a4e872b3dcc03c00da5e1641644/airbyte-0.38.0.tar.gz", hash = "sha256:bbe3f266cc1ea0149e8b57d42a82717abf836014923192c0773a86c0cdf4e56f", size = 466537, upload-time = "2026-02-07T06:34:44.021Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/1f/8307f17ce24bc4cfa93d3ef48455eae95f63716b36f2c441d0269ebff16d/airbyte-0.38.0-py3-none-any.whl", hash = "sha256:d06acc70feaa13e080be3992b705dc7723bbe6bcdcf3bf1c351d3d69f96bebda", size = 268108, upload-time = "2026-02-07T06:34:41.903Z" }, +] + +[[package]] +name = "airbyte-api" +version = "0.53.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "dataclasses-json" }, + { name = "idna" }, + { name = "jsonpath-python" }, + { name = "marshmallow" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "six" }, + { name = "typing-extensions" }, + { name = "typing-inspect" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/23/8debf9c4ca8652c9ceb60b8074e654191ba2053001616151993b39d6cdcb/airbyte-api-0.53.0.tar.gz", hash = "sha256:f054ed170f9a691c3304e93a5212670fd2a38e5debb667c72d5ef8eb89cf7e9d", size = 330090, upload-time = "2025-10-02T19:55:43.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/f5/ca44b6f3919f049a4441755958c042fcea09a44e876352a6005f5e5a9e46/airbyte_api-0.53.0-py3-none-any.whl", hash = "sha256:0bd86d5122789a7c97115a4b52ce492b013d0ea8eeab597b6495b0b310332f28", size = 773825, upload-time = "2025-10-02T19:55:42.043Z" }, +] + +[[package]] +name = "airbyte-cdk" +version = "7.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "airbyte-protocol-models-dataclasses" }, + { name = "anyascii" }, + { name = "backoff" }, + { name = "boltons" }, + { name = "cachetools" }, + { name = "click" }, + { name = "cryptography" }, + { name = "dateparser" }, + { name = "dpath" }, + { name = "dunamai" }, + { name = "genson" }, + { name = "google-cloud-secret-manager" }, + { name = "isodate" }, + { name = "jinja2" }, + { name = "jsonref" }, + { name = "jsonschema" }, + { name = "nltk" }, + { name = "orjson" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pydantic" }, + { name = "pyjwt" }, + { name = "pyrate-limiter" }, + { name = "python-dateutil" }, + { name = "python-ulid" }, + { name = "pytz" }, + { name = "pyyaml" }, + { name = "rapidfuzz" }, + { name = "referencing" }, + { name = "requests" }, + { name = "requests-cache" }, + { name = "rich" }, + { name = "rich-click" }, + { name = "serpyco-rs" }, + { name = "setuptools" }, + { name = "typing-extensions" }, + { name = "unidecode" }, + { name = "wcmatch" }, + { name = "whenever" }, + { name = "xmltodict" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/77/7b0bcb7103b39934d519a13966f8b1fc68344bc01b55e158b210a5d7faa3/airbyte_cdk-7.10.1.tar.gz", hash = "sha256:0918b1bd0de6b167a2e02ef5d87c5bb0fe6f2f319667dbb1083a8f54a0c9cafb", size = 547281, upload-time = "2026-02-17T10:30:40.927Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/e2/e335bfb2a8e3b4379f5606d7307f50cfa7b2e62f0e3885cf81a98146ab52/airbyte_cdk-7.10.1-py3-none-any.whl", hash = "sha256:b11d796a54900d52cc32d1340da3f98736bbf6401ea2eff3743173883c93d9a7", size = 776298, upload-time = "2026-02-17T10:30:39.285Z" }, +] + +[[package]] +name = "airbyte-protocol-models-dataclasses" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/2b/c372db5dcab8a1602e7ca1affaff94cdfc0874d1c3ff70745a1e0a97eaa2/airbyte_protocol_models_dataclasses-0.17.1.tar.gz", hash = "sha256:cbccfdf84fabd0b6e325cc57fa0682ae9d386fce8fcb5943faa5df2b7e599919", size = 6558, upload-time = "2025-06-17T16:16:37.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/33/4316412499f5678bd0cfe46dc8cceb54335de97965126b35d8d8f9f795da/airbyte_protocol_models_dataclasses-0.17.1-py3-none-any.whl", hash = "sha256:ef83ac56de6208afe0a21ce05bcfbcfc98b98300a76fb3cdf4db2e7f720f1df0", size = 7121, upload-time = "2025-06-17T16:16:36.194Z" }, +] + +[[package]] +name = "airbyte-protocol-models-pdv2" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/f8/9846ce65f9fa9b363f7dde7003e03aaaa69b5b3c37e599e7f35018b96696/airbyte_protocol_models_pdv2-0.18.0.tar.gz", hash = "sha256:8ff9f2685f4ca77571db380419c37db5ba61b82d834758782fa817c2b437c68f", size = 13900, upload-time = "2025-06-27T21:01:36.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/24/414fe44348ca7c7ae71030012e77302b3e41d4ee217cc5931dabfbf90496/airbyte_protocol_models_pdv2-0.18.0-py3-none-any.whl", hash = "sha256:62b08659b3da0f03316c60d84736f12dbfd79340a49947155a1687c13314db8b", size = 12805, upload-time = "2025-06-27T21:01:35.637Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyascii" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/ba/edebda727008390936da4a9bf677c19cd63b32d51e864656d2cbd1028e25/anyascii-0.3.3.tar.gz", hash = "sha256:c94e9dd9d47b3d9494eca305fef9447d00b4bf1a32aff85aa746fa3ec7fb95c3", size = 264680, upload-time = "2025-06-29T03:33:30.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/76/783b75a21ce3563b8709050de030ae253853b147bd52e141edc1025aa268/anyascii-0.3.3-py3-none-any.whl", hash = "sha256:f5ab5e53c8781a36b5a40e1296a0eeda2f48c649ef10c3921c1381b1d00dee7a", size = 345090, upload-time = "2025-06-29T03:33:28.356Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "asn1crypto" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", size = 121080, upload-time = "2022-03-15T14:46:52.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045, upload-time = "2022-03-15T14:46:51.055Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attributes-doc" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/8b/bcfe09838dfc88474c29aeca781860938b3909e6df47a3d0d70e97ad79c2/attributes-doc-0.4.0.tar.gz", hash = "sha256:b1576c94a714e9fc2c65c47cf10d0c8e1a5f7c4f5ae7f69006be108d95cbfbfb", size = 5242, upload-time = "2024-01-09T10:34:42.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/97/c2ca0e0e4c2de15996b5f738964d7ca0b9c8d6e116edb307ad30d5e56a59/attributes_doc-0.4.0-py2.py3-none-any.whl", hash = "sha256:4c3007d9e58f3a6cb4b9c614c4d4ce2d92161581f28e594ddd8241cc3a113bdd", size = 4598, upload-time = "2024-01-09T10:34:41.492Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +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/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]] +name = "authlib" +version = "1.6.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" }, +] + +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + +[[package]] +name = "boltons" +version = "25.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/54/71a94d8e02da9a865587fb3fff100cb0fc7aa9f4d5ed9ed3a591216ddcc7/boltons-25.0.0.tar.gz", hash = "sha256:e110fbdc30b7b9868cb604e3f71d4722dd8f4dcb4a5ddd06028ba8f1ab0b5ace", size = 246294, upload-time = "2025-02-03T05:57:59.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/7f/0e961cf3908bc4c1c3e027de2794f867c6c89fb4916fc7dba295a0e80a2d/boltons-25.0.0-py3-none-any.whl", hash = "sha256:dc9fb38bf28985715497d1b54d00b62ea866eca3938938ea9043e254a3a6ca62", size = 194210, upload-time = "2025-02-03T05:57:56.705Z" }, +] + +[[package]] +name = "boto3" +version = "1.42.59" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/4e/499cb52aaee9468c346bcc1158965e24e72b4e2a20052725b680e0ac949b/boto3-1.42.59.tar.gz", hash = "sha256:6c4a14a4eb37b58a9048901bdeefbe1c529638b73e8f55413319a25f010ca211", size = 112725, upload-time = "2026-02-27T20:25:33.228Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/c0/22d868b9408dc5a33935a72896ec8d638b2766c459668d1b37c3e5ac2066/boto3-1.42.59-py3-none-any.whl", hash = "sha256:7a66e3e8e2087ea4403e135e9de592e6d63fc9a91080d8dac415bb74df873a72", size = 140557, upload-time = "2026-02-27T20:25:31.774Z" }, +] + +[[package]] +name = "botocore" +version = "1.42.59" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/ae/50fb33bdf1911c216d50f98d989dd032a506f054cf829ebd737c6fa7e3e6/botocore-1.42.59.tar.gz", hash = "sha256:5314f19e1da8fc0ebc41bdb8bbe17c9a7397d87f4d887076ac8bdef972a34138", size = 14950271, upload-time = "2026-02-27T20:25:20.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/df/9d52819e0d804ead073d53ab1823bc0f0cb172a250fba31107b0b43fbb04/botocore-1.42.59-py3-none-any.whl", hash = "sha256:d2f2ff7ecc31e86ef46b5daee112cfbca052c13801285fb23af909f7bff5b657", size = 14619293, upload-time = "2026-02-27T20:25:17.455Z" }, +] + +[[package]] +name = "bracex" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, +] + +[[package]] +name = "brickbyte" +version = "0.1.0rc1" +source = { editable = "." } +dependencies = [ + { name = "airbyte" }, + { name = "databricks-sdk" }, + { name = "databricks-sql-connector" }, + { name = "pyarrow" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] + +[package.optional-dependencies] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] +local-spark = [ + { name = "delta-spark" }, + { name = "pyspark" }, +] +progress = [ + { name = "tqdm" }, +] + +[package.metadata] +requires-dist = [ + { name = "airbyte", specifier = "==0.38.0" }, + { name = "databricks-sdk", specifier = "==0.95.0" }, + { name = "databricks-sql-connector", specifier = "==4.2.5" }, + { name = "delta-spark", marker = "extra == 'local-spark'", specifier = ">=3.2.0" }, + { name = "pyarrow", specifier = "==21.0.0" }, + { name = "pyspark", marker = "extra == 'local-spark'", specifier = ">=3.5.0" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "pyyaml", specifier = "==6.0.3" }, + { name = "ruff", marker = "extra == 'dev'", specifier = "==0.6.4" }, + { name = "tqdm", marker = "extra == 'progress'", specifier = "==4.67.3" }, + { name = "virtualenv", specifier = "==20.29.3" }, +] +provides-extras = ["dev", "progress", "local-spark"] + +[[package]] +name = "cachetools" +version = "7.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/c7/342b33cc6877eebc6c9bb45cb9f78e170e575839699f6f3cc96050176431/cachetools-7.0.2.tar.gz", hash = "sha256:7e7f09a4ca8b791d8bb4864afc71e9c17e607a28e6839ca1a644253c97dbeae0", size = 36983, upload-time = "2026-03-02T19:45:16.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/04/4b6968e77c110f12da96fdbfcb39c6557c2e5e81bd7afcf8ed893d5bc588/cachetools-7.0.2-py3-none-any.whl", hash = "sha256:938dcad184827c5e94928c4fd5526e2b46692b7fb1ae94472da9131d0299343c", size = 13793, upload-time = "2026-03-02T19:45:15.495Z" }, +] + +[[package]] +name = "cattrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/ec/ba18945e7d6e55a58364d9fb2e46049c1c2998b3d805f19b703f14e81057/cattrs-26.1.0.tar.gz", hash = "sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40", size = 495672, upload-time = "2026-02-18T22:15:19.406Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/56/60547f7801b97c67e97491dc3d9ade9fbccbd0325058fd3dfcb2f5d98d90/cattrs-26.1.0-py3-none-any.whl", hash = "sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096", size = 73054, upload-time = "2026-02-18T22:15:17.958Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +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" }, +] + +[[package]] +name = "charset-normalizer" +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.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +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/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]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "croniter" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/2f/44d1ae153a0e27be56be43465e5cb39b9650c781e001e7864389deb25090/croniter-6.0.0.tar.gz", hash = "sha256:37c504b313956114a983ece2c2b07790b1f1094fe9d81cc94739214748255577", size = 64481, upload-time = "2024-12-17T17:17:47.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/4b/290b4c3efd6417a8b0c284896de19b1d5855e6dbdb97d2a35e68fa42de85/croniter-6.0.0-py2.py3-none-any.whl", hash = "sha256:2f878c3856f17896979b2a4379ba1f09c83e374931ea15cc835c5dd2eee9b368", size = 25468, upload-time = "2024-12-17T17:17:45.359Z" }, +] + +[[package]] +name = "cryptography" +version = "44.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/d6/1411ab4d6108ab167d06254c5be517681f1e331f90edf1379895bcb87020/cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053", size = 711096, upload-time = "2025-05-02T19:36:04.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/53/c776d80e9d26441bb3868457909b4e74dd9ccabd182e10b2b0ae7a07e265/cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88", size = 6670281, upload-time = "2025-05-02T19:34:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/6a/06/af2cf8d56ef87c77319e9086601bef621bedf40f6f59069e1b6d1ec498c5/cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137", size = 3959305, upload-time = "2025-05-02T19:34:53.042Z" }, + { url = "https://files.pythonhosted.org/packages/ae/01/80de3bec64627207d030f47bf3536889efee8913cd363e78ca9a09b13c8e/cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c", size = 4171040, upload-time = "2025-05-02T19:34:54.675Z" }, + { url = "https://files.pythonhosted.org/packages/bd/48/bb16b7541d207a19d9ae8b541c70037a05e473ddc72ccb1386524d4f023c/cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76", size = 3963411, upload-time = "2025-05-02T19:34:56.61Z" }, + { url = "https://files.pythonhosted.org/packages/42/b2/7d31f2af5591d217d71d37d044ef5412945a8a8e98d5a2a8ae4fd9cd4489/cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359", size = 3689263, upload-time = "2025-05-02T19:34:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/25/50/c0dfb9d87ae88ccc01aad8eb93e23cfbcea6a6a106a9b63a7b14c1f93c75/cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43", size = 4196198, upload-time = "2025-05-02T19:35:00.988Z" }, + { url = "https://files.pythonhosted.org/packages/66/c9/55c6b8794a74da652690c898cb43906310a3e4e4f6ee0b5f8b3b3e70c441/cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01", size = 3966502, upload-time = "2025-05-02T19:35:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f7/7cb5488c682ca59a02a32ec5f975074084db4c983f849d47b7b67cc8697a/cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d", size = 4196173, upload-time = "2025-05-02T19:35:05.018Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0b/2f789a8403ae089b0b121f8f54f4a3e5228df756e2146efdf4a09a3d5083/cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904", size = 4087713, upload-time = "2025-05-02T19:35:07.187Z" }, + { url = "https://files.pythonhosted.org/packages/1d/aa/330c13655f1af398fc154089295cf259252f0ba5df93b4bc9d9c7d7f843e/cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44", size = 4299064, upload-time = "2025-05-02T19:35:08.879Z" }, + { url = "https://files.pythonhosted.org/packages/10/a8/8c540a421b44fd267a7d58a1fd5f072a552d72204a3f08194f98889de76d/cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d", size = 2773887, upload-time = "2025-05-02T19:35:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0d/c4b1657c39ead18d76bbd122da86bd95bdc4095413460d09544000a17d56/cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d", size = 3209737, upload-time = "2025-05-02T19:35:12.12Z" }, + { url = "https://files.pythonhosted.org/packages/34/a3/ad08e0bcc34ad436013458d7528e83ac29910943cea42ad7dd4141a27bbb/cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f", size = 6673501, upload-time = "2025-05-02T19:35:13.775Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f0/7491d44bba8d28b464a5bc8cc709f25a51e3eac54c0a4444cf2473a57c37/cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759", size = 3960307, upload-time = "2025-05-02T19:35:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/e5c5d0e1364d3346a5747cdcd7ecbb23ca87e6dea4f942a44e88be349f06/cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645", size = 4170876, upload-time = "2025-05-02T19:35:18.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/96/025cb26fc351d8c7d3a1c44e20cf9a01e9f7cf740353c9c7a17072e4b264/cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2", size = 3964127, upload-time = "2025-05-02T19:35:19.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/44/eb6522db7d9f84e8833ba3bf63313f8e257729cf3a8917379473fcfd6601/cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54", size = 3689164, upload-time = "2025-05-02T19:35:21.449Z" }, + { url = "https://files.pythonhosted.org/packages/68/fb/d61a4defd0d6cee20b1b8a1ea8f5e25007e26aeb413ca53835f0cae2bcd1/cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93", size = 4198081, upload-time = "2025-05-02T19:35:23.187Z" }, + { url = "https://files.pythonhosted.org/packages/1b/50/457f6911d36432a8811c3ab8bd5a6090e8d18ce655c22820994913dd06ea/cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c", size = 3967716, upload-time = "2025-05-02T19:35:25.426Z" }, + { url = "https://files.pythonhosted.org/packages/35/6e/dca39d553075980ccb631955c47b93d87d27f3596da8d48b1ae81463d915/cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f", size = 4197398, upload-time = "2025-05-02T19:35:27.678Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9d/d1f2fe681eabc682067c66a74addd46c887ebacf39038ba01f8860338d3d/cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5", size = 4087900, upload-time = "2025-05-02T19:35:29.312Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f5/3599e48c5464580b73b236aafb20973b953cd2e7b44c7c2533de1d888446/cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b", size = 4301067, upload-time = "2025-05-02T19:35:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/d2c48c8137eb39d0c193274db5c04a75dab20d2f7c3f81a7dcc3a8897701/cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028", size = 2775467, upload-time = "2025-05-02T19:35:33.805Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ad/51f212198681ea7b0deaaf8846ee10af99fba4e894f67b353524eab2bbe5/cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334", size = 3210375, upload-time = "2025-05-02T19:35:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/7f/10/abcf7418536df1eaba70e2cfc5c8a0ab07aa7aa02a5cbc6a78b9d8b4f121/cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d", size = 3393192, upload-time = "2025-05-02T19:35:37.468Z" }, + { url = "https://files.pythonhosted.org/packages/06/59/ecb3ef380f5891978f92a7f9120e2852b1df6f0a849c277b8ea45b865db2/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8", size = 3898419, upload-time = "2025-05-02T19:35:39.065Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d0/35e2313dbb38cf793aa242182ad5bc5ef5c8fd4e5dbdc380b936c7d51169/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4", size = 4117892, upload-time = "2025-05-02T19:35:40.839Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c8/31fb6e33b56c2c2100d76de3fd820afaa9d4d0b6aea1ccaf9aaf35dc7ce3/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff", size = 3900855, upload-time = "2025-05-02T19:35:42.599Z" }, + { url = "https://files.pythonhosted.org/packages/43/2a/08cc2ec19e77f2a3cfa2337b429676406d4bb78ddd130a05c458e7b91d73/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06", size = 4117619, upload-time = "2025-05-02T19:35:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/fc3d3f84022a75f2ac4b1a1c0e5d6a0c2ea259e14cd4aae3e0e68e56483c/cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9", size = 3136570, upload-time = "2025-05-02T19:35:46.94Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4b/c11ad0b6c061902de5223892d680e89c06c7c4d606305eb8de56c5427ae6/cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375", size = 3390230, upload-time = "2025-05-02T19:35:49.062Z" }, + { url = "https://files.pythonhosted.org/packages/58/11/0a6bf45d53b9b2290ea3cec30e78b78e6ca29dc101e2e296872a0ffe1335/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647", size = 3895216, upload-time = "2025-05-02T19:35:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/0a/27/b28cdeb7270e957f0077a2c2bfad1b38f72f1f6d699679f97b816ca33642/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259", size = 4115044, upload-time = "2025-05-02T19:35:53.044Z" }, + { url = "https://files.pythonhosted.org/packages/35/b0/ec4082d3793f03cb248881fecefc26015813199b88f33e3e990a43f79835/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff", size = 3898034, upload-time = "2025-05-02T19:35:54.72Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7f/adf62e0b8e8d04d50c9a91282a57628c00c54d4ae75e2b02a223bd1f2613/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5", size = 4114449, upload-time = "2025-05-02T19:35:57.139Z" }, + { url = "https://files.pythonhosted.org/packages/87/62/d69eb4a8ee231f4bf733a92caf9da13f1c81a44e874b1d4080c25ecbb723/cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c", size = 3134369, upload-time = "2025-05-02T19:35:58.907Z" }, +] + +[[package]] +name = "cyclopts" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser" }, + { name = "rich" }, + { name = "rich-rst" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/5c/88a4068c660a096bbe87efc5b7c190080c9e86919c36ec5f092cb08d852f/cyclopts-4.6.0.tar.gz", hash = "sha256:483c4704b953ea6da742e8de15972f405d2e748d19a848a4d61595e8e5360ee5", size = 162724, upload-time = "2026-02-23T15:44:49.286Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/eb/1e8337755a70dc7d7ff10a73dc8f20e9352c9ad6c2256ed863ac95cd3539/cyclopts-4.6.0-py3-none-any.whl", hash = "sha256:0a891cb55bfd79a3cdce024db8987b33316aba11071e5258c21ac12a640ba9f2", size = 200518, upload-time = "2026-02-23T15:44:47.854Z" }, +] + +[[package]] +name = "databricks-sdk" +version = "0.95.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/0b/f76daeb62f3f9b47eedb0e90b7f04f9d401c08bb2dfdb7f7804ac4ab7cdb/databricks_sdk-0.95.0.tar.gz", hash = "sha256:c958a2c662aebcac2ffc4a4b09926719ff4665ce02128e9d7c55dcb7bfa104ca", size = 864224, upload-time = "2026-03-02T08:15:46.525Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/55/29f4dce7e7d06b5d37c8d90561c81d55b6a0083ce1ae7c2ef1ae886d9931/databricks_sdk-0.95.0-py3-none-any.whl", hash = "sha256:bb2a851a7f58475b57da732e2e9aaea0a395cd6993cab97c3d14599711fb6e1e", size = 813458, upload-time = "2026-03-02T08:15:44.783Z" }, +] + +[[package]] +name = "databricks-sql-connector" +version = "4.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lz4" }, + { name = "oauthlib" }, + { name = "openpyxl" }, + { name = "pandas" }, + { name = "pybreaker" }, + { name = "pyjwt" }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "thrift" }, + { name = "urllib3" }, +] +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/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]] +name = "dataclasses-json" +version = "0.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow" }, + { name = "typing-inspect" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, +] + +[[package]] +name = "dateparser" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "regex" }, + { name = "tzlocal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/668dfb8c073a5dde3efb80fa382de1502e3b14002fd386a8c1b0b49e92a9/dateparser-1.3.0.tar.gz", hash = "sha256:5bccf5d1ec6785e5be71cc7ec80f014575a09b4923e762f850e57443bddbf1a5", size = 337152, upload-time = "2026-02-04T16:00:06.162Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/c7/95349670e193b2891176e1b8e5f43e12b31bff6d9994f70e74ab385047f6/dateparser-1.3.0-py3-none-any.whl", hash = "sha256:8dc678b0a526e103379f02ae44337d424bd366aac727d3c6cf52ce1b01efbb5a", size = 318688, upload-time = "2026-02-04T16:00:04.652Z" }, +] + +[[package]] +name = "delta-spark" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "pyspark" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/09/d394015eb956c4475f6a949fb5fccedf7af19f97e981acbc81629e868a5e/delta_spark-4.1.0.tar.gz", hash = "sha256:98f73c2744f972919e0472974467f85d157810b617341ebf586374d91b8eadc7", size = 36808, upload-time = "2026-02-20T18:37:59.8Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/03/2e440efd4a49c8ecfbcb665dea7db94583cf33a365d8480e47fb0aa0dc39/delta_spark-4.1.0-py3-none-any.whl", hash = "sha256:d80f6ebca542df48f257f2535f9c8c21bbfda65771b6ec21be843c0087e1dece", size = 43877, upload-time = "2026-02-20T18:37:58.15Z" }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +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/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]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "dpath" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/ce/e1fd64d36e4a5717bd5e6b2ad188f5eaa2e902fde871ea73a79875793fc9/dpath-2.2.0.tar.gz", hash = "sha256:34f7e630dc55ea3f219e555726f5da4b4b25f2200319c8e6902c394258dd6a3e", size = 28266, upload-time = "2024-06-12T22:08:03.686Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/d1/8952806fbf9583004ab479d8f58a9496c3d35f6b6009ddd458bdd9978eaf/dpath-2.2.0-py3-none-any.whl", hash = "sha256:b330a375ded0a0d2ed404440f6c6a715deae5313af40bbb01c8a41d891900576", size = 17618, upload-time = "2024-06-12T22:08:01.881Z" }, +] + +[[package]] +name = "duckdb" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/da/17c3eb5458af69d54dedc8d18e4a32ceaa8ce4d4c699d45d6d8287e790c3/duckdb-1.4.3.tar.gz", hash = "sha256:fea43e03604c713e25a25211ada87d30cd2a044d8f27afab5deba26ac49e5268", size = 18478418, upload-time = "2025-12-09T10:59:22.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/3a/ea8e237e1ba40203dea4ed6a8798ea51e66a4c4f34605697025e5fa06fdd/duckdb-1.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:efa7f1191c59e34b688fcd4e588c1b903a4e4e1f4804945902cf0b20e08a9001", size = 29016021, upload-time = "2025-12-09T10:57:46.847Z" }, + { url = "https://files.pythonhosted.org/packages/48/88/07615298a2871362b454237b6a2d7724e6ba0afba2bddedddde5bbf129d5/duckdb-1.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4fef6a053a1c485292000bf0c338bba60f89d334f6a06fc76ba4085a5a322b76", size = 15405906, upload-time = "2025-12-09T10:57:49.213Z" }, + { url = "https://files.pythonhosted.org/packages/fa/66/b407ab3cd4822191aa5defb27522213b6ba670437c7da09a062d8b75b0a4/duckdb-1.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:702dabbc22b27dc5b73e7599c60deef3d8c59968527c36b391773efddd8f4cf1", size = 13732991, upload-time = "2025-12-09T10:57:51.189Z" }, + { url = "https://files.pythonhosted.org/packages/33/f0/e8edab80446d87b4e0faf3aaa440f9cfd9d0609c21a4be56174c8ba7d23c/duckdb-1.4.3-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854b79375fa618f6ffa8d84fb45cbc9db887f6c4834076ea10d20bc106f1fd90", size = 18471503, upload-time = "2025-12-09T10:57:53.186Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7a/8d257bc847f0ac6a6639ae0a6e7f35f0b5bfbae472ee4846ee32404670a6/duckdb-1.4.3-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bb8bd5a3dd205983726185b280a211eacc9f5bc0c4d4505bec8c87ac33a8ccb", size = 20466012, upload-time = "2025-12-09T10:57:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d1/8f6bdaf2da6a076dd63c84ed87fb82d0741c9f4acb3dd476d73ca0a08ffe/duckdb-1.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:d0ff08388ef8b1d1a4c95c321d6c5fa11201b241036b1ee740f9d841df3d6ba2", size = 12328392, upload-time = "2025-12-09T10:57:57.718Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bc/7c5e50e440c8629495678bc57bdfc1bb8e62f61090f2d5441e2bd0a0ed96/duckdb-1.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:366bf607088053dce845c9d24c202c04d78022436cc5d8e4c9f0492de04afbe7", size = 29019361, upload-time = "2025-12-09T10:57:59.845Z" }, + { url = "https://files.pythonhosted.org/packages/26/15/c04a4faf0dfddad2259cab72bf0bd4b3d010f2347642541bd254d516bf93/duckdb-1.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d080e8d1bf2d226423ec781f539c8f6b6ef3fd42a9a58a7160de0a00877a21f", size = 15407465, upload-time = "2025-12-09T10:58:02.465Z" }, + { url = "https://files.pythonhosted.org/packages/cb/54/a049490187c9529932fc153f7e1b92a9e145586281fe4e03ce0535a0497c/duckdb-1.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9dc049ba7e906cb49ca2b6d4fbf7b6615ec3883193e8abb93f0bef2652e42dda", size = 13735781, upload-time = "2025-12-09T10:58:04.847Z" }, + { url = "https://files.pythonhosted.org/packages/14/b7/ee594dcecbc9469ec3cd1fb1f81cb5fa289ab444b80cfb5640c8f467f75f/duckdb-1.4.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b30245375ea94ab528c87c61fc3ab3e36331180b16af92ee3a37b810a745d24", size = 18470729, upload-time = "2025-12-09T10:58:07.116Z" }, + { url = "https://files.pythonhosted.org/packages/df/5f/a6c1862ed8a96d8d930feb6af5e55aadd983310aab75142468c2cb32a2a3/duckdb-1.4.3-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7c864df027da1ee95f0c32def67e15d02cd4a906c9c1cbae82c09c5112f526b", size = 20471399, upload-time = "2025-12-09T10:58:09.714Z" }, + { url = "https://files.pythonhosted.org/packages/5b/80/c05c0b6a6107b618927b7dcabe3bba6a7eecd951f25c9dbcd9c1f9577cc8/duckdb-1.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:813f189039b46877b5517f1909c7b94a8fe01b4bde2640ab217537ea0fe9b59b", size = 12329359, upload-time = "2025-12-09T10:58:12.147Z" }, + { url = "https://files.pythonhosted.org/packages/b0/83/9d8fc3413f854effa680dcad1781f68f3ada8679863c0c94ba3b36bae6ff/duckdb-1.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:fbc63ffdd03835f660155b37a1b6db2005bcd46e5ad398b8cac141eb305d2a3d", size = 13070898, upload-time = "2025-12-09T10:58:14.301Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d7/fdc2139b94297fc5659110a38adde293d025e320673ae5e472b95d323c50/duckdb-1.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6302452e57aef29aae3977063810ed7b2927967b97912947b9cca45c1c21955f", size = 29033112, upload-time = "2025-12-09T10:58:16.52Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/ca93df1ce19aef8f799e3aaacf754a4dde7e9169c0b333557752d21d076a/duckdb-1.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:deab351ac43b6282a3270e3d40e3d57b3b50f472d9fd8c30975d88a31be41231", size = 15414646, upload-time = "2025-12-09T10:58:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/16/90/9f2748e740f5fc05b739e7c5c25aab6ab4363e5da4c3c70419c7121dc806/duckdb-1.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5634e40e1e2d972e4f75bced1fbdd9e9e90faa26445c1052b27de97ee546944a", size = 13740477, upload-time = "2025-12-09T10:58:21.778Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ec/279723615b4fb454efd823b7efe97cf2504569e2e74d15defbbd6b027901/duckdb-1.4.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:274d4a31aba63115f23e7e7b401e3e3a937f3626dc9dea820a9c7d3073f450d2", size = 18483715, upload-time = "2025-12-09T10:58:24.346Z" }, + { url = "https://files.pythonhosted.org/packages/10/63/af20cd20fd7fd6565ea5a1578c16157b6a6e07923e459a6f9b0dc9ada308/duckdb-1.4.3-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f868a7e6d9b37274a1aa34849ea92aa964e9bd59a5237d6c17e8540533a1e4f", size = 20495188, upload-time = "2025-12-09T10:58:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ab/0acb4b64afb2cc6c1d458a391c64e36be40137460f176c04686c965ce0e0/duckdb-1.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:ef7ef15347ce97201b1b5182a5697682679b04c3374d5a01ac10ba31cf791b95", size = 12335622, upload-time = "2025-12-09T10:58:29.707Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/2a795745f6597a5e65770141da6efdc4fd754e5ee6d652f74bcb7f9c7759/duckdb-1.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:1b9b445970fd18274d5ac07a0b24c032e228f967332fb5ebab3d7db27738c0e4", size = 13075834, upload-time = "2025-12-09T10:58:32.036Z" }, +] + +[[package]] +name = "duckdb-engine" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "duckdb" }, + { name = "packaging" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/d5/c0d8d0a4ca3ffea92266f33d92a375e2794820ad89f9be97cf0c9a9697d0/duckdb_engine-0.17.0.tar.gz", hash = "sha256:396b23869754e536aa80881a92622b8b488015cf711c5a40032d05d2cf08f3cf", size = 48054, upload-time = "2025-03-29T09:49:17.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/a2/e90242f53f7ae41554419b1695b4820b364df87c8350aa420b60b20cab92/duckdb_engine-0.17.0-py3-none-any.whl", hash = "sha256:3aa72085e536b43faab635f487baf77ddc5750069c16a2f8d9c6c3cb6083e979", size = 49676, upload-time = "2025-03-29T09:49:15.564Z" }, +] + +[[package]] +name = "dunamai" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/c4/346cef905782df6152f29f02d9c8ed4acf7ae66b0e66210b7156c5575ccb/dunamai-1.26.0.tar.gz", hash = "sha256:5396ac43aa20ed059040034e9f9798c7464cf4334c6fc3da3732e29273a2f97d", size = 45500, upload-time = "2026-02-15T02:58:55.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/10/2c7edbf230e5c507d38367af498fa94258ed97205d9b4b6f63a921fe9c49/dunamai-1.26.0-py3-none-any.whl", hash = "sha256:f584edf0fda0d308cce0961f807bc90a8fe3d9ff4d62f94e72eca7b43f0ed5f6", size = 27322, upload-time = "2026-02-15T02:58:54.143Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +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/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]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fakeredis" +version = "2.34.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, + { name = "sortedcontainers" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/40/fd09efa66205eb32253d2b2ebc63537281384d2040f0a88bcd2289e120e4/fakeredis-2.34.1.tar.gz", hash = "sha256:4ff55606982972eecce3ab410e03d746c11fe5deda6381d913641fbd8865ea9b", size = 177315, upload-time = "2026-02-25T13:17:51.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/b5/82f89307d0d769cd9bf46a54fb9136be08e4e57c5570ae421db4c9a2ba62/fakeredis-2.34.1-py3-none-any.whl", hash = "sha256:0107ec99d48913e7eec2a5e3e2403d1bd5f8aa6489d1a634571b975289c48f12", size = 122160, upload-time = "2026-02-25T13:17:49.701Z" }, +] + +[package.optional-dependencies] +lua = [ + { name = "lupa" }, +] + +[[package]] +name = "fastmcp" +version = "2.14.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib" }, + { name = "cyclopts" }, + { name = "exceptiongroup" }, + { name = "httpx" }, + { name = "jsonref" }, + { name = "jsonschema-path" }, + { name = "mcp" }, + { name = "openapi-pydantic" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "py-key-value-aio", extra = ["disk", "keyring", "memory"] }, + { name = "pydantic", extra = ["email"] }, + { name = "pydocket" }, + { name = "pyperclip" }, + { name = "python-dotenv" }, + { name = "rich" }, + { name = "uvicorn" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/32/982678d44f13849530a74ab101ed80e060c2ee6cf87471f062dcf61705fd/fastmcp-2.14.5.tar.gz", hash = "sha256:38944dc582c541d55357082bda2241cedb42cd3a78faea8a9d6a2662c62a42d7", size = 8296329, upload-time = "2026-02-03T15:35:21.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/c1/1a35ec68ff76ea8443aa115b18bcdee748a4ada2124537ee90522899ff9f/fastmcp-2.14.5-py3-none-any.whl", hash = "sha256:d81e8ec813f5089d3624bec93944beaefa86c0c3a4ef1111cbef676a761ebccf", size = 417784, upload-time = "2026-02-03T15:35:18.489Z" }, +] + +[[package]] +name = "fastmcp-extensions" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastmcp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/38/95549e7bb6bfe6ffedb5c388488b7c81b6a70d5e649d5a08047687a0e811/fastmcp_extensions-0.2.0.tar.gz", hash = "sha256:c456d4d00a96d9fe41b630e51cc6cb4b9920796e6943185e797669d10fe7e917", size = 156381, upload-time = "2026-01-19T23:02:38.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/bc/0d2edadb8629afaaec9f05fe05ca9184256d8f046601f71d07cbc8ff8aeb/fastmcp_extensions-0.2.0-py3-none-any.whl", hash = "sha256:b48f13ecfbceb8e5bc75569e41029f451efa2f0f69b390cf2ad23ed41d1160e0", size = 34431, upload-time = "2026-01-19T23:02:37.096Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158, upload-time = "2026-03-01T15:08:45.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427, upload-time = "2026-03-01T15:08:44.593Z" }, +] + +[[package]] +name = "genson" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/cf/2303c8ad276dcf5ee2ad6cf69c4338fd86ef0f471a5207b069adf7a393cf/genson-1.3.0.tar.gz", hash = "sha256:e02db9ac2e3fd29e65b5286f7135762e2cd8a986537c075b06fc5f1517308e37", size = 34919, upload-time = "2024-05-15T22:08:49.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/5c/e226de133afd8bb267ec27eead9ae3d784b95b39a287ed404caab39a5f50/genson-1.3.0-py3-none-any.whl", hash = "sha256:468feccd00274cc7e4c09e84b08704270ba8d95232aa280f65b986139cec67f7", size = 21470, upload-time = "2024-05-15T22:08:47.056Z" }, +] + +[[package]] +name = "google-api-core" +version = "2.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/98/586ec94553b569080caef635f98a3723db36a38eac0e3d7eb3ea9d2e4b9a/google_api_core-2.30.0.tar.gz", hash = "sha256:02edfa9fab31e17fc0befb5f161b3bf93c9096d99aed584625f38065c511ad9b", size = 176959, upload-time = "2026-02-18T20:28:11.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/27/09c33d67f7e0dcf06d7ac17d196594e66989299374bfb0d4331d1038e76b/google_api_core-2.30.0-py3-none-any.whl", hash = "sha256:80be49ee937ff9aba0fd79a6eddfde35fe658b9953ab9b79c57dd7061afa8df5", size = 173288, upload-time = "2026-02-18T20:28:10.367Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + +[[package]] +name = "google-auth" +version = "2.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, +] + +[[package]] +name = "google-cloud-bigquery" +version = "3.40.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-resumable-media" }, + { name = "packaging" }, + { name = "python-dateutil" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/0c/153ee546c288949fcc6794d58811ab5420f3ecad5fa7f9e73f78d9512a6e/google_cloud_bigquery-3.40.1.tar.gz", hash = "sha256:75afcfb6e007238fe1deefb2182105249321145ff921784fe7b1de2b4ba24506", size = 511761, upload-time = "2026-02-12T18:44:18.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/f5/081cf5b90adfe524ae0d671781b0d497a75a0f2601d075af518828e22d8f/google_cloud_bigquery-3.40.1-py3-none-any.whl", hash = "sha256:9082a6b8193aba87bed6a2c79cf1152b524c99bb7e7ac33a785e333c09eac868", size = 262018, upload-time = "2026-02-12T18:44:16.913Z" }, +] + +[[package]] +name = "google-cloud-bigquery-storage" +version = "2.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/fa/877e0059349369be38a64586b135c59ceadb87d0386084043d8c440ef929/google_cloud_bigquery_storage-2.36.2.tar.gz", hash = "sha256:ad49d8c09ad6cd82da4efe596fcfcdbc1458bf05b93915e3c5c00f1e700ae128", size = 308672, upload-time = "2026-02-19T16:03:10.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/07/62dbe78ef773569be0a1d2c1b845e9214889b404e506126519b4d33ee999/google_cloud_bigquery_storage-2.36.2-py3-none-any.whl", hash = "sha256:823a73db0c4564e8ad3eedcfd5049f3d5aa41775267863b5627211ec36be2dbf", size = 304398, upload-time = "2026-02-19T16:02:55.112Z" }, +] + +[[package]] +name = "google-cloud-core" +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/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/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-secret-manager" +version = "2.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/9c/a6c7144bc96df77376ae3fcc916fb639c40814c2e4bba2051d31dc136cd0/google_cloud_secret_manager-2.26.0.tar.gz", hash = "sha256:0d1d6f76327685a0ed78a4cf50f289e1bfbbe56026ed0affa98663b86d6d50d6", size = 277603, upload-time = "2025-12-18T00:29:31.065Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/30/a58739dd12cec0f7f761ed1efb518aed2250a407d4ed14c5a0eeee7eaaf9/google_cloud_secret_manager-2.26.0-py3-none-any.whl", hash = "sha256:940a5447a6ec9951446fd1a0f22c81a4303fde164cd747aae152c5f5c8e6723e", size = 223623, upload-time = "2025-12-18T00:29:29.311Z" }, +] + +[[package]] +name = "google-crc32c" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/ac/6f7bc93886a823ab545948c2dd48143027b2355ad1944c7cf852b338dc91/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff", size = 31296, upload-time = "2025-12-16T00:19:07.261Z" }, + { url = "https://files.pythonhosted.org/packages/f7/97/a5accde175dee985311d949cfcb1249dcbb290f5ec83c994ea733311948f/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288", size = 30870, upload-time = "2025-12-16T00:29:17.669Z" }, + { url = "https://files.pythonhosted.org/packages/3d/63/bec827e70b7a0d4094e7476f863c0dbd6b5f0f1f91d9c9b32b76dcdfeb4e/google_crc32c-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d", size = 33214, upload-time = "2025-12-16T00:40:19.618Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/11b70614df04c289128d782efc084b9035ef8466b3d0a8757c1b6f5cf7ac/google_crc32c-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092", size = 33589, upload-time = "2025-12-16T00:40:20.7Z" }, + { url = "https://files.pythonhosted.org/packages/3e/00/a08a4bc24f1261cc5b0f47312d8aebfbe4b53c2e6307f1b595605eed246b/google_crc32c-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733", size = 34437, upload-time = "2025-12-16T00:35:19.437Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8", size = 31298, upload-time = "2025-12-16T00:20:32.241Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b8/f8413d3f4b676136e965e764ceedec904fe38ae8de0cdc52a12d8eb1096e/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7", size = 30872, upload-time = "2025-12-16T00:33:58.785Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15", size = 33243, upload-time = "2025-12-16T00:40:21.46Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a", size = 33608, upload-time = "2025-12-16T00:40:22.204Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2", size = 34439, upload-time = "2025-12-16T00:35:20.458Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, + { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, + { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, + { url = "https://files.pythonhosted.org/packages/52/c5/c171e4d8c44fec1422d801a6d2e5d7ddabd733eeda505c79730ee9607f07/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93", size = 28615, upload-time = "2025-12-16T00:40:29.298Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/7d75fe37a7a6ed171a2cf17117177e7aab7e6e0d115858741b41e9dd4254/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c", size = 28800, upload-time = "2025-12-16T00:40:30.322Z" }, +] + +[[package]] +name = "google-resumable-media" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-crc32c" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/d7/520b62a35b23038ff005e334dba3ffc75fcf583bee26723f1fd8fd4b6919/google_resumable_media-2.8.0.tar.gz", hash = "sha256:f1157ed8b46994d60a1bc432544db62352043113684d4e030ee02e77ebe9a1ae", size = 2163265, upload-time = "2025-11-17T15:38:06.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl", hash = "sha256:dd14a116af303845a8d932ddae161a26e86cc229645bc98b39f026f9b1717582", size = 81340, upload-time = "2025-11-17T15:38:05.594Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, +] + +[[package]] +name = "greenlet" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" }, + { url = "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" }, + { url = "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" }, + { url = "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" }, + { url = "https://files.pythonhosted.org/packages/ac/78/f93e840cbaef8becaf6adafbaf1319682a6c2d8c1c20224267a5c6c8c891/greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f", size = 230092, upload-time = "2026-02-20T20:17:09.379Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, + { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, + { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, +] + +[[package]] +name = "grpc-google-iam-v1" +version = "0.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", extra = ["grpc"] }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/1e/1011451679a983f2f5c6771a1682542ecb027776762ad031fd0d7129164b/grpc_google_iam_v1-0.14.3.tar.gz", hash = "sha256:879ac4ef33136c5491a6300e27575a9ec760f6cdf9a2518798c1b8977a5dc389", size = 23745, upload-time = "2025-10-15T21:14:53.318Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/bd/330a1bbdb1afe0b96311249e699b6dc9cfc17916394fd4503ac5aca2514b/grpc_google_iam_v1-0.14.3-py3-none-any.whl", hash = "sha256:7a7f697e017a067206a3dfef44e4c634a34d3dee135fe7d7a4613fe3e59217e6", size = 32690, upload-time = "2025-10-15T21:14:51.72Z" }, +] + +[[package]] +name = "grpcio" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/a8/690a085b4d1fe066130de97a87de32c45062cf2ecd218df9675add895550/grpcio-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5", size = 5946986, upload-time = "2026-02-06T09:54:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1b/e5213c5c0ced9d2d92778d30529ad5bb2dcfb6c48c4e2d01b1f302d33d64/grpcio-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2", size = 11816533, upload-time = "2026-02-06T09:54:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/1ba32dccf0a324cc5ace744c44331e300b000a924bf14840f948c559ede7/grpcio-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d", size = 6519964, upload-time = "2026-02-06T09:54:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f5/c0e178721b818072f2e8b6fde13faaba942406c634009caf065121ce246b/grpcio-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb", size = 7198058, upload-time = "2026-02-06T09:54:42.389Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/40d43c91ae9cd667edc960135f9f08e58faa1576dc95af29f66ec912985f/grpcio-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7", size = 6727212, upload-time = "2026-02-06T09:54:44.91Z" }, + { url = "https://files.pythonhosted.org/packages/ed/88/9da42eed498f0efcfcd9156e48ae63c0cde3bea398a16c99fb5198c885b6/grpcio-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec", size = 7300845, upload-time = "2026-02-06T09:54:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/23/3f/1c66b7b1b19a8828890e37868411a6e6925df5a9030bfa87ab318f34095d/grpcio-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a", size = 8284605, upload-time = "2026-02-06T09:54:50.475Z" }, + { url = "https://files.pythonhosted.org/packages/94/c4/ca1bd87394f7b033e88525384b4d1e269e8424ab441ea2fba1a0c5b50986/grpcio-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813", size = 7726672, upload-time = "2026-02-06T09:54:53.11Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/f16e487d4cc65ccaf670f6ebdd1a17566b965c74fc3d93999d3b2821e052/grpcio-1.78.0-cp310-cp310-win32.whl", hash = "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de", size = 4076715, upload-time = "2026-02-06T09:54:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/2a/32/4ce60d94e242725fd3bcc5673c04502c82a8e87b21ea411a63992dc39f8f/grpcio-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf", size = 4799157, upload-time = "2026-02-06T09:54:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, + { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, + { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, + { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, +] + +[[package]] +name = "grpcio-status" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/cd/89ce482a931b543b92cdd9b2888805518c4620e0094409acb8c81dd4610a/grpcio_status-1.78.0.tar.gz", hash = "sha256:a34cfd28101bfea84b5aa0f936b4b423019e9213882907166af6b3bddc59e189", size = 13808, upload-time = "2026-02-06T10:01:48.034Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/8a/1241ec22c41028bddd4a052ae9369267b4475265ad0ce7140974548dc3fa/grpcio_status-1.78.0-py3-none-any.whl", hash = "sha256:b492b693d4bf27b47a6c32590701724f1d3b9444b36491878fb71f6208857f34", size = 14523, upload-time = "2026-02-06T10:01:32.584Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +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 = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +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/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +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/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 = "isodate" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/7a/c0a56c7d56c7fa723988f122fa1f1ccf8c5c4ccc48efad0d214b49e5b1af/isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9", size = 28443, upload-time = "2021-12-13T20:28:31.525Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/85/7882d311924cbcfc70b1890780763e36ff0b140c7e51c110fc59a532f087/isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96", size = 41722, upload-time = "2021-12-13T20:28:29.073Z" }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "jsonpath-python" +version = "1.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/bf/626a72f2d093c5eb4f4de55b443714afa7231beeae40d4a1c69b5c5aa4d1/jsonpath_python-1.1.4.tar.gz", hash = "sha256:bb3e13854e4807c078a1503ae2d87c211b8bff4d9b40b6455ed583b3b50a7fdd", size = 84766, upload-time = "2025-11-25T12:08:39.521Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/bc/52e5bf0d9839e082b976c19afcab7561d0d719c7627483bf5dc251d27eed/jsonpath_python-1.1.4-py3-none-any.whl", hash = "sha256:8700cb8610c44da6e5e9bff50232779c44bf7dc5bc62662d49319ee746898442", size = 12687, upload-time = "2025-11-25T12:08:38.453Z" }, +] + +[[package]] +name = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-path" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/b4/41315eea8301a5353bca3578792767135b8edbc081b20618a3f0b4d78307/jsonschema_path-0.4.4.tar.gz", hash = "sha256:4c55842890fc384262a59fb63a25c86cc0e2b059e929c18b851c1d19ef612026", size = 14923, upload-time = "2026-02-28T11:58:26.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/36/cb2cd6543776d02875de600f12fcd81611daf359544c9ad2abb12d3122a5/jsonschema_path-0.4.4-py3-none-any.whl", hash = "sha256:669bb69cb92cd4c54acf38ee2ff7c3d9ab6b69991698f7a2f17d2bb0e5c9c394", size = 19226, upload-time = "2026-02-28T11:58:25.143Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +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/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]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "lupa" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282, upload-time = "2025-10-24T07:20:29.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/15/713cab5d0dfa4858f83b99b3e0329072df33dc14fc3ebbaa017e0f9755c4/lupa-2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b3dabda836317e63c5ad052826e156610f356a04b3003dfa0dbe66b5d54d671", size = 954828, upload-time = "2025-10-24T07:17:15.726Z" }, + { url = "https://files.pythonhosted.org/packages/2e/71/704740cbc6e587dd6cc8dabf2f04820ac6a671784e57cc3c29db795476db/lupa-2.6-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8726d1c123bbe9fbb974ce29825e94121824e66003038ff4532c14cc2ed0c51c", size = 1919259, upload-time = "2025-10-24T07:17:18.586Z" }, + { url = "https://files.pythonhosted.org/packages/eb/18/f248341c423c5d48837e35584c6c3eb4acab7e722b6057d7b3e28e42dae8/lupa-2.6-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:f4e159e7d814171199b246f9235ca8961f6461ea8c1165ab428afa13c9289a94", size = 984998, upload-time = "2025-10-24T07:17:20.428Z" }, + { url = "https://files.pythonhosted.org/packages/44/1e/8a4bd471e018aad76bcb9455d298c2c96d82eced20f2ae8fcec8cd800948/lupa-2.6-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:202160e80dbfddfb79316692a563d843b767e0f6787bbd1c455f9d54052efa6c", size = 1174871, upload-time = "2025-10-24T07:17:22.755Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5c/3a3f23fd6a91b0986eea1ceaf82ad3f9b958fe3515a9981fb9c4eb046c8b/lupa-2.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5deede7c5b36ab64f869dae4831720428b67955b0bb186c8349cf6ea121c852b", size = 1057471, upload-time = "2025-10-24T07:17:24.908Z" }, + { url = "https://files.pythonhosted.org/packages/45/ac/01be1fed778fb0c8f46ee8cbe344e4d782f6806fac12717f08af87aa4355/lupa-2.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86f04901f920bbf7c0cac56807dc9597e42347123e6f1f3ca920f15f54188ce5", size = 2100592, upload-time = "2025-10-24T07:17:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6c/1a05bb873e30830f8574e10cd0b4cdbc72e9dbad2a09e25810b5e3b1f75d/lupa-2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6deef8f851d6afb965c84849aa5b8c38856942df54597a811ce0369ced678610", size = 1081396, upload-time = "2025-10-24T07:17:29.064Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c2/a19dd80d6dc98b39bbf8135b8198e38aa7ca3360b720eac68d1d7e9286b5/lupa-2.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:21f2b5549681c2a13b1170a26159d30875d367d28f0247b81ca347222c755038", size = 1192007, upload-time = "2025-10-24T07:17:31.362Z" }, + { url = "https://files.pythonhosted.org/packages/4f/43/e1b297225c827f55752e46fdbfb021c8982081b0f24490e42776ea69ae3b/lupa-2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:66eea57630eab5e6f49fdc5d7811c0a2a41f2011be4ea56a087ea76112011eb7", size = 2196661, upload-time = "2025-10-24T07:17:33.484Z" }, + { url = "https://files.pythonhosted.org/packages/2e/8f/2272d429a7fa9dc8dbd6e9c5c9073a03af6007eb22a4c78829fec6a34b80/lupa-2.6-cp310-cp310-win32.whl", hash = "sha256:60a403de8cab262a4fe813085dd77010effa6e2eb1886db2181df803140533b1", size = 1412738, upload-time = "2025-10-24T07:17:35.11Z" }, + { url = "https://files.pythonhosted.org/packages/35/2a/1708911271dd49ad87b4b373b5a4b0e0a0516d3d2af7b76355946c7ee171/lupa-2.6-cp310-cp310-win_amd64.whl", hash = "sha256:e4656a39d93dfa947cf3db56dc16c7916cb0cc8024acd3a952071263f675df64", size = 1656898, upload-time = "2025-10-24T07:17:36.949Z" }, + { url = "https://files.pythonhosted.org/packages/ca/29/1f66907c1ebf1881735afa695e646762c674f00738ebf66d795d59fc0665/lupa-2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d988c0f9331b9f2a5a55186701a25444ab10a1432a1021ee58011499ecbbdd5", size = 962875, upload-time = "2025-10-24T07:17:39.107Z" }, + { url = "https://files.pythonhosted.org/packages/e6/67/4a748604be360eb9c1c215f6a0da921cd1a2b44b2c5951aae6fb83019d3a/lupa-2.6-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:ebe1bbf48259382c72a6fe363dea61a0fd6fe19eab95e2ae881e20f3654587bf", size = 1935390, upload-time = "2025-10-24T07:17:41.427Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/8ef9ee933a350428b7bdb8335a37ef170ab0bb008bbf9ca8f4f4310116b6/lupa-2.6-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:a8fcee258487cf77cdd41560046843bb38c2e18989cd19671dd1e2596f798306", size = 992193, upload-time = "2025-10-24T07:17:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/65/46/e6c7facebdb438db8a65ed247e56908818389c1a5abbf6a36aab14f1057d/lupa-2.6-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:561a8e3be800827884e767a694727ed8482d066e0d6edfcbf423b05e63b05535", size = 1165844, upload-time = "2025-10-24T07:17:45.437Z" }, + { url = "https://files.pythonhosted.org/packages/1c/26/9f1154c6c95f175ccbf96aa96c8f569c87f64f463b32473e839137601a8b/lupa-2.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af880a62d47991cae78b8e9905c008cbfdc4a3a9723a66310c2634fc7644578c", size = 1048069, upload-time = "2025-10-24T07:17:47.181Z" }, + { url = "https://files.pythonhosted.org/packages/68/67/2cc52ab73d6af81612b2ea24c870d3fa398443af8e2875e5befe142398b1/lupa-2.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80b22923aa4023c86c0097b235615f89d469a0c4eee0489699c494d3367c4c85", size = 2079079, upload-time = "2025-10-24T07:17:49.755Z" }, + { url = "https://files.pythonhosted.org/packages/2e/dc/f843f09bbf325f6e5ee61730cf6c3409fc78c010d968c7c78acba3019ca7/lupa-2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:153d2cc6b643f7efb9cfc0c6bb55ec784d5bac1a3660cfc5b958a7b8f38f4a75", size = 1071428, upload-time = "2025-10-24T07:17:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/2e/60/37533a8d85bf004697449acb97ecdacea851acad28f2ad3803662487dd2a/lupa-2.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3fa8777e16f3ded50b72967dc17e23f5a08e4f1e2c9456aff2ebdb57f5b2869f", size = 1181756, upload-time = "2025-10-24T07:17:53.752Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f2/cf29b20dbb4927b6a3d27c339ac5d73e74306ecc28c8e2c900b2794142ba/lupa-2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8dbdcbe818c02a2f56f5ab5ce2de374dab03e84b25266cfbaef237829bc09b3f", size = 2175687, upload-time = "2025-10-24T07:17:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/050e02f80c7131b63db1474bff511e63c545b5a8636a24cbef3fc4da20b6/lupa-2.6-cp311-cp311-win32.whl", hash = "sha256:defaf188fde8f7a1e5ce3a5e6d945e533b8b8d547c11e43b96c9b7fe527f56dc", size = 1412592, upload-time = "2025-10-24T07:17:59.062Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/6f2af98aa5d771cea661f66c8eb8f53772ec1ab1dfbce24126cfcd189436/lupa-2.6-cp311-cp311-win_amd64.whl", hash = "sha256:9505ae600b5c14f3e17e70f87f88d333717f60411faca1ddc6f3e61dce85fa9e", size = 1669194, upload-time = "2025-10-24T07:18:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/94/86/ce243390535c39d53ea17ccf0240815e6e457e413e40428a658ea4ee4b8d/lupa-2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47ce718817ef1cc0c40d87c3d5ae56a800d61af00fbc0fad1ca9be12df2f3b56", size = 951707, upload-time = "2025-10-24T07:18:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/cedea5e6cbeb54396fdcc55f6b741696f3f036d23cfaf986d50d680446da/lupa-2.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7aba985b15b101495aa4b07112cdc08baa0c545390d560ad5cfde2e9e34f4d58", size = 1916703, upload-time = "2025-10-24T07:18:05.6Z" }, + { url = "https://files.pythonhosted.org/packages/24/be/3d6b5f9a8588c01a4d88129284c726017b2089f3a3fd3ba8bd977292fea0/lupa-2.6-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:b766f62f95b2739f2248977d29b0722e589dcf4f0ccfa827ccbd29f0148bd2e5", size = 985152, upload-time = "2025-10-24T07:18:08.561Z" }, + { url = "https://files.pythonhosted.org/packages/eb/23/9f9a05beee5d5dce9deca4cb07c91c40a90541fc0a8e09db4ee670da550f/lupa-2.6-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:00a934c23331f94cb51760097ebfab14b005d55a6b30a2b480e3c53dd2fa290d", size = 1159599, upload-time = "2025-10-24T07:18:10.346Z" }, + { url = "https://files.pythonhosted.org/packages/40/4e/e7c0583083db9d7f1fd023800a9767d8e4391e8330d56c2373d890ac971b/lupa-2.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21de9f38bd475303e34a042b7081aabdf50bd9bafd36ce4faea2f90fd9f15c31", size = 1038686, upload-time = "2025-10-24T07:18:12.112Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/5a4f7d959d4feba5e203ff0c31889e74d1ca3153122be4a46dca7d92bf7c/lupa-2.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf3bda96d3fc41237e964a69c23647d50d4e28421111360274d4799832c560e9", size = 2071956, upload-time = "2025-10-24T07:18:14.572Z" }, + { url = "https://files.pythonhosted.org/packages/92/34/2f4f13ca65d01169b1720176aedc4af17bc19ee834598c7292db232cb6dc/lupa-2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a76ead245da54801a81053794aa3975f213221f6542d14ec4b859ee2e7e0323", size = 1057199, upload-time = "2025-10-24T07:18:16.379Z" }, + { url = "https://files.pythonhosted.org/packages/35/2a/5f7d2eebec6993b0dcd428e0184ad71afb06a45ba13e717f6501bfed1da3/lupa-2.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8dd0861741caa20886ddbda0a121d8e52fb9b5bb153d82fa9bba796962bf30e8", size = 1173693, upload-time = "2025-10-24T07:18:18.153Z" }, + { url = "https://files.pythonhosted.org/packages/e4/29/089b4d2f8e34417349af3904bb40bec40b65c8731f45e3fd8d497ca573e5/lupa-2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:239e63948b0b23023f81d9a19a395e768ed3da6a299f84e7963b8f813f6e3f9c", size = 2164394, upload-time = "2025-10-24T07:18:20.403Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1b/79c17b23c921f81468a111cad843b076a17ef4b684c4a8dff32a7969c3f0/lupa-2.6-cp312-cp312-win32.whl", hash = "sha256:325894e1099499e7a6f9c351147661a2011887603c71086d36fe0f964d52d1ce", size = 1420647, upload-time = "2025-10-24T07:18:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/b8/15/5121e68aad3584e26e1425a5c9a79cd898f8a152292059e128c206ee817c/lupa-2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c735a1ce8ee60edb0fe71d665f1e6b7c55c6021f1d340eb8c865952c602cd36f", size = 1688529, upload-time = "2025-10-24T07:18:25.523Z" }, +] + +[[package]] +name = "lz4" +version = "4.4.5" +source = { registry = "https://pypi.org/simple" } +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]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/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 = "marshmallow" +version = "3.26.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, +] + +[[package]] +name = "mcp" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +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 = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nltk" +version = "3.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "joblib" }, + { name = "regex" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/87/db8be88ad32c2d042420b6fd9ffd4a149f9a0d7f0e86b3f543be2eeeedd2/nltk-3.9.1.tar.gz", hash = "sha256:87d127bd3de4bd89a4f81265e5fa59cb1b199b27440175370f7417d2bc7ae868", size = 2904691, upload-time = "2024-08-18T19:48:37.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/66/7d9e26593edda06e8cb531874633f7c2372279c3b0f46235539fe546df8b/nltk-3.9.1-py3-none-any.whl", hash = "sha256:4fa26829c5b00715afe3061398a8989dc643b92ce7dd93fb4585a70930d168a1", size = 1505442, upload-time = "2024-08-18T19:48:21.909Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, + { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, + { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, + { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, + { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, +] + +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1a/a373746fa6d0e116dd9e54371a7b54622c44d12296d5d0f3ad5e3ff33490/orjson-3.11.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a02c833f38f36546ba65a452127633afce4cf0dd7296b753d3bb54e55e5c0174", size = 229140, upload-time = "2026-02-02T15:37:06.082Z" }, + { url = "https://files.pythonhosted.org/packages/52/a2/fa129e749d500f9b183e8a3446a193818a25f60261e9ce143ad61e975208/orjson-3.11.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63c6e6738d7c3470ad01601e23376aa511e50e1f3931395b9f9c722406d1a67", size = 128670, upload-time = "2026-02-02T15:37:08.002Z" }, + { url = "https://files.pythonhosted.org/packages/08/93/1e82011cd1e0bd051ef9d35bed1aa7fb4ea1f0a055dc2c841b46b43a9ebd/orjson-3.11.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:043d3006b7d32c7e233b8cfb1f01c651013ea079e08dcef7189a29abd8befe11", size = 123832, upload-time = "2026-02-02T15:37:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d8/a26b431ef962c7d55736674dddade876822f3e33223c1f47a36879350d04/orjson-3.11.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57036b27ac8a25d81112eb0cc9835cd4833c5b16e1467816adc0015f59e870dc", size = 129171, upload-time = "2026-02-02T15:37:11.112Z" }, + { url = "https://files.pythonhosted.org/packages/a7/19/f47819b84a580f490da260c3ee9ade214cf4cf78ac9ce8c1c758f80fdfc9/orjson-3.11.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:733ae23ada68b804b222c44affed76b39e30806d38660bf1eb200520d259cc16", size = 141967, upload-time = "2026-02-02T15:37:12.282Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cd/37ece39a0777ba077fdcdbe4cccae3be8ed00290c14bf8afdc548befc260/orjson-3.11.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5fdfad2093bdd08245f2e204d977facd5f871c88c4a71230d5bcbd0e43bf6222", size = 130991, upload-time = "2026-02-02T15:37:13.465Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ed/f2b5d66aa9b6b5c02ff5f120efc7b38c7c4962b21e6be0f00fd99a5c348e/orjson-3.11.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cededd6738e1c153530793998e31c05086582b08315db48ab66649768f326baa", size = 133674, upload-time = "2026-02-02T15:37:14.694Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6e/baa83e68d1aa09fa8c3e5b2c087d01d0a0bd45256de719ed7bc22c07052d/orjson-3.11.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:14f440c7268c8f8633d1b3d443a434bd70cb15686117ea6beff8fdc8f5917a1e", size = 138722, upload-time = "2026-02-02T15:37:16.501Z" }, + { url = "https://files.pythonhosted.org/packages/0c/47/7f8ef4963b772cd56999b535e553f7eb5cd27e9dd6c049baee6f18bfa05d/orjson-3.11.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3a2479753bbb95b0ebcf7969f562cdb9668e6d12416a35b0dda79febf89cdea2", size = 409056, upload-time = "2026-02-02T15:37:17.895Z" }, + { url = "https://files.pythonhosted.org/packages/38/eb/2df104dd2244b3618f25325a656f85cc3277f74bbd91224752410a78f3c7/orjson-3.11.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:71924496986275a737f38e3f22b4e0878882b3f7a310d2ff4dc96e812789120c", size = 144196, upload-time = "2026-02-02T15:37:19.349Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2a/ee41de0aa3a6686598661eae2b4ebdff1340c65bfb17fcff8b87138aab21/orjson-3.11.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4a9eefdc70bf8bf9857f0290f973dec534ac84c35cd6a7f4083be43e7170a8f", size = 134979, upload-time = "2026-02-02T15:37:20.906Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fa/92fc5d3d402b87a8b28277a9ed35386218a6a5287c7fe5ee9b9f02c53fb2/orjson-3.11.7-cp310-cp310-win32.whl", hash = "sha256:ae9e0b37a834cef7ce8f99de6498f8fad4a2c0bf6bfc3d02abd8ed56aa15b2de", size = 127968, upload-time = "2026-02-02T15:37:23.178Z" }, + { url = "https://files.pythonhosted.org/packages/07/29/a576bf36d73d60df06904d3844a9df08e25d59eba64363aaf8ec2f9bff41/orjson-3.11.7-cp310-cp310-win_amd64.whl", hash = "sha256:d772afdb22555f0c58cfc741bdae44180122b3616faa1ecadb595cd526e4c993", size = 125128, upload-time = "2026-02-02T15:37:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, + { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, + { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, + { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, + { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, + { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, + { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, +] + +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827, upload-time = "2024-09-20T13:08:42.347Z" }, + { url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897, upload-time = "2024-09-20T13:08:45.807Z" }, + { url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908, upload-time = "2024-09-20T18:37:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210, upload-time = "2024-09-20T13:08:48.325Z" }, + { url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292, upload-time = "2024-09-20T19:01:54.443Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379, upload-time = "2024-09-20T13:08:50.882Z" }, + { url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471, upload-time = "2024-09-20T13:08:53.332Z" }, + { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222, upload-time = "2024-09-20T13:08:56.254Z" }, + { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274, upload-time = "2024-09-20T13:08:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836, upload-time = "2024-09-20T19:01:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505, upload-time = "2024-09-20T13:09:01.501Z" }, + { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420, upload-time = "2024-09-20T19:02:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457, upload-time = "2024-09-20T13:09:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166, upload-time = "2024-09-20T13:09:06.917Z" }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893, upload-time = "2024-09-20T13:09:09.655Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475, upload-time = "2024-09-20T13:09:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645, upload-time = "2024-09-20T19:02:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445, upload-time = "2024-09-20T13:09:17.621Z" }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235, upload-time = "2024-09-20T19:02:07.094Z" }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756, upload-time = "2024-09-20T13:09:20.474Z" }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248, upload-time = "2024-09-20T13:09:23.137Z" }, +] + +[[package]] +name = "pathable" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/55/b748445cb4ea6b125626f15379be7c96d1035d4fa3e8fee362fa92298abf/pathable-0.5.0.tar.gz", hash = "sha256:d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1", size = 16655, upload-time = "2026-02-20T08:47:00.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-py3-none-any.whl", hash = "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", size = 16867, upload-time = "2026-02-20T08:46:59.536Z" }, +] + +[[package]] +name = "pathvalidate" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262, upload-time = "2025-06-15T09:07:20.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +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 = "prometheus-client" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, +] + +[[package]] +name = "proto-plus" +version = "1.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/02/8832cde80e7380c600fbf55090b6ab7b62bd6825dbedde6d6657c15a1f8e/proto_plus-1.27.1.tar.gz", hash = "sha256:912a7460446625b792f6448bade9e55cd4e41e6ac10e27009ef71a7f317fa147", size = 56929, upload-time = "2026-02-02T17:34:49.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/79/ac273cbbf744691821a9cca88957257f41afe271637794975ca090b9588b/proto_plus-1.27.1-py3-none-any.whl", hash = "sha256:e4643061f3a4d0de092d62aa4ad09fa4756b2cbb89d4627f3985018216f9fefc", size = 50480, upload-time = "2026-02-02T17:34:47.339Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.5" +source = { registry = "https://pypi.org/simple" } +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/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 = "psycopg" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/b6/379d0a960f8f435ec78720462fd94c4863e7a31237cf81bf76d0af5883bf/psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9", size = 165624, upload-time = "2026-02-18T16:52:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] +pool = [ + { name = "psycopg-pool" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/d8/a763308a41e2ecfb6256ba0877d340c2f2b124c8b2746401863d96fa2c7a/psycopg_binary-3.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b3385b58b2fe408a13d084c14b8dcf468cd36cbbe774408250facc128f9fa75c", size = 4609758, upload-time = "2026-02-18T16:46:33.132Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a9/f8a683e85400c1208685e7c895abc049dc13aa0b6ea989e6adf0a3681fe0/psycopg_binary-3.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1bef235a50a80f6aba05147002bc354559657cb6386dbd04d8e1c97d1d7cbe84", size = 4676740, upload-time = "2026-02-18T16:46:42.904Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7d/03512c4aaac8a58fc3b1221f38293aa517a1950d10ef8646c72c49addc7d/psycopg_binary-3.3.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:97c839717bf8c8df3f6d983a20949c4fb22e2a34ee172e3e427ede363feda27b", size = 5496335, upload-time = "2026-02-18T16:46:51.517Z" }, + { url = "https://files.pythonhosted.org/packages/8a/bc/23319b4b1c2c0b810d225e1b6f16efbb16150074fc0ea96bfcabdf59ee09/psycopg_binary-3.3.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:48e500cf1c0984dacf1f28ea482c3cdbb4c2288d51c336c04bc64198ab21fc51", size = 5172032, upload-time = "2026-02-18T16:47:00.878Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c8/6d61dc0a56654c558a37b2d9b2094e470aa12621305cc7935fd769122e32/psycopg_binary-3.3.3-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb36a08859b9432d94ea6b26ec41a2f98f83f14868c91321d0c1e11f672eeae7", size = 6763107, upload-time = "2026-02-18T16:47:11.784Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b5/e2a3c90aa1059f5b5f593379caad7be3cc3c2ce1ddfc7730e39854e174fe/psycopg_binary-3.3.3-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dde92cfde09293fb63b3f547919ba7d73bd2654573c03502b3263dd0218e44e", size = 5006494, upload-time = "2026-02-18T16:47:17.062Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3e/bf126e0a1f864e191b7f3eeea667ee2ce13d582b036255fb8b12946d1f7a/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:78c9ce98caaf82ac8484d269791c1b403d7598633e0e4e2fa1097baae244e2f1", size = 4533850, upload-time = "2026-02-18T16:47:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d8/bb5e8d395deb945629aa0c65d12ab90ec3bfcbdf56be89e2a84d001864c9/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d593612758d0041cb13cb0003f7f8d3fabb7ad9319e651e78afae49b1cf5860e", size = 4223316, upload-time = "2026-02-18T16:47:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/c2/70/33eef61b0f0fd41ebf93b9699f44067313a45016827f67b3c8cc41f0a7ab/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f24e8e17035200a465c178e9ea945527ad0738118694184c450f1192a452ff25", size = 3954515, upload-time = "2026-02-18T16:47:30.434Z" }, + { url = "https://files.pythonhosted.org/packages/ea/db/27c2b3b9698e713e83e11e8540daa27516f9e90390ec21a41091cb15fcaf/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e7b607f0e14f2a4cf7e78a05ebd13df6144acfba87cb90842e70d3f125d9f53f", size = 4260274, upload-time = "2026-02-18T16:47:36.128Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3b/71e5d603059bf5474215f573a3e2d357a4e95672b26e04d41674400d4862/psycopg_binary-3.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:b27d3a23c79fa59557d2cc63a7e8bb4c7e022c018558eda36f9d7c4e6b99a6e0", size = 3557375, upload-time = "2026-02-18T16:47:42.799Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/b389119dd754483d316805260f3e73cdcad97925839107cc7a296f6132b1/psycopg_binary-3.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a89bb9ee11177b2995d87186b1d9fa892d8ea725e85eab28c6525e4cc14ee048", size = 4609740, upload-time = "2026-02-18T16:47:51.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9976eef20f61840285174d360da4c820a311ab39d6b82fa09fbb545be825/psycopg_binary-3.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f7d0cf072c6fbac3795b08c98ef9ea013f11db609659dcfc6b1f6cc31f9e181", size = 4676837, upload-time = "2026-02-18T16:47:55.523Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f2/d28ba2f7404fd7f68d41e8a11df86313bd646258244cb12a8dd83b868a97/psycopg_binary-3.3.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:90eecd93073922f085967f3ed3a98ba8c325cbbc8c1a204e300282abd2369e13", size = 5497070, upload-time = "2026-02-18T16:47:59.929Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/6c5c54b815edeb30a281cfcea96dc93b3bb6be939aea022f00cab7aa1420/psycopg_binary-3.3.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dac7ee2f88b4d7bb12837989ca354c38d400eeb21bce3b73dac02622f0a3c8d6", size = 5172410, upload-time = "2026-02-18T16:48:05.665Z" }, + { url = "https://files.pythonhosted.org/packages/51/75/8206c7008b57de03c1ada46bd3110cc3743f3fd9ed52031c4601401d766d/psycopg_binary-3.3.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b62cf8784eb6d35beaee1056d54caf94ec6ecf2b7552395e305518ab61eb8fd2", size = 6763408, upload-time = "2026-02-18T16:48:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5a/ea1641a1e6c8c8b3454b0fcb43c3045133a8b703e6e824fae134088e63bd/psycopg_binary-3.3.3-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a39f34c9b18e8f6794cca17bfbcd64572ca2482318db644268049f8c738f35a6", size = 5006255, upload-time = "2026-02-18T16:48:22.176Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fb/538df099bf55ae1637d52d7ccb6b9620b535a40f4c733897ac2b7bb9e14c/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:883d68d48ca9ff3cb3d10c5fdebea02c79b48eecacdddbf7cce6e7cdbdc216b8", size = 4532694, upload-time = "2026-02-18T16:48:27.338Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d1/00780c0e187ea3c13dfc53bd7060654b2232cd30df562aac91a5f1c545ac/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:cab7bc3d288d37a80aa8c0820033250c95e40b1c2b5c57cf59827b19c2a8b69d", size = 4222833, upload-time = "2026-02-18T16:48:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/a07f1ff713c51d64dc9f19f2c32be80299a2055d5d109d5853662b922cb4/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:56c767007ca959ca32f796b42379fc7e1ae2ed085d29f20b05b3fc394f3715cc", size = 3952818, upload-time = "2026-02-18T16:48:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/d3/67/d33f268a7759b4445f3c9b5a181039b01af8c8263c865c1be7a6444d4749/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:da2f331a01af232259a21573a01338530c6016dcfad74626c01330535bcd8628", size = 4258061, upload-time = "2026-02-18T16:48:41.365Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3b/0d8d2c5e8e29ccc07d28c8af38445d9d9abcd238d590186cac82ee71fc84/psycopg_binary-3.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:19f93235ece6dbfc4036b5e4f6d8b13f0b8f2b3eeb8b0bd2936d406991bcdd40", size = 3558915, upload-time = "2026-02-18T16:48:46.679Z" }, + { url = "https://files.pythonhosted.org/packages/90/15/021be5c0cbc5b7c1ab46e91cc3434eb42569f79a0592e67b8d25e66d844d/psycopg_binary-3.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6698dbab5bcef8fdb570fc9d35fd9ac52041771bfcfe6fd0fc5f5c4e36f1e99d", size = 4591170, upload-time = "2026-02-18T16:48:55.594Z" }, + { url = "https://files.pythonhosted.org/packages/f1/54/a60211c346c9a2f8c6b272b5f2bbe21f6e11800ce7f61e99ba75cf8b63e1/psycopg_binary-3.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:329ff393441e75f10b673ae99ab45276887993d49e65f141da20d915c05aafd8", size = 4670009, upload-time = "2026-02-18T16:49:03.608Z" }, + { url = "https://files.pythonhosted.org/packages/c1/53/ac7c18671347c553362aadbf65f92786eef9540676ca24114cc02f5be405/psycopg_binary-3.3.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:eb072949b8ebf4082ae24289a2b0fd724da9adc8f22743409d6fd718ddb379df", size = 5469735, upload-time = "2026-02-18T16:49:10.128Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c3/4f4e040902b82a344eff1c736cde2f2720f127fe939c7e7565706f96dd44/psycopg_binary-3.3.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:263a24f39f26e19ed7fc982d7859a36f17841b05bebad3eb47bb9cd2dd785351", size = 5152919, upload-time = "2026-02-18T16:49:16.335Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/d929679c6a5c212bcf738806c7c89f5b3d0919f2e1685a0e08d6ff877945/psycopg_binary-3.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5152d50798c2fa5bd9b68ec68eb68a1b71b95126c1d70adaa1a08cd5eefdc23d", size = 6738785, upload-time = "2026-02-18T16:49:22.687Z" }, + { url = "https://files.pythonhosted.org/packages/69/b0/09703aeb69a9443d232d7b5318d58742e8ca51ff79f90ffe6b88f1db45e7/psycopg_binary-3.3.3-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d6a1e56dd267848edb824dbeb08cf5bac649e02ee0b03ba883ba3f4f0bd54f2", size = 4979008, upload-time = "2026-02-18T16:49:27.313Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a6/e662558b793c6e13a7473b970fee327d635270e41eded3090ef14045a6a5/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73eaaf4bb04709f545606c1db2f65f4000e8a04cdbf3e00d165a23004692093e", size = 4508255, upload-time = "2026-02-18T16:49:31.575Z" }, + { url = "https://files.pythonhosted.org/packages/5f/7f/0f8b2e1d5e0093921b6f324a948a5c740c1447fbb45e97acaf50241d0f39/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:162e5675efb4704192411eaf8e00d07f7960b679cd3306e7efb120bb8d9456cc", size = 4189166, upload-time = "2026-02-18T16:49:35.801Z" }, + { url = "https://files.pythonhosted.org/packages/92/ec/ce2e91c33bc8d10b00c87e2f6b0fb570641a6a60042d6a9ae35658a3a797/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:fab6b5e37715885c69f5d091f6ff229be71e235f272ebaa35158d5a46fd548a0", size = 3924544, upload-time = "2026-02-18T16:49:41.129Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2f/7718141485f73a924205af60041c392938852aa447a94c8cbd222ff389a1/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a4aab31bd6d1057f287c96c0effca3a25584eb9cc702f282ecb96ded7814e830", size = 4235297, upload-time = "2026-02-18T16:49:46.726Z" }, + { url = "https://files.pythonhosted.org/packages/57/f9/1add717e2643a003bbde31b1b220172e64fbc0cb09f06429820c9173f7fc/psycopg_binary-3.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:59aa31fe11a0e1d1bcc2ce37ed35fe2ac84cd65bb9036d049b1a1c39064d0f14", size = 3547659, upload-time = "2026-02-18T16:49:52.999Z" }, +] + +[[package]] +name = "psycopg-pool" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/9a/9470d013d0d50af0da9c4251614aeb3c1823635cab3edc211e3839db0bcf/psycopg_pool-3.3.0.tar.gz", hash = "sha256:fa115eb2860bd88fce1717d75611f41490dec6135efb619611142b24da3f6db5", size = 31606, upload-time = "2025-12-01T11:34:33.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c3/26b8a0908a9db249de3b4169692e1c7c19048a9bc41a4d3209cee7dbb758/psycopg_pool-3.3.0-py3-none-any.whl", hash = "sha256:2e44329155c410b5e8666372db44276a8b1ebd8c90f1c3026ebba40d4bc81063", size = 39995, upload-time = "2025-12-01T11:34:29.761Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/f2/8e377d29c2ecf99f6062d35ea606b036e8800720eccfec5fe3dd672c2b24/psycopg2_binary-2.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2", size = 3756506, upload-time = "2025-10-10T11:10:30.144Z" }, + { url = "https://files.pythonhosted.org/packages/24/cc/dc143ea88e4ec9d386106cac05023b69668bd0be20794c613446eaefafe5/psycopg2_binary-2.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087", size = 3863943, upload-time = "2025-10-10T11:10:34.586Z" }, + { url = "https://files.pythonhosted.org/packages/8c/df/16848771155e7c419c60afeb24950b8aaa3ab09c0a091ec3ccca26a574d0/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d", size = 4410873, upload-time = "2025-10-10T11:10:38.951Z" }, + { url = "https://files.pythonhosted.org/packages/43/79/5ef5f32621abd5a541b89b04231fe959a9b327c874a1d41156041c75494b/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2", size = 4468016, upload-time = "2025-10-10T11:10:43.319Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9b/d7542d0f7ad78f57385971f426704776d7b310f5219ed58da5d605b1892e/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b", size = 4164996, upload-time = "2025-10-10T11:10:46.705Z" }, + { url = "https://files.pythonhosted.org/packages/14/ed/e409388b537fa7414330687936917c522f6a77a13474e4238219fcfd9a84/psycopg2_binary-2.9.11-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14", size = 3981881, upload-time = "2025-10-30T02:54:57.182Z" }, + { url = "https://files.pythonhosted.org/packages/bf/30/50e330e63bb05efc6fa7c1447df3e08954894025ca3dcb396ecc6739bc26/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd", size = 3650857, upload-time = "2025-10-10T11:10:50.112Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e0/4026e4c12bb49dd028756c5b0bc4c572319f2d8f1c9008e0dad8cc9addd7/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b", size = 3296063, upload-time = "2025-10-10T11:10:54.089Z" }, + { url = "https://files.pythonhosted.org/packages/2c/34/eb172be293c886fef5299fe5c3fcf180a05478be89856067881007934a7c/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152", size = 3043464, upload-time = "2025-10-30T02:55:02.483Z" }, + { url = "https://files.pythonhosted.org/packages/18/1c/532c5d2cb11986372f14b798a95f2eaafe5779334f6a80589a68b5fcf769/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e", size = 3345378, upload-time = "2025-10-10T11:11:01.039Z" }, + { url = "https://files.pythonhosted.org/packages/70/e7/de420e1cf16f838e1fa17b1120e83afff374c7c0130d088dba6286fcf8ea/psycopg2_binary-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39", size = 2713904, upload-time = "2025-10-10T11:11:04.81Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ae/8d8266f6dd183ab4d48b95b9674034e1b482a3f8619b33a0d86438694577/psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", size = 3756452, upload-time = "2025-10-10T11:11:11.583Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/aa03d327739c1be70e09d01182619aca8ebab5970cd0cfa50dd8b9cec2ac/psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", size = 3863957, upload-time = "2025-10-10T11:11:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/48/89/3fdb5902bdab8868bbedc1c6e6023a4e08112ceac5db97fc2012060e0c9a/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", size = 4410955, upload-time = "2025-10-10T11:11:21.21Z" }, + { url = "https://files.pythonhosted.org/packages/ce/24/e18339c407a13c72b336e0d9013fbbbde77b6fd13e853979019a1269519c/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", size = 4468007, upload-time = "2025-10-10T11:11:24.831Z" }, + { url = "https://files.pythonhosted.org/packages/91/7e/b8441e831a0f16c159b5381698f9f7f7ed54b77d57bc9c5f99144cc78232/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", size = 4165012, upload-time = "2025-10-10T11:11:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/0d/61/4aa89eeb6d751f05178a13da95516c036e27468c5d4d2509bb1e15341c81/psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", size = 3981881, upload-time = "2025-10-30T02:55:07.332Z" }, + { url = "https://files.pythonhosted.org/packages/76/a1/2f5841cae4c635a9459fe7aca8ed771336e9383b6429e05c01267b0774cf/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", size = 3650985, upload-time = "2025-10-10T11:11:34.975Z" }, + { url = "https://files.pythonhosted.org/packages/84/74/4defcac9d002bca5709951b975173c8c2fa968e1a95dc713f61b3a8d3b6a/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", size = 3296039, upload-time = "2025-10-10T11:11:40.432Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c2/782a3c64403d8ce35b5c50e1b684412cf94f171dc18111be8c976abd2de1/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", size = 3043477, upload-time = "2025-10-30T02:55:11.182Z" }, + { url = "https://files.pythonhosted.org/packages/c8/31/36a1d8e702aa35c38fc117c2b8be3f182613faa25d794b8aeaab948d4c03/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", size = 3345842, upload-time = "2025-10-10T11:11:45.366Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b4/a5375cda5b54cb95ee9b836930fea30ae5a8f14aa97da7821722323d979b/psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", size = 2713894, upload-time = "2025-10-10T11:11:48.775Z" }, + { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, + { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, + { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" }, +] + +[[package]] +name = "py-key-value-aio" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "py-key-value-shared" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/ce/3136b771dddf5ac905cc193b461eb67967cf3979688c6696e1f2cdcde7ea/py_key_value_aio-0.3.0.tar.gz", hash = "sha256:858e852fcf6d696d231266da66042d3355a7f9871650415feef9fca7a6cd4155", size = 50801, upload-time = "2025-11-17T16:50:04.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/10/72f6f213b8f0bce36eff21fda0a13271834e9eeff7f9609b01afdc253c79/py_key_value_aio-0.3.0-py3-none-any.whl", hash = "sha256:1c781915766078bfd608daa769fefb97e65d1d73746a3dfb640460e322071b64", size = 96342, upload-time = "2025-11-17T16:50:03.801Z" }, +] + +[package.optional-dependencies] +disk = [ + { name = "diskcache" }, + { name = "pathvalidate" }, +] +keyring = [ + { name = "keyring" }, +] +memory = [ + { name = "cachetools" }, +] +redis = [ + { name = "redis" }, +] + +[[package]] +name = "py-key-value-shared" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/e4/1971dfc4620a3a15b4579fe99e024f5edd6e0967a71154771a059daff4db/py_key_value_shared-0.3.0.tar.gz", hash = "sha256:8fdd786cf96c3e900102945f92aa1473138ebe960ef49da1c833790160c28a4b", size = 11666, upload-time = "2025-11-17T16:50:06.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560, upload-time = "2025-11-17T16:50:05.954Z" }, +] + +[[package]] +name = "py4j" +version = "0.10.9.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/31/0b210511177070c8d5d3059556194352e5753602fa64b85b7ab81ec1a009/py4j-0.10.9.9.tar.gz", hash = "sha256:f694cad19efa5bd1dee4f3e5270eb406613c974394035e5bfc4ec1aba870b879", size = 761089, upload-time = "2025-01-15T03:53:18.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/db/ea0203e495be491c85af87b66e37acfd3bf756fd985f87e46fc5e3bf022c/py4j-0.10.9.9-py2.py3-none-any.whl", hash = "sha256:c7c26e4158defb37b0bb124933163641a2ff6e3a3913f7811b0ddbe07ed61533", size = 203008, upload-time = "2025-01-15T03:53:15.648Z" }, +] + +[[package]] +name = "pyarrow" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487, upload-time = "2025-07-18T00:57:31.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d9/110de31880016e2afc52d8580b397dbe47615defbf09ca8cf55f56c62165/pyarrow-21.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26", size = 31196837, upload-time = "2025-07-18T00:54:34.755Z" }, + { url = "https://files.pythonhosted.org/packages/df/5f/c1c1997613abf24fceb087e79432d24c19bc6f7259cab57c2c8e5e545fab/pyarrow-21.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79", size = 32659470, upload-time = "2025-07-18T00:54:38.329Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ed/b1589a777816ee33ba123ba1e4f8f02243a844fed0deec97bde9fb21a5cf/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7be45519b830f7c24b21d630a31d48bcebfd5d4d7f9d3bdb49da9cdf6d764edb", size = 41055619, upload-time = "2025-07-18T00:54:42.172Z" }, + { url = "https://files.pythonhosted.org/packages/44/28/b6672962639e85dc0ac36f71ab3a8f5f38e01b51343d7aa372a6b56fa3f3/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:26bfd95f6bff443ceae63c65dc7e048670b7e98bc892210acba7e4995d3d4b51", size = 42733488, upload-time = "2025-07-18T00:54:47.132Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cc/de02c3614874b9089c94eac093f90ca5dfa6d5afe45de3ba847fd950fdf1/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd04ec08f7f8bd113c55868bd3fc442a9db67c27af098c5f814a3091e71cc61a", size = 43329159, upload-time = "2025-07-18T00:54:51.686Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3e/99473332ac40278f196e105ce30b79ab8affab12f6194802f2593d6b0be2/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594", size = 45050567, upload-time = "2025-07-18T00:54:56.679Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f5/c372ef60593d713e8bfbb7e0c743501605f0ad00719146dc075faf11172b/pyarrow-21.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634", size = 26217959, upload-time = "2025-07-18T00:55:00.482Z" }, + { url = "https://files.pythonhosted.org/packages/94/dc/80564a3071a57c20b7c32575e4a0120e8a330ef487c319b122942d665960/pyarrow-21.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c077f48aab61738c237802836fc3844f85409a46015635198761b0d6a688f87b", size = 31243234, upload-time = "2025-07-18T00:55:03.812Z" }, + { url = "https://files.pythonhosted.org/packages/ea/cc/3b51cb2db26fe535d14f74cab4c79b191ed9a8cd4cbba45e2379b5ca2746/pyarrow-21.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:689f448066781856237eca8d1975b98cace19b8dd2ab6145bf49475478bcaa10", size = 32714370, upload-time = "2025-07-18T00:55:07.495Z" }, + { url = "https://files.pythonhosted.org/packages/24/11/a4431f36d5ad7d83b87146f515c063e4d07ef0b7240876ddb885e6b44f2e/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:479ee41399fcddc46159a551705b89c05f11e8b8cb8e968f7fec64f62d91985e", size = 41135424, upload-time = "2025-07-18T00:55:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/74/dc/035d54638fc5d2971cbf1e987ccd45f1091c83bcf747281cf6cc25e72c88/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:40ebfcb54a4f11bcde86bc586cbd0272bac0d516cfa539c799c2453768477569", size = 42823810, upload-time = "2025-07-18T00:55:16.301Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/89fced102448a9e3e0d4dded1f37fa3ce4700f02cdb8665457fcc8015f5b/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8d58d8497814274d3d20214fbb24abcad2f7e351474357d552a8d53bce70c70e", size = 43391538, upload-time = "2025-07-18T00:55:23.82Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/ea7f1bd08978d39debd3b23611c293f64a642557e8141c80635d501e6d53/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:585e7224f21124dd57836b1530ac8f2df2afc43c861d7bf3d58a4870c42ae36c", size = 45120056, upload-time = "2025-07-18T00:55:28.231Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0b/77ea0600009842b30ceebc3337639a7380cd946061b620ac1a2f3cb541e2/pyarrow-21.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:555ca6935b2cbca2c0e932bedd853e9bc523098c39636de9ad4693b5b1df86d6", size = 26220568, upload-time = "2025-07-18T00:55:32.122Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d4/d4f817b21aacc30195cf6a46ba041dd1be827efa4a623cc8bf39a1c2a0c0/pyarrow-21.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd", size = 31160305, upload-time = "2025-07-18T00:55:35.373Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9c/dcd38ce6e4b4d9a19e1d36914cb8e2b1da4e6003dd075474c4cfcdfe0601/pyarrow-21.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876", size = 32684264, upload-time = "2025-07-18T00:55:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/4f/74/2a2d9f8d7a59b639523454bec12dba35ae3d0a07d8ab529dc0809f74b23c/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d", size = 41108099, upload-time = "2025-07-18T00:55:42.889Z" }, + { url = "https://files.pythonhosted.org/packages/ad/90/2660332eeb31303c13b653ea566a9918484b6e4d6b9d2d46879a33ab0622/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e", size = 42829529, upload-time = "2025-07-18T00:55:47.069Z" }, + { url = "https://files.pythonhosted.org/packages/33/27/1a93a25c92717f6aa0fca06eb4700860577d016cd3ae51aad0e0488ac899/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82", size = 43367883, upload-time = "2025-07-18T00:55:53.069Z" }, + { url = "https://files.pythonhosted.org/packages/05/d9/4d09d919f35d599bc05c6950095e358c3e15148ead26292dfca1fb659b0c/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623", size = 45133802, upload-time = "2025-07-18T00:55:57.714Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/f3795b6e192c3ab881325ffe172e526499eb3780e306a15103a2764916a2/pyarrow-21.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18", size = 26203175, upload-time = "2025-07-18T00:56:01.364Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +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 = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "pydocket" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "croniter" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "fakeredis", extra = ["lua"] }, + { name = "opentelemetry-api" }, + { name = "prometheus-client" }, + { name = "py-key-value-aio", extra = ["memory", "redis"] }, + { name = "python-json-logger" }, + { name = "redis" }, + { name = "rich" }, + { name = "taskgroup", marker = "python_full_version < '3.11'" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, + { name = "uncalled-for" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/da/5f76e42214c76402e1a2b4b59610211635c1068cab85509c78f1ca49a385/pydocket-0.18.0.tar.gz", hash = "sha256:cd5b6e7386331ca05a0163401f392b08b07e61342b5333c3ece6a7ca5435f984", size = 354637, upload-time = "2026-03-02T16:22:17.356Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/57/ac0d47cd3550d859138647c2c4fbd53a2db05db8729433eaa6128e9964ba/pydocket-0.18.0-py3-none-any.whl", hash = "sha256:d995d9a3c88af0402fda640c18e1b51561041b9e3af1a92dce2fdc6c8f6c7090", size = 98848, upload-time = "2026-03-02T16:22:15.792Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pyopenssl" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/8c/cd89ad05804f8e3c17dea8f178c3f40eeab5694c30e0c9f5bcd49f576fc3/pyopenssl-25.1.0.tar.gz", hash = "sha256:8d031884482e0c67ee92bf9a4d8cceb08d92aba7136432ffb0703c5280fc205b", size = 179937, upload-time = "2025-05-17T16:28:31.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/28/2659c02301b9500751f8d42f9a6632e1508aa5120de5e43042b8b30f8d5d/pyopenssl-25.1.0-py3-none-any.whl", hash = "sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab", size = 56771, upload-time = "2025-05-17T16:28:29.197Z" }, +] + +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + +[[package]] +name = "pyrate-limiter" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/4c/83a2c03f4a169f3d9fdb6570f8f82d85e3903f6f183c21ee7d8cbd6655b4/pyrate_limiter-3.1.1.tar.gz", hash = "sha256:2f57eda712687e6eccddf6afe8f8a15b409b97ed675fe64a626058f12863b7b7", size = 276906, upload-time = "2024-01-02T09:35:24.751Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/34/089a939b8cda558e1fccada8ba4b422cd8de0eccd4b87f686c185366b78e/pyrate_limiter-3.1.1-py3-none-any.whl", hash = "sha256:c51906f1d51d56dc992ff6c26e8300e32151bc6cfa3e6559792e31971dfd4e2b", size = 23478, upload-time = "2024-01-02T09:35:22.802Z" }, +] + +[[package]] +name = "pyspark" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "py4j" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/bf/58ee13add151469c25825b7125bbf62c3bdcec05eec4d458fcb5c5516066/pyspark-4.1.1.tar.gz", hash = "sha256:77f78984aa84fbe865c717dd37b49913b4e5c97d76ef6824f932f1aefa6621ec", size = 455359625, upload-time = "2026-01-09T09:38:38.28Z" } + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-json-logger" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +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/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]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + +[[package]] +name = "python-ulid" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/7e/0d6c82b5ccc71e7c833aed43d9e8468e1f2ff0be1b3f657a6fcafbb8433d/python_ulid-3.1.0.tar.gz", hash = "sha256:ff0410a598bc5f6b01b602851a3296ede6f91389f913a5d5f8c496003836f636", size = 93175, upload-time = "2025-08-18T16:09:26.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/a0/4ed6632b70a52de845df056654162acdebaf97c20e3212c559ac43e7216e/python_ulid-3.1.0-py3-none-any.whl", hash = "sha256:e2cdc979c8c877029b4b7a38a6fba3bc4578e4f109a308419ff4d3ccf0a46619", size = 11577, upload-time = "2025-08-18T16:09:25.047Z" }, +] + +[[package]] +name = "pytz" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692, upload-time = "2024-09-11T02:24:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002, upload-time = "2024-09-11T02:24:45.8Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "pyyaml" +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 = "rapidfuzz" +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 = "redis" +version = "7.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/31/1476f206482dd9bc53fdbbe9f6fbd5e05d153f18e54667ce839df331f2e6/redis-7.2.1.tar.gz", hash = "sha256:6163c1a47ee2d9d01221d8456bc1c75ab953cbda18cfbc15e7140e9ba16ca3a5", size = 4906735, upload-time = "2026-02-25T20:05:18.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/98/1dd1a5c060916cf21d15e67b7d6a7078e26e2605d5c37cbc9f4f5454c478/redis-7.2.1-py3-none-any.whl", hash = "sha256:49e231fbc8df2001436ae5252b3f0f3dc930430239bfeb6da4c7ee92b16e5d33", size = 396057, upload-time = "2026-02-25T20:05:16.533Z" }, +] + +[[package]] +name = "referencing" +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/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/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 = "regex" +version = "2026.2.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/b8/845a927e078f5e5cc55d29f57becbfde0003d52806544531ab3f2da4503c/regex-2026.2.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fc48c500838be6882b32748f60a15229d2dea96e59ef341eaa96ec83538f498d", size = 488461, upload-time = "2026-02-28T02:15:48.405Z" }, + { url = "https://files.pythonhosted.org/packages/32/f9/8a0034716684e38a729210ded6222249f29978b24b684f448162ef21f204/regex-2026.2.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2afa673660928d0b63d84353c6c08a8a476ddfc4a47e11742949d182e6863ce8", size = 290774, upload-time = "2026-02-28T02:15:51.738Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ba/b27feefffbb199528dd32667cd172ed484d9c197618c575f01217fbe6103/regex-2026.2.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7ab218076eb0944549e7fe74cf0e2b83a82edb27e81cc87411f76240865e04d5", size = 288737, upload-time = "2026-02-28T02:15:53.534Z" }, + { url = "https://files.pythonhosted.org/packages/18/c5/65379448ca3cbfe774fcc33774dc8295b1ee97dc3237ae3d3c7b27423c9d/regex-2026.2.28-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d63db12e45a9b9f064bfe4800cefefc7e5f182052e4c1b774d46a40ab1d9bb", size = 782675, upload-time = "2026-02-28T02:15:55.488Z" }, + { url = "https://files.pythonhosted.org/packages/aa/30/6fa55bef48090f900fbd4649333791fc3e6467380b9e775e741beeb3231f/regex-2026.2.28-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:195237dc327858a7721bf8b0bbbef797554bc13563c3591e91cd0767bacbe359", size = 850514, upload-time = "2026-02-28T02:15:57.509Z" }, + { url = "https://files.pythonhosted.org/packages/a9/28/9ca180fb3787a54150209754ac06a42409913571fa94994f340b3bba4e1e/regex-2026.2.28-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b387a0d092dac157fb026d737dde35ff3e49ef27f285343e7c6401851239df27", size = 896612, upload-time = "2026-02-28T02:15:59.682Z" }, + { url = "https://files.pythonhosted.org/packages/46/b5/f30d7d3936d6deecc3ea7bea4f7d3c5ee5124e7c8de372226e436b330a55/regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3935174fa4d9f70525a4367aaff3cb8bc0548129d114260c29d9dfa4a5b41692", size = 791691, upload-time = "2026-02-28T02:16:01.752Z" }, + { url = "https://files.pythonhosted.org/packages/f5/34/96631bcf446a56ba0b2a7f684358a76855dfe315b7c2f89b35388494ede0/regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b2b23587b26496ff5fd40df4278becdf386813ec00dc3533fa43a4cf0e2ad3c", size = 783111, upload-time = "2026-02-28T02:16:03.651Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/f95cb7a85fe284d41cd2f3625e0f2ae30172b55dfd2af1d9b4eaef6259d7/regex-2026.2.28-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3b24bd7e9d85dc7c6a8bd2aa14ecd234274a0248335a02adeb25448aecdd420d", size = 767512, upload-time = "2026-02-28T02:16:05.616Z" }, + { url = "https://files.pythonhosted.org/packages/3d/af/a650f64a79c02a97f73f64d4e7fc4cc1984e64affab14075e7c1f9a2db34/regex-2026.2.28-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd477d5f79920338107f04aa645f094032d9e3030cc55be581df3d1ef61aa318", size = 773920, upload-time = "2026-02-28T02:16:08.325Z" }, + { url = "https://files.pythonhosted.org/packages/72/f8/3f9c2c2af37aedb3f5a1e7227f81bea065028785260d9cacc488e43e6997/regex-2026.2.28-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b49eb78048c6354f49e91e4b77da21257fecb92256b6d599ae44403cab30b05b", size = 846681, upload-time = "2026-02-28T02:16:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/8db04a334571359f4d127d8f89550917ec6561a2fddfd69cd91402b47482/regex-2026.2.28-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a25c7701e4f7a70021db9aaf4a4a0a67033c6318752146e03d1b94d32006217e", size = 755565, upload-time = "2026-02-28T02:16:11.972Z" }, + { url = "https://files.pythonhosted.org/packages/da/bc/91c22f384d79324121b134c267a86ca90d11f8016aafb1dc5bee05890ee3/regex-2026.2.28-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9dd450db6458387167e033cfa80887a34c99c81d26da1bf8b0b41bf8c9cac88e", size = 835789, upload-time = "2026-02-28T02:16:14.036Z" }, + { url = "https://files.pythonhosted.org/packages/46/a7/4cc94fd3af01dcfdf5a9ed75c8e15fd80fcd62cc46da7592b1749e9c35db/regex-2026.2.28-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2954379dd20752e82d22accf3ff465311cbb2bac6c1f92c4afd400e1757f7451", size = 780094, upload-time = "2026-02-28T02:16:15.468Z" }, + { url = "https://files.pythonhosted.org/packages/3c/21/e5a38f420af3c77cab4a65f0c3a55ec02ac9babf04479cfd282d356988a6/regex-2026.2.28-cp310-cp310-win32.whl", hash = "sha256:1f8b17be5c27a684ea6759983c13506bd77bfc7c0347dff41b18ce5ddd2ee09a", size = 266025, upload-time = "2026-02-28T02:16:16.828Z" }, + { url = "https://files.pythonhosted.org/packages/4d/0a/205c4c1466a36e04d90afcd01d8908bac327673050c7fe316b2416d99d3d/regex-2026.2.28-cp310-cp310-win_amd64.whl", hash = "sha256:dd8847c4978bc3c7e6c826fb745f5570e518b8459ac2892151ce6627c7bc00d5", size = 277965, upload-time = "2026-02-28T02:16:18.752Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4d/29b58172f954b6ec2c5ed28529a65e9026ab96b4b7016bcd3858f1c31d3c/regex-2026.2.28-cp310-cp310-win_arm64.whl", hash = "sha256:73cdcdbba8028167ea81490c7f45280113e41db2c7afb65a276f4711fa3bcbff", size = 270336, upload-time = "2026-02-28T02:16:20.735Z" }, + { url = "https://files.pythonhosted.org/packages/04/db/8cbfd0ba3f302f2d09dd0019a9fcab74b63fee77a76c937d0e33161fb8c1/regex-2026.2.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9", size = 488462, upload-time = "2026-02-28T02:16:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/5d/10/ccc22c52802223f2368731964ddd117799e1390ffc39dbb31634a83022ee/regex-2026.2.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97", size = 290774, upload-time = "2026-02-28T02:16:23.993Z" }, + { url = "https://files.pythonhosted.org/packages/62/b9/6796b3bf3101e64117201aaa3a5a030ec677ecf34b3cd6141b5d5c6c67d5/regex-2026.2.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703", size = 288724, upload-time = "2026-02-28T02:16:25.403Z" }, + { url = "https://files.pythonhosted.org/packages/9c/02/291c0ae3f3a10cea941d0f5366da1843d8d1fa8a25b0671e20a0e454bb38/regex-2026.2.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098", size = 791924, upload-time = "2026-02-28T02:16:26.863Z" }, + { url = "https://files.pythonhosted.org/packages/0f/57/f0235cc520d9672742196c5c15098f8f703f2758d48d5a7465a56333e496/regex-2026.2.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2", size = 860095, upload-time = "2026-02-28T02:16:28.772Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/393c94cbedda79a0f5f2435ebd01644aba0b338d327eb24b4aa5b8d6c07f/regex-2026.2.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64", size = 906583, upload-time = "2026-02-28T02:16:30.977Z" }, + { url = "https://files.pythonhosted.org/packages/2c/73/a72820f47ca5abf2b5d911d0407ba5178fc52cf9780191ed3a54f5f419a2/regex-2026.2.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022", size = 800234, upload-time = "2026-02-28T02:16:32.55Z" }, + { url = "https://files.pythonhosted.org/packages/34/b3/6e6a4b7b31fa998c4cf159a12cbeaf356386fbd1a8be743b1e80a3da51e4/regex-2026.2.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1", size = 772803, upload-time = "2026-02-28T02:16:34.029Z" }, + { url = "https://files.pythonhosted.org/packages/10/e7/5da0280c765d5a92af5e1cd324b3fe8464303189cbaa449de9a71910e273/regex-2026.2.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a", size = 781117, upload-time = "2026-02-28T02:16:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/76/39/0b8d7efb256ae34e1b8157acc1afd8758048a1cf0196e1aec2e71fd99f4b/regex-2026.2.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27", size = 854224, upload-time = "2026-02-28T02:16:38.119Z" }, + { url = "https://files.pythonhosted.org/packages/21/ff/a96d483ebe8fe6d1c67907729202313895d8de8495569ec319c6f29d0438/regex-2026.2.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae", size = 761898, upload-time = "2026-02-28T02:16:40.333Z" }, + { url = "https://files.pythonhosted.org/packages/89/bd/d4f2e75cb4a54b484e796017e37c0d09d8a0a837de43d17e238adf163f4e/regex-2026.2.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea", size = 844832, upload-time = "2026-02-28T02:16:41.875Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a7/428a135cf5e15e4e11d1e696eb2bf968362f8ea8a5f237122e96bc2ae950/regex-2026.2.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b", size = 788347, upload-time = "2026-02-28T02:16:43.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/59/68691428851cf9c9c3707217ab1d9b47cfeec9d153a49919e6c368b9e926/regex-2026.2.28-cp311-cp311-win32.whl", hash = "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15", size = 266033, upload-time = "2026-02-28T02:16:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/1483de1c57024e89296cbcceb9cccb3f625d416ddb46e570be185c9b05a9/regex-2026.2.28-cp311-cp311-win_amd64.whl", hash = "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61", size = 277978, upload-time = "2026-02-28T02:16:46.75Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/abec45dc6e7252e3dbc797120496e43bb5730a7abf0d9cb69340696a2f2d/regex-2026.2.28-cp311-cp311-win_arm64.whl", hash = "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a", size = 270340, upload-time = "2026-02-28T02:16:48.626Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/9061b03cf0fc4b5fa2c3984cbbaed54324377e440a5c5a29d29a72518d62/regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", size = 489574, upload-time = "2026-02-28T02:16:50.455Z" }, + { url = "https://files.pythonhosted.org/packages/77/83/0c8a5623a233015595e3da499c5a1c13720ac63c107897a6037bb97af248/regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", size = 291426, upload-time = "2026-02-28T02:16:52.52Z" }, + { url = "https://files.pythonhosted.org/packages/9e/06/3ef1ac6910dc3295ebd71b1f9bfa737e82cfead211a18b319d45f85ddd09/regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", size = 289200, upload-time = "2026-02-28T02:16:54.08Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c9/8cc8d850b35ab5650ff6756a1cb85286e2000b66c97520b29c1587455344/regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", size = 796765, upload-time = "2026-02-28T02:16:55.905Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5d/57702597627fc23278ebf36fbb497ac91c0ce7fec89ac6c81e420ca3e38c/regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", size = 863093, upload-time = "2026-02-28T02:16:58.094Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/f3ecad537ca2811b4d26b54ca848cf70e04fcfc138667c146a9f3157779c/regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", size = 909455, upload-time = "2026-02-28T02:17:00.918Z" }, + { url = "https://files.pythonhosted.org/packages/9e/40/bb226f203caa22c1043c1ca79b36340156eca0f6a6742b46c3bb222a3a57/regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", size = 802037, upload-time = "2026-02-28T02:17:02.842Z" }, + { url = "https://files.pythonhosted.org/packages/44/7c/c6d91d8911ac6803b45ca968e8e500c46934e58c0903cbc6d760ee817a0a/regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", size = 775113, upload-time = "2026-02-28T02:17:04.506Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/4a9368d168d47abd4158580b8c848709667b1cd293ff0c0c277279543bd0/regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", size = 784194, upload-time = "2026-02-28T02:17:06.888Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bf/2c72ab5d8b7be462cb1651b5cc333da1d0068740342f350fcca3bca31947/regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", size = 856846, upload-time = "2026-02-28T02:17:09.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f4/6b65c979bb6d09f51bb2d2a7bc85de73c01ec73335d7ddd202dcb8cd1c8f/regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", size = 763516, upload-time = "2026-02-28T02:17:11.004Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/29ea5e27400ee86d2cc2b4e80aa059df04eaf78b4f0c18576ae077aeff68/regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", size = 849278, upload-time = "2026-02-28T02:17:12.693Z" }, + { url = "https://files.pythonhosted.org/packages/1d/91/3233d03b5f865111cd517e1c95ee8b43e8b428d61fa73764a80c9bb6f537/regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", size = 790068, upload-time = "2026-02-28T02:17:14.9Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/abc706c1fb03b4580a09645b206a3fc032f5a9f457bc1a8038ac555658ab/regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", size = 266416, upload-time = "2026-02-28T02:17:17.15Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/2a6f7dff190e5fa9df9fb4acf2fdf17a1aa0f7f54596cba8de608db56b3a/regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", size = 277297, upload-time = "2026-02-28T02:17:18.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f0/58a2484851fadf284458fdbd728f580d55c1abac059ae9f048c63b92f427/regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", size = 270408, upload-time = "2026-02-28T02:17:20.328Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +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/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]] +name = "requests-cache" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cattrs" }, + { name = "platformdirs" }, + { name = "requests" }, + { name = "url-normalize" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/6c/deaf1a9462ce8b6a9ac0ee3603d9ba32917be8e48c8f6799770d5418c3cb/requests_cache-1.3.0.tar.gz", hash = "sha256:070e357ccef11a300ccef4294a85de1ab265833c5d9c9538b26cd7ba4085d54a", size = 97720, upload-time = "2026-02-02T23:17:33.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/3f/dfa42bb16be96d53351aa151cb1e39fcaafe6cda01389c530a2ec809ef8a/requests_cache-1.3.0-py3-none-any.whl", hash = "sha256:f09f27bbf100c250886acf13a9db35b53cf2852fddd71977b47c71ea7d90dbba", size = 69626, upload-time = "2026-02-02T23:17:31.718Z" }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, +] + +[[package]] +name = "rich-click" +version = "1.9.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/27/091e140ea834272188e63f8dd6faac1f5c687582b687197b3e0ec3c78ebf/rich_click-1.9.7.tar.gz", hash = "sha256:022997c1e30731995bdbc8ec2f82819340d42543237f033a003c7b1f843fc5dc", size = 74838, upload-time = "2026-01-31T04:29:27.707Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/e5/d708d262b600a352abe01c2ae360d8ff75b0af819b78e9af293191d928e6/rich_click-1.9.7-py3-none-any.whl", hash = "sha256:2f99120fca78f536e07b114d3b60333bc4bb2a0969053b1250869bcdc1b5351b", size = 71491, upload-time = "2026-01-31T04:29:26.777Z" }, +] + +[[package]] +name = "rich-rst" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "ruff" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/55/9f485266e6326cab707369601b13e3e72eb90ba3eee2d6779549a00a0d58/ruff-0.6.4.tar.gz", hash = "sha256:ac3b5bfbee99973f80aa1b7cbd1c9cbce200883bdd067300c22a6cc1c7fba212", size = 2469375, upload-time = "2024-09-05T15:51:36.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/78/307591f81d09c8721b5e64539f287c82c81a46f46d16278eb27941ac17f9/ruff-0.6.4-py3-none-linux_armv6l.whl", hash = "sha256:c4b153fc152af51855458e79e835fb6b933032921756cec9af7d0ba2aa01a258", size = 9692673, upload-time = "2024-09-05T15:50:50.469Z" }, + { url = "https://files.pythonhosted.org/packages/69/63/ef398fcacdbd3995618ed30b5a6c809a1ebbf112ba604b3f5b8c3be464cf/ruff-0.6.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bedff9e4f004dad5f7f76a9d39c4ca98af526c9b1695068198b3bda8c085ef60", size = 9481182, upload-time = "2024-09-05T15:50:54.027Z" }, + { url = "https://files.pythonhosted.org/packages/a6/fd/8784e3bbd79bc17de0a62de05fe5165f494ff7d77cb06630d6428c2f10d2/ruff-0.6.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d02a4127a86de23002e694d7ff19f905c51e338c72d8e09b56bfb60e1681724f", size = 9174356, upload-time = "2024-09-05T15:50:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/6d/bc/c69db2d68ac7bfbb222c81dc43a86e0402d0063e20b13e609f7d17d81d3f/ruff-0.6.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7862f42fc1a4aca1ea3ffe8a11f67819d183a5693b228f0bb3a531f5e40336fc", size = 10129365, upload-time = "2024-09-05T15:50:59.674Z" }, + { url = "https://files.pythonhosted.org/packages/3b/10/8ed14ff60a4e5eb08cac0a04a9b4e8590c72d1ce4d29ef22cef97d19536d/ruff-0.6.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eebe4ff1967c838a1a9618a5a59a3b0a00406f8d7eefee97c70411fefc353617", size = 9483351, upload-time = "2024-09-05T15:51:02.296Z" }, + { url = "https://files.pythonhosted.org/packages/a9/69/13316b8d64ffd6a43627cf0753339a7f95df413450c301a60904581bee6e/ruff-0.6.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:932063a03bac394866683e15710c25b8690ccdca1cf192b9a98260332ca93408", size = 10301099, upload-time = "2024-09-05T15:51:04.68Z" }, + { url = "https://files.pythonhosted.org/packages/42/00/9623494087272643e8f02187c266638306c6829189a5bf1446968bbe438b/ruff-0.6.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:50e30b437cebef547bd5c3edf9ce81343e5dd7c737cb36ccb4fe83573f3d392e", size = 11033216, upload-time = "2024-09-05T15:51:07.111Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/e0c9d881db42ea1267e075c29aafe0db5a8a3024b131f952747f6234f858/ruff-0.6.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c44536df7b93a587de690e124b89bd47306fddd59398a0fb12afd6133c7b3818", size = 10618140, upload-time = "2024-09-05T15:51:10.005Z" }, + { url = "https://files.pythonhosted.org/packages/5b/35/f1d8b746aedd4c8fde4f83397e940cc4c8fc619860ebbe3073340381a34d/ruff-0.6.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ea086601b22dc5e7693a78f3fcfc460cceabfdf3bdc36dc898792aba48fbad6", size = 11606672, upload-time = "2024-09-05T15:51:12.554Z" }, + { url = "https://files.pythonhosted.org/packages/c5/70/899b03cbb3eb48ed0507d4b32b6f7aee562bc618ef9ffda855ec98c0461a/ruff-0.6.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b52387d3289ccd227b62102c24714ed75fbba0b16ecc69a923a37e3b5e0aaaa", size = 10288013, upload-time = "2024-09-05T15:51:15.487Z" }, + { url = "https://files.pythonhosted.org/packages/17/c6/906bf895640521ca5115ccdd857b2bac42bd61facde6620fdc2efc0a4806/ruff-0.6.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0308610470fcc82969082fc83c76c0d362f562e2f0cdab0586516f03a4e06ec6", size = 10109473, upload-time = "2024-09-05T15:51:17.623Z" }, + { url = "https://files.pythonhosted.org/packages/28/da/1284eb04172f8a5d42eb52fce9d643dd747ac59a4ed6c5d42729f72e934d/ruff-0.6.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:803b96dea21795a6c9d5bfa9e96127cc9c31a1987802ca68f35e5c95aed3fc0d", size = 9568817, upload-time = "2024-09-05T15:51:20.771Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e2/f8250b54edbb2e9222e22806e1bcc35a192ac18d1793ea556fa4977a843a/ruff-0.6.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:66dbfea86b663baab8fcae56c59f190caba9398df1488164e2df53e216248baa", size = 9910840, upload-time = "2024-09-05T15:51:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7c/dcf2c10562346ecdf6f0e5f6669b2ddc9a74a72956c3f419abd6820c2aff/ruff-0.6.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:34d5efad480193c046c86608dbba2bccdc1c5fd11950fb271f8086e0c763a5d1", size = 10354263, upload-time = "2024-09-05T15:51:26.604Z" }, + { url = "https://files.pythonhosted.org/packages/f1/94/c39d7ac5729e94788110503d928c98c203488664b0fb92c2b801cb832bec/ruff-0.6.4-py3-none-win32.whl", hash = "sha256:f0f8968feea5ce3777c0d8365653d5e91c40c31a81d95824ba61d871a11b8523", size = 7958602, upload-time = "2024-09-05T15:51:29.563Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d2/2dee8c547bee3d4cfdd897f7b8e38510383acaff2c8130ea783b67631d72/ruff-0.6.4-py3-none-win_amd64.whl", hash = "sha256:549daccee5227282289390b0222d0fbee0275d1db6d514550d65420053021a58", size = 8795059, upload-time = "2024-09-05T15:51:31.994Z" }, + { url = "https://files.pythonhosted.org/packages/07/1a/23280818aa4fa89bd0552aab10857154e1d3b90f27b5b745f09ec1ac6ad8/ruff-0.6.4-py3-none-win_arm64.whl", hash = "sha256:ac4b75e898ed189b3708c9ab3fc70b79a433219e1e87193b4f2b77251d058d14", size = 8239636, upload-time = "2024-09-05T15:51:34.17Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "serpyco-rs" +version = "1.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attributes-doc" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/24/a218c68f25152789bd80c40aaa8124b549a06e50c74f742d4d4590e18788/serpyco_rs-1.19.0.tar.gz", hash = "sha256:a9fe01ddbc7ffa4f0f57896dda2759ad676937943f66d3c5af2b0472ca6672e6", size = 84709, upload-time = "2026-02-03T16:58:15.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/f2/8885389eb7b06be6e8ca480f36a9a912b2f56b666575a2596004ea9d683a/serpyco_rs-1.19.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0aa06769e2a0a165439af070795dff7c272b8510d803005da06b3d5e0e56c186", size = 918522, upload-time = "2026-02-03T16:57:09.726Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bb/4ae9c5e9a6cfea82dbf01225c00e66754c7b2e9e2684d2a148b99b3f8a7b/serpyco_rs-1.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d98ff9749e04a351eccb74cd61cac67ace4ea2a1b1aafecafb7b6a3164241c2c", size = 493183, upload-time = "2026-02-03T16:57:11.33Z" }, + { url = "https://files.pythonhosted.org/packages/83/57/c3df2882fb7e6acb861e0901c70d5c6564f2da52627fe538dae2a5345660/serpyco_rs-1.19.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a2133ebfe1cb773f310d76355819eec99a136c2df638c4e7bd93cc6bd3180b59", size = 516458, upload-time = "2026-02-03T16:57:12.433Z" }, + { url = "https://files.pythonhosted.org/packages/62/8e/0832df8171e93876968db9674fa38cba3415257784c55df75e6d94e0a946/serpyco_rs-1.19.0-cp310-cp310-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0dcf2f44aacce39eec9303d6d6f80245a31c1ae9d6f6f1595b5f523b4883e41a", size = 550060, upload-time = "2026-02-03T16:57:14.097Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d8/82c9dce5133d674cef856a4060536eabfb567319d7a5c6d110db9daf13eb/serpyco_rs-1.19.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a17548844908206fbabda3ad0480edbd880e832cbe7e25ae0aac51ed145f9d39", size = 538512, upload-time = "2026-02-03T16:57:15.202Z" }, + { url = "https://files.pythonhosted.org/packages/a2/8d/83d1d8680fc52349aaba3fc9494f5c7a17ece8ddfa2a21479e9d7fac6996/serpyco_rs-1.19.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e0fd84a4f272b2afa468bd6f39edb6554e9542e5c8edc4bd232f8d4dbb4beff", size = 529017, upload-time = "2026-02-03T16:57:16.364Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d0/2000a2f52ab7960f0cdfda870898cb97e8f820244aafa326afc79c795c94/serpyco_rs-1.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09c2ab08c0da4dabdf12d044e9f441251ee48353b7fcd0a5e712381356fe0d82", size = 492812, upload-time = "2026-02-03T16:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/74/55/471150cbb3bf4b6741afdc41e6a83ae4e414c452c53c291354b1f562fad4/serpyco_rs-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:6d8ea33f71bddc5f2bda1e23aa374966fb3b10d2179b0c92ac3778c2193c4c14", size = 365946, upload-time = "2026-02-03T16:57:18.671Z" }, + { url = "https://files.pythonhosted.org/packages/4a/fd/9fbdb5c22b8b17f2950a58556b4eadac8ee0599ea5292c3704b6b1f7c77b/serpyco_rs-1.19.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:03ff7330c499b9b8f48dac7156bdf2557225ec5f0a8773838c077f95460e45a8", size = 926864, upload-time = "2026-02-03T16:57:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/ee/64/e4ca47f328cf6e3ba4ba8904f5843c83a27861c0ddac61b7f9c97a3babba/serpyco_rs-1.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3245f70159cefe26c30e62cc474b9daa094e9282a71c880cfd92730ea8c7242d", size = 493113, upload-time = "2026-02-03T16:57:20.947Z" }, + { url = "https://files.pythonhosted.org/packages/34/13/bf5353ed30ad105a4ac5eaa5322de2df2c1d6705786f90c14fb16f3c7583/serpyco_rs-1.19.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0be7762f3f2cd745fa294efddf141b4d7bac75f3eca9deeaddf7c5d0f7411198", size = 515979, upload-time = "2026-02-03T16:57:22.042Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a2/c107501742adf54cd2d207bace88d27e0facccbd338e3c31073075700e80/serpyco_rs-1.19.0-cp311-cp311-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:63d295a091a5e51fb919987b44bbbf13596609ebe160a18b8ad954ea2346cd2e", size = 550219, upload-time = "2026-02-03T16:57:23.158Z" }, + { url = "https://files.pythonhosted.org/packages/8e/22/e303fbde23c0aad4ff3b7b01b6fae419d61fd821102363381296b2777c67/serpyco_rs-1.19.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b47511401744a927eb0bd502c450571b8adcb470150ed30065cba8c3bc6cb4a", size = 538669, upload-time = "2026-02-03T16:57:24.312Z" }, + { url = "https://files.pythonhosted.org/packages/10/9e/2a92dab92354092779cb58a2b860741c0b0abcdd56f2e5ba728b74ce6406/serpyco_rs-1.19.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a754b12375ddb3efdab72895172d87c7ea8353d3214d3ce09616db4367075f9", size = 528833, upload-time = "2026-02-03T16:57:25.578Z" }, + { url = "https://files.pythonhosted.org/packages/9c/aa/ab577f2bb460a978b1ba32b010215048f94a0a396dacdc6e5cb16967ae59/serpyco_rs-1.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26cfe3331ae75dad1b8ffccb274577396680d78bc76418c89e515f967bc382ab", size = 494817, upload-time = "2026-02-03T16:57:26.631Z" }, + { url = "https://files.pythonhosted.org/packages/d6/90/ca99f3e417a4c2a6f402bb3aa8a608dfbde6721751e87ef1a2d3c1fd0d86/serpyco_rs-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:5fce5eaedcca4e427b31377f6b15be6a9ac14303b49c8686a9ca7c8c4c63e467", size = 365019, upload-time = "2026-02-03T16:57:27.793Z" }, + { url = "https://files.pythonhosted.org/packages/e8/78/183403059f653732532899130716596d91af0e6c287f1c7b34b8da177bec/serpyco_rs-1.19.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b5aab95fe9707c40bfdc78cfcc00f93b04de98e1d7e19ed1bb132dd1e6174022", size = 927981, upload-time = "2026-02-03T16:57:29.122Z" }, + { url = "https://files.pythonhosted.org/packages/53/c4/863510c5ebf9b6c9a746de67d5689450a6e5dec5c222b81c15a25a4798be/serpyco_rs-1.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee815d41ac955f0bff87a35e682fa6299cfe138fbdf6f8fe7deaa501a0b69bbf", size = 497449, upload-time = "2026-02-03T16:57:30.523Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f3/e443b85833b023dd4a86ff1023dbb2b64dae633b6a06afcda69c8ae20cf9/serpyco_rs-1.19.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a396fe59f0d4de76b0b005461e36ce4230a0c84aa08e5bfd9465f1463061b11c", size = 518887, upload-time = "2026-02-03T16:57:31.855Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/a0e6cedf8306593464afee2918c3a532851e2f969f3ef6d7165862e51686/serpyco_rs-1.19.0-cp312-cp312-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fae8e4d16de8b145123b052a095e75e257d73305dc440cddcee020d9ae3385ea", size = 549122, upload-time = "2026-02-03T16:57:33.179Z" }, + { url = "https://files.pythonhosted.org/packages/12/a4/3da8515d952f82e85f96dd206b151839ebb65bb4c8d81cb2790773d717f1/serpyco_rs-1.19.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff027e6c066d0fd65f157f026d748fdcb142cef2ff67368ba896665ff6ce0e6b", size = 536491, upload-time = "2026-02-03T16:57:34.363Z" }, + { url = "https://files.pythonhosted.org/packages/6a/24/ad406db5e87e4ae0252b1c753625370fd4e6644442082b000dafadeea2d1/serpyco_rs-1.19.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3785f7fdb2e7b8b4b5b3c73c2d155934cdc8156398ce331f55d2102aa47221f2", size = 537238, upload-time = "2026-02-03T16:57:35.422Z" }, + { url = "https://files.pythonhosted.org/packages/bc/4a/165799cb31719d9267f2b617d34e9950b483f6de4be9a5fb862f1443f8c0/serpyco_rs-1.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66a8aa4547b2ecb03a02f434df3566e29be657f83baac5fe67726729ce9afbc4", size = 492510, upload-time = "2026-02-03T16:57:36.553Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c7/9ad1671d9ec28eb21399f8a2963f841e067524c6b904ae75b012187299d8/serpyco_rs-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:1bb8204cecdd173ddc5650caf5cfbf10646d1aa25bbe3458fe6168708177a583", size = 365760, upload-time = "2026-02-03T16:57:38.28Z" }, +] + +[[package]] +name = "setuptools" +version = "80.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "snowflake-connector-python" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asn1crypto" }, + { name = "boto3" }, + { name = "botocore" }, + { name = "certifi" }, + { name = "cffi" }, + { name = "charset-normalizer" }, + { name = "cryptography" }, + { name = "filelock" }, + { name = "idna" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "pyjwt" }, + { name = "pyopenssl" }, + { name = "pytz" }, + { name = "requests" }, + { name = "sortedcontainers" }, + { name = "tomlkit" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/df/41fe26b68801e3d59653a5dc7ce87a92e9d967dcad7b59b035b8c9804815/snowflake_connector_python-3.18.0.tar.gz", hash = "sha256:41a46eb9824574c5f8068e3ed5c02a2dc0a733ed08ee81fa1fb3dd0ebe921728", size = 798019, upload-time = "2025-10-06T12:15:34.301Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/66/2be9bfebaad12f8b0fbeee68542f14b7a37801b991e3f99adab98ca235ff/snowflake_connector_python-3.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e17a9e806823d3a0e578cf9349f6a93810a582b3132903ea9e1683854d08da00", size = 1014396, upload-time = "2025-10-06T12:15:36.069Z" }, + { url = "https://files.pythonhosted.org/packages/8e/38/e00f81687b56a9419c2b0de3adcab449fc1e7d7a5383c29835148b1bb311/snowflake_connector_python-3.18.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:1841b60dc376639493dfc520cf39ad4f4da1f30286bba57e878d57414263d628", size = 1027175, upload-time = "2025-10-06T12:15:37.632Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ae/f45696a00e63fad3e153c01b8fe5c2d55aba954bb69bd09c7d2d0a290cba/snowflake_connector_python-3.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65d37263dd288abb649820b7e34af96dc6b2d2115bf5521a2526245f81ddb0cb", size = 2650237, upload-time = "2025-10-06T12:15:14.24Z" }, + { url = "https://files.pythonhosted.org/packages/c1/dd/843ac8067efb061f66c4e186c293270b887103b162a73559660b4fb0d31e/snowflake_connector_python-3.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fb9fc9d8c2c7d209ba89282d367a32e75b0688afd4a3f02409e24f153c1a32e", size = 2678195, upload-time = "2025-10-06T12:15:16.975Z" }, + { url = "https://files.pythonhosted.org/packages/e8/b2/035e0e316f875f4410d880e12a2765063c054e12e0184a3d86f2728818e5/snowflake_connector_python-3.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:cfa6b234f53ec624149e21156d0a98e43408d194f2e65bcfaf30acefd35a581e", size = 1161494, upload-time = "2025-10-06T12:15:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/87/7e/b932b9897ea7e83b2184443c5222af2f71526e8bce91ecd64ac20b87527c/snowflake_connector_python-3.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a5fcb9a25a9b77b6cd86dfc6a6324b9910e15a493a916983229011ce3509b5f", size = 1014589, upload-time = "2025-10-06T12:15:39.26Z" }, + { url = "https://files.pythonhosted.org/packages/7e/79/97f777d3d26392901910e27f0d41e9a6dc72fba40cb2499febfca7e51e81/snowflake_connector_python-3.18.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d89f608fde2fb0597ca5e020c4ac602027dc67f11b61b4d1e5448163bae4edc", size = 1027163, upload-time = "2025-10-06T12:15:40.651Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9f/553f9a2ec3ce4ab960c8d3611ecbd2e66f972841ef1e037dcbcd5148abae/snowflake_connector_python-3.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1afbd9e21180d2b4a76500ac2978b11865fdb3230609f2a9d80ba459fc27f2e4", size = 2661951, upload-time = "2025-10-06T12:15:18.676Z" }, + { url = "https://files.pythonhosted.org/packages/8a/bb/8213c682ea69cf50ba028db47469455cb7dba31b152b867ac3a468dcca19/snowflake_connector_python-3.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c068c8d3cd0c9736cb0679a9f544d34327e64415303bbfe07ec8ce3c5dae800", size = 2692086, upload-time = "2025-10-06T12:15:21.5Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/e651de2061f88e30cd271a023cc79e2e2683ff6aa2cb1e1045b8ba62d365/snowflake_connector_python-3.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:b211b4240596a225b895261a4ced2633e0262e82e2e32f6fb8dfc7d4bfedf8ca", size = 1161558, upload-time = "2025-10-06T12:15:53.091Z" }, + { url = "https://files.pythonhosted.org/packages/da/67/0df7829f295988c121f385c562d60c7a4989bc8f72885d04669ce5cd6516/snowflake_connector_python-3.18.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fee7035f865088f948510b094101c8a0e5b22501891f2115f7fb1cb555de76a", size = 1013717, upload-time = "2025-10-06T12:15:41.906Z" }, + { url = "https://files.pythonhosted.org/packages/4d/90/35353d5311735ebe85f0224f3a6e4f136c29e1b3e4ce6c7466c9b7e7931b/snowflake_connector_python-3.18.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:283366b35df88cd0c71caf0215ba80370ddef4dd37d2adf43b24208c747231ee", size = 1025471, upload-time = "2025-10-06T12:15:43.073Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/d490c00546ca8842d314de689ac718c73c9fe0f9b042e06703449282de7c/snowflake_connector_python-3.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e4c285cc6a7f6431cff98c8f235a0fe9da2262462dd3dfc2b97120574a95cf9", size = 2684000, upload-time = "2025-10-06T12:15:23.411Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cb/4bc697af4138e17cccde506f28233492a6e1919ced7a65aa31b6f1e8bb6c/snowflake_connector_python-3.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94e041e347b5151b66d19d6cfc3b3172dac1f51e44bbf7cf58f3989427dd464a", size = 2715472, upload-time = "2025-10-06T12:15:25.062Z" }, + { url = "https://files.pythonhosted.org/packages/d9/72/815a4b9795ddce224a1392849dd34a408f2dac240bcdcb0539d42cfd31b1/snowflake_connector_python-3.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:7116cfa410d517328fd25fabffb54845b88667586718578c4333ce034fead1ba", size = 1160435, upload-time = "2025-10-06T12:15:55.046Z" }, +] + +[[package]] +name = "snowflake-sqlalchemy" +version = "1.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "snowflake-connector-python" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/0b/5e90eb28191ad6e0318254394c7e2902c4037fd566aa299dc8b5b16238f8/snowflake_sqlalchemy-1.8.2.tar.gz", hash = "sha256:91ca38719e117f94dd195ba94c22dd22f69c585b136ed129ba4e2dd93252b0c2", size = 122603, upload-time = "2025-12-10T08:33:49.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/77/c3af74a84eb00c1004a8e3c8a98627a3eecb2563f4ee01e621326c947bce/snowflake_sqlalchemy-1.8.2-py3-none-any.whl", hash = "sha256:13ad79bf51654cdaaedfbcc60d20bee417c0a128f8710eabbf4aba65b50f6d3d", size = 72726, upload-time = "2025-12-10T08:33:48.106Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.43" +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" }, +] + +[[package]] +name = "sqlalchemy-bigquery" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-cloud-bigquery" }, + { name = "packaging" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/39/6d2fb718e61d18c07cfc3de84362c198aa429e3dcf3c1d0a1e476e474196/sqlalchemy_bigquery-1.12.0.tar.gz", hash = "sha256:12783ad83ffad34e8e6e14046cb14bb2f1a3e7fb52676f5a24e940ff5cdeb864", size = 113993, upload-time = "2024-10-02T21:32:50.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ba/684540c3970f70ba68405283671dd23fd42fd7de559bf0aea5bf4117c9e7/sqlalchemy_bigquery-1.12.0-py2.py3-none-any.whl", hash = "sha256:5b2b77bdaefe9c0663db213d9475a5abbae88fa46108c352d19fa6fc51a47a1a", size = 38258, upload-time = "2024-10-02T21:32:48.593Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/9f/c3695c2d2d4ef70072c3a06992850498b01c6bc9be531950813716b426fa/sse_starlette-3.3.2.tar.gz", hash = "sha256:678fca55a1945c734d8472a6cad186a55ab02840b4f6786f5ee8770970579dcd", size = 32326, upload-time = "2026-02-28T11:24:34.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/28/8cb142d3fe80c4a2d8af54ca0b003f47ce0ba920974e7990fa6e016402d1/sse_starlette-3.3.2-py3-none-any.whl", hash = "sha256:5c3ea3dad425c601236726af2f27689b74494643f57017cafcb6f8c9acfbb862", size = 14270, upload-time = "2026-02-28T11:24:32.984Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "structlog" +version = "24.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/a3/e811a94ac3853826805253c906faa99219b79951c7d58605e89c79e65768/structlog-24.4.0.tar.gz", hash = "sha256:b27bfecede327a6d2da5fbc96bd859f114ecc398a6389d664f62085ee7ae6fc4", size = 1348634, upload-time = "2024-07-17T12:38:43.483Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/65/813fc133609ebcb1299be6a42e5aea99d6344afb35ccb43f67e7daaa3b92/structlog-24.4.0-py3-none-any.whl", hash = "sha256:597f61e80a91cc0749a9fd2a098ed76715a1c8a01f73e336b746504d1aad7610", size = 67180, upload-time = "2024-07-17T12:38:41.043Z" }, +] + +[[package]] +name = "taskgroup" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/8d/e218e0160cc1b692e6e0e5ba34e8865dbb171efeb5fc9a704544b3020605/taskgroup-0.2.2.tar.gz", hash = "sha256:078483ac3e78f2e3f973e2edbf6941374fbea81b9c5d0a96f51d297717f4752d", size = 11504, upload-time = "2025-01-03T09:24:13.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/b1/74babcc824a57904e919f3af16d86c08b524c0691504baf038ef2d7f655c/taskgroup-0.2.2-py2.py3-none-any.whl", hash = "sha256:e2c53121609f4ae97303e9ea1524304b4de6faf9eb2c9280c7f87976479a52fb", size = 14237, upload-time = "2025-01-03T09:24:11.41Z" }, +] + +[[package]] +name = "thrift" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/2d/8946864f716ac82dcc88d290ed613cba7a80ec75df4f553ec3ff275f486e/thrift-0.20.0.tar.gz", hash = "sha256:4dd662eadf6b8aebe8a41729527bd69adf6ceaa2a8681cbef64d1273b3e8feba", size = 62295, upload-time = "2024-03-22T22:53:08.228Z" } + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, +] + +[[package]] +name = "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 = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspect" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +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/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + +[[package]] +name = "uncalled-for" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/7c/b5b7d8136f872e3f13b0584e576886de0489d7213a12de6bebf29ff6ebfc/uncalled_for-0.2.0.tar.gz", hash = "sha256:b4f8fdbcec328c5a113807d653e041c5094473dd4afa7c34599ace69ccb7e69f", size = 49488, upload-time = "2026-02-27T17:40:58.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/7f/4320d9ce3be404e6310b915c3629fe27bf1e2f438a1a7a3cb0396e32e9a9/uncalled_for-0.2.0-py3-none-any.whl", hash = "sha256:2c0bd338faff5f930918f79e7eb9ff48290df2cb05fcc0b40a7f334e55d4d85f", size = 11351, upload-time = "2026-02-27T17:40:56.804Z" }, +] + +[[package]] +name = "unidecode" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/7d/a8a765761bbc0c836e397a2e48d498305a865b70a8600fd7a942e85dcf63/Unidecode-1.4.0.tar.gz", hash = "sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23", size = 200149, upload-time = "2025-04-24T08:45:03.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/b7/559f59d57d18b44c6d1250d2eeaa676e028b9c527431f5d0736478a73ba1/Unidecode-1.4.0-py3-none-any.whl", hash = "sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021", size = 235837, upload-time = "2025-04-24T08:45:01.609Z" }, +] + +[[package]] +name = "url-normalize" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/31/febb777441e5fcdaacb4522316bf2a527c44551430a4873b052d545e3279/url_normalize-2.2.1.tar.gz", hash = "sha256:74a540a3b6eba1d95bdc610c24f2c0141639f3ba903501e61a52a8730247ff37", size = 18846, upload-time = "2025-04-26T20:37:58.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/d9/5ec15501b675f7bc07c5d16aa70d8d778b12375686b6efd47656efdc67cd/url_normalize-2.2.1-py3-none-any.whl", hash = "sha256:3deb687587dc91f7b25c9ae5162ffc0f057ae85d22b1e15cf5698311247f567b", size = 14728, upload-time = "2025-04-26T20:37:57.217Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uuid7" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/19/7472bd526591e2192926247109dbf78692e709d3e56775792fec877a7720/uuid7-0.1.0.tar.gz", hash = "sha256:8c57aa32ee7456d3cc68c95c4530bc571646defac01895cfc73545449894a63c", size = 14052, upload-time = "2021-12-29T01:38:21.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/77/8852f89a91453956582a85024d80ad96f30a41fed4c2b3dce0c9f12ecc7e/uuid7-0.1.0-py2.py3-none-any.whl", hash = "sha256:5e259bb63c8cb4aded5927ff41b444a80d0c7124e8a0ced7cf44efa1f5cccf61", size = 7477, upload-time = "2021-12-29T01:38:20.418Z" }, +] + +[[package]] +name = "uv" +version = "0.8.24" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/a3/ccb253bb014987c998398b1cc86a4d5a07d091c885b17535e6b00546c0ea/uv-0.8.24.tar.gz", hash = "sha256:34349d22278fff4b5fb37d58fd4fb8c10d75dc7a0cbec80a8cb34bfbf7cb00d5", size = 3668752, upload-time = "2025-10-07T03:34:19.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/fe/29bf6822cab74ef4f636ee7baff542ef60747085e7ccb8f83a3503e1b79d/uv-0.8.24-py3-none-linux_armv6l.whl", hash = "sha256:5a373ee953f341306c70028131a700c42ddef9848829e1b58f4cd62364824546", size = 20578081, upload-time = "2025-10-07T03:33:16.174Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/26702561b06650efe7eb36008e7a93e877cd51a9bb54141cc159113b37b1/uv-0.8.24-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1cd064933beb3c7392a8dc88d903be809b70612c563e2d659a96d505cac1daf5", size = 19584945, upload-time = "2025-10-07T03:33:21.151Z" }, + { url = "https://files.pythonhosted.org/packages/ea/00/08f4e93989129bb3378f20315dddcac6f8cf26a12bdd90443a340e7ecdb4/uv-0.8.24-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2bd708a545c1c21d7be8575f4cff00d0cff26be13fc81e3f7e54b8751fb90c0", size = 18187983, upload-time = "2025-10-07T03:33:24.533Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8f/3fce919d6794c6c3ecfc948d875ead078fc407346dd01dbbd5a64b46bf49/uv-0.8.24-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:8595ca23e4f0b8ea934a29f8080578cd197ada22931b358498e9445ddd2bac5c", size = 19984225, upload-time = "2025-10-07T03:33:29.237Z" }, + { url = "https://files.pythonhosted.org/packages/40/4d/e320ba9573a07942ddfd0c895f9567253edc5eb2b42689dd95ec40e087db/uv-0.8.24-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f33083111a9cebd1eb2a53225250a51eb9652a79a1cd3bade14a3b52d217bf3", size = 20193175, upload-time = "2025-10-07T03:33:33.045Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/915a605a67d5b00502e28dd8d221f08a1cb3bd006cd6e0485c29a55f2e83/uv-0.8.24-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4dce6ddc7de84a205ec411a042eaa94324d14fae4f345abf4e4ce74bc804fcf", size = 21051270, upload-time = "2025-10-07T03:33:36.353Z" }, + { url = "https://files.pythonhosted.org/packages/4a/af/0efe560170533fb932239dfc0e2bf3be9e854bc564143a9bf06bd303d43b/uv-0.8.24-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fa00f8f05468f827f6f7eda1c4b47abbe16147ea04e10ba9cdf39b5d9a6f0227", size = 22550404, upload-time = "2025-10-07T03:33:40.246Z" }, + { url = "https://files.pythonhosted.org/packages/f8/3c/cb3ba8ecbabf83b9fd0d0bf01053d78bf1e759ce462ed0956250e8523424/uv-0.8.24-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f884f1336f141cab06a85cd24732fe4dc2a577a6c623be2a0209e0f6fba98aca", size = 22175389, upload-time = "2025-10-07T03:33:43.531Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0a/e89df282539780742b3621c42b48307cab1a86c9ec7b93fba22ee9b83632/uv-0.8.24-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3aeac20d6909bcb54d7976ec6b0492e7613cc23dd4509505e08b931cf29ed384", size = 21281276, upload-time = "2025-10-07T03:33:46.919Z" }, + { url = "https://files.pythonhosted.org/packages/15/91/0cb0e416a8b7cfdc7f15e39d700dc06a9689073206fee4c7bf8f1fd68331/uv-0.8.24-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a75005a146e81ed1bbb0b6e55db18c8ad1e7d714392cdb94a63fa7a3259ad4f", size = 21242977, upload-time = "2025-10-07T03:33:50.231Z" }, + { url = "https://files.pythonhosted.org/packages/01/ea/547fffd3c779fae3e5dfccf64e893eac33126f20454b883b51148e9b8493/uv-0.8.24-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:1143a8c6e59f4600dfc1b96b335c7fa47428246be37ac19b6f6dd1535c385ccd", size = 20107480, upload-time = "2025-10-07T03:33:53.407Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/6256cb6b47fed16baa24f85fbc2026cbb24eed4b9bc8c87cd4102bf92c8a/uv-0.8.24-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:17d39a1b32e18ce87ad7038f6d321f1d71ae9e59ac1a10b9d59423d7a7c8c216", size = 21191676, upload-time = "2025-10-07T03:33:56.735Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/cba6bf21ab9f0f998a44e3257516c451e55b98256aa585295ee3f1157df6/uv-0.8.24-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:87032d770da97ecba265123aa27f37885a116cc6c3492e9f7b045edf96690ef4", size = 20164806, upload-time = "2025-10-07T03:34:00.037Z" }, + { url = "https://files.pythonhosted.org/packages/23/a8/fd7894621caef02a15283473e9398bcc949232a4c118ed11cfe7600ba969/uv-0.8.24-py3-none-musllinux_1_1_i686.whl", hash = "sha256:17ab3f303d23c04043829b6154f2623a711b76331f199269bcd7df827bb3ea5c", size = 20507984, upload-time = "2025-10-07T03:34:03.337Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b5/e9f1b332c59ea5aac3f1d715700ce670a35bcfe9a92b5c87e2572ce743fe/uv-0.8.24-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:bd1576fe700b064ee0f4f56908dc112b65df4c780ced04fabdf83eb6e3ec7322", size = 21409149, upload-time = "2025-10-07T03:34:06.621Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8e/53d36dbe52c432457307381c5895f0d2f4809decc30991a71b3671f26041/uv-0.8.24-py3-none-win32.whl", hash = "sha256:c0089dacd349d054689da0391f67f655288bb1b4c402a40e6a4599354d22f21d", size = 19343695, upload-time = "2025-10-07T03:34:10.217Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d1/12d251ecd36aea66ddfa1431b61c4782b2905ecbf188bbffd5aee8f5ceef/uv-0.8.24-py3-none-win_amd64.whl", hash = "sha256:59d2527b9afdd89361d057b0c8077fca3212e7335df46532bf9057c6fc5eb9ff", size = 21370118, upload-time = "2025-10-07T03:34:13.72Z" }, + { url = "https://files.pythonhosted.org/packages/65/40/839b2987cf4045c13f4c4946a136797871fd7968f75b7f866978ceea59b8/uv-0.8.24-py3-none-win_arm64.whl", hash = "sha256:712af0dcb2e1522b85e168e10a1dcb9fe5775e81cee632d8a6d7e95054a096f3", size = 19803089, upload-time = "2025-10-07T03:34:17.307Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.41.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/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.29.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/9c/57d19fa093bcf5ac61a48087dd44d00655f85421d1aa9722f8befbf3f40a/virtualenv-20.29.3.tar.gz", hash = "sha256:95e39403fcf3940ac45bc717597dba16110b74506131845d9b687d5e73d947ac", size = 4320280, upload-time = "2025-03-06T19:54:19.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/eb/c6db6e3001d58c6a9e67c74bb7b4206767caa3ccc28c6b9eaf4c23fb4e34/virtualenv-20.29.3-py3-none-any.whl", hash = "sha256:3e3d00f5807e83b234dfb6122bf37cfadf4be216c53a49ac059d02414f819170", size = 4301458, upload-time = "2025-03-06T19:54:16.923Z" }, +] + +[[package]] +name = "wcmatch" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/ab/b3a52228538ccb983653c446c1656eddf1d5303b9cb8b9aef6a91299f862/wcmatch-10.0.tar.gz", hash = "sha256:e72f0de09bba6a04e0de70937b0cf06e55f36f37b3deb422dfaf854b867b840a", size = 115578, upload-time = "2024-09-26T18:39:52.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/df/4ee467ab39cc1de4b852c212c1ed3becfec2e486a51ac1ce0091f85f38d7/wcmatch-10.0-py3-none-any.whl", hash = "sha256:0dd927072d03c0a6527a20d2e6ad5ba8d0380e60870c383bc533b71744df7b7a", size = 39347, upload-time = "2024-09-26T18:39:51.002Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "whenever" +version = "0.8.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/67/cfc23dfe54ced1e4388826b29db9b9ab2c70a342b33b7e92cf15866f35a6/whenever-0.8.10.tar.gz", hash = "sha256:5e2a3da71527e299f98eec5bb38c4e79d9527a127107387456125005884fb235", size = 240223, upload-time = "2025-10-16T20:31:23.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/22/b7db5ebe8b74f60f8dc2af8264adac158dbd7d199731509299c3345f7874/whenever-0.8.10-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d9ecb6b649cb7e5c85742f626ddd56d5cf5d276c632a47ec5d72714350300564", size = 390187, upload-time = "2025-10-16T20:30:54.648Z" }, + { url = "https://files.pythonhosted.org/packages/10/65/ed912e437c68e83a3f22477c9f3c67f1810228b8d6566a999f12921025d1/whenever-0.8.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0698cbd2209413f7a0cb84507405587e7b3995ce22504e50477a1a65ec3b65b9", size = 375020, upload-time = "2025-10-16T20:30:45.408Z" }, + { url = "https://files.pythonhosted.org/packages/17/a9/a3e7a9731af8c350e605eff2d2d03d2517a839767efd308337498410d32b/whenever-0.8.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30b2f25ee740f5d201f643982c50f0d6ba2fdbb69704630467d85286e290fdab", size = 397139, upload-time = "2025-10-16T20:29:17.616Z" }, + { url = "https://files.pythonhosted.org/packages/7c/32/8568934b927a122582a4e5551cbc86b9c32c781a5abe32c4697aead88fa4/whenever-0.8.10-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb6abd25e03e1aaa9c4ab949c1b02d755be6ea2f18d6a86e0d024a66705beec6", size = 436865, upload-time = "2025-10-16T20:29:35.073Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3b/c3765b975fb6ceb0ded61d60a815c2045c1ec2f1e70f1afc2df8645175a1/whenever-0.8.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:228860bfc14e63b7c2c6980e41dee7f4efb397accc06eabc51e9dfeaf633ad5a", size = 430843, upload-time = "2025-10-16T20:29:51.467Z" }, + { url = "https://files.pythonhosted.org/packages/dc/76/b809b29be1b11868275c6e50e23f1b387b9a40953e717a87ec33dd83bfa9/whenever-0.8.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0af24862ded1dcb71e096e7570e6e031f934e7cfa57123363ef21049f8f9fdd4", size = 450953, upload-time = "2025-10-16T20:29:59.761Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c9/b9f2655e6715e2314b25e722eff7de92e75795001bdc85fed6cf9edfbd8d/whenever-0.8.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6331ebf85dd234d33fdd627146f20808c6eb39f8056dbd09715055f21cd7c494", size = 412096, upload-time = "2025-10-16T20:30:27.719Z" }, + { url = "https://files.pythonhosted.org/packages/e7/6f/773ad493573fcddd89d5080589ca334335c07e6ce70c7aef743c66a05067/whenever-0.8.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ce5dfa7769444e12ae8f0fba8bdce05a8081e1829a9de68d4cc02a11ff71131", size = 451054, upload-time = "2025-10-16T20:30:08.951Z" }, + { url = "https://files.pythonhosted.org/packages/1c/54/3558f92753e2356558973980c31c6e118330c354caecaa0144d1690a6854/whenever-0.8.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9768562c5a871b2a6377697eb76943fd798c663a4a96b499e4d2fa69c42d7397", size = 575509, upload-time = "2025-10-16T20:29:26.622Z" }, + { url = "https://files.pythonhosted.org/packages/27/b7/f96f250bb9f420b8c42b265046f3623f6a9ffda9d469fe27f9027895c407/whenever-0.8.10-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f88d9ec50f2dfa4981924cb87fb287708ccb5f770fd93dd9c6fc27641e686c1c", size = 701546, upload-time = "2025-10-16T20:29:42.776Z" }, + { url = "https://files.pythonhosted.org/packages/84/8f/351d01a0aca6c579c5253555d999760f351b6c06cfe3e7699f0236aa8e79/whenever-0.8.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:507462b0f02d7d4cdfe90888a0158ee3d6c5d49fa3ddcd1b44901c6778fd7381", size = 625427, upload-time = "2025-10-16T20:30:17.872Z" }, + { url = "https://files.pythonhosted.org/packages/1f/60/f4e5d3ae33cda28dc30b21fdf494372d2eaeec7f72f458524e7dcc08301a/whenever-0.8.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ba2d930b5e428e1b0c01ef6c8af14eb94f84792c37d79352f954cd9ea791838e", size = 583161, upload-time = "2025-10-16T20:30:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/15/72/e47c7a6ceb2743f65e825121b1a3f95267a865b58a6c8eda310a863ef358/whenever-0.8.10-cp310-cp310-win32.whl", hash = "sha256:b598be861fd711d2df683d32dbb15d05279e2e932a4c31f2f7bfd28196985662", size = 328384, upload-time = "2025-10-16T20:31:03.569Z" }, + { url = "https://files.pythonhosted.org/packages/ac/09/12cd9a390780cd9b31891fd37a18014b6d5c3d2416665f3513a87a291ce1/whenever-0.8.10-cp310-cp310-win_amd64.whl", hash = "sha256:66eab892d56685a84a9d933b8252c68794eede39b5105f20d06b000ff17275d4", size = 320937, upload-time = "2025-10-16T20:31:13.019Z" }, + { url = "https://files.pythonhosted.org/packages/39/d1/74854e557ae9f57e410bb4b7e8685a8fb0ec301926ca36e71f6e541ce6ae/whenever-0.8.10-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3f03f9bef7e3bfe40461e74c74af0cf8dc90489dacc2360069faccf2997f4bca", size = 390188, upload-time = "2025-10-16T20:30:56.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/fe/dc9d824164824909c230d5c0100332aa62d260fd859cfcfb4f53d119fa79/whenever-0.8.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f42eb10aaf2818b0e26a5d5230c6cb735ca109882ec4b19cb5cf646c0d28120", size = 375020, upload-time = "2025-10-16T20:30:47.115Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ff/ea8e043b511a18057b5144ad78206198bf4671e241d5ce4b2338613c478b/whenever-0.8.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de0b3ddb300e32b19dd9af391d98ba62b21288d628ec17acf4752d96443a3174", size = 397139, upload-time = "2025-10-16T20:29:20.005Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/65dd001e09198d608ff3ceb9ca93f6a7087b4fb4610734840b4980b5f69c/whenever-0.8.10-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:907e7d9fca7dfdaa2fae187320442c1f10d41cadefd1bb58b11b9b30ad36a51f", size = 436865, upload-time = "2025-10-16T20:29:36.641Z" }, + { url = "https://files.pythonhosted.org/packages/94/62/2415de31d92c153f5b6fff0c1218c69d2889882e029935887011cc080a9a/whenever-0.8.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:671380d09a5cf7beae203d4fcb03e4434e41604d8f5832bd67bc060675e7ba93", size = 430844, upload-time = "2025-10-16T20:29:53.131Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a7/b8fad31741a0ba48a28a124b41c11675a460bb140f1ac458eac95f60a932/whenever-0.8.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:816a6ae3b5129afee5ecbac958a828efbad56908db9d6ca4c90cc57133145071", size = 450951, upload-time = "2025-10-16T20:30:00.978Z" }, + { url = "https://files.pythonhosted.org/packages/8c/de/fe07b243f21c071d43974ffc1ee2630383e1ee21b329a39fb61810181eac/whenever-0.8.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f5a51878bdf520655d131a50ca03e7b8a20ec249042e26bf76eeef64e79f3cb", size = 412096, upload-time = "2025-10-16T20:30:29.272Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9d/95468cd837d02a882873a0ef921d6d79960db3b27ac875738d5d138cd577/whenever-0.8.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:071fba23f80a3857db6cbe6c449dd2e0f0cea29d4466c960e52699ef3ed126ae", size = 451052, upload-time = "2025-10-16T20:30:10.533Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c2/5a669c99ff4b470e7beb0990600a6f4df21c6c74b0799b469921fa210c91/whenever-0.8.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c50060b2d3561762dc15d742d03b3c1377778b2896d6c6f3824f15f943d12b62", size = 575510, upload-time = "2025-10-16T20:29:27.734Z" }, + { url = "https://files.pythonhosted.org/packages/79/9d/86a77a04fc42ead5cd06fe7f1c40e59cde943a701ca984e837a9f259eb04/whenever-0.8.10-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2d1b3d00388ce26f450841c34b513fe963ae473a94e6e9c113a534803a70702b", size = 701548, upload-time = "2025-10-16T20:29:43.912Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c3/938c96615593176246debdea9b6567a9ff1bd578da389ec123b25ad03279/whenever-0.8.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e9dc6510beda89e520608459da41b10092e770c58b3b472418fec2633c50857d", size = 625429, upload-time = "2025-10-16T20:30:19.205Z" }, + { url = "https://files.pythonhosted.org/packages/05/0f/b6955e7e79c3d766a372189159018f6f957d0f37f0e287e73e781d115861/whenever-0.8.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:08bae07abb1d2cdc017d38451a3cae5b5577b5b875b65f89847516e6380201dd", size = 583165, upload-time = "2025-10-16T20:30:37.609Z" }, + { url = "https://files.pythonhosted.org/packages/55/7a/06563d2cac97754761488df0c578604fb09e663cb9d7adb7dcefbdeaa5fd/whenever-0.8.10-cp311-cp311-win32.whl", hash = "sha256:96fc39933480786efc074f469157e290414d14bae1a6198bb7e44bc6f6b3531a", size = 328376, upload-time = "2025-10-16T20:31:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b3/b5722d5c033e873970fb616ef4979ed986062250cccf5344bdd964fd45a5/whenever-0.8.10-cp311-cp311-win_amd64.whl", hash = "sha256:a5bad9acce99b46f6dd5dc64c2aab62a0ffba8dcdeeebbd462e37431af0bf243", size = 320938, upload-time = "2025-10-16T20:31:14.386Z" }, + { url = "https://files.pythonhosted.org/packages/26/77/2f76f02e4af09814b2353ba80eefa84472bb586163de792e28df581a9fe0/whenever-0.8.10-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9877982944af2b5055d3aeedcdc3f7af78767f5ce7be8994c3f54b3ffba272e9", size = 391415, upload-time = "2025-10-16T20:30:57.426Z" }, + { url = "https://files.pythonhosted.org/packages/f1/1b/cd64476b64399a5b4f39ee926de721dba38e56ece7ed082c702ea2c401f1/whenever-0.8.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:72db2f4e2511e0c01e63d16a8f539ce82096a08111fa9c63d718c6f49768dce6", size = 375207, upload-time = "2025-10-16T20:30:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/b5/57/2096658c25bec85a804769a786211226ca0929177b3d4becafe2bd0bfad2/whenever-0.8.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da0e929bcc4aa807a68aa766bf040ae314bb4ad291dcc9e75d9e472b5eccec0f", size = 396804, upload-time = "2025-10-16T20:29:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/86/f7/62afb72eb0e1caae28f6b2899069431249a0cc79c8aa70b3599f1b6faa39/whenever-0.8.10-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11c9bea3260edc9018d0c08d20d836fb9d69fdd2dfb25f8f71896de70e1d88c1", size = 437439, upload-time = "2025-10-16T20:29:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/60/48/8deb4bd33e4bf5536f1e12096eaab59b47561c4ca2db1a8b5d0363345cb9/whenever-0.8.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e8c14d7c5418db4e3e52bb4e33138334f86d1c4e6059aa2642325bf5270cc06", size = 432826, upload-time = "2025-10-16T20:29:54.432Z" }, + { url = "https://files.pythonhosted.org/packages/63/32/604450523cb9eca3cd5039a6fe91862c10841dd684a687e64a23ae068bf4/whenever-0.8.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be8156fd0b84b57b52f43f0df41e5bf775df6fce8323f2d69bc0b0a36b08836b", size = 451699, upload-time = "2025-10-16T20:30:02.242Z" }, + { url = "https://files.pythonhosted.org/packages/5c/11/0d5a236b6a0502e0c809d105bdc63d9a370f7b482b05cf514a08a570b128/whenever-0.8.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3381092c1944baff5b80b1e81f63684e365a84274f80145cbd6f07f505725ae2", size = 412902, upload-time = "2025-10-16T20:30:30.673Z" }, + { url = "https://files.pythonhosted.org/packages/77/f2/01497ff97563d0b5b3b2baaa008699d7c2d50acdfd1f59ec82358c6f8a6e/whenever-0.8.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0792c5f0f5bea0749fccd3f1612594305ba1e7c3a5173ff096f32895bb3de0d", size = 452110, upload-time = "2025-10-16T20:30:11.98Z" }, + { url = "https://files.pythonhosted.org/packages/91/6f/6c736d2eae3012d126f9c3a5cf6bc176f3eef63536aeacef8ef3250b21ba/whenever-0.8.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:49cca1b92b1dd7da33b7f4f5f699d6c3a376ad8ea293f67c23b2b00df218a3ea", size = 575306, upload-time = "2025-10-16T20:29:28.955Z" }, + { url = "https://files.pythonhosted.org/packages/a8/79/9935bb4891e88152e3b520ef7ebf243a53955b9b9c79673588da140881df/whenever-0.8.10-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1791288d70931319910860ac4e941d944da3a7c189199dc37a877a9844f8af01", size = 702203, upload-time = "2025-10-16T20:29:45.609Z" }, + { url = "https://files.pythonhosted.org/packages/16/1d/e59c731844b587fbdef79fdd8a1bb71488c83891d9da8f7ed3d01bbde157/whenever-0.8.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:162da8253584608100e35b8b6b95a1fe7edced64b13ceac70351d30459425d67", size = 626912, upload-time = "2025-10-16T20:30:20.924Z" }, + { url = "https://files.pythonhosted.org/packages/7f/25/72463deca11ba6ba846905e72c14b21f0f3c60654d6e9f6b1c2ab662f905/whenever-0.8.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8ce5529a859321c88b25bee659f761447281fe3fbe52352c7c9aa49f0ee8d7ff", size = 584097, upload-time = "2025-10-16T20:30:39.144Z" }, + { url = "https://files.pythonhosted.org/packages/24/6b/a2b711c849d114d65273ec3a9671b5434bd6371a65db17d2e76a6821a122/whenever-0.8.10-cp312-cp312-win32.whl", hash = "sha256:7e756ea4c89995e702ca6cfb061c9536fac3395667e1737c23ca7eb7462e6ce7", size = 329993, upload-time = "2025-10-16T20:31:06.34Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5d/50cff202663587bca2659042c1d049a91d78ce86d0aa2440eaf1d8e2d6dd/whenever-0.8.10-cp312-cp312-win_amd64.whl", hash = "sha256:19c4279bc5907881cbfe310cfe32ba58163ce1c515c056962d121875231be03f", size = 322358, upload-time = "2025-10-16T20:31:16.25Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b6/a485fb8bd32d29b81949bf863ae673955458bbeb28bd852d1c210ccbe75d/whenever-0.8.10-py3-none-any.whl", hash = "sha256:5393187037cff776fe1f5e0fe6094cb52f4509945459d239b9fcc09d95696f43", size = 53506, upload-time = "2025-10-16T20:31:22.043Z" }, +] + +[[package]] +name = "xmltodict" +version = "0.14.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/05/51dcca9a9bf5e1bce52582683ce50980bcadbc4fa5143b9f2b19ab99958f/xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553", size = 51942, upload-time = "2024-10-16T06:10:29.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/45/fc303eb433e8a2a271739c98e953728422fa61a3c1f36077a49e395c972e/xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac", size = 9981, upload-time = "2024-10-16T06:10:27.649Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +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" }, +]